From 3f2de136c3f2918ed50b43eb89b9470a1f8d5fac Mon Sep 17 00:00:00 2001 From: dancoombs Date: Fri, 8 Sep 2023 10:49:35 -0400 Subject: [PATCH] feat(builder): add polygon bloxroute sender --- src/builder/sender/bloxroute.rs | 180 ++++++++++++++++++++++++++++++++ src/builder/sender/mod.rs | 24 ++++- src/builder/task.rs | 6 +- src/cli/builder.rs | 13 ++- 4 files changed, 216 insertions(+), 7 deletions(-) create mode 100644 src/builder/sender/bloxroute.rs diff --git a/src/builder/sender/bloxroute.rs b/src/builder/sender/bloxroute.rs new file mode 100644 index 000000000..b8acc2265 --- /dev/null +++ b/src/builder/sender/bloxroute.rs @@ -0,0 +1,180 @@ +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 serde::{Deserialize, Serialize}; +use serde_json::value::RawValue; +use tokio::time; +use tonic::async_trait; + +use crate::{ + builder::sender::{fill_and_sign, SentTxInfo, TransactionSender, TxStatus}, + common::types::ExpectedStorage, +}; + +pub 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")?; + Ok(match tx { + // 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. + None => TxStatus::Pending, + Some(tx) => match tx.block_number { + None => TxStatus::Pending, + Some(block_number) => TxStatus::Mined { + block_number: block_number.as_u64(), + }, + }, + }) + } + + 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 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 { + None => {} + Some(tx) => match 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 { + pub 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/src/builder/sender/mod.rs b/src/builder/sender/mod.rs index 03844a285..fb6f07391 100644 --- a/src/builder/sender/mod.rs +++ b/src/builder/sender/mod.rs @@ -1,8 +1,9 @@ +mod bloxroute; mod conditional; mod flashbots; mod raw; -use std::sync::Arc; +use std::{sync::Arc, time::Duration}; use anyhow::Context; pub use conditional::ConditionalTransactionSender; @@ -11,7 +12,8 @@ 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; @@ -21,6 +23,7 @@ use mockall::automock; pub use raw::RawTransactionSender; use tonic::async_trait; +use self::bloxroute::PolygonBloxrouteTransactionSender; use crate::common::types::ExpectedStorage; #[derive(Debug)] @@ -62,6 +65,7 @@ where Raw(RawTransactionSender), Conditional(ConditionalTransactionSender), Flashbots(FlashbotsTransactionSender), + PolygonBloxroute(PolygonBloxrouteTransactionSender), } async fn fill_and_sign( @@ -92,16 +96,26 @@ pub 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 { + if chain_id != Chain::Polygon as u64 { + panic!("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/src/builder/task.rs b/src/builder/task.rs index e9c72ae5a..32270acf4 100644 --- a/src/builder/task.rs +++ b/src/builder/task.rs @@ -64,6 +64,7 @@ pub struct Args { pub max_blocks_to_wait_for_mine: u64, pub replacement_fee_percent_increase: u64, pub max_fee_increases: u64, + pub bloxroute_auth_header: Option, } #[derive(Debug)] @@ -146,7 +147,10 @@ impl Task for BuilderTask { 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/src/cli/builder.rs b/src/cli/builder.rs index 11a68f5f3..65d61d489 100644 --- a/src/cli/builder.rs +++ b/src/cli/builder.rs @@ -135,7 +135,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", @@ -144,6 +145,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 { @@ -205,6 +215,7 @@ impl BuilderArgs { max_blocks_to_wait_for_mine: self.max_blocks_to_wait_for_mine, replacement_fee_percent_increase: self.replacement_fee_percent_increase, max_fee_increases: self.max_fee_increases, + bloxroute_auth_header: self.bloxroute_auth_header.clone(), }) }