From 9ffc1433d998ba63d055c632a97bc1960c84ad78 Mon Sep 17 00:00:00 2001 From: Thomas Coratger <60488569+tcoratger@users.noreply.github.com> Date: Mon, 16 Oct 2023 13:47:34 +0200 Subject: [PATCH] feat(rpc): move mev types to `rpc-types` (#5035) Co-authored-by: Matthias Seitz --- crates/primitives/src/serde_helper/mod.rs | 1 - crates/rpc/rpc-types/src/lib.rs | 2 + crates/rpc/rpc-types/src/mev.rs | 951 ++++++++++++++++++++++ 3 files changed, 953 insertions(+), 1 deletion(-) create mode 100644 crates/rpc/rpc-types/src/mev.rs diff --git a/crates/primitives/src/serde_helper/mod.rs b/crates/primitives/src/serde_helper/mod.rs index e561ef8bdc..4792c2e984 100644 --- a/crates/primitives/src/serde_helper/mod.rs +++ b/crates/primitives/src/serde_helper/mod.rs @@ -4,7 +4,6 @@ use crate::{B256, U64}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; mod storage; - pub use storage::*; mod jsonu256; diff --git a/crates/rpc/rpc-types/src/lib.rs b/crates/rpc/rpc-types/src/lib.rs index 988a38a5fc..5e59041b0f 100644 --- a/crates/rpc/rpc-types/src/lib.rs +++ b/crates/rpc/rpc-types/src/lib.rs @@ -13,10 +13,12 @@ mod admin; mod eth; +mod mev; mod otterscan; mod rpc; pub use admin::*; pub use eth::*; +pub use mev::*; pub use otterscan::*; pub use rpc::*; diff --git a/crates/rpc/rpc-types/src/mev.rs b/crates/rpc/rpc-types/src/mev.rs new file mode 100644 index 0000000000..87e3ab204d --- /dev/null +++ b/crates/rpc/rpc-types/src/mev.rs @@ -0,0 +1,951 @@ +//! MEV-share bundle type bindings +#![allow(missing_docs)] +use reth_primitives::{Address, BlockId, BlockNumber, Bytes, Log, TxHash, B256, U256, U64}; +use serde::{ + ser::{SerializeSeq, Serializer}, + Deserialize, Deserializer, 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, +} + +/// 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. + pub block: U64, + /// The last block the bundle is valid for. + #[serde(skip_serializing_if = "Option::is_none")] + pub max_block: Option, +} + +impl Inclusion { + /// Creates a new inclusion with the given min block.. + pub fn at_block(block: u64) -> Self { + Self { block: U64::from(block), max_block: None } + } + + /// Returns the block number of the first block the bundle is valid for. + #[inline] + pub fn block_number(&self) -> u64 { + self.block.to() + } + + /// 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.to()) + } +} + +/// 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, + }, +} + +/// 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>, +} + +/// Specifies the minimum percent of a given bundle's earnings to redistribute +/// for it to be included in a builder's block. +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct Refund { + /// The index of the transaction in the bundle. + pub body_idx: u64, + /// The minimum percent of the bundle's earnings to redistribute. + 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. + pub percent: u64, +} + +/// 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 { + pub fn with_calldata(mut self) -> Self { + self.calldata = true; + self + } + + pub fn with_contract_address(mut self) -> Self { + self.contract_address = true; + self + } + + pub fn with_logs(mut self) -> Self { + self.logs = true; + self + } + + pub fn with_function_selector(mut self) -> Self { + self.function_selector = true; + self + } + + pub fn with_hash(mut self) -> Self { + self.hash = true; + self + } + + pub fn with_tx_hash(mut self) -> Self { + self.tx_hash = true; + self + } + + pub fn has_calldata(&self) -> bool { + self.calldata + } + + pub fn has_contract_address(&self) -> bool { + self.contract_address + } + + pub fn has_logs(&self) -> bool { + self.logs + } + + pub fn has_function_selector(&self) -> bool { + self.function_selector + } + + pub fn has_hash(&self) -> bool { + self.hash + } + + pub fn has_tx_hash(&self) -> bool { + self.tx_hash + } + + 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 = PrivacyHint::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) + } +} + +/// 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, +} + +/// 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, +} + +/// 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(skip_serializing_if = "Option::is_none")] + pub parent_block: Option, + /// Block number used for simulation, defaults to parentBlock.number + 1 + #[serde(skip_serializing_if = "Option::is_none")] + pub block_number: Option, + /// Coinbase used for simulation, defaults to parentBlock.coinbase + #[serde(skip_serializing_if = "Option::is_none")] + pub coinbase: Option
, + /// Timestamp used for simulation, defaults to parentBlock.timestamp + 12 + #[serde(skip_serializing_if = "Option::is_none")] + pub timestamp: Option, + /// Gas limit used for simulation, defaults to parentBlock.gasLimit + #[serde(skip_serializing_if = "Option::is_none")] + pub gas_limit: Option, + /// Base fee used for simulation, defaults to parentBlock.baseFeePerGas + #[serde(skip_serializing_if = "Option::is_none")] + pub base_fee: Option, + /// Timeout in seconds, defaults to 5 + #[serde(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(skip_serializing_if = "Option::is_none")] + pub error: Option, + /// The block number of the simulated block. + pub state_block: U64, + /// The gas price of the simulated block. + pub mev_gas_price: U64, + /// The profit of the simulated block. + pub profit: U64, + /// The refundable value of the simulated block. + pub refundable_value: U64, + /// The gas used by the simulated block. + pub gas_used: U64, + /// Logs returned by mev_simBundle. + #[serde(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(skip_serializing_if = "Option::is_none")] + pub tx_logs: Option>, + /// Logs for bundles in bundle. + #[serde(skip_serializing_if = "Option::is_none")] + pub bundle_logs: Option>, +} + +impl SendBundleRequest { + /// Create a new bundle request. + pub 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, + } + } +} + +/// 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_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(skip_serializing_if = "Option::is_none")] + pub max_block_number: Option, + #[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(skip_serializing_if = "Option::is_none")] + pub validity: Option, + /// Preferences on what data should be shared about the bundle and its transactions + #[serde(skip_serializing_if = "Option::is_none")] + pub privacy: Option, +} + +impl PrivateTransactionPreferences { + /// Returns true if the preferences are empty. + pub fn is_empty(&self) -> bool { + self.validity.is_none() && self.privacy.is_none() + } +} + +/// 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, +} + +// 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 { + BundleStats::Unknown => serde_json::json!({"isSimulated": false}).serialize(serializer), + BundleStats::Seen(stats) => stats.serialize(serializer), + BundleStats::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(BundleStats::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, +} + +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConsideredByBuildersAt { + pub pubkey: String, + pub timestamp: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SealedByBuildersAt { + pub pubkey: String, + pub timestamp: String, +} + +/// 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, +} + +/// 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 struct EthSendBundle { + /// A list of hex-encoded signed transactions + pub txs: Vec, + /// hex-encoded block number for which this bundle is valid + pub block_number: U64, + /// unix timestamp when this bundle becomes active + #[serde(skip_serializing_if = "Option::is_none")] + pub min_timestamp: Option, + /// unix timestamp how long this bundle stays valid + #[serde(skip_serializing_if = "Option::is_none")] + pub max_timestamp: Option, + /// list of hashes of possibly reverting txs + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub reverting_tx_hashes: Vec, + /// UUID that can be used to cancel/replace this bundle + #[serde(rename = "replacementUuid", skip_serializing_if = "Option::is_none")] + pub replacement_uuid: Option, +} + +/// Response from the matchmaker after sending a bundle. +#[derive(Deserialize, Debug, Serialize, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct EthBundleHash { + /// Hash of the bundle bodies. + pub bundle_hash: B256, +} + +/// 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 + 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: BlockNumber, + /// the timestamp to use for this bundle simulation, in seconds since the unix epoch + #[serde(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 { + #[serde(with = "u256_numeric_string")] + pub bundle_gas_price: U256, + pub bundle_hash: String, + #[serde(with = "u256_numeric_string")] + pub coinbase_diff: U256, + #[serde(with = "u256_numeric_string")] + pub eth_sent_to_coinbase: U256, + #[serde(with = "u256_numeric_string")] + pub gas_fees: U256, + pub results: Vec, + pub state_block_number: u64, + 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 { + #[serde(with = "u256_numeric_string")] + pub coinbase_diff: U256, + #[serde(with = "u256_numeric_string")] + pub eth_sent_to_coinbase: U256, + pub from_address: Address, + #[serde(with = "u256_numeric_string")] + pub gas_fees: U256, + #[serde(with = "u256_numeric_string")] + pub gas_price: U256, + pub gas_used: u64, + pub to_address: Address, + pub tx_hash: B256, + pub value: Bytes, +} + +mod u256_numeric_string { + use reth_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()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use reth_primitives::Bytes; + use std::str::FromStr; + + #[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: U64::from(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); + } + + #[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(); + } + + #[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)); + } + } +}