diff --git a/Cargo.lock b/Cargo.lock index c40ff4945..5c08ff6e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,16 +81,15 @@ dependencies = [ [[package]] name = "anstream" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e579a7752471abc2a8268df8b20005e3eadd975f585398f17efcfd8d4927371" +checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", - "is-terminal", "utf8parse", ] @@ -120,9 +119,9 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "1.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcd8291a340dd8ac70e18878bc4501dd7b4ff970cfa21c207d36ece51ea88fd" +checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" dependencies = [ "anstyle", "windows-sys 0.48.0", @@ -711,25 +710,23 @@ dependencies = [ [[package]] name = "clap" -version = "4.2.4" +version = "4.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "956ac1f6381d8d82ab4684768f89c0ea3afe66925ceadb4eeb3fc452ffc55d62" +checksum = "b1d7b8d5ec32af0fadc644bf1fd509a688c2103b185644bb1e29d164e0703136" dependencies = [ "clap_builder", - "clap_derive 4.2.0", - "once_cell", + "clap_derive 4.4.2", ] [[package]] name = "clap_builder" -version = "4.2.4" +version = "4.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84080e799e54cff944f4b4a4b0e71630b0e0443b25b985175c7dddc1a859b749" +checksum = "5179bb514e4d7c2051749d8fcefa2ed6d06a9f4e6d69faf3805f5d80b8cf8d56" dependencies = [ "anstream", "anstyle", - "bitflags", - "clap_lex 0.4.1", + "clap_lex 0.5.1", "strsim", ] @@ -748,9 +745,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.2.0" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9644cd56d6b87dbe899ef8b053e331c0637664e9e21a33dfcdc36093f5c5c4" +checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" dependencies = [ "heck", "proc-macro2", @@ -769,9 +766,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.4.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1" +checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" [[package]] name = "codespan-reporting" @@ -3938,7 +3935,7 @@ name = "rundler" version = "0.1.0-beta" dependencies = [ "anyhow", - "clap 4.2.4", + "clap 4.4.4", "dotenv", "ethers", "itertools 0.11.0", @@ -4144,7 +4141,7 @@ name = "rundler-tools" version = "0.1.0-beta" dependencies = [ "anyhow", - "clap 4.2.4", + "clap 4.4.4", "dotenv", "ethers", "ethers-signers", diff --git a/bin/rundler/Cargo.toml b/bin/rundler/Cargo.toml index a6ef959fb..999612d6f 100644 --- a/bin/rundler/Cargo.toml +++ b/bin/rundler/Cargo.toml @@ -21,7 +21,7 @@ rundler-utils = { path = "../../crates/utils" } # CLI dependencies anyhow.workspace = true -clap = { version = "4.2.4", features = ["derive", "env"] } +clap = { version = "4.4.4", features = ["derive", "env"] } dotenv = "0.15.0" ethers.workspace = true itertools = "0.11.0" diff --git a/bin/rundler/src/cli/builder.rs b/bin/rundler/src/cli/builder.rs index 6fee6b7f1..e924ac411 100644 --- a/bin/rundler/src/cli/builder.rs +++ b/bin/rundler/src/cli/builder.rs @@ -18,6 +18,7 @@ use clap::Args; use ethers::types::H256; use rundler_builder::{ self, BuilderEvent, BuilderEventKind, BuilderTask, BuilderTaskArgs, LocalBuilderBuilder, + TransactionSenderType, }; use rundler_pool::RemotePoolClient; use rundler_sim::{MempoolConfig, PriorityFeeMode}; @@ -107,17 +108,17 @@ pub struct BuilderArgs { )] pub submit_url: Option, - /// If true, will use the provider's `eth_sendRawTransactionConditional` - /// method instead `eth_sendRawTransaction`, passing in expected storage - /// values determined through simulation. Must not be set on networks - /// which do not support this method. + /// Choice of what sender type to to use for transaction submission. + /// Defaults to the value of `raw`. Other options inclue `flashbots`, + /// `conditional` and `polygon_bloxroute` #[arg( - long = "builder.use_conditional_send_transaction", - name = "builder.use_conditional_send_transaction", - env = "BUILDER_USE_CONDITIONAL_SEND_TRANSACTION", - default_value = "false" + long = "builder.sender", + name = "builder.sender", + env = "BUILDER_SENDER", + value_enum, + default_value = "raw" )] - use_conditional_send_transaction: bool, + pub sender_type: TransactionSenderType, /// After submitting a bundle transaction, the maximum number of blocks to /// wait for that transaction to mine before we try resending with higher @@ -218,7 +219,7 @@ impl BuilderArgs { use_bundle_priority_fee: common.use_bundle_priority_fee, bundle_priority_fee_overhead_percent: common.bundle_priority_fee_overhead_percent, priority_fee_mode, - use_conditional_send_transaction: self.use_conditional_send_transaction, + sender_type: self.sender_type, eth_poll_interval: Duration::from_millis(common.eth_poll_interval_millis), sim_settings: common.try_into()?, mempool_configs, diff --git a/crates/builder/src/lib.rs b/crates/builder/src/lib.rs index 53de6181d..8e7d9ccb4 100644 --- a/crates/builder/src/lib.rs +++ b/crates/builder/src/lib.rs @@ -26,6 +26,7 @@ mod emit; pub use emit::{BuilderEvent, BuilderEventKind}; mod sender; +pub use sender::TransactionSenderType; mod server; pub use server::{ diff --git a/crates/builder/src/sender/flashbots.rs b/crates/builder/src/sender/flashbots.rs index f08e7bdf9..3d12b6ed1 100644 --- a/crates/builder/src/sender/flashbots.rs +++ b/crates/builder/src/sender/flashbots.rs @@ -21,22 +21,28 @@ use std::{ task::{Context as TaskContext, Poll}, }; -use anyhow::{bail, Context}; +use anyhow::{bail, Context, Result}; use ethers::{ middleware::SignerMiddleware, providers::{interval, JsonRpcClient, Middleware, Provider}, - types::{transaction::eip2718::TypedTransaction, Address, TransactionReceipt, H256, U256, U64}, + types::{ + transaction::eip2718::TypedTransaction, Address, Bytes, TransactionReceipt, TxHash, H256, + U256, U64, + }, }; use ethers_signers::Signer; use futures_timer::Delay; use futures_util::{Stream, StreamExt, TryFutureExt}; +use jsonrpsee::{ + core::{client::ClientT, traits::ToRpcParams}, + http_client::{transport::HttpBackend, HttpClient, HttpClientBuilder}, +}; use pin_project::pin_project; -use rundler_sim::ExpectedStorage; -use serde::{de, Deserialize}; -use serde_json::Value; +use serde::{de, Deserialize, Serialize}; +use serde_json::{value::RawValue, Value}; use tonic::async_trait; -use crate::sender::{fill_and_sign, SentTxInfo, TransactionSender, TxStatus}; +use super::{fill_and_sign, ExpectedStorage, SentTxInfo, TransactionSender, TxStatus}; #[derive(Debug)] pub(crate) struct FlashbotsTransactionSender @@ -62,9 +68,8 @@ where let (raw_tx, nonce) = fill_and_sign(&self.provider, tx).await?; let tx_hash = self - .provider - .provider() - .request("eth_sendRawTransaction", (raw_tx,)) + .client + .send_transaction(raw_tx) .await .context("should send raw transaction to node")?; @@ -117,11 +122,11 @@ where C: JsonRpcClient + 'static, S: Signer + 'static, { - pub(crate) fn new(provider: Arc>, signer: S) -> Self { - Self { + pub(crate) fn new(provider: Arc>, signer: S) -> Result { + Ok(Self { provider: SignerMiddleware::new(provider, signer), - client: FlashbotsClient::default(), - } + client: FlashbotsClient::new()?, + }) } } @@ -167,17 +172,52 @@ struct FlashbotsAPIResponse { seen_in_mempool: bool, } -#[derive(Debug, Default)] -struct FlashbotsClient {} +#[derive(Debug)] +struct FlashbotsClient { + client: HttpClient, +} impl FlashbotsClient { - async fn status(&self, tx_hash: H256) -> anyhow::Result { + fn new() -> Result { + let client = HttpClientBuilder::default().build("https://rpc.flashbots.net")?; + Ok(Self { client }) + } + + async fn status(&self, tx_hash: H256) -> Result { let url = format!("https://protect.flashbots.net/tx/{:?}", tx_hash); let resp = reqwest::get(&url).await?; resp.json::() .await .context("should deserialize FlashbotsAPIResponse") } + + async fn send_transaction(&self, raw_tx: Bytes) -> anyhow::Result { + let response: FlashbotsResponse = self + .client + .request("eth_sendRawTransaction", (raw_tx,)) + .await?; + Ok(response.tx_hash) + } +} + +#[derive(Serialize)] +struct FlashbotsRequest { + transaction: String, +} + +impl ToRpcParams for FlashbotsRequest { + fn to_rpc_params(self) -> Result>, jsonrpsee::core::Error> { + let s = String::from_utf8(serde_json::to_vec(&self)?).expect("Valid UTF8 format"); + RawValue::from_string(s) + .map(Some) + .map_err(jsonrpsee::core::Error::ParseError) + } +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct FlashbotsResponse { + tx_hash: TxHash, } type PinBoxFut<'a, T> = Pin> + Send + 'a>>; diff --git a/crates/builder/src/sender/mod.rs b/crates/builder/src/sender/mod.rs index 9d3c3c5cd..d9587597b 100644 --- a/crates/builder/src/sender/mod.rs +++ b/crates/builder/src/sender/mod.rs @@ -15,10 +15,9 @@ mod bloxroute; mod conditional; mod flashbots; mod raw; +use std::{str::FromStr, sync::Arc, time::Duration}; -use std::{sync::Arc, time::Duration}; - -use anyhow::Context; +use anyhow::{bail, Context, Error}; use async_trait::async_trait; pub(crate) use bloxroute::PolygonBloxrouteTransactionSender; pub(crate) use conditional::ConditionalTransactionSender; @@ -37,6 +36,7 @@ pub(crate) use flashbots::FlashbotsTransactionSender; use mockall::automock; pub(crate) use raw::RawTransactionSender; use rundler_sim::ExpectedStorage; +use serde::Serialize; #[derive(Debug)] pub(crate) struct SentTxInfo { @@ -80,6 +80,109 @@ where PolygonBloxroute(PolygonBloxrouteTransactionSender), } +/// Transaction sender types +#[derive(Debug, Clone, Copy, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum TransactionSenderType { + /// Raw transaction sender + Raw, + /// Conditional transaction sender + Conditional, + /// Flashbots transaction sender + /// + /// Currently only supported on Eth mainnet + Flashbots, + /// Bloxroute transaction sender + /// + /// Currently only supported on Polygon mainnet + PolygonBloxroute, +} + +impl FromStr for TransactionSenderType { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s.to_ascii_lowercase().as_str() { + "raw" => Ok(TransactionSenderType::Raw), + "conditional" => Ok(TransactionSenderType::Conditional), + "flashbots" => Ok(TransactionSenderType::Flashbots), + "polygon_bloxroute" => Ok(TransactionSenderType::PolygonBloxroute), + _ => bail!("Invalid sender input. Must be one of either 'raw', 'conditional', 'flashbots' or 'polygon_bloxroute'"), + } + } +} + +impl TransactionSenderType { + fn into_snake_case(self) -> String { + match self { + TransactionSenderType::Raw => "raw", + TransactionSenderType::Conditional => "conditional", + TransactionSenderType::Flashbots => "flashbots", + TransactionSenderType::PolygonBloxroute => "polygon_bloxroute", + } + .to_string() + } + + pub(crate) fn into_sender( + self, + client: Arc>, + signer: S, + chain_id: u64, + eth_poll_interval: Duration, + bloxroute_header: &Option, + ) -> Result, SenderConstructorErrors> { + let sender = match self { + Self::Raw => TransactionSenderEnum::Raw(RawTransactionSender::new(client, signer)), + Self::Conditional => TransactionSenderEnum::Conditional( + ConditionalTransactionSender::new(client, signer), + ), + Self::Flashbots => { + if chain_id != Chain::Mainnet as u64 { + return Err(SenderConstructorErrors::InvalidChainForSender( + chain_id, + self.into_snake_case(), + )); + } + TransactionSenderEnum::Flashbots(FlashbotsTransactionSender::new(client, signer)?) + } + Self::PolygonBloxroute => { + if let Some(header) = bloxroute_header { + if chain_id == Chain::Polygon as u64 { + return Err(SenderConstructorErrors::InvalidChainForSender( + chain_id, + self.into_snake_case(), + )); + } + + TransactionSenderEnum::PolygonBloxroute(PolygonBloxrouteTransactionSender::new( + client, + signer, + eth_poll_interval, + header, + )?) + } else { + return Err(SenderConstructorErrors::BloxRouteMissingToken); + } + } + }; + Ok(sender) + } +} + +/// Custom errors for the sender constructor +#[derive(Debug, thiserror::Error)] +pub(crate) enum SenderConstructorErrors { + /// Anyhow error fallback + #[error(transparent)] + Internal(#[from] anyhow::Error), + /// Invalid Chain ID error for sender + #[error("Chain ID: {0} cannot be used with the {1} sender")] + InvalidChainForSender(u64, String), + /// Bloxroute missing token error + #[error("Missing token for Bloxroute API")] + BloxRouteMissingToken, +} + async fn fill_and_sign( provider: &SignerMiddleware>, S>, mut tx: TypedTransaction, @@ -102,33 +205,3 @@ where .context("should sign transaction before sending")?; Ok((tx.rlp_signed(&signature), nonce)) } - -pub(crate) fn get_sender( - provider: Arc>, - signer: S, - is_conditional: bool, - url: &str, - chain_id: u64, - poll_interval: Duration, - bloxroute_auth_header: &Option, -) -> anyhow::Result> -where - C: JsonRpcClient + 'static, - S: Signer + 'static, -{ - let sender = if is_conditional { - ConditionalTransactionSender::new(provider, signer).into() - } else if url.contains("flashbots") { - FlashbotsTransactionSender::new(provider, signer).into() - } else if let Some(auth_header) = bloxroute_auth_header { - assert!( - chain_id == Chain::Polygon as u64, - "Bloxroute sender is only supported on Polygon mainnet" - ); - PolygonBloxrouteTransactionSender::new(provider, signer, poll_interval, auth_header)?.into() - } else { - RawTransactionSender::new(provider, signer).into() - }; - - Ok(sender) -} diff --git a/crates/builder/src/task.rs b/crates/builder/src/task.rs index efc58db54..5fe468afa 100644 --- a/crates/builder/src/task.rs +++ b/crates/builder/src/task.rs @@ -47,7 +47,7 @@ use crate::{ bundle_proposer::{self, BundleProposerImpl}, bundle_sender::{self, BundleSender, BundleSenderImpl, SendBundleRequest}, emit::BuilderEvent, - sender::get_sender, + sender::TransactionSenderType, server::{spawn_remote_builder_server, LocalBuilderBuilder}, signer::{BundlerSigner, KmsSigner, LocalSigner}, transaction_tracker::{self, TransactionTrackerImpl}, @@ -87,8 +87,8 @@ pub struct Args { pub bundle_priority_fee_overhead_percent: u64, /// Priority fee mode to use for operation priority fee minimums pub priority_fee_mode: PriorityFeeMode, - /// Whether to use conditional send transactions - pub use_conditional_send_transaction: bool, + /// Sender to be used by the builder + pub sender_type: TransactionSenderType, /// RPC node poll interval pub eth_poll_interval: Duration, /// Operation simulation settings @@ -130,7 +130,7 @@ where P: PoolServer + Clone, { async fn run(mut self: Box, shutdown_token: CancellationToken) -> anyhow::Result<()> { - tracing::info!("Mempool config: {:?}", self.args.mempool_configs); + info!("Mempool config: {:?}", self.args.mempool_configs); let provider = eth::new_provider(&self.args.rpc_url, self.args.eth_poll_interval)?; let manual_bundling_mode = Arc::new(AtomicBool::new(false)); @@ -184,7 +184,7 @@ where handle::flatten_handle(remote_handle), ) { Ok(_) => { - tracing::info!("Builder server shutdown"); + info!("Builder server shutdown"); Ok(()) } Err(e) => { @@ -284,20 +284,21 @@ where let submit_provider = eth::new_provider(&self.args.submit_url, self.args.eth_poll_interval)?; - let transaction_sender = get_sender( + + let transaction_sender = self.args.sender_type.into_sender( submit_provider, signer, - self.args.use_conditional_send_transaction, - &self.args.submit_url, self.args.chain_id, self.args.eth_poll_interval, &self.args.bloxroute_auth_header, )?; + let tracker_settings = transaction_tracker::Settings { poll_interval: self.args.eth_poll_interval, max_blocks_to_wait_for_mine: self.args.max_blocks_to_wait_for_mine, replacement_fee_percent_increase: self.args.replacement_fee_percent_increase, }; + let transaction_tracker = TransactionTrackerImpl::new( Arc::clone(&provider), transaction_sender, diff --git a/docs/cli.md b/docs/cli.md index 98baf885c..fa3596684 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -151,8 +151,8 @@ List of command line options for configuring the Builder. - env: *BUILDER_MAX_BUNDLE_SIZE* - `--builder.submit_url`: If present, the URL of the ETH provider that will be used to send transactions. Defaults to the value of `node_http`. - env: *BUILDER_SUBMIT_URL* -- `--builder.use_conditional_send_transaction`: If true, will use the provider's `eth_sendRawTransactionConditional` method instead of `eth_sendRawTransaction`, passing in expected storage values determined through simulation. Must not be set on networks which do not support this method (default: `false`) - - env: *BUILDER_USE_CONDITIONAL_SEND_TRANSACTION* +- `--builder.sender`: Choice of what sender type to to use for transaction submission. (default: `raw`, options: `raw`, `conditional`, `flashbots`, `polygon_bloxroute`) + - env: *BUILDER_SENDER* - `--builder.max_blocks_to_wait_for_mine`: After submitting a bundle transaction, the maximum number of blocks to wait for that transaction to mine before trying to resend with higher gas fees (default: `2`) - env: *BUILDER_MAX_BLOCKS_TO_WAIT_FOR_MINE* - `--builder.replacement_fee_percent_increase`: Percentage amount to increase gas fees when retrying a transaction after it failed to mine (default: `10`)