Skip to content

Commit bc7ae9e

Browse files
authored
Merge pull request #9 from sigp/mev-bundle-types
2 parents 56f0e60 + ab81444 commit bc7ae9e

File tree

8 files changed

+261
-1
lines changed

8 files changed

+261
-1
lines changed

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ members = [
99
"relay-api-types",
1010
"relay-client",
1111
"relay-server",
12+
"searcher-api-types",
13+
"searcher-client",
1214
"common"
1315
]
1416

@@ -27,7 +29,7 @@ reqwest = { version = "0.12.5", features = ["json"] }
2729
serde = { version = "1.0", features = ["derive"] }
2830
serde_json = { version = "1", features = ["raw_value"] }
2931
superstruct = "0.8"
30-
tokio = { version = "1", default-features = false, features = ["signal", "rt-multi-thread"] }
32+
tokio = { version = "1", default-features = false, features = ["signal", "rt-multi-thread", "macros"] }
3133
tokio-tungstenite = "0.24.0"
3234
tracing = { version = "0.1", features = ["attributes"] }
3335
types = { git = "https://github.com/sigp/lighthouse.git", rev = "c33307d70287fd3b7a70785f89dadcb737214903" }

searcher-api-types/Cargo.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "searcher-api-types"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
serde = { workspace = true }
8+
serde_json = { workspace = true }
9+
10+
alloy-primitives = "0.8"
11+
alloy-rlp = "0.3"
12+
alloy-rpc-types-mev = "0.8"
13+
eyre = "0.6.12"
14+
serde_with = "3.12.0"

searcher-api-types/src/beaver.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
//! RPC types that are supported by Beaverbuild
2+
use alloy_primitives::{hex::FromHex, Address, BlockNumber, Bytes, TxHash};
3+
use alloy_rpc_types_mev::EthSendBundle;
4+
use serde::Serialize;
5+
use serde_with::{serde_as, skip_serializing_none};
6+
7+
/// Bundle as recognised by Beaverbuild
8+
///
9+
/// Consult <https://beaverbuild.org/docs.html>. Note that the deprecated `replacementUuid` field
10+
/// has been omitted.
11+
#[serde_as]
12+
#[skip_serializing_none]
13+
#[derive(Clone, Debug, Default, Serialize)]
14+
pub struct BeaverBundle {
15+
#[serde(flatten)]
16+
pub bundle: EthSendBundle,
17+
#[serde(skip_serializing_if = "Vec::is_empty")]
18+
/// A list of transaction hashes contained in the bundle, that can be allowed to be removed from your bundle if it's deemed useful (but not revert)
19+
pub dropping_transaction_hashes: Vec<TxHash>,
20+
/// An integer between 1-99. How much of the total priority fee + coinbase payment you want to be refunded for. This will negatively impact your prioritization because this refund is gonna eat into your bundle payment. Example: if a bundle pays 0.2 ETH of priority fee plus 1 ETH to coinbase, a refundPercent set to 50 will result in a transaction being appended after the bundle, paying 0.59 ETH back to the EOA. This is assuming the payout tx will cost beaver 0.01 ETH in fees, which are deduced from the 0.6 ETH payout.
21+
pub refund_percent: Option<u64>,
22+
/// You can specify an address that the funds from `refundPercent` will be sent to. If not specified, they will be sent to the `from` address of the first transaction
23+
pub refund_recipient: Option<Address>,
24+
#[serde(skip_serializing_if = "Vec::is_empty")]
25+
/// The hashes of transactions in the bundle that the refund will be based on. If it's empty, we'll use the last transaction
26+
pub refund_transaction_hashes: Vec<TxHash>,
27+
}
28+
29+
pub fn bundle_from_rlp_hex(
30+
txs: Vec<String>,
31+
block_number: BlockNumber,
32+
) -> eyre::Result<EthSendBundle> {
33+
Ok(EthSendBundle {
34+
txs: txs
35+
.iter()
36+
.map(Bytes::from_hex)
37+
.collect::<Result<Vec<Bytes>, _>>()?,
38+
block_number,
39+
..EthSendBundle::default()
40+
})
41+
}
42+
43+
#[cfg(test)]
44+
mod test {
45+
use super::*;
46+
47+
#[test]
48+
fn test_beaver_bundle_serialisation() {
49+
assert!(serde_json::to_string(&BeaverBundle::default()).is_ok());
50+
assert_eq!(
51+
serde_json::to_string(&BeaverBundle::default()).unwrap(),
52+
"{\"txs\":[],\"blockNumber\":\"0x0\"}".to_string()
53+
);
54+
55+
assert!(serde_json::to_string(&BeaverBundle {
56+
bundle: EthSendBundle {
57+
txs: vec![],
58+
block_number: 21862873,
59+
..Default::default()
60+
},
61+
..Default::default()
62+
})
63+
.is_ok());
64+
assert_eq!(
65+
serde_json::to_string(&BeaverBundle {
66+
bundle: EthSendBundle {
67+
txs: vec![],
68+
block_number: 21862873,
69+
..Default::default()
70+
},
71+
..Default::default()
72+
})
73+
.unwrap(),
74+
"{\"txs\":[],\"blockNumber\":\"0x14d99d9\"}".to_string()
75+
);
76+
assert!(
77+
serde_json::to_string(&
78+
bundle_from_rlp_hex(vec!["0x02f8b20181948449bdee618501dcd6500083016b93942dabcea55a12d73191aece59f508b191fb68adac80b844095ea7b300000000000000000000000054e44dbb92dba848ace27f44c0cb4268981ef1cc00000000000000000000000000000000000000000000000052616e065f6915ebc080a0c497b6e53d7cb78e68c37f6186c8bb9e1b8a55c3e22462163495979b25c2caafa052769811779f438b73159c4cc6a05a889da8c1a16e432c2e37e3415c9a0b9887".to_string()], 21862873).unwrap()
79+
)
80+
.is_ok());
81+
assert_eq!(
82+
serde_json::to_string(&
83+
bundle_from_rlp_hex(vec!["0x02f8b20181948449bdee618501dcd6500083016b93942dabcea55a12d73191aece59f508b191fb68adac80b844095ea7b300000000000000000000000054e44dbb92dba848ace27f44c0cb4268981ef1cc00000000000000000000000000000000000000000000000052616e065f6915ebc080a0c497b6e53d7cb78e68c37f6186c8bb9e1b8a55c3e22462163495979b25c2caafa052769811779f438b73159c4cc6a05a889da8c1a16e432c2e37e3415c9a0b9887".to_string()], 21862873).unwrap()
84+
)
85+
.unwrap(),
86+
"{\"txs\":[\"0x02f8b20181948449bdee618501dcd6500083016b93942dabcea55a12d73191aece59f508b191fb68adac80b844095ea7b300000000000000000000000054e44dbb92dba848ace27f44c0cb4268981ef1cc00000000000000000000000000000000000000000000000052616e065f6915ebc080a0c497b6e53d7cb78e68c37f6186c8bb9e1b8a55c3e22462163495979b25c2caafa052769811779f438b73159c4cc6a05a889da8c1a16e432c2e37e3415c9a0b9887\"],\"blockNumber\":\"0x14d99d9\"}".to_string()
87+
);
88+
}
89+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
use alloy_rpc_types_mev::EthSendBundle;
2+
3+
pub type FlashbotsBundle = EthSendBundle;

searcher-api-types/src/lib.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
use alloy_primitives::{BlockNumber, Bytes, TxHash};
2+
use alloy_rlp::encode;
3+
use serde::Serialize;
4+
5+
pub mod beaver;
6+
pub mod flashbots;
7+
pub mod titan;
8+
9+
pub use beaver::*;
10+
pub use flashbots::*;
11+
pub use titan::*;
12+
13+
/// Universal bundle submission RPC type
14+
///
15+
/// This type represents what Lynx accepts from external order flow providers.
16+
#[derive(Clone, Debug, Serialize)]
17+
#[serde(untagged)]
18+
pub enum SendBundleRequest {
19+
/// Flashbots bundle
20+
Flashbots(FlashbotsBundle),
21+
/// Beaverbuild bundle
22+
Beaver(BeaverBundle),
23+
/// Titan Builder bundle
24+
Titan(TitanBundle),
25+
}
26+
27+
impl SendBundleRequest {
28+
pub fn min_timestamp(&self) -> Option<u64> {
29+
match self {
30+
SendBundleRequest::Flashbots(bundle) => bundle.min_timestamp,
31+
SendBundleRequest::Beaver(bundle) => bundle.bundle.min_timestamp,
32+
SendBundleRequest::Titan(bundle) => bundle.bundle.min_timestamp,
33+
}
34+
}
35+
36+
pub fn max_timestamp(&self) -> Option<u64> {
37+
match self {
38+
SendBundleRequest::Flashbots(bundle) => bundle.max_timestamp,
39+
SendBundleRequest::Beaver(bundle) => bundle.bundle.max_timestamp,
40+
SendBundleRequest::Titan(bundle) => bundle.bundle.max_timestamp,
41+
}
42+
}
43+
44+
pub fn block_number(&self) -> BlockNumber {
45+
match self {
46+
SendBundleRequest::Flashbots(bundle) => bundle.block_number,
47+
SendBundleRequest::Beaver(bundle) => bundle.bundle.block_number,
48+
SendBundleRequest::Titan(bundle) => bundle.bundle.block_number,
49+
}
50+
}
51+
52+
pub fn tx_bytes(&self) -> Vec<Bytes> {
53+
match self {
54+
SendBundleRequest::Flashbots(bundle) => bundle.txs.clone(),
55+
SendBundleRequest::Beaver(bundle) => bundle
56+
.bundle
57+
.txs
58+
.iter()
59+
.map(|tx| encode(tx).into())
60+
.collect(),
61+
SendBundleRequest::Titan(bundle) => bundle
62+
.bundle
63+
.txs
64+
.iter()
65+
.map(|tx| encode(tx).into())
66+
.collect(),
67+
}
68+
}
69+
70+
pub fn reverting_tx_hashes(&self) -> Vec<TxHash> {
71+
match self {
72+
SendBundleRequest::Flashbots(_) => vec![],
73+
SendBundleRequest::Beaver(bundle) => bundle.bundle.reverting_tx_hashes.clone(),
74+
SendBundleRequest::Titan(bundle) => bundle.bundle.reverting_tx_hashes.clone(),
75+
}
76+
}
77+
}

searcher-api-types/src/titan.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
use crate::beaver::BeaverBundle;
2+
3+
pub type TitanBundle = BeaverBundle;

searcher-client/Cargo.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[package]
2+
name = "searcher-client"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
tokio = { workspace = true }
8+
9+
alloy-rpc-types-mev = "0.8"
10+
eyre = "0.6.12"
11+
url = "2.5.4"
12+
jsonrpsee = { version = "0.24.8", features = ["jsonrpsee-http-client", "jsonrpsee-core"] }
13+
14+
searcher-api-types = { path = "../searcher-api-types" }
15+
jsonrpsee-core = "0.24.8"
16+

searcher-client/src/lib.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
use alloy_rpc_types_mev::EthBundleHash;
2+
use jsonrpsee::http_client::HttpClient;
3+
use jsonrpsee::rpc_params;
4+
use jsonrpsee_core::client::ClientT;
5+
use url::Url;
6+
7+
use searcher_api_types::SendBundleRequest;
8+
9+
pub async fn send_bundle(url: Url, bundle: &SendBundleRequest) -> eyre::Result<EthBundleHash> {
10+
Ok(HttpClient::builder()
11+
.build(url)?
12+
.request("eth_sendBundle", rpc_params![bundle])
13+
.await?)
14+
}
15+
16+
#[cfg(test)]
17+
mod test {
18+
use super::*;
19+
20+
use alloy_rpc_types_mev::EthSendBundle;
21+
use searcher_api_types::{bundle_from_rlp_hex, BeaverBundle, SendBundleRequest};
22+
23+
const TEST_ENDPOINT: &str = "https://rpc.beaverbuild.org";
24+
25+
fn test_endpoint() -> Url {
26+
TEST_ENDPOINT.parse().unwrap()
27+
}
28+
29+
#[tokio::test]
30+
async fn test_send_bundle_beaver_rejects_empty_bundle() {
31+
let empty_bundle = SendBundleRequest::Beaver(BeaverBundle {
32+
bundle: EthSendBundle {
33+
txs: vec![],
34+
block_number: 0,
35+
..EthSendBundle::default()
36+
},
37+
..BeaverBundle::default()
38+
});
39+
let res = send_bundle(test_endpoint(), &empty_bundle).await;
40+
assert!(res.is_err());
41+
}
42+
43+
#[tokio::test]
44+
async fn test_send_bundle_beaver_success() {
45+
let bundle = SendBundleRequest::Beaver(
46+
BeaverBundle { bundle: bundle_from_rlp_hex(vec!["0x02f8b20181948449bdee618501dcd6500083016b93942dabcea55a12d73191aece59f508b191fb68adac80b844095ea7b300000000000000000000000054e44dbb92dba848ace27f44c0cb4268981ef1cc00000000000000000000000000000000000000000000000052616e065f6915ebc080a0c497b6e53d7cb78e68c37f6186c8bb9e1b8a55c3e22462163495979b25c2caafa052769811779f438b73159c4cc6a05a889da8c1a16e432c2e37e3415c9a0b9887".to_string()], 1).expect("illegal RLP bytes for bundle"), ..BeaverBundle::default()}
47+
);
48+
let res = send_bundle(test_endpoint(), &bundle).await;
49+
assert!(res.is_ok());
50+
let resp = res.unwrap();
51+
assert_eq!(
52+
resp.bundle_hash.to_string(),
53+
"0xbfe05fa7cb2f9de981eeefe7246c9c9be6f69c3a3b33a05fdbf6afac42ddd294"
54+
);
55+
}
56+
}

0 commit comments

Comments
 (0)