signet_bundle/send/
bundle.rs

1//! Signet bundle types.
2use alloy::{
3    consensus::TxEnvelope,
4    eips::Decodable2718,
5    primitives::{Bytes, B256},
6    rlp::Buf,
7    rpc::types::mev::EthSendBundle,
8};
9use serde::{Deserialize, Serialize};
10use trevm::{
11    inspectors::{Layered, TimeLimit},
12    revm::{inspector::NoOpInspector, Database},
13    BundleError,
14};
15
16/// The inspector type required by the Signet bundle driver.
17pub type BundleInspector<I = NoOpInspector> = Layered<TimeLimit, I>;
18
19/// Bundle of transactions for `signet_sendBundle`.
20///
21/// The Signet bundle contains the following:
22///
23/// - A standard [`EthSendBundle`] with the transactions to simulate.
24/// - Host transactions to be included in the host bundle.
25///
26/// This is based on the flashbots `eth_sendBundle` bundle. See [their docs].
27///
28/// [their docs]: https://docs.flashbots.net/flashbots-auction/advanced/rpc-endpoint
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(rename_all = "camelCase")]
31pub struct SignetEthBundle {
32    /// The bundle of transactions to simulate. Same structure as a Flashbots [`EthSendBundle`] bundle.
33    #[serde(flatten)]
34    pub bundle: EthSendBundle,
35
36    /// Host transactions to be included in the host bundle.
37    #[serde(default, skip_serializing_if = "Vec::is_empty")]
38    pub host_txs: Vec<Bytes>,
39}
40
41impl SignetEthBundle {
42    /// Returns the transactions in this bundle.
43    #[allow(clippy::missing_const_for_fn)] // false positive
44    pub fn txs(&self) -> &[Bytes] {
45        &self.bundle.txs
46    }
47
48    /// Returns the block number for this bundle.
49    pub const fn block_number(&self) -> u64 {
50        self.bundle.block_number
51    }
52
53    /// Returns the minimum timestamp for this bundle.
54    pub const fn min_timestamp(&self) -> Option<u64> {
55        self.bundle.min_timestamp
56    }
57
58    /// Returns the maximum timestamp for this bundle.
59    pub const fn max_timestamp(&self) -> Option<u64> {
60        self.bundle.max_timestamp
61    }
62
63    /// Returns the reverting tx hashes for this bundle.
64    pub fn reverting_tx_hashes(&self) -> &[B256] {
65        self.bundle.reverting_tx_hashes.as_slice()
66    }
67
68    /// Returns the replacement uuid for this bundle.
69    pub fn replacement_uuid(&self) -> Option<&str> {
70        self.bundle.replacement_uuid.as_deref()
71    }
72
73    /// Checks if the bundle is valid at a given timestamp.
74    pub fn is_valid_at_timestamp(&self, timestamp: u64) -> bool {
75        let min_timestamp = self.bundle.min_timestamp.unwrap_or(0);
76        let max_timestamp = self.bundle.max_timestamp.unwrap_or(u64::MAX);
77        timestamp >= min_timestamp && timestamp <= max_timestamp
78    }
79
80    /// Checks if the bundle is valid at a given block number.
81    pub const fn is_valid_at_block_number(&self, block_number: u64) -> bool {
82        self.bundle.block_number == block_number
83    }
84
85    /// Decode and validate the transactions in the bundle.
86    pub fn decode_and_validate_txs<Db: Database>(
87        &self,
88    ) -> Result<Vec<TxEnvelope>, BundleError<Db>> {
89        // Decode and validate the transactions in the bundle
90        let txs = self
91            .txs()
92            .iter()
93            .map(|tx| TxEnvelope::decode_2718(&mut tx.chunk()))
94            .collect::<Result<Vec<_>, _>>()
95            .map_err(|err| BundleError::TransactionDecodingError(err))?;
96
97        if txs.iter().any(|tx| tx.is_eip4844()) {
98            return Err(BundleError::UnsupportedTransactionType);
99        }
100
101        Ok(txs)
102    }
103
104    /// Decode and validate the host transactions in the bundle.
105    pub fn decode_and_validate_host_txs<Db: Database>(
106        &self,
107    ) -> Result<Vec<TxEnvelope>, BundleError<Db>> {
108        // Decode and validate the host transactions in the bundle
109        let txs = self
110            .host_txs
111            .iter()
112            .map(|tx| TxEnvelope::decode_2718(&mut tx.chunk()))
113            .collect::<Result<Vec<_>, _>>()
114            .map_err(|err| BundleError::TransactionDecodingError(err))?;
115
116        Ok(txs)
117    }
118}
119
120#[cfg(test)]
121mod test {
122    use super::*;
123
124    #[test]
125    fn send_bundle_ser_roundtrip() {
126        let bundle = SignetEthBundle {
127            bundle: EthSendBundle {
128                txs: vec![b"tx1".into(), b"tx2".into()],
129                block_number: 1,
130                min_timestamp: Some(2),
131                max_timestamp: Some(3),
132                reverting_tx_hashes: vec![B256::repeat_byte(4), B256::repeat_byte(5)],
133                replacement_uuid: Some("uuid".to_owned()),
134                ..Default::default()
135            },
136            host_txs: vec![b"host_tx1".into(), b"host_tx2".into()],
137        };
138
139        let serialized = serde_json::to_string(&bundle).unwrap();
140        let deserialized: SignetEthBundle = serde_json::from_str(&serialized).unwrap();
141
142        assert_eq!(bundle, deserialized);
143    }
144
145    #[test]
146    fn send_bundle_ser_roundtrip_no_host_no_fills() {
147        let bundle = SignetEthBundle {
148            bundle: EthSendBundle {
149                txs: vec![b"tx1".into(), b"tx2".into()],
150                block_number: 1,
151                min_timestamp: Some(2),
152                max_timestamp: Some(3),
153                reverting_tx_hashes: vec![B256::repeat_byte(4), B256::repeat_byte(5)],
154                replacement_uuid: Some("uuid".to_owned()),
155                ..Default::default()
156            },
157            host_txs: vec![],
158        };
159
160        let serialized = serde_json::to_string(&bundle).unwrap();
161        let deserialized: SignetEthBundle = serde_json::from_str(&serialized).unwrap();
162
163        assert_eq!(bundle, deserialized);
164    }
165
166    #[test]
167    fn test_deser_bundle_no_host_no_fills() {
168        let json = r#"
169        {"txs":["0x747831","0x747832"],"blockNumber":"0x1","minTimestamp":2,"maxTimestamp":3,"revertingTxHashes":["0x0404040404040404040404040404040404040404040404040404040404040404","0x0505050505050505050505050505050505050505050505050505050505050505"],"replacementUuid":"uuid"}"#;
170
171        let deserialized: SignetEthBundle = serde_json::from_str(json).unwrap();
172
173        assert!(deserialized.host_txs.is_empty());
174    }
175}