1use crate::utils::{deserialize_optional_h160, deserialize_u256, deserialize_u64};
2use chrono::{DateTime, Utc};
3use ethers::core::{
4 types::{transaction::response::Transaction, Address, Bytes, TxHash, H256, U256, U64},
5 utils::keccak256,
6};
7use serde::{Deserialize, Serialize, Serializer};
8use uuid::Uuid;
9
10pub type BundleHash = H256;
12
13#[derive(Debug, Clone)]
15pub enum BundleTransaction {
16 Signed(Box<Transaction>),
18 Raw(Bytes),
20}
21
22impl From<Transaction> for BundleTransaction {
23 fn from(tx: Transaction) -> Self {
24 Self::Signed(Box::new(tx))
25 }
26}
27
28impl From<Bytes> for BundleTransaction {
29 fn from(tx: Bytes) -> Self {
30 Self::Raw(tx)
31 }
32}
33#[derive(Clone, Debug, Default, Serialize)]
49#[serde(rename_all = "camelCase")]
50pub struct BundleRequest {
51 #[serde(rename = "txs")]
52 #[serde(serialize_with = "serialize_txs")]
53 transactions: Vec<BundleTransaction>,
54 #[serde(rename = "revertingTxHashes")]
55 #[serde(skip_serializing_if = "Vec::is_empty")]
56 revertible_transaction_hashes: Vec<H256>,
57
58 #[serde(rename = "blockNumber")]
59 #[serde(skip_serializing_if = "Option::is_none")]
60 target_block: Option<U64>,
61
62 #[serde(skip_serializing_if = "Option::is_none")]
63 min_timestamp: Option<u64>,
64
65 #[serde(skip_serializing_if = "Option::is_none")]
66 max_timestamp: Option<u64>,
67
68 #[serde(rename = "replacementUuid")]
69 #[serde(skip_serializing_if = "Option::is_none")]
70 #[serde(serialize_with = "serialize_uuid_as_string")]
71 uuid: Option<Uuid>,
72
73 #[serde(rename = "stateBlockNumber")]
74 #[serde(skip_serializing_if = "Option::is_none")]
75 simulation_block: Option<U64>,
76
77 #[serde(skip_serializing_if = "Option::is_none")]
78 #[serde(rename = "timestamp")]
79 simulation_timestamp: Option<u64>,
80
81 #[serde(skip_serializing_if = "Option::is_none")]
82 #[serde(rename = "baseFee")]
83 simulation_basefee: Option<u64>,
84}
85
86fn serialize_uuid_as_string<S>(x: &Option<Uuid>, s: S) -> Result<S::Ok, S::Error>
87where
88 S: Serializer,
89{
90 s.serialize_str(&x.unwrap().to_string())
93}
94
95pub fn serialize_txs<S>(txs: &[BundleTransaction], s: S) -> Result<S::Ok, S::Error>
96where
97 S: Serializer,
98{
99 let raw_txs: Vec<Bytes> = txs
100 .iter()
101 .map(|tx| match tx {
102 BundleTransaction::Signed(inner) => inner.rlp(),
103 BundleTransaction::Raw(inner) => inner.clone(),
104 })
105 .collect();
106
107 raw_txs.serialize(s)
108}
109
110impl BundleRequest {
111 pub fn new() -> Self {
113 Default::default()
114 }
115
116 pub fn push_transaction<T: Into<BundleTransaction>>(mut self, tx: T) -> Self {
122 self.transactions.push(tx.into());
123 self
124 }
125
126 pub fn add_transaction<T: Into<BundleTransaction>>(&mut self, tx: T) {
133 self.transactions.push(tx.into());
134 }
135
136 pub fn push_revertible_transaction<T: Into<BundleTransaction>>(mut self, tx: T) -> Self {
141 let tx = tx.into();
142 self.transactions.push(tx.clone());
143
144 let tx_hash: H256 = match tx {
145 BundleTransaction::Signed(inner) => inner.hash(),
146 BundleTransaction::Raw(inner) => keccak256(inner).into(),
147 };
148 self.revertible_transaction_hashes.push(tx_hash);
149
150 self
151 }
152
153 pub fn add_revertible_transaction<T: Into<BundleTransaction>>(&mut self, tx: T) {
161 let tx = tx.into();
162 self.transactions.push(tx.clone());
163
164 let tx_hash: H256 = match tx {
165 BundleTransaction::Signed(inner) => inner.hash(),
166 BundleTransaction::Raw(inner) => keccak256(inner).into(),
167 };
168 self.revertible_transaction_hashes.push(tx_hash);
169 }
170
171 pub fn transactions(&self) -> &Vec<BundleTransaction> {
173 &self.transactions
174 }
175
176 pub fn transaction_hashes(&self) -> Vec<TxHash> {
178 self.transactions
179 .iter()
180 .map(|tx| match tx {
181 BundleTransaction::Signed(inner) => keccak256(inner.rlp()).into(),
182 BundleTransaction::Raw(inner) => keccak256(inner).into(),
183 })
184 .collect()
185 }
186
187 pub fn uuid(&self) -> &Option<Uuid> {
189 &self.uuid
190 }
191
192 pub fn set_uuid(mut self, uuid: Uuid) -> Self {
195 self.uuid = Some(uuid);
196 self
197 }
198
199 pub fn block(&self) -> Option<U64> {
201 self.target_block
202 }
203
204 pub fn set_block(mut self, block: U64) -> Self {
206 self.target_block = Some(block);
207 self
208 }
209
210 pub fn simulation_block(&self) -> Option<U64> {
217 self.simulation_block
218 }
219
220 pub fn set_simulation_block(mut self, block: U64) -> Self {
222 self.simulation_block = Some(block);
223 self
224 }
225
226 pub fn simulation_timestamp(&self) -> Option<u64> {
233 self.simulation_timestamp
234 }
235
236 pub fn set_simulation_timestamp(mut self, timestamp: u64) -> Self {
238 self.simulation_timestamp = Some(timestamp);
239 self
240 }
241
242 pub fn simulation_basefee(&self) -> Option<u64> {
249 self.simulation_basefee
250 }
251
252 pub fn set_simulation_basefee(mut self, basefee: u64) -> Self {
255 self.simulation_basefee = Some(basefee);
256 self
257 }
258
259 pub fn min_timestamp(&self) -> Option<u64> {
262 self.min_timestamp
263 }
264
265 pub fn set_min_timestamp(mut self, timestamp: u64) -> Self {
268 self.min_timestamp = Some(timestamp);
269 self
270 }
271
272 pub fn max_timestamp(&self) -> Option<u64> {
275 self.max_timestamp
276 }
277
278 pub fn set_max_timestamp(mut self, timestamp: u64) -> Self {
281 self.max_timestamp = Some(timestamp);
282 self
283 }
284}
285
286#[derive(Debug, Clone, Deserialize)]
291pub struct SimulatedTransaction {
292 #[serde(rename = "txHash")]
294 pub hash: H256,
295 #[serde(rename = "coinbaseDiff")]
299 #[serde(deserialize_with = "deserialize_u256")]
300 pub coinbase_diff: U256,
301 #[serde(rename = "ethSentToCoinbase")]
303 #[serde(deserialize_with = "deserialize_u256")]
304 pub coinbase_tip: U256,
305 #[serde(rename = "gasPrice")]
307 #[serde(deserialize_with = "deserialize_u256")]
308 pub gas_price: U256,
309 #[serde(rename = "gasUsed")]
311 #[serde(deserialize_with = "deserialize_u256")]
312 pub gas_used: U256,
313 #[serde(rename = "gasFees")]
315 #[serde(deserialize_with = "deserialize_u256")]
316 pub gas_fees: U256,
317 #[serde(rename = "fromAddress")]
319 pub from: Address,
320 #[serde(rename = "toAddress")]
325 #[serde(deserialize_with = "deserialize_optional_h160")]
326 pub to: Option<Address>,
327 pub value: Option<Bytes>,
329 pub error: Option<String>,
331 pub revert: Option<String>,
333}
334
335impl SimulatedTransaction {
336 pub fn effective_gas_price(&self) -> U256 {
339 self.coinbase_diff / self.gas_used
340 }
341}
342
343#[derive(Debug, Clone, Deserialize)]
347pub struct SimulatedBundle {
348 #[serde(rename = "bundleHash")]
350 pub hash: BundleHash,
351 #[serde(rename = "coinbaseDiff")]
355 #[serde(deserialize_with = "deserialize_u256")]
356 pub coinbase_diff: U256,
357 #[serde(rename = "ethSentToCoinbase")]
359 #[serde(deserialize_with = "deserialize_u256")]
360 pub coinbase_tip: U256,
361 #[serde(rename = "bundleGasPrice")]
363 #[serde(deserialize_with = "deserialize_u256")]
364 pub gas_price: U256,
365 #[serde(rename = "totalGasUsed")]
367 #[serde(deserialize_with = "deserialize_u256")]
368 pub gas_used: U256,
369 #[serde(rename = "gasFees")]
371 #[serde(deserialize_with = "deserialize_u256")]
372 pub gas_fees: U256,
373 #[serde(rename = "stateBlockNumber")]
375 #[serde(deserialize_with = "deserialize_u64")]
376 pub simulation_block: U64,
377 #[serde(rename = "results")]
379 pub transactions: Vec<SimulatedTransaction>,
380}
381
382impl SimulatedBundle {
383 pub fn effective_gas_price(&self) -> U256 {
389 self.coinbase_diff / self.gas_used
390 }
391}
392
393#[derive(Deserialize, Debug)]
399#[serde(rename_all = "camelCase")]
400pub struct BundleStats {
401 pub is_high_priority: bool,
403 pub is_simulated: bool,
405 pub simulated_at: Option<DateTime<Utc>>,
407 pub received_at: Option<DateTime<Utc>>,
409 #[serde(default = "Vec::new")]
411 pub considered_by_builders_at: Vec<BuilderEntry>,
412 #[serde(default = "Vec::new")]
414 pub sealed_by_builders_at: Vec<BuilderEntry>,
415}
416
417#[derive(Deserialize, Debug)]
420pub struct BuilderEntry {
421 pub pubkey: Bytes,
423 pub timestamp: Option<DateTime<Utc>>,
425}
426
427#[cfg(test)]
428mod tests {
429 use super::*;
430 use std::str::FromStr;
431 use uuid::uuid;
432
433 #[test]
434 fn bundle_serialize() {
435 let bundle = BundleRequest::new()
436 .push_transaction(Bytes::from(vec![0x1]))
437 .push_revertible_transaction(Bytes::from(vec![0x2]))
438 .set_block(2.into())
439 .set_min_timestamp(1000)
440 .set_max_timestamp(2000)
441 .set_simulation_timestamp(1000)
442 .set_simulation_block(1.into())
443 .set_simulation_basefee(333333);
444
445 assert_eq!(
446 &serde_json::to_string(&bundle).unwrap(),
447 r#"{"txs":["0x01","0x02"],"revertingTxHashes":["0xf2ee15ea639b73fa3db9b34a245bdfa015c260c598b211bf05a1ecc4b3e3b4f2"],"blockNumber":"0x2","minTimestamp":1000,"maxTimestamp":2000,"stateBlockNumber":"0x1","timestamp":1000,"baseFee":333333}"#
448 );
449 }
450
451 #[test]
452 fn bundle_serialize_add_transactions() {
453 let mut bundle = BundleRequest::new()
454 .push_transaction(Bytes::from(vec![0x1]))
455 .push_revertible_transaction(Bytes::from(vec![0x2]))
456 .set_block(2.into())
457 .set_min_timestamp(1000)
458 .set_max_timestamp(2000)
459 .set_simulation_timestamp(1000)
460 .set_simulation_block(1.into())
461 .set_simulation_basefee(333333)
462 .set_uuid(uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8"));
463
464 bundle.add_transaction(Bytes::from(vec![0x3]));
465 bundle.add_revertible_transaction(Bytes::from(vec![0x4]));
466
467 assert_eq!(
468 &serde_json::to_string(&bundle).unwrap(),
469 r#"{"txs":["0x01","0x02","0x03","0x04"],"revertingTxHashes":["0xf2ee15ea639b73fa3db9b34a245bdfa015c260c598b211bf05a1ecc4b3e3b4f2","0xf343681465b9efe82c933c3e8748c70cb8aa06539c361de20f72eac04e766393"],"blockNumber":"0x2","minTimestamp":1000,"maxTimestamp":2000,"replacementUuid":"67e55044-10b1-426f-9247-bb680e5fe0c8","stateBlockNumber":"0x1","timestamp":1000,"baseFee":333333}"#
470 );
471 }
472
473 #[test]
474 fn simulated_bundle_deserialize() {
475 let simulated_bundle: SimulatedBundle = serde_json::from_str(
476 r#"{
477 "bundleGasPrice": "476190476193",
478 "bundleHash": "0x73b1e258c7a42fd0230b2fd05529c5d4b6fcb66c227783f8bece8aeacdd1db2e",
479 "coinbaseDiff": "20000000000126000",
480 "ethSentToCoinbase": "20000000000000000",
481 "gasFees": "126000",
482 "results": [
483 {
484 "coinbaseDiff": "10000000000063000",
485 "ethSentToCoinbase": "10000000000000000",
486 "fromAddress": "0x02A727155aeF8609c9f7F2179b2a1f560B39F5A0",
487 "gasFees": "63000",
488 "gasPrice": "476190476193",
489 "gasUsed": 21000,
490 "toAddress": "0x73625f59CAdc5009Cb458B751b3E7b6b48C06f2C",
491 "txHash": "0x669b4704a7d993a946cdd6e2f95233f308ce0c4649d2e04944e8299efcaa098a",
492 "value": "0x",
493 "error": "execution reverted"
494 },
495 {
496 "coinbaseDiff": "10000000000063000",
497 "ethSentToCoinbase": "10000000000000000",
498 "fromAddress": "0x02A727155aeF8609c9f7F2179b2a1f560B39F5A0",
499 "gasFees": "63000",
500 "gasPrice": "476190476193",
501 "gasUsed": 21000,
502 "toAddress": "0x73625f59CAdc5009Cb458B751b3E7b6b48C06f2C",
503 "txHash": "0xa839ee83465657cac01adc1d50d96c1b586ed498120a84a64749c0034b4f19fa",
504 "value": "0x01"
505 },
506 {
507 "coinbaseDiff": "10000000000063000",
508 "ethSentToCoinbase": "10000000000000000",
509 "fromAddress": "0x02A727155aeF8609c9f7F2179b2a1f560B39F5A0",
510 "gasFees": "63000",
511 "gasPrice": "476190476193",
512 "gasUsed": 21000,
513 "toAddress": "0x",
514 "txHash": "0xa839ee83465657cac01adc1d50d96c1b586ed498120a84a64749c0034b4f19fa",
515 "value": "0x"
516 }
517 ],
518 "stateBlockNumber": 5221585,
519 "totalGasUsed": 42000
520 }"#,
521 )
522 .unwrap();
523
524 assert_eq!(
525 simulated_bundle.hash,
526 H256::from_str("0x73b1e258c7a42fd0230b2fd05529c5d4b6fcb66c227783f8bece8aeacdd1db2e")
527 .expect("could not deserialize hash")
528 );
529 assert_eq!(
530 simulated_bundle.coinbase_diff,
531 U256::from(20000000000126000u64)
532 );
533 assert_eq!(
534 simulated_bundle.coinbase_tip,
535 U256::from(20000000000000000u64)
536 );
537 assert_eq!(simulated_bundle.gas_price, U256::from(476190476193u64));
538 assert_eq!(simulated_bundle.gas_used, U256::from(42000));
539 assert_eq!(simulated_bundle.gas_fees, U256::from(126000));
540 assert_eq!(simulated_bundle.simulation_block, U64::from(5221585));
541 assert_eq!(simulated_bundle.transactions.len(), 3);
542 assert_eq!(
543 simulated_bundle.transactions[0].value,
544 Some(Bytes::from(vec![]))
545 );
546 assert_eq!(
547 simulated_bundle.transactions[0].error,
548 Some("execution reverted".into())
549 );
550 assert_eq!(simulated_bundle.transactions[1].error, None);
551 assert_eq!(
552 simulated_bundle.transactions[1].value,
553 Some(Bytes::from(vec![0x1]))
554 );
555 assert_eq!(simulated_bundle.transactions[2].to, None);
556 }
557
558 #[test]
559 fn simulated_transaction_deserialize() {
560 let tx: SimulatedTransaction = serde_json::from_str(
561 r#"{
562 "coinbaseDiff": "10000000000063000",
563 "ethSentToCoinbase": "10000000000000000",
564 "fromAddress": "0x02A727155aeF8609c9f7F2179b2a1f560B39F5A0",
565 "gasFees": "63000",
566 "gasPrice": "476190476193",
567 "gasUsed": 21000,
568 "toAddress": "0x",
569 "txHash": "0xa839ee83465657cac01adc1d50d96c1b586ed498120a84a64749c0034b4f19fa",
570 "error": "execution reverted"
571 }"#,
572 )
573 .unwrap();
574 assert_eq!(tx.error, Some("execution reverted".into()));
575
576 let tx: SimulatedTransaction = serde_json::from_str(
577 r#"{
578 "coinbaseDiff": "10000000000063000",
579 "ethSentToCoinbase": "10000000000000000",
580 "fromAddress": "0x02A727155aeF8609c9f7F2179b2a1f560B39F5A0",
581 "gasFees": "63000",
582 "gasPrice": "476190476193",
583 "gasUsed": 21000,
584 "toAddress": "0x",
585 "txHash": "0xa839ee83465657cac01adc1d50d96c1b586ed498120a84a64749c0034b4f19fa",
586 "error": "execution reverted",
587 "revert": "transfer failed"
588 }"#,
589 )
590 .unwrap();
591
592 assert_eq!(tx.error, Some("execution reverted".into()));
593 assert_eq!(tx.revert, Some("transfer failed".into()));
594 }
595
596 #[test]
597 fn bundle_stats_deserialize() {
598 let bundle_stats: BundleStats = serde_json::from_str(
599 r#"{
600 "isHighPriority": true,
601 "isSimulated": true,
602 "simulatedAt": "2022-10-06T21:36:06.317Z",
603 "receivedAt": "2022-10-06T21:36:06.250Z",
604 "consideredByBuildersAt": [{
605 "pubkey": "0x81babeec8c9f2bb9c329fd8a3b176032fe0ab5f3b92a3f44d4575a231c7bd9c31d10b6328ef68ed1e8c02a3dbc8e80f9",
606 "timestamp": "2022-10-06T21:36:06.343Z"
607 }, {
608 "pubkey": "0x81beef03aafd3dd33ffd7deb337407142c80fea2690e5b3190cfc01bde5753f28982a7857c96172a75a234cb7bcb994f",
609 "timestamp": "2022-10-06T21:36:06.394Z"
610 }, {
611 "pubkey": "0xa1dead1e65f0a0eee7b5170223f20c8f0cbf122eac3324d61afbdb33a8885ff8cab2ef514ac2c7698ae0d6289ef27fcf",
612 "timestamp": "2022-10-06T21:36:06.322Z"
613 }],
614 "sealedByBuildersAt": [{
615 "pubkey": "0x81beef03aafd3dd33ffd7deb337407142c80fea2690e5b3190cfc01bde5753f28982a7857c96172a75a234cb7bcb994f",
616 "timestamp": "2022-10-06T21:36:07.742Z"
617 }]
618 }"#,
619 )
620 .unwrap();
621
622 assert!(bundle_stats.is_high_priority);
623 assert!(bundle_stats.is_simulated);
624 assert_eq!(
625 bundle_stats.simulated_at.unwrap().to_rfc3339(),
626 "2022-10-06T21:36:06.317+00:00"
627 );
628 assert_eq!(
629 bundle_stats.received_at.unwrap().to_rfc3339(),
630 "2022-10-06T21:36:06.250+00:00"
631 );
632
633 assert_eq!(bundle_stats.considered_by_builders_at.len(), 3);
634 assert_eq!(bundle_stats.sealed_by_builders_at.len(), 1);
635 }
636}