Skip to content

Commit

Permalink
feat(builder): add polygon bloxroute sender
Browse files Browse the repository at this point in the history
  • Loading branch information
dancoombs committed Sep 20, 2023
1 parent d2e2690 commit 3e3526b
Show file tree
Hide file tree
Showing 8 changed files with 219 additions and 8 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
13 changes: 12 additions & 1 deletion bin/rundler/src/cli/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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<String>,
}

impl BuilderArgs {
Expand Down Expand Up @@ -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(),
})
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/builder/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
173 changes: 173 additions & 0 deletions crates/builder/src/sender/bloxroute.rs
Original file line number Diff line number Diff line change
@@ -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<C, S>
where
C: JsonRpcClient + 'static,
S: Signer + 'static,
{
provider: SignerMiddleware<Arc<Provider<C>>, S>,
raw_provider: Arc<Provider<C>>,
client: PolygonBloxrouteClient,
poll_interval: Duration,
}

#[async_trait]
impl<C, S> TransactionSender for PolygonBloxrouteTransactionSender<C, S>
where
C: JsonRpcClient + 'static,
S: Signer + 'static,
{
async fn send_transaction(
&self,
tx: TypedTransaction,
_expected_storage: &ExpectedStorage,
) -> anyhow::Result<SentTxInfo> {
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<TxStatus> {
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<Option<TransactionReceipt>> {
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<C, S> PolygonBloxrouteTransactionSender<C, S>
where
C: JsonRpcClient + 'static,
S: Signer + 'static,
{
pub(crate) fn new(
provider: Arc<Provider<C>>,
signer: S,
poll_interval: Duration,
auth_header: &str,
) -> anyhow::Result<Self> {
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<Provider<C>>,
poll_interval: Duration,
) -> anyhow::Result<Option<TransactionReceipt>> {
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<HttpBackend>,
}

impl PolygonBloxrouteClient {
fn new(auth_header: &str) -> anyhow::Result<Self> {
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<TxHash> {
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<Option<Box<RawValue>>, 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,
}
25 changes: 20 additions & 5 deletions crates/builder/src/sender/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -61,6 +64,7 @@ where
Raw(RawTransactionSender<C, S>),
Conditional(ConditionalTransactionSender<C, S>),
Flashbots(FlashbotsTransactionSender<C, S>),
PolygonBloxroute(PolygonBloxrouteTransactionSender<C, S>),
}

async fn fill_and_sign<C, S>(
Expand Down Expand Up @@ -91,16 +95,27 @@ pub(crate) fn get_sender<C, S>(
signer: S,
is_conditional: bool,
url: &str,
) -> TransactionSenderEnum<C, S>
chain_id: u64,
poll_interval: Duration,
bloxroute_auth_header: &Option<String>,
) -> anyhow::Result<TransactionSenderEnum<C, S>>
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)
}
11 changes: 10 additions & 1 deletion crates/builder/src/task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SocketAddr>,
/// Optional Bloxroute auth header
///
/// This is only used for Polygon.
///
/// Checked ~after~ checking for conditional sender or Flashbots sender.
pub bloxroute_auth_header: Option<String>,
}

/// Builder task
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion crates/rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 3e3526b

Please sign in to comment.