diff --git a/Cargo.lock b/Cargo.lock index c8e09718e..bced7abe7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3979,6 +3979,7 @@ dependencies = [ "futures", "futures-timer", "futures-util", + "jsonrpsee", "linked-hash-map", "metrics", "mockall", diff --git a/Cargo.toml b/Cargo.toml index 7f56c18fd..e3b7f0620 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ cargo-husky = { version = "1", default-features = false, features = ["user-hooks ethers = "2.0.8" futures = "0.3.28" futures-util = "0.3.28" +jsonrpsee = "0.20.1" metrics = "0.21.0" mockall = "0.11.4" parse-display = "0.8.0" diff --git a/bin/rundler/src/cli/builder.rs b/bin/rundler/src/cli/builder.rs index de1971a04..d01feda50 100644 --- a/bin/rundler/src/cli/builder.rs +++ b/bin/rundler/src/cli/builder.rs @@ -133,7 +133,8 @@ pub struct BuilderArgs { )] replacement_fee_percent_increase: u64, - /// + /// Maximum number of times to increase gas fees when retrying a transaction + /// before giving up. #[arg( long = "builder.max_fee_increases", name = "builder.max_fee_increases", @@ -142,6 +143,15 @@ pub struct BuilderArgs { default_value = "7" )] max_fee_increases: u64, + + /// If using Polygon Mainnet, the auth header to use + /// for Bloxroute polygon_private_tx sender + #[arg( + long = "builder.bloxroute_auth_header", + name = "builder.bloxroute_auth_header", + env = "BUILDER_BLOXROUTE_AUTH_HEADER" + )] + bloxroute_auth_header: Option, } impl BuilderArgs { @@ -201,6 +211,7 @@ impl BuilderArgs { replacement_fee_percent_increase: self.replacement_fee_percent_increase, max_fee_increases: self.max_fee_increases, remote_address, + bloxroute_auth_header: self.bloxroute_auth_header.clone(), }) } } diff --git a/crates/builder/Cargo.toml b/crates/builder/Cargo.toml index 6ddcec899..872db3942 100644 --- a/crates/builder/Cargo.toml +++ b/crates/builder/Cargo.toml @@ -22,6 +22,7 @@ ethers-signers = {version = "2.0.8", features = ["aws"] } futures.workspace = true futures-timer = "3.0.2" futures-util.workspace = true +jsonrpsee = { workspace = true, features = [ "http-client" ]} linked-hash-map = "0.5.6" metrics.workspace = true pin-project.workspace = true diff --git a/crates/builder/src/sender/bloxroute.rs b/crates/builder/src/sender/bloxroute.rs new file mode 100644 index 000000000..2f28373e7 --- /dev/null +++ b/crates/builder/src/sender/bloxroute.rs @@ -0,0 +1,173 @@ +use std::{sync::Arc, time::Duration}; + +use anyhow::Context; +use ethers::{ + middleware::SignerMiddleware, + providers::{JsonRpcClient, Middleware, Provider}, + types::{ + transaction::eip2718::TypedTransaction, Address, Bytes, TransactionReceipt, TxHash, H256, + }, + utils::hex, +}; +use ethers_signers::Signer; +use jsonrpsee::{ + core::{client::ClientT, traits::ToRpcParams}, + http_client::{transport::HttpBackend, HttpClient, HttpClientBuilder}, +}; +use reqwest::header::{HeaderMap, HeaderValue}; +use rundler_sim::ExpectedStorage; +use serde::{Deserialize, Serialize}; +use serde_json::value::RawValue; +use tokio::time; +use tonic::async_trait; + +use crate::sender::{fill_and_sign, SentTxInfo, TransactionSender, TxStatus}; + +pub(crate) struct PolygonBloxrouteTransactionSender +where + C: JsonRpcClient + 'static, + S: Signer + 'static, +{ + provider: SignerMiddleware>, S>, + raw_provider: Arc>, + client: PolygonBloxrouteClient, + poll_interval: Duration, +} + +#[async_trait] +impl TransactionSender for PolygonBloxrouteTransactionSender +where + C: JsonRpcClient + 'static, + S: Signer + 'static, +{ + async fn send_transaction( + &self, + tx: TypedTransaction, + _expected_storage: &ExpectedStorage, + ) -> anyhow::Result { + let (raw_tx, nonce) = fill_and_sign(&self.provider, tx).await?; + let tx_hash = self + .client + .send_transaction(raw_tx) + .await + .context("should send bloxroute polygon private tx")?; + Ok(SentTxInfo { nonce, tx_hash }) + } + + async fn get_transaction_status(&self, tx_hash: H256) -> anyhow::Result { + let tx = self + .provider + .get_transaction(tx_hash) + .await + .context("provider should return transaction status")?; + // BDN transactions will not always show up in the node's transaction pool + // so we can't rely on this to determine if the transaction was dropped + // Thus, always return pending. + Ok(tx + .and_then(|tx| tx.block_number) + .map(|block_number| TxStatus::Mined { + block_number: block_number.as_u64(), + }) + .unwrap_or(TxStatus::Pending)) + } + + async fn wait_until_mined(&self, tx_hash: H256) -> anyhow::Result> { + Self::wait_until_mined_no_drop(tx_hash, Arc::clone(&self.raw_provider), self.poll_interval) + .await + } + + fn address(&self) -> Address { + self.provider.address() + } +} + +impl PolygonBloxrouteTransactionSender +where + C: JsonRpcClient + 'static, + S: Signer + 'static, +{ + pub(crate) fn new( + provider: Arc>, + signer: S, + poll_interval: Duration, + auth_header: &str, + ) -> anyhow::Result { + Ok(Self { + provider: SignerMiddleware::new(Arc::clone(&provider), signer), + raw_provider: provider, + client: PolygonBloxrouteClient::new(auth_header)?, + poll_interval, + }) + } + + async fn wait_until_mined_no_drop( + tx_hash: H256, + provider: Arc>, + poll_interval: Duration, + ) -> anyhow::Result> { + loop { + let tx = provider + .get_transaction(tx_hash) + .await + .context("provider should return transaction status")?; + + match tx.and_then(|tx| tx.block_number) { + None => {} + Some(_) => { + let receipt = provider + .get_transaction_receipt(tx_hash) + .await + .context("provider should return transaction receipt")?; + return Ok(receipt); + } + } + + time::sleep(poll_interval).await; + } + } +} + +struct PolygonBloxrouteClient { + client: HttpClient, +} + +impl PolygonBloxrouteClient { + fn new(auth_header: &str) -> anyhow::Result { + let mut headers = HeaderMap::new(); + headers.insert("Authorization", HeaderValue::from_str(auth_header)?); + let client = HttpClientBuilder::default() + .set_headers(headers) + .build("https://api.blxrbdn.com:443")?; + Ok(Self { client }) + } + + async fn send_transaction(&self, raw_tx: Bytes) -> anyhow::Result { + let request = BloxrouteRequest { + transaction: hex::encode(raw_tx), + }; + let response: BloxrouteResponse = + self.client.request("polygon_private_tx", request).await?; + Ok(response.tx_hash) + } +} + +#[derive(Serialize)] + +struct BloxrouteRequest { + transaction: String, +} + +impl ToRpcParams for BloxrouteRequest { + 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 BloxrouteResponse { + tx_hash: TxHash, +} diff --git a/crates/builder/src/sender/mod.rs b/crates/builder/src/sender/mod.rs index 932a781ce..f233c7682 100644 --- a/crates/builder/src/sender/mod.rs +++ b/crates/builder/src/sender/mod.rs @@ -1,18 +1,21 @@ +mod bloxroute; mod conditional; mod flashbots; mod raw; -use std::sync::Arc; +use std::{sync::Arc, time::Duration}; use anyhow::Context; use async_trait::async_trait; +pub(crate) use bloxroute::PolygonBloxrouteTransactionSender; pub(crate) use conditional::ConditionalTransactionSender; use enum_dispatch::enum_dispatch; use ethers::{ prelude::SignerMiddleware, providers::{JsonRpcClient, Middleware, Provider}, types::{ - transaction::eip2718::TypedTransaction, Address, Bytes, TransactionReceipt, H256, U256, + transaction::eip2718::TypedTransaction, Address, Bytes, Chain, TransactionReceipt, H256, + U256, }, }; use ethers_signers::Signer; @@ -61,6 +64,7 @@ where Raw(RawTransactionSender), Conditional(ConditionalTransactionSender), Flashbots(FlashbotsTransactionSender), + PolygonBloxroute(PolygonBloxrouteTransactionSender), } async fn fill_and_sign( @@ -91,16 +95,27 @@ pub(crate) fn get_sender( signer: S, is_conditional: bool, url: &str, -) -> TransactionSenderEnum + chain_id: u64, + poll_interval: Duration, + bloxroute_auth_header: &Option, +) -> anyhow::Result> where C: JsonRpcClient + 'static, S: Signer + 'static, { - if is_conditional { + 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 55567529d..cbc79c195 100644 --- a/crates/builder/src/task.rs +++ b/crates/builder/src/task.rs @@ -88,6 +88,12 @@ pub struct Args { pub max_fee_increases: u64, /// Address to bind the remote builder server to, if any. If none, no server is starter. pub remote_address: Option, + /// Optional Bloxroute auth header + /// + /// This is only used for Polygon. + /// + /// Checked ~after~ checking for conditional sender or Flashbots sender. + pub bloxroute_auth_header: Option, } /// Builder task @@ -167,7 +173,10 @@ where 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, diff --git a/crates/rpc/Cargo.toml b/crates/rpc/Cargo.toml index fcd7a5128..1d7903998 100644 --- a/crates/rpc/Cargo.toml +++ b/crates/rpc/Cargo.toml @@ -18,7 +18,7 @@ rundler-utils = { path = "../utils" } anyhow.workspace = true async-trait.workspace = true ethers.workspace = true -jsonrpsee = { version = "0.20.0", features = ["client", "macros", "server"] } +jsonrpsee = { workspace = true , features = ["client", "macros", "server"] } metrics.workspace = true thiserror.workspace = true tokio.workspace = true