diff --git a/Cargo.toml b/Cargo.toml index 0a84729864a..dcb5682d85b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ alloy-rpc-types-anvil = { version = "0.1", path = "crates/rpc-types-anvil", defa alloy-rpc-types-beacon = { version = "0.1", path = "crates/rpc-types-beacon", default-features = false } alloy-rpc-types-engine = { version = "0.1", path = "crates/rpc-types-engine", default-features = false } alloy-rpc-types-eth = { version = "0.1", path = "crates/rpc-types-eth", default-features = false } +alloy-rpc-types-mev = { version = "0.1", path = "crates/rpc-types-mev", default-features = false } alloy-rpc-types-trace = { version = "0.1", path = "crates/rpc-types-trace", default-features = false } alloy-rpc-types-txpool = { version = "0.1", path = "crates/rpc-types-txpool", default-features = false } alloy-rpc-types = { version = "0.1", path = "crates/rpc-types", default-features = false } diff --git a/crates/rpc-types-mev/Cargo.toml b/crates/rpc-types-mev/Cargo.toml new file mode 100644 index 00000000000..58fb3e4dd3e --- /dev/null +++ b/crates/rpc-types-mev/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "alloy-rpc-types-mev" +description = "Types for the MEV JSON-RPC namespace" + +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +exclude.workspace = true + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +alloy-eips = { workspace = true, features = ["serde"] } +alloy-primitives.workspace = true +alloy-serde.workspace = true + +serde.workspace = true +serde_json.workspace = true + +[lints] +workspace = true diff --git a/crates/rpc-types-mev/README.md b/crates/rpc-types-mev/README.md new file mode 100644 index 00000000000..42ef4fc27c1 --- /dev/null +++ b/crates/rpc-types-mev/README.md @@ -0,0 +1,3 @@ +# alloy-rpc-types-mev + +Types for the MEV bundle JSON-RPC namespace. diff --git a/crates/rpc-types-mev/src/common.rs b/crates/rpc-types-mev/src/common.rs new file mode 100644 index 00000000000..8c138137eff --- /dev/null +++ b/crates/rpc-types-mev/src/common.rs @@ -0,0 +1,244 @@ +use alloy_primitives::Address; +use serde::{ser::SerializeSeq, Deserialize, Deserializer, Serialize, Serializer}; + +/// The version of the MEV-share API to use. +#[derive(Deserialize, Debug, Serialize, Clone, Default, PartialEq, Eq)] +pub enum ProtocolVersion { + #[default] + #[serde(rename = "beta-1")] + /// The beta-1 version of the API. + Beta1, + /// The 0.1 version of the API. + #[serde(rename = "v0.1")] + V0_1, +} + +/// Represents information about when a bundle was considered by a builder. +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConsideredByBuildersAt { + /// The public key of the builder. + pub pubkey: String, + /// The timestamp indicating when the bundle was considered by the builder. + pub timestamp: String, +} + +/// Represents information about when a bundle was sealed by a builder. +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SealedByBuildersAt { + /// The public key of the builder. + pub pubkey: String, + /// The timestamp indicating when the bundle was sealed by the builder. + pub timestamp: String, +} + +/// Requirements for the bundle to be included in the block. +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct Validity { + /// Specifies the minimum percent of a given bundle's earnings to redistribute + /// for it to be included in a builder's block. + #[serde(skip_serializing_if = "Option::is_none")] + pub refund: Option>, + /// Specifies what addresses should receive what percent of the overall refund for this bundle, + /// if it is enveloped by another bundle (eg. a searcher backrun). + #[serde(skip_serializing_if = "Option::is_none")] + pub refund_config: Option>, +} + +/// Preferences on what data should be shared about the bundle and its transactions +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct Privacy { + /// Hints on what data should be shared about the bundle and its transactions + #[serde(skip_serializing_if = "Option::is_none")] + pub hints: Option, + /// The addresses of the builders that should be allowed to see the bundle/transaction. + #[serde(skip_serializing_if = "Option::is_none")] + pub builders: Option>, +} + +/// Hints on what data should be shared about the bundle and its transactions +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct PrivacyHint { + /// The calldata of the bundle's transactions should be shared. + pub calldata: bool, + /// The address of the bundle's transactions should be shared. + pub contract_address: bool, + /// The logs of the bundle's transactions should be shared. + pub logs: bool, + /// The function selector of the bundle's transactions should be shared. + pub function_selector: bool, + /// The hash of the bundle's transactions should be shared. + pub hash: bool, + /// The hash of the bundle should be shared. + pub tx_hash: bool, +} + +impl PrivacyHint { + /// Sets the flag indicating inclusion of calldata and returns the modified `PrivacyHint` + /// instance. + pub const fn with_calldata(mut self) -> Self { + self.calldata = true; + self + } + + /// Sets the flag indicating inclusion of contract address and returns the modified + /// `PrivacyHint` instance. + pub const fn with_contract_address(mut self) -> Self { + self.contract_address = true; + self + } + + /// Sets the flag indicating inclusion of logs and returns the modified `PrivacyHint` instance. + pub const fn with_logs(mut self) -> Self { + self.logs = true; + self + } + + /// Sets the flag indicating inclusion of function selector and returns the modified + /// `PrivacyHint` instance. + pub const fn with_function_selector(mut self) -> Self { + self.function_selector = true; + self + } + + /// Sets the flag indicating inclusion of hash and returns the modified `PrivacyHint` instance. + pub const fn with_hash(mut self) -> Self { + self.hash = true; + self + } + + /// Sets the flag indicating inclusion of transaction hash and returns the modified + /// `PrivacyHint` instance. + pub const fn with_tx_hash(mut self) -> Self { + self.tx_hash = true; + self + } + + /// Checks if calldata inclusion flag is set. + pub const fn has_calldata(&self) -> bool { + self.calldata + } + + /// Checks if contract address inclusion flag is set. + pub const fn has_contract_address(&self) -> bool { + self.contract_address + } + + /// Checks if logs inclusion flag is set. + pub const fn has_logs(&self) -> bool { + self.logs + } + + /// Checks if function selector inclusion flag is set. + pub const fn has_function_selector(&self) -> bool { + self.function_selector + } + + /// Checks if hash inclusion flag is set. + pub const fn has_hash(&self) -> bool { + self.hash + } + + /// Checks if transaction hash inclusion flag is set. + pub const fn has_tx_hash(&self) -> bool { + self.tx_hash + } + + /// Calculates the number of hints set within the `PrivacyHint` instance. + const fn num_hints(&self) -> usize { + let mut num_hints = 0; + if self.calldata { + num_hints += 1; + } + if self.contract_address { + num_hints += 1; + } + if self.logs { + num_hints += 1; + } + if self.function_selector { + num_hints += 1; + } + if self.hash { + num_hints += 1; + } + if self.tx_hash { + num_hints += 1; + } + num_hints + } +} + +impl Serialize for PrivacyHint { + fn serialize(&self, serializer: S) -> Result { + let mut seq = serializer.serialize_seq(Some(self.num_hints()))?; + if self.calldata { + seq.serialize_element("calldata")?; + } + if self.contract_address { + seq.serialize_element("contract_address")?; + } + if self.logs { + seq.serialize_element("logs")?; + } + if self.function_selector { + seq.serialize_element("function_selector")?; + } + if self.hash { + seq.serialize_element("hash")?; + } + if self.tx_hash { + seq.serialize_element("tx_hash")?; + } + seq.end() + } +} + +impl<'de> Deserialize<'de> for PrivacyHint { + fn deserialize>(deserializer: D) -> Result { + let hints = Vec::::deserialize(deserializer)?; + let mut privacy_hint = Self::default(); + for hint in hints { + match hint.as_str() { + "calldata" => privacy_hint.calldata = true, + "contract_address" => privacy_hint.contract_address = true, + "logs" => privacy_hint.logs = true, + "function_selector" => privacy_hint.function_selector = true, + "hash" => privacy_hint.hash = true, + "tx_hash" => privacy_hint.tx_hash = true, + _ => return Err(serde::de::Error::custom("invalid privacy hint")), + } + } + Ok(privacy_hint) + } +} + +/// Specifies the minimum percent of a given bundle's earnings to redistribute +/// for it to be included in a builder's block. +/// Related endpoint: `mev_sendBundle`, `mev_simBundle`, `eth_sendPrivateTransaction`, +/// `eth_sendPrivateRawTransaction` +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct Refund { + /// The index of the transaction in the bundle. + #[serde(with = "alloy_serde::quantity")] + pub body_idx: u64, + /// The minimum percent of the bundle's earnings to redistribute. + #[serde(with = "alloy_serde::quantity")] + pub percent: u64, +} + +/// Specifies what addresses should receive what percent of the overall refund for this bundle, +/// if it is enveloped by another bundle (eg. a searcher backrun). +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct RefundConfig { + /// The address to refund. + pub address: Address, + /// The minimum percent of the bundle's earnings to redistribute. + #[serde(with = "alloy_serde::quantity")] + pub percent: u64, +} diff --git a/crates/rpc-types-mev/src/eth_calls.rs b/crates/rpc-types-mev/src/eth_calls.rs new file mode 100644 index 00000000000..cfdc3cb19b1 --- /dev/null +++ b/crates/rpc-types-mev/src/eth_calls.rs @@ -0,0 +1,221 @@ +use crate::{u256_numeric_string, Privacy, Validity}; + +use alloy_eips::BlockNumberOrTag; +use alloy_primitives::{Address, Bytes, B256, U256}; +use serde::{Deserialize, Serialize}; + +/// Bundle of transactions for `eth_callBundle` +/// +/// +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EthCallBundle { + /// A list of hex-encoded signed transactions + pub txs: Vec, + /// hex encoded block number for which this bundle is valid on + #[serde(with = "alloy_serde::quantity")] + pub block_number: u64, + /// Either a hex encoded number or a block tag for which state to base this simulation on + pub state_block_number: BlockNumberOrTag, + /// the timestamp to use for this bundle simulation, in seconds since the unix epoch + #[serde(default, with = "alloy_serde::quantity::opt", skip_serializing_if = "Option::is_none")] + pub timestamp: Option, +} + +/// Response for `eth_callBundle` +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct EthCallBundleResponse { + /// The hash of the bundle bodies. + pub bundle_hash: B256, + /// The gas price of the entire bundle + #[serde(with = "u256_numeric_string")] + pub bundle_gas_price: U256, + /// The difference in Ether sent to the coinbase after all transactions in the bundle + #[serde(with = "u256_numeric_string")] + pub coinbase_diff: U256, + /// The total amount of Ether sent to the coinbase after all transactions in the bundle + #[serde(with = "u256_numeric_string")] + pub eth_sent_to_coinbase: U256, + /// The total gas fees paid for all transactions in the bundle + #[serde(with = "u256_numeric_string")] + pub gas_fees: U256, + /// Results of individual transactions within the bundle + pub results: Vec, + /// The block number used as a base for this simulation + #[serde(with = "alloy_serde::quantity")] + pub state_block_number: u64, + /// The total gas used by all transactions in the bundle + #[serde(with = "alloy_serde::quantity")] + pub total_gas_used: u64, +} + +/// Result of a single transaction in a bundle for `eth_callBundle` +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EthCallBundleTransactionResult { + /// The difference in Ether sent to the coinbase after the transaction + #[serde(with = "u256_numeric_string")] + pub coinbase_diff: U256, + /// The amount of Ether sent to the coinbase after the transaction + #[serde(with = "u256_numeric_string")] + pub eth_sent_to_coinbase: U256, + /// The address from which the transaction originated + pub from_address: Address, + /// The gas fees paid for the transaction + #[serde(with = "u256_numeric_string")] + pub gas_fees: U256, + /// The gas price used for the transaction + #[serde(with = "u256_numeric_string")] + pub gas_price: U256, + /// The amount of gas used by the transaction + #[serde(with = "alloy_serde::quantity")] + pub gas_used: u64, + /// The address to which the transaction is sent (optional) + pub to_address: Option
, + /// The transaction hash + pub tx_hash: B256, + /// Contains the return data if the transaction succeeded + /// + /// Note: this is mutually exclusive with `revert` + #[serde(skip_serializing_if = "Option::is_none")] + pub value: Option, + /// Contains the return data if the transaction reverted + #[serde(skip_serializing_if = "Option::is_none")] + pub revert: Option, +} + +/// Request for `eth_cancelBundle` +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct CancelBundleRequest { + /// Bundle hash of the bundle to be canceled + pub bundle_hash: String, +} + +/// Request for `eth_cancelPrivateTransaction` +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct CancelPrivateTransactionRequest { + /// Transaction hash of the transaction to be canceled + pub tx_hash: B256, +} + +/// Bundle of transactions for `eth_sendBundle` +/// +/// Note: this is for `eth_sendBundle` and not `mev_sendBundle` +/// +/// +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct EthSendBundle { + /// A list of hex-encoded signed transactions + pub(crate) txs: Vec, + /// hex-encoded block number for which this bundle is valid + #[serde(with = "alloy_serde::quantity")] + pub(crate) block_number: u64, + /// unix timestamp when this bundle becomes active + #[serde(default, with = "alloy_serde::quantity::opt", skip_serializing_if = "Option::is_none")] + pub(crate) min_timestamp: Option, + /// unix timestamp how long this bundle stays valid + #[serde(default, with = "alloy_serde::quantity::opt", skip_serializing_if = "Option::is_none")] + pub(crate) max_timestamp: Option, + /// list of hashes of possibly reverting txs + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub(crate) reverting_tx_hashes: Vec, + /// UUID that can be used to cancel/replace this bundle + #[serde(default, rename = "replacementUuid", skip_serializing_if = "Option::is_none")] + pub(crate) replacement_uuid: Option, +} + +/// Response from the matchmaker after sending a bundle. +#[derive(Deserialize, Debug, Serialize, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct EthBundleHash { + /// Hash of the bundle bodies. + pub(crate) bundle_hash: B256, +} + +/// Response from the matchmaker after sending a bundle. +#[derive(Deserialize, Debug, Serialize, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct SendBundleResponse { + /// Hash of the bundle bodies. + pub bundle_hash: B256, +} + +/// Request for `eth_sendPrivateTransaction` +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct PrivateTransactionRequest { + /// raw signed transaction + pub tx: Bytes, + /// Hex-encoded number string, optional. Highest block number in which the transaction should + /// be included. + #[serde(default, with = "alloy_serde::quantity::opt", skip_serializing_if = "Option::is_none")] + pub max_block_number: Option, + /// Preferences for private transaction. + #[serde(default, skip_serializing_if = "PrivateTransactionPreferences::is_empty")] + pub preferences: PrivateTransactionPreferences, +} + +/// Additional preferences for `eth_sendPrivateTransaction` +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +pub struct PrivateTransactionPreferences { + /// Requirements for the bundle to be included in the block. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub validity: Option, + /// Preferences on what data should be shared about the bundle and its transactions + #[serde(default, skip_serializing_if = "Option::is_none")] + pub privacy: Option, +} + +impl PrivateTransactionPreferences { + /// Returns true if the preferences are empty. + pub const fn is_empty(&self) -> bool { + self.validity.is_none() && self.privacy.is_none() + } +} + +#[cfg(test)] +mod tests { + use super::EthCallBundleResponse; + + #[test] + fn can_deserialize_eth_call_resp() { + let s = r#"{ "bundleGasPrice": "476190476193", +"bundleHash": "0x73b1e258c7a42fd0230b2fd05529c5d4b6fcb66c227783f8bece8aeacdd1db2e", +"coinbaseDiff": "20000000000126000", +"ethSentToCoinbase": "20000000000000000", +"gasFees": "126000", +"results": [ + { + "coinbaseDiff": "10000000000063000", + "ethSentToCoinbase": "10000000000000000", + "fromAddress": "0x02A727155aeF8609c9f7F2179b2a1f560B39F5A0", + "gasFees": "63000", + "gasPrice": "476190476193", + "gasUsed": 21000, + "toAddress": "0x73625f59CAdc5009Cb458B751b3E7b6b48C06f2C", + "txHash": "0x669b4704a7d993a946cdd6e2f95233f308ce0c4649d2e04944e8299efcaa098a", + "value": "0x" + }, + { + "coinbaseDiff": "10000000000063000", + "ethSentToCoinbase": "10000000000000000", + "fromAddress": "0x02A727155aeF8609c9f7F2179b2a1f560B39F5A0", + "gasFees": "63000", + "gasPrice": "476190476193", + "gasUsed": 21000, + "toAddress": "0x73625f59CAdc5009Cb458B751b3E7b6b48C06f2C", + "txHash": "0xa839ee83465657cac01adc1d50d96c1b586ed498120a84a64749c0034b4f19fa", + "value": "0x" + } +], +"stateBlockNumber": 5221585, +"totalGasUsed": 42000 +}"#; + + let _call = serde_json::from_str::(s).unwrap(); + } +} diff --git a/crates/rpc-types-mev/src/lib.rs b/crates/rpc-types-mev/src/lib.rs new file mode 100644 index 00000000000..d2ae96a69a3 --- /dev/null +++ b/crates/rpc-types-mev/src/lib.rs @@ -0,0 +1,23 @@ +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/alloy.jpg", + html_favicon_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/favicon.ico" +)] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +mod eth_calls; +pub use eth_calls::*; + +mod mev_calls; +pub use mev_calls::*; + +// types for stats endpoint like flashbots_getUserStats and flashbots_getBundleStats +mod stats; +pub use stats::*; + +mod common; +pub use common::*; + +// serde helper to serialize/deserialize u256 as numeric string +mod u256_numeric_string; diff --git a/crates/rpc-types-mev/src/mev_calls.rs b/crates/rpc-types-mev/src/mev_calls.rs new file mode 100644 index 00000000000..f85ecfa294a --- /dev/null +++ b/crates/rpc-types-mev/src/mev_calls.rs @@ -0,0 +1,330 @@ +use crate::common::{Privacy, ProtocolVersion, Validity}; + +use alloy_eips::BlockId; +use alloy_primitives::{Address, Bytes, Log, TxHash}; +use serde::{Deserialize, Serialize}; + +/// A bundle of transactions to send to the matchmaker. +/// +/// Note: this is for `mev_sendBundle` and not `eth_sendBundle`. +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct SendBundleRequest { + /// The version of the MEV-share API to use. + #[serde(rename = "version")] + pub protocol_version: ProtocolVersion, + /// Data used by block builders to check if the bundle should be considered for inclusion. + #[serde(rename = "inclusion")] + pub inclusion: Inclusion, + /// The transactions to include in the bundle. + #[serde(rename = "body")] + pub bundle_body: Vec, + /// Requirements for the bundle to be included in the block. + #[serde(rename = "validity", skip_serializing_if = "Option::is_none")] + pub validity: Option, + /// Preferences on what data should be shared about the bundle and its transactions + #[serde(rename = "privacy", skip_serializing_if = "Option::is_none")] + pub privacy: Option, +} + +impl SendBundleRequest { + /// Create a new bundle request. + pub const fn new( + block_num: u64, + max_block: Option, + protocol_version: ProtocolVersion, + bundle_body: Vec, + ) -> Self { + Self { + protocol_version, + inclusion: Inclusion { block: block_num, max_block }, + bundle_body, + validity: None, + privacy: None, + } + } +} + +/// Data used by block builders to check if the bundle should be considered for inclusion. +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct Inclusion { + /// The first block the bundle is valid for. + #[serde(with = "alloy_serde::quantity")] + pub block: u64, + /// The last block the bundle is valid for. + #[serde(default, with = "alloy_serde::quantity::opt", skip_serializing_if = "Option::is_none")] + pub max_block: Option, +} + +impl Inclusion { + /// Creates a new inclusion with the given min block.. + pub const fn at_block(block: u64) -> Self { + Self { block, max_block: None } + } + + /// Returns the block number of the first block the bundle is valid for. + #[inline] + pub const fn block_number(&self) -> u64 { + self.block + } + + /// Returns the block number of the last block the bundle is valid for. + #[inline] + pub fn max_block_number(&self) -> Option { + self.max_block.as_ref().map(|b| *b) + } +} + +/// A bundle tx, which can either be a transaction hash, or a full tx. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(untagged)] +#[serde(rename_all = "camelCase")] +pub enum BundleItem { + /// The hash of either a transaction or bundle we are trying to backrun. + Hash { + /// Tx hash. + hash: TxHash, + }, + /// A new signed transaction. + #[serde(rename_all = "camelCase")] + Tx { + /// Bytes of the signed transaction. + tx: Bytes, + /// If true, the transaction can revert without the bundle being considered invalid. + can_revert: bool, + }, +} + +/// Optional fields to override simulation state. +#[derive(Deserialize, Debug, Serialize, Clone, Default, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct SimBundleOverrides { + /// Block used for simulation state. Defaults to latest block. + /// Block header data will be derived from parent block by default. + /// Specify other params to override the default values. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub parent_block: Option, + /// Block number used for simulation, defaults to parentBlock.number + 1 + #[serde(default, with = "alloy_serde::quantity::opt")] + pub block_number: Option, + /// Coinbase used for simulation, defaults to parentBlock.coinbase + #[serde(default, skip_serializing_if = "Option::is_none")] + pub coinbase: Option
, + /// Timestamp used for simulation, defaults to parentBlock.timestamp + 12 + #[serde(default, with = "alloy_serde::quantity::opt", skip_serializing_if = "Option::is_none")] + pub timestamp: Option, + /// Gas limit used for simulation, defaults to parentBlock.gasLimit + #[serde(default, with = "alloy_serde::quantity::opt", skip_serializing_if = "Option::is_none")] + pub gas_limit: Option, + /// Base fee used for simulation, defaults to parentBlock.baseFeePerGas + #[serde(default, with = "alloy_serde::quantity::opt", skip_serializing_if = "Option::is_none")] + pub base_fee: Option, + /// Timeout in seconds, defaults to 5 + #[serde(default, with = "alloy_serde::quantity::opt", skip_serializing_if = "Option::is_none")] + pub timeout: Option, +} + +/// Response from the matchmaker after sending a simulation request. +#[derive(Deserialize, Debug, Serialize, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct SimBundleResponse { + /// Whether the simulation was successful. + pub success: bool, + /// Error message if the simulation failed. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub error: Option, + /// The block number of the simulated block. + #[serde(with = "alloy_serde::quantity")] + pub state_block: u64, + /// The gas price of the simulated block. + #[serde(with = "alloy_serde::quantity")] + pub mev_gas_price: u64, + /// The profit of the simulated block. + #[serde(with = "alloy_serde::quantity")] + pub profit: u64, + /// The refundable value of the simulated block. + #[serde(with = "alloy_serde::quantity")] + pub refundable_value: u64, + /// The gas used by the simulated block. + #[serde(with = "alloy_serde::quantity")] + pub gas_used: u64, + /// Logs returned by `mev_simBundle`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub logs: Option>, +} + +/// Logs returned by `mev_simBundle`. +#[derive(Deserialize, Debug, Serialize, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct SimBundleLogs { + /// Logs for transactions in bundle. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tx_logs: Option>, + /// Logs for bundles in bundle. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub bundle_logs: Option>, +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use crate::{common::PrivacyHint, RefundConfig}; + use alloy_primitives::Bytes; + + use super::*; + + #[test] + fn can_deserialize_simple() { + let str = r#" + [{ + "version": "v0.1", + "inclusion": { + "block": "0x1" + }, + "body": [{ + "tx": "0x02f86b0180843b9aca00852ecc889a0082520894c87037874aed04e51c29f582394217a0a2b89d808080c080a0a463985c616dd8ee17d7ef9112af4e6e06a27b071525b42182fe7b0b5c8b4925a00af5ca177ffef2ff28449292505d41be578bebb77110dfc09361d2fb56998260", + "canRevert": false + }] + }] + "#; + let res: Result, _> = serde_json::from_str(str); + assert!(res.is_ok()); + } + + #[test] + fn can_deserialize_complex() { + let str = r#" + [{ + "version": "v0.1", + "inclusion": { + "block": "0x1" + }, + "body": [{ + "tx": "0x02f86b0180843b9aca00852ecc889a0082520894c87037874aed04e51c29f582394217a0a2b89d808080c080a0a463985c616dd8ee17d7ef9112af4e6e06a27b071525b42182fe7b0b5c8b4925a00af5ca177ffef2ff28449292505d41be578bebb77110dfc09361d2fb56998260", + "canRevert": false + }], + "privacy": { + "hints": [ + "calldata" + ] + }, + "validity": { + "refundConfig": [ + { + "address": "0x8EC1237b1E80A6adf191F40D4b7D095E21cdb18f", + "percent": 100 + } + ] + } + }] + "#; + let res: Result, _> = serde_json::from_str(str); + assert!(res.is_ok()); + } + + #[test] + fn can_serialize_complex() { + let str = r#" + [{ + "version": "v0.1", + "inclusion": { + "block": "0x1" + }, + "body": [{ + "tx": "0x02f86b0180843b9aca00852ecc889a0082520894c87037874aed04e51c29f582394217a0a2b89d808080c080a0a463985c616dd8ee17d7ef9112af4e6e06a27b071525b42182fe7b0b5c8b4925a00af5ca177ffef2ff28449292505d41be578bebb77110dfc09361d2fb56998260", + "canRevert": false + }], + "privacy": { + "hints": [ + "calldata" + ] + }, + "validity": { + "refundConfig": [ + { + "address": "0x8EC1237b1E80A6adf191F40D4b7D095E21cdb18f", + "percent": 100 + } + ] + } + }] + "#; + let bundle_body = vec![BundleItem::Tx { + tx: Bytes::from_str("0x02f86b0180843b9aca00852ecc889a0082520894c87037874aed04e51c29f582394217a0a2b89d808080c080a0a463985c616dd8ee17d7ef9112af4e6e06a27b071525b42182fe7b0b5c8b4925a00af5ca177ffef2ff28449292505d41be578bebb77110dfc09361d2fb56998260").unwrap(), + can_revert: false, + }]; + + let validity = Some(Validity { + refund_config: Some(vec![RefundConfig { + address: "0x8EC1237b1E80A6adf191F40D4b7D095E21cdb18f".parse().unwrap(), + percent: 100, + }]), + ..Default::default() + }); + + let privacy = Some(Privacy { + hints: Some(PrivacyHint { calldata: true, ..Default::default() }), + ..Default::default() + }); + + let bundle = SendBundleRequest { + protocol_version: ProtocolVersion::V0_1, + inclusion: Inclusion { block: 1, max_block: None }, + bundle_body, + validity, + privacy, + }; + let expected = serde_json::from_str::>(str).unwrap(); + assert_eq!(bundle, expected[0]); + } + + #[test] + fn can_serialize_privacy_hint() { + let hint = PrivacyHint { + calldata: true, + contract_address: true, + logs: true, + function_selector: true, + hash: true, + tx_hash: true, + }; + let expected = + r#"["calldata","contract_address","logs","function_selector","hash","tx_hash"]"#; + let actual = serde_json::to_string(&hint).unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn can_deserialize_privacy_hint() { + let hint = PrivacyHint { + calldata: true, + contract_address: false, + logs: true, + function_selector: false, + hash: true, + tx_hash: false, + }; + let expected = r#"["calldata","logs","hash"]"#; + let actual: PrivacyHint = serde_json::from_str(expected).unwrap(); + assert_eq!(actual, hint); + } + + #[test] + fn can_dererialize_sim_response() { + let expected = r#" + { + "success": true, + "stateBlock": "0x8b8da8", + "mevGasPrice": "0x74c7906005", + "profit": "0x4bc800904fc000", + "refundableValue": "0x4bc800904fc000", + "gasUsed": "0xa620", + "logs": [{},{}] + } + "#; + let actual: SimBundleResponse = serde_json::from_str(expected).unwrap(); + assert!(actual.success); + } +} diff --git a/crates/rpc-types-mev/src/stats.rs b/crates/rpc-types-mev/src/stats.rs new file mode 100644 index 00000000000..74529d46543 --- /dev/null +++ b/crates/rpc-types-mev/src/stats.rs @@ -0,0 +1,209 @@ +use crate::{u256_numeric_string, ConsideredByBuildersAt, SealedByBuildersAt}; + +use alloy_primitives::U256; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +// TODO(@optimiz-r): Revisit after is closed. +/// Response for `flashbots_getBundleStatsV2` represents stats for a single bundle +/// +/// Note: this is V2: +/// +/// Timestamp format: "2022-10-06T21:36:06.322Z" +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub enum BundleStats { + /// The relayer has not yet seen the bundle. + #[default] + Unknown, + /// The relayer has seen the bundle, but has not simulated it yet. + Seen(StatsSeen), + /// The relayer has seen the bundle and has simulated it. + Simulated(StatsSimulated), +} + +impl Serialize for BundleStats { + fn serialize(&self, serializer: S) -> Result { + match self { + Self::Unknown => serde_json::json!({"isSimulated": false}).serialize(serializer), + Self::Seen(stats) => stats.serialize(serializer), + Self::Simulated(stats) => stats.serialize(serializer), + } + } +} + +impl<'de> Deserialize<'de> for BundleStats { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let map = serde_json::Map::deserialize(deserializer)?; + + if map.get("receivedAt").is_none() { + Ok(Self::Unknown) + } else if map["isSimulated"] == false { + StatsSeen::deserialize(serde_json::Value::Object(map)) + .map(BundleStats::Seen) + .map_err(serde::de::Error::custom) + } else { + StatsSimulated::deserialize(serde_json::Value::Object(map)) + .map(BundleStats::Simulated) + .map_err(serde::de::Error::custom) + } + } +} + +/// Response for `flashbots_getBundleStatsV2` represents stats for a single bundle +/// +/// Note: this is V2: +/// +/// Timestamp format: "2022-10-06T21:36:06.322Z +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StatsSeen { + /// boolean representing if this searcher has a high enough reputation to be in the high + /// priority queue + pub is_high_priority: bool, + /// representing whether the bundle gets simulated. All other fields will be omitted except + /// simulated field if API didn't receive bundle + pub is_simulated: bool, + /// time at which the bundle API received the bundle + pub received_at: String, +} + +/// Response for `flashbots_getBundleStatsV2` represents stats for a single bundle +/// +/// Note: this is V2: +/// +/// Timestamp format: "2022-10-06T21:36:06.322Z +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StatsSimulated { + /// boolean representing if this searcher has a high enough reputation to be in the high + /// priority queue + pub is_high_priority: bool, + /// representing whether the bundle gets simulated. All other fields will be omitted except + /// simulated field if API didn't receive bundle + pub is_simulated: bool, + /// time at which the bundle gets simulated + pub simulated_at: String, + /// time at which the bundle API received the bundle + pub received_at: String, + /// indicates time at which each builder selected the bundle to be included in the target + /// block + #[serde(default = "Vec::new")] + pub considered_by_builders_at: Vec, + /// indicates time at which each builder sealed a block containing the bundle + #[serde(default = "Vec::new")] + pub sealed_by_builders_at: Vec, +} + +/// Response for `flashbots_getUserStatsV2` represents stats for a searcher. +/// +/// Note: this is V2: +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UserStats { + /// Represents whether this searcher has a high enough reputation to be in the high priority + /// queue. + pub is_high_priority: bool, + /// The total amount paid to validators over all time. + #[serde(with = "u256_numeric_string")] + pub all_time_validator_payments: U256, + /// The total amount of gas simulated across all bundles submitted to Flashbots. + /// This is the actual gas used in simulations, not gas limit. + #[serde(with = "u256_numeric_string")] + pub all_time_gas_simulated: U256, + /// The total amount paid to validators the last 7 days. + #[serde(with = "u256_numeric_string")] + pub last_7d_validator_payments: U256, + /// The total amount of gas simulated across all bundles submitted to Flashbots in the last 7 + /// days. This is the actual gas used in simulations, not gas limit. + #[serde(with = "u256_numeric_string")] + pub last_7d_gas_simulated: U256, + /// The total amount paid to validators the last day. + #[serde(with = "u256_numeric_string")] + pub last_1d_validator_payments: U256, + /// The total amount of gas simulated across all bundles submitted to Flashbots in the last + /// day. This is the actual gas used in simulations, not gas limit. + #[serde(with = "u256_numeric_string")] + pub last_1d_gas_simulated: U256, +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::SealedByBuildersAt; + + #[test] + fn can_serialize_deserialize_bundle_stats() { + let fixtures = [ + ( + r#"{ + "isSimulated": false + }"#, + BundleStats::Unknown, + ), + ( + r#"{ + "isHighPriority": false, + "isSimulated": false, + "receivedAt": "476190476193" + }"#, + BundleStats::Seen(StatsSeen { + is_high_priority: false, + is_simulated: false, + received_at: "476190476193".to_string(), + }), + ), + ( + r#"{ + "isHighPriority": true, + "isSimulated": true, + "simulatedAt": "111", + "receivedAt": "222", + "consideredByBuildersAt":[], + "sealedByBuildersAt": [ + { + "pubkey": "333", + "timestamp": "444" + }, + { + "pubkey": "555", + "timestamp": "666" + } + ] + }"#, + BundleStats::Simulated(StatsSimulated { + is_high_priority: true, + is_simulated: true, + simulated_at: String::from("111"), + received_at: String::from("222"), + considered_by_builders_at: vec![], + sealed_by_builders_at: vec![ + SealedByBuildersAt { + pubkey: String::from("333"), + timestamp: String::from("444"), + }, + SealedByBuildersAt { + pubkey: String::from("555"), + timestamp: String::from("666"), + }, + ], + }), + ), + ]; + + let strip_whitespaces = + |input: &str| input.chars().filter(|&c| !c.is_whitespace()).collect::(); + + for (serialized, deserialized) in fixtures { + // Check de-serialization + let deserialized_expected = serde_json::from_str::(serialized).unwrap(); + assert_eq!(deserialized, deserialized_expected); + + // Check serialization + let serialized_expected = &serde_json::to_string(&deserialized).unwrap(); + assert_eq!(strip_whitespaces(serialized), strip_whitespaces(serialized_expected)); + } + } +} diff --git a/crates/rpc-types-mev/src/u256_numeric_string.rs b/crates/rpc-types-mev/src/u256_numeric_string.rs new file mode 100644 index 00000000000..cb3b1fc6066 --- /dev/null +++ b/crates/rpc-types-mev/src/u256_numeric_string.rs @@ -0,0 +1,30 @@ +use alloy_primitives::U256; +use serde::{de, Deserialize, Serializer}; +use std::str::FromStr; + +pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de>, +{ + let val = serde_json::Value::deserialize(deserializer)?; + match val { + serde_json::Value::String(s) => { + if let Ok(val) = s.parse::() { + return Ok(U256::from(val)); + } + U256::from_str(&s).map_err(de::Error::custom) + } + serde_json::Value::Number(num) => { + num.as_u64().map(U256::from).ok_or_else(|| de::Error::custom("invalid u256")) + } + _ => Err(de::Error::custom("invalid u256")), + } +} + +pub(crate) fn serialize(val: &U256, serializer: S) -> Result +where + S: Serializer, +{ + let val: u128 = (*val).try_into().map_err(serde::ser::Error::custom)?; + serializer.serialize_str(&val.to_string()) +} diff --git a/crates/rpc-types/Cargo.toml b/crates/rpc-types/Cargo.toml index 93b30c8b37c..fedb641cde3 100644 --- a/crates/rpc-types/Cargo.toml +++ b/crates/rpc-types/Cargo.toml @@ -25,6 +25,7 @@ alloy-rpc-types-anvil = { workspace = true, optional = true } alloy-rpc-types-beacon = { workspace = true, optional = true } alloy-rpc-types-engine = { workspace = true, optional = true } alloy-rpc-types-eth = { workspace = true, optional = true } +alloy-rpc-types-mev = { workspace = true, optional = true } alloy-rpc-types-trace = { workspace = true, optional = true } alloy-rpc-types-txpool = { workspace = true, optional = true } @@ -35,6 +36,7 @@ anvil = ["dep:alloy-rpc-types-anvil"] beacon = ["dep:alloy-rpc-types-beacon"] engine = ["dep:alloy-rpc-types-engine"] eth = ["dep:alloy-rpc-types-eth"] +mev = ["dep:alloy-rpc-types-mev"] trace = ["dep:alloy-rpc-types-trace"] txpool = ["dep:alloy-rpc-types-txpool"] diff --git a/crates/rpc-types/src/lib.rs b/crates/rpc-types/src/lib.rs index 00891214f24..003a3797db7 100644 --- a/crates/rpc-types/src/lib.rs +++ b/crates/rpc-types/src/lib.rs @@ -25,6 +25,9 @@ pub use alloy_rpc_types_eth as eth; #[cfg(feature = "eth")] pub use eth::*; +#[cfg(feature = "mev")] +pub use alloy_rpc_types_mev as mev; + #[cfg(feature = "trace")] pub use alloy_rpc_types_trace as trace;