use alloy::{
consensus::{
transaction::{Recovered, SignerRecoverable},
TxEnvelope,
},
eips::{eip2718::Eip2718Result, Decodable2718},
primitives::{Address, Bytes, TxHash, B256},
rlp::Buf,
rpc::types::mev::EthSendBundle,
};
use serde::{Deserialize, Serialize};
use trevm::{
inspectors::{Layered, TimeLimit},
revm::{inspector::NoOpInspector, Database},
BundleError,
};
use crate::{BundleRecoverError, RecoverError, RecoveredBundle};
pub type BundleInspector<I = NoOpInspector> = Layered<TimeLimit, I>;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SignetEthBundle {
#[serde(flatten)]
pub bundle: EthSendBundle,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub host_txs: Vec<Bytes>,
}
impl SignetEthBundle {
pub const fn new(bundle: EthSendBundle, host_txs: Vec<Bytes>) -> Self {
Self { bundle, host_txs }
}
pub fn into_parts(self) -> (EthSendBundle, Vec<Bytes>) {
(self.bundle, self.host_txs)
}
pub const fn txs(&self) -> &[Bytes] {
self.bundle.txs.as_slice()
}
pub const fn host_txs(&self) -> &[Bytes] {
self.host_txs.as_slice()
}
pub const fn host_txs_mut(&mut self) -> &mut Vec<Bytes> {
&mut self.host_txs
}
pub fn decode_txs(&self) -> impl Iterator<Item = Eip2718Result<TxEnvelope>> + '_ {
self.txs().iter().map(|tx| TxEnvelope::decode_2718(&mut tx.chunk()))
}
pub fn decode_host_txs(&self) -> impl Iterator<Item = Eip2718Result<TxEnvelope>> + '_ {
self.host_txs.iter().map(|tx| TxEnvelope::decode_2718(&mut tx.chunk()))
}
pub fn recover_txs(
&self,
) -> impl Iterator<Item = Result<Recovered<TxEnvelope>, BundleRecoverError>> + '_ {
self.decode_txs().enumerate().map(|(index, res)| match res {
Ok(tx) => {
tx.try_into_recovered().map_err(|err| BundleRecoverError::new(err, false, index))
}
Err(err) => Err(BundleRecoverError::new(err, false, index)),
})
}
pub fn recover_host_txs(
&self,
) -> impl Iterator<Item = Result<Recovered<TxEnvelope>, BundleRecoverError>> + '_ {
self.decode_host_txs().enumerate().map(|(index, res)| match res {
Ok(tx) => {
tx.try_into_recovered().map_err(|err| BundleRecoverError::new(err, true, index))
}
Err(err) => Err(BundleRecoverError::new(err, true, index)),
})
}
pub fn try_into_recovered(self) -> Result<RecoveredBundle, BundleRecoverError> {
if self.txs().is_empty() {
return Err(BundleRecoverError::new(RecoverError::EmptyBundle, false, 0));
}
let txs = self.recover_txs().collect::<Result<Vec<_>, _>>()?;
let host_txs = self.recover_host_txs().collect::<Result<Vec<_>, _>>()?;
Ok(RecoveredBundle {
txs,
host_txs,
block_number: self.bundle.block_number,
min_timestamp: self.bundle.min_timestamp,
max_timestamp: self.bundle.max_timestamp,
reverting_tx_hashes: self.bundle.reverting_tx_hashes,
replacement_uuid: self.bundle.replacement_uuid,
dropping_tx_hashes: self.bundle.dropping_tx_hashes,
refund_percent: self.bundle.refund_percent,
refund_recipient: self.bundle.refund_recipient,
refund_tx_hashes: self.bundle.refund_tx_hashes,
extra_fields: self.bundle.extra_fields,
})
}
pub fn try_to_recovered(&self) -> Result<RecoveredBundle, BundleRecoverError> {
self.clone().try_into_recovered()
}
pub fn signers(&self) -> impl Iterator<Item = Option<(TxHash, Address)>> + '_ {
self.txs().iter().map(|tx| {
TxEnvelope::decode_2718(&mut tx.chunk())
.ok()
.and_then(|envelope| envelope.recover_signer().ok().map(|s| (*envelope.hash(), s)))
})
}
pub fn signers_lossy(&self) -> impl Iterator<Item = (TxHash, Address)> + '_ {
self.signers().flatten()
}
pub const fn block_number(&self) -> u64 {
self.bundle.block_number
}
pub const fn min_timestamp(&self) -> Option<u64> {
self.bundle.min_timestamp
}
pub const fn max_timestamp(&self) -> Option<u64> {
self.bundle.max_timestamp
}
pub const fn reverting_tx_hashes(&self) -> &[B256] {
self.bundle.reverting_tx_hashes.as_slice()
}
pub const fn replacement_uuid(&self) -> Option<&str> {
let Some(uuid) = &self.bundle.replacement_uuid else { return None };
Some(uuid.as_str())
}
pub fn is_valid_at_timestamp(&self, timestamp: u64) -> bool {
let min_timestamp = self.min_timestamp().unwrap_or(0);
let max_timestamp = self.max_timestamp().unwrap_or(u64::MAX);
(min_timestamp..=max_timestamp).contains(×tamp)
}
pub const fn is_valid_at_block_number(&self, block_number: u64) -> bool {
self.bundle.block_number == block_number
}
pub fn decode_and_validate_txs<Db: Database>(
&self,
) -> Result<Vec<TxEnvelope>, BundleError<Db>> {
let txs = self
.decode_txs()
.collect::<Result<Vec<_>, _>>()
.map_err(|err| BundleError::TransactionDecodingError(err))?;
if txs.iter().any(|tx| tx.is_eip4844()) {
return Err(BundleError::UnsupportedTransactionType);
}
Ok(txs)
}
pub fn decode_and_validate_host_txs<Db: Database>(
&self,
) -> Result<Vec<TxEnvelope>, BundleError<Db>> {
self.decode_host_txs()
.collect::<Result<Vec<_>, _>>()
.map_err(|err| BundleError::TransactionDecodingError(err))
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn send_bundle_ser_roundtrip() {
let bundle = SignetEthBundle::new(
EthSendBundle {
txs: vec![b"tx1".into(), b"tx2".into()],
block_number: 1,
min_timestamp: Some(2),
max_timestamp: Some(3),
reverting_tx_hashes: vec![B256::repeat_byte(4), B256::repeat_byte(5)],
replacement_uuid: Some("uuid".to_owned()),
..Default::default()
},
vec![b"host_tx1".into(), b"host_tx2".into()],
);
let serialized = serde_json::to_string(&bundle).unwrap();
let deserialized: SignetEthBundle = serde_json::from_str(&serialized).unwrap();
assert_eq!(bundle, deserialized);
}
#[test]
fn send_bundle_ser_roundtrip_no_host_no_fills() {
let bundle = SignetEthBundle::new(
EthSendBundle {
txs: vec![b"tx1".into(), b"tx2".into()],
block_number: 1,
min_timestamp: Some(2),
max_timestamp: Some(3),
reverting_tx_hashes: vec![B256::repeat_byte(4), B256::repeat_byte(5)],
replacement_uuid: Some("uuid".to_owned()),
..Default::default()
},
vec![],
);
let serialized = serde_json::to_string(&bundle).unwrap();
let deserialized: SignetEthBundle = serde_json::from_str(&serialized).unwrap();
assert_eq!(bundle, deserialized);
}
#[test]
fn test_deser_bundle_no_host_no_fills() {
let json = r#"
{"txs":["0x747831","0x747832"],"blockNumber":"0x1","minTimestamp":2,"maxTimestamp":3,"revertingTxHashes":["0x0404040404040404040404040404040404040404040404040404040404040404","0x0505050505050505050505050505050505050505050505050505050505050505"],"replacementUuid":"uuid"}"#;
let deserialized: SignetEthBundle = serde_json::from_str(json).unwrap();
assert!(deserialized.host_txs.is_empty());
}
#[test]
#[ignore]
fn generate_eth_bundle_vectors() {
use alloy::primitives::Address;
let vectors = vec![
(
"minimal",
SignetEthBundle::new(
EthSendBundle {
txs: vec![b"\x02\xf8test_tx_1".into()],
block_number: 12345678,
..Default::default()
},
vec![],
),
),
(
"with_timestamps",
SignetEthBundle::new(
EthSendBundle {
txs: vec![b"\x02\xf8test_tx_1".into()],
block_number: 12345678,
min_timestamp: Some(1700000000),
max_timestamp: Some(1700003600),
..Default::default()
},
vec![],
),
),
(
"with_reverting_hashes",
SignetEthBundle::new(
EthSendBundle {
txs: vec![b"\x02\xf8test_tx_1".into(), b"\x02\xf8test_tx_2".into()],
block_number: 12345678,
reverting_tx_hashes: vec![B256::repeat_byte(0xab), B256::repeat_byte(0xcd)],
..Default::default()
},
vec![],
),
),
(
"with_host_txs",
SignetEthBundle::new(
EthSendBundle {
txs: vec![b"\x02\xf8rollup_tx".into()],
block_number: 12345678,
..Default::default()
},
vec![b"\x02\xf8host_tx_1".into(), b"\x02\xf8host_tx_2".into()],
),
),
(
"full_bundle",
SignetEthBundle::new(
EthSendBundle {
txs: vec![b"\x02\xf8tx_1".into(), b"\x02\xf8tx_2".into()],
block_number: 12345678,
min_timestamp: Some(1700000000),
max_timestamp: Some(1700003600),
reverting_tx_hashes: vec![B256::repeat_byte(0xef)],
dropping_tx_hashes: vec![B256::repeat_byte(0x11)],
refund_percent: Some(90),
refund_recipient: Some(Address::repeat_byte(0x22)),
refund_tx_hashes: vec![B256::repeat_byte(0x33)],
..Default::default()
},
vec![b"\x02\xf8host_tx".into()],
),
),
(
"replacement_bundle",
SignetEthBundle::new(
EthSendBundle {
txs: vec![b"\x02\xf8replacement_tx".into()],
block_number: 12345678,
replacement_uuid: Some("550e8400-e29b-41d4-a716-446655440000".to_owned()),
..Default::default()
},
vec![],
),
),
];
let output: Vec<_> = vectors
.into_iter()
.map(|(name, bundle)| {
serde_json::json!({
"name": name,
"bundle": bundle,
})
})
.collect();
println!("// SignetEthBundle vectors\n{}", serde_json::to_string_pretty(&output).unwrap());
}
}