1use alloy::{
3 consensus::{
4 transaction::{Recovered, SignerRecoverable},
5 TxEnvelope,
6 },
7 eips::{eip2718::Eip2718Result, Decodable2718},
8 primitives::{Address, Bytes, TxHash, B256},
9 rlp::Buf,
10 rpc::types::mev::EthSendBundle,
11};
12use serde::{Deserialize, Serialize};
13use trevm::{
14 inspectors::{Layered, TimeLimit},
15 revm::{inspector::NoOpInspector, Database},
16 BundleError,
17};
18
19use crate::{BundleRecoverError, RecoverError, RecoveredBundle};
20
21pub type BundleInspector<I = NoOpInspector> = Layered<TimeLimit, I>;
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "camelCase")]
36pub struct SignetEthBundle {
37 #[serde(flatten)]
39 pub bundle: EthSendBundle,
40
41 #[serde(default, skip_serializing_if = "Vec::is_empty")]
43 pub host_txs: Vec<Bytes>,
44}
45
46impl SignetEthBundle {
47 pub const fn new(bundle: EthSendBundle, host_txs: Vec<Bytes>) -> Self {
49 Self { bundle, host_txs }
50 }
51
52 pub fn into_parts(self) -> (EthSendBundle, Vec<Bytes>) {
54 (self.bundle, self.host_txs)
55 }
56
57 pub const fn txs(&self) -> &[Bytes] {
59 self.bundle.txs.as_slice()
60 }
61
62 pub const fn host_txs(&self) -> &[Bytes] {
64 self.host_txs.as_slice()
65 }
66
67 pub const fn host_txs_mut(&mut self) -> &mut Vec<Bytes> {
69 &mut self.host_txs
70 }
71
72 pub fn decode_txs(&self) -> impl Iterator<Item = Eip2718Result<TxEnvelope>> + '_ {
74 self.txs().iter().map(|tx| TxEnvelope::decode_2718(&mut tx.chunk()))
75 }
76
77 pub fn decode_host_txs(&self) -> impl Iterator<Item = Eip2718Result<TxEnvelope>> + '_ {
81 self.host_txs.iter().map(|tx| TxEnvelope::decode_2718(&mut tx.chunk()))
82 }
83
84 pub fn recover_txs(
87 &self,
88 ) -> impl Iterator<Item = Result<Recovered<TxEnvelope>, BundleRecoverError>> + '_ {
89 self.decode_txs().enumerate().map(|(index, res)| match res {
90 Ok(tx) => {
91 tx.try_into_recovered().map_err(|err| BundleRecoverError::new(err, false, index))
92 }
93 Err(err) => Err(BundleRecoverError::new(err, false, index)),
94 })
95 }
96
97 pub fn recover_host_txs(
100 &self,
101 ) -> impl Iterator<Item = Result<Recovered<TxEnvelope>, BundleRecoverError>> + '_ {
102 self.decode_host_txs().enumerate().map(|(index, res)| match res {
103 Ok(tx) => {
104 tx.try_into_recovered().map_err(|err| BundleRecoverError::new(err, true, index))
105 }
106 Err(err) => Err(BundleRecoverError::new(err, true, index)),
107 })
108 }
109
110 pub fn try_into_recovered(self) -> Result<RecoveredBundle, BundleRecoverError> {
113 if self.txs().is_empty() {
114 return Err(BundleRecoverError::new(RecoverError::EmptyBundle, false, 0));
115 }
116
117 let txs = self.recover_txs().collect::<Result<Vec<_>, _>>()?;
118
119 let host_txs = self.recover_host_txs().collect::<Result<Vec<_>, _>>()?;
120
121 Ok(RecoveredBundle {
122 txs,
123 host_txs,
124 block_number: self.bundle.block_number,
125 min_timestamp: self.bundle.min_timestamp,
126 max_timestamp: self.bundle.max_timestamp,
127 reverting_tx_hashes: self.bundle.reverting_tx_hashes,
128 replacement_uuid: self.bundle.replacement_uuid,
129 dropping_tx_hashes: self.bundle.dropping_tx_hashes,
130 refund_percent: self.bundle.refund_percent,
131 refund_recipient: self.bundle.refund_recipient,
132 refund_tx_hashes: self.bundle.refund_tx_hashes,
133 extra_fields: self.bundle.extra_fields,
134 })
135 }
136
137 pub fn try_to_recovered(&self) -> Result<RecoveredBundle, BundleRecoverError> {
140 self.clone().try_into_recovered()
141 }
142
143 pub fn signers(&self) -> impl Iterator<Item = Option<(TxHash, Address)>> + '_ {
151 self.txs().iter().map(|tx| {
152 TxEnvelope::decode_2718(&mut tx.chunk())
153 .ok()
154 .and_then(|envelope| envelope.recover_signer().ok().map(|s| (*envelope.hash(), s)))
155 })
156 }
157
158 pub fn signers_lossy(&self) -> impl Iterator<Item = (TxHash, Address)> + '_ {
161 self.signers().flatten()
162 }
163
164 pub const fn block_number(&self) -> u64 {
166 self.bundle.block_number
167 }
168
169 pub const fn min_timestamp(&self) -> Option<u64> {
171 self.bundle.min_timestamp
172 }
173
174 pub const fn max_timestamp(&self) -> Option<u64> {
176 self.bundle.max_timestamp
177 }
178
179 pub const fn reverting_tx_hashes(&self) -> &[B256] {
181 self.bundle.reverting_tx_hashes.as_slice()
182 }
183
184 pub const fn replacement_uuid(&self) -> Option<&str> {
186 let Some(uuid) = &self.bundle.replacement_uuid else { return None };
187
188 Some(uuid.as_str())
189 }
190
191 pub fn is_valid_at_timestamp(&self, timestamp: u64) -> bool {
193 let min_timestamp = self.min_timestamp().unwrap_or(0);
194 let max_timestamp = self.max_timestamp().unwrap_or(u64::MAX);
195
196 (min_timestamp..=max_timestamp).contains(×tamp)
197 }
198
199 pub const fn is_valid_at_block_number(&self, block_number: u64) -> bool {
201 self.bundle.block_number == block_number
202 }
203
204 pub fn decode_and_validate_txs<Db: Database>(
206 &self,
207 ) -> Result<Vec<TxEnvelope>, BundleError<Db>> {
208 let txs = self
210 .decode_txs()
211 .collect::<Result<Vec<_>, _>>()
212 .map_err(|err| BundleError::TransactionDecodingError(err))?;
213
214 if txs.iter().any(|tx| tx.is_eip4844()) {
215 return Err(BundleError::UnsupportedTransactionType);
216 }
217
218 Ok(txs)
219 }
220
221 pub fn decode_and_validate_host_txs<Db: Database>(
223 &self,
224 ) -> Result<Vec<TxEnvelope>, BundleError<Db>> {
225 self.decode_host_txs()
226 .collect::<Result<Vec<_>, _>>()
227 .map_err(|err| BundleError::TransactionDecodingError(err))
228 }
229}
230
231#[cfg(test)]
232mod test {
233 use super::*;
234
235 #[test]
236 fn send_bundle_ser_roundtrip() {
237 let bundle = SignetEthBundle::new(
238 EthSendBundle {
239 txs: vec![b"tx1".into(), b"tx2".into()],
240 block_number: 1,
241 min_timestamp: Some(2),
242 max_timestamp: Some(3),
243 reverting_tx_hashes: vec![B256::repeat_byte(4), B256::repeat_byte(5)],
244 replacement_uuid: Some("uuid".to_owned()),
245 ..Default::default()
246 },
247 vec![b"host_tx1".into(), b"host_tx2".into()],
248 );
249
250 let serialized = serde_json::to_string(&bundle).unwrap();
251 let deserialized: SignetEthBundle = serde_json::from_str(&serialized).unwrap();
252
253 assert_eq!(bundle, deserialized);
254 }
255
256 #[test]
257 fn send_bundle_ser_roundtrip_no_host_no_fills() {
258 let bundle = SignetEthBundle::new(
259 EthSendBundle {
260 txs: vec![b"tx1".into(), b"tx2".into()],
261 block_number: 1,
262 min_timestamp: Some(2),
263 max_timestamp: Some(3),
264 reverting_tx_hashes: vec![B256::repeat_byte(4), B256::repeat_byte(5)],
265 replacement_uuid: Some("uuid".to_owned()),
266 ..Default::default()
267 },
268 vec![],
269 );
270
271 let serialized = serde_json::to_string(&bundle).unwrap();
272 let deserialized: SignetEthBundle = serde_json::from_str(&serialized).unwrap();
273
274 assert_eq!(bundle, deserialized);
275 }
276
277 #[test]
278 fn test_deser_bundle_no_host_no_fills() {
279 let json = r#"
280 {"txs":["0x747831","0x747832"],"blockNumber":"0x1","minTimestamp":2,"maxTimestamp":3,"revertingTxHashes":["0x0404040404040404040404040404040404040404040404040404040404040404","0x0505050505050505050505050505050505050505050505050505050505050505"],"replacementUuid":"uuid"}"#;
281
282 let deserialized: SignetEthBundle = serde_json::from_str(json).unwrap();
283
284 assert!(deserialized.host_txs.is_empty());
285 }
286
287 #[test]
291 #[ignore]
292 fn generate_eth_bundle_vectors() {
293 use alloy::primitives::Address;
294
295 let vectors = vec![
296 (
297 "minimal",
298 SignetEthBundle::new(
299 EthSendBundle {
300 txs: vec![b"\x02\xf8test_tx_1".into()],
301 block_number: 12345678,
302 ..Default::default()
303 },
304 vec![],
305 ),
306 ),
307 (
308 "with_timestamps",
309 SignetEthBundle::new(
310 EthSendBundle {
311 txs: vec![b"\x02\xf8test_tx_1".into()],
312 block_number: 12345678,
313 min_timestamp: Some(1700000000),
314 max_timestamp: Some(1700003600),
315 ..Default::default()
316 },
317 vec![],
318 ),
319 ),
320 (
321 "with_reverting_hashes",
322 SignetEthBundle::new(
323 EthSendBundle {
324 txs: vec![b"\x02\xf8test_tx_1".into(), b"\x02\xf8test_tx_2".into()],
325 block_number: 12345678,
326 reverting_tx_hashes: vec![B256::repeat_byte(0xab), B256::repeat_byte(0xcd)],
327 ..Default::default()
328 },
329 vec![],
330 ),
331 ),
332 (
333 "with_host_txs",
334 SignetEthBundle::new(
335 EthSendBundle {
336 txs: vec![b"\x02\xf8rollup_tx".into()],
337 block_number: 12345678,
338 ..Default::default()
339 },
340 vec![b"\x02\xf8host_tx_1".into(), b"\x02\xf8host_tx_2".into()],
341 ),
342 ),
343 (
344 "full_bundle",
345 SignetEthBundle::new(
346 EthSendBundle {
347 txs: vec![b"\x02\xf8tx_1".into(), b"\x02\xf8tx_2".into()],
348 block_number: 12345678,
349 min_timestamp: Some(1700000000),
350 max_timestamp: Some(1700003600),
351 reverting_tx_hashes: vec![B256::repeat_byte(0xef)],
352 dropping_tx_hashes: vec![B256::repeat_byte(0x11)],
353 refund_percent: Some(90),
354 refund_recipient: Some(Address::repeat_byte(0x22)),
355 refund_tx_hashes: vec![B256::repeat_byte(0x33)],
356 ..Default::default()
357 },
358 vec![b"\x02\xf8host_tx".into()],
359 ),
360 ),
361 (
362 "replacement_bundle",
363 SignetEthBundle::new(
364 EthSendBundle {
365 txs: vec![b"\x02\xf8replacement_tx".into()],
366 block_number: 12345678,
367 replacement_uuid: Some("550e8400-e29b-41d4-a716-446655440000".to_owned()),
368 ..Default::default()
369 },
370 vec![],
371 ),
372 ),
373 ];
374
375 let output: Vec<_> = vectors
376 .into_iter()
377 .map(|(name, bundle)| {
378 serde_json::json!({
379 "name": name,
380 "bundle": bundle,
381 })
382 })
383 .collect();
384
385 println!("// SignetEthBundle vectors\n{}", serde_json::to_string_pretty(&output).unwrap());
386 }
387}