Skip to content

Commit

Permalink
feat(builder): cancel transaction when replacement underpriced
Browse files Browse the repository at this point in the history
  • Loading branch information
dancoombs committed Jun 11, 2024
1 parent 7ae5087 commit a908eef
Show file tree
Hide file tree
Showing 8 changed files with 480 additions and 112 deletions.
21 changes: 19 additions & 2 deletions crates/builder/src/bundle_proposer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,21 @@ impl<UO: UserOperation> Bundle<UO> {
pub(crate) trait BundleProposer: Send + Sync + 'static {
type UO: UserOperation;

/// Constructs the next bundle
///
/// If `min_fees` is `Some`, the proposer will ensure the bundle has
/// at least `min_fees`.
async fn make_bundle(
&self,
required_fees: Option<GasFees>,
min_fees: Option<GasFees>,
is_replacement: bool,
) -> anyhow::Result<Bundle<Self::UO>>;

/// Gets the current gas fees
///
/// If `min_fees` is `Some`, the proposer will ensure the gas fees returned are at least `min_fees`.
async fn estimate_gas_fees(&self, min_fees: Option<GasFees>)
-> anyhow::Result<(GasFees, U256)>;
}

#[derive(Debug)]
Expand Down Expand Up @@ -138,6 +148,13 @@ where
{
type UO = UO;

async fn estimate_gas_fees(
&self,
required_fees: Option<GasFees>,
) -> anyhow::Result<(GasFees, U256)> {
self.fee_estimator.required_bundle_fees(required_fees).await
}

async fn make_bundle(
&self,
required_fees: Option<GasFees>,
Expand All @@ -148,7 +165,7 @@ where
self.provider
.get_latest_block_hash_and_number()
.map_err(anyhow::Error::from),
self.fee_estimator.required_bundle_fees(required_fees)
self.estimate_gas_fees(required_fees)
)?;
if ops.is_empty() {
return Ok(Bundle::default());
Expand Down
276 changes: 194 additions & 82 deletions crates/builder/src/bundle_sender.rs

Large diffs are not rendered by default.

30 changes: 29 additions & 1 deletion crates/builder/src/sender/bloxroute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use ethers::{
providers::{JsonRpcClient, Middleware, Provider},
types::{
transaction::eip2718::TypedTransaction, Address, Bytes, TransactionReceipt, TxHash, H256,
U256,
},
utils::hex,
};
Expand All @@ -28,12 +29,16 @@ use jsonrpsee::{
http_client::{transport::HttpBackend, HeaderMap, HeaderValue, HttpClient, HttpClientBuilder},
};
use rundler_sim::ExpectedStorage;
use rundler_types::GasFees;
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
use tokio::time;
use tonic::async_trait;

use super::{fill_and_sign, Result, SentTxInfo, TransactionSender, TxStatus};
use super::{
create_hard_cancel_tx, fill_and_sign, CancelTxInfo, Result, SentTxInfo, TransactionSender,
TxStatus,
};

pub(crate) struct PolygonBloxrouteTransactionSender<C, S>
where
Expand Down Expand Up @@ -62,6 +67,29 @@ where
Ok(SentTxInfo { nonce, tx_hash })
}

async fn cancel_transaction(
&self,
_tx_hash: H256,
nonce: U256,
to: Address,
gas_fees: GasFees,
) -> Result<CancelTxInfo> {
let tx = create_hard_cancel_tx(self.provider.address(), to, nonce, gas_fees);

let (raw_tx, _) = fill_and_sign(&self.provider, tx).await?;

let tx_hash = self
.provider
.provider()
.request("eth_sendRawTransaction", (raw_tx,))
.await?;

Ok(CancelTxInfo {
tx_hash,
soft_cancelled: false,
})
}

async fn get_transaction_status(&self, tx_hash: H256) -> Result<TxStatus> {
let tx = self
.provider
Expand Down
31 changes: 29 additions & 2 deletions crates/builder/src/sender/conditional.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,18 @@ use anyhow::Context;
use ethers::{
middleware::SignerMiddleware,
providers::{JsonRpcClient, Middleware, PendingTransaction, Provider},
types::{transaction::eip2718::TypedTransaction, Address, TransactionReceipt, H256},
types::{transaction::eip2718::TypedTransaction, Address, TransactionReceipt, H256, U256},
};
use ethers_signers::Signer;
use rundler_sim::ExpectedStorage;
use rundler_types::GasFees;
use serde_json::json;
use tonic::async_trait;

use super::{fill_and_sign, Result, SentTxInfo, TransactionSender, TxStatus};
use super::{
create_hard_cancel_tx, fill_and_sign, CancelTxInfo, Result, SentTxInfo, TransactionSender,
TxStatus,
};

pub(crate) struct ConditionalTransactionSender<C, S>
where
Expand Down Expand Up @@ -62,6 +66,29 @@ where
Ok(SentTxInfo { nonce, tx_hash })
}

async fn cancel_transaction(
&self,
_tx_hash: H256,
nonce: U256,
to: Address,
gas_fees: GasFees,
) -> Result<CancelTxInfo> {
let tx = create_hard_cancel_tx(self.provider.address(), to, nonce, gas_fees);

let (raw_tx, _) = fill_and_sign(&self.provider, tx).await?;

let tx_hash = self
.provider
.provider()
.request("eth_sendRawTransaction", (raw_tx,))
.await?;

Ok(CancelTxInfo {
tx_hash,
soft_cancelled: false,
})
}

async fn get_transaction_status(&self, tx_hash: H256) -> Result<TxStatus> {
let tx = self
.provider
Expand Down
99 changes: 79 additions & 20 deletions crates/builder/src/sender/flashbots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ use ethers::{
middleware::SignerMiddleware,
providers::{interval, JsonRpcClient, Middleware, Provider},
types::{
transaction::eip2718::TypedTransaction, Address, Bytes, TransactionReceipt, TxHash, H256,
U256, U64,
transaction::eip2718::TypedTransaction, Address, Bytes, TransactionReceipt, H256, U256, U64,
},
utils,
};
Expand All @@ -37,15 +36,17 @@ use futures_util::{Stream, StreamExt, TryFutureExt};
use pin_project::pin_project;
use reqwest::{
header::{HeaderMap, HeaderValue, CONTENT_TYPE},
Client,
Client, Response,
};
use rundler_types::GasFees;
use serde::{de, Deserialize, Serialize};
use serde_json::{json, Value};
use tonic::async_trait;

use super::{
fill_and_sign, ExpectedStorage, Result, SentTxInfo, TransactionSender, TxSenderError, TxStatus,
};
use crate::sender::CancelTxInfo;

#[derive(Debug)]
pub(crate) struct FlashbotsTransactionSender<C, S, FS> {
Expand Down Expand Up @@ -75,6 +76,28 @@ where
Ok(SentTxInfo { nonce, tx_hash })
}

async fn cancel_transaction(
&self,
tx_hash: H256,
_nonce: U256,
_to: Address,
_gas_fees: GasFees,
) -> Result<CancelTxInfo> {
let success = self
.flashbots_client
.cancel_private_transaction(tx_hash)
.await?;

if !success {
return Err(TxSenderError::SoftCancelFailed);
}

Ok(CancelTxInfo {
tx_hash: H256::zero(),
soft_cancelled: true,
})
}

async fn get_transaction_status(&self, tx_hash: H256) -> Result<TxStatus> {
let status = self.flashbots_client.status(tx_hash).await?;
Ok(match status.status {
Expand Down Expand Up @@ -181,13 +204,31 @@ struct Refund {

#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
struct FlashbotsPrivateTransaction {
struct FlashbotsSendPrivateTransactionRequest {
tx: Bytes,
#[serde(skip_serializing_if = "Option::is_none")]
max_block_number: Option<U256>,
preferences: Preferences,
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct FlashbotsSendPrivateTransactionResponse {
result: H256,
}

#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
struct FlashbotsCancelPrivateTransactionRequest {
tx_hash: H256,
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct FlashbotsCancelPrivateTransactionResponse {
result: bool,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
Expand Down Expand Up @@ -277,14 +318,45 @@ where
"jsonrpc": "2.0",
"method": "eth_sendPrivateTransaction",
"params": [
FlashbotsPrivateTransaction {
FlashbotsSendPrivateTransactionRequest {
tx: raw_tx,
max_block_number: None,
preferences,
}],
"id": 1
});

let response = self.sign_send_request(body).await?;

let parsed_response = response
.json::<FlashbotsSendPrivateTransactionResponse>()
.await
.map_err(|e| anyhow!("failed to deserialize Flashbots response: {:?}", e))?;

Ok(parsed_response.result)
}

async fn cancel_private_transaction(&self, tx_hash: H256) -> anyhow::Result<bool> {
let body = json!({
"jsonrpc": "2.0",
"method": "eth_cancelPrivateTransaction",
"params": [
FlashbotsCancelPrivateTransactionRequest { tx_hash }
],
"id": 1
});

let response = self.sign_send_request(body).await?;

let parsed_response = response
.json::<FlashbotsCancelPrivateTransactionResponse>()
.await
.map_err(|e| anyhow!("failed to deserialize Flashbots response: {:?}", e))?;

Ok(parsed_response.result)
}

async fn sign_send_request(&self, body: Value) -> anyhow::Result<Response> {
let signature = self
.signer
.sign_message(format!(
Expand All @@ -302,29 +374,16 @@ where
headers.insert("x-flashbots-signature", header_val);

// Send the request
let response = self
.http_client
self.http_client
.post(&self.relay_url)
.headers(headers)
.body(body.to_string())
.send()
.await
.map_err(|e| anyhow!("failed to send transaction to Flashbots: {:?}", e))?;

let parsed_response = response
.json::<FlashbotsResponse>()
.await
.map_err(|e| anyhow!("failed to deserialize Flashbots response: {:?}", e))?;

Ok(parsed_response.result)
.map_err(|e| anyhow!("failed to send request to Flashbots: {:?}", e))
}
}

#[derive(Deserialize, Debug)]
struct FlashbotsResponse {
result: TxHash,
}

type PinBoxFut<'a, T> = Pin<Box<dyn Future<Output = anyhow::Result<T>> + Send + 'a>>;

enum PendingFlashbotsTxState<'a> {
Expand Down
Loading

0 comments on commit a908eef

Please sign in to comment.