ethers_flashbots/
bundle.rs

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
10/// A bundle hash.
11pub type BundleHash = H256;
12
13/// A transaction that can be added to a bundle.
14#[derive(Debug, Clone)]
15pub enum BundleTransaction {
16    /// A pre-signed transaction.
17    Signed(Box<Transaction>),
18    /// An RLP encoded signed transaction.
19    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/// A bundle that can be submitted to a Flashbots relay.
34///
35/// The bundle can include your own transactions and transactions from
36/// the mempool.
37///
38/// Additionally, this bundle can be simulated through a relay if simulation
39/// parameters are provided using [`BundleRequest::set_simulation_block`] and
40/// [`BundleRequest::set_simulation_timestamp`].
41///
42/// Please note that some parameters are required, and submitting a bundle
43/// without them will get it rejected pre-flight. The required parameters
44/// include:
45///
46/// - At least one transaction ([`BundleRequest::push_transaction`])
47/// - A target block ([`BundleRequest::set_block`])
48#[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    // Don't need to handle None option here as handled by
91    // #[serde(skip_serializing_if = "Option::is_none")]
92    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    /// Creates an empty bundle request.
112    pub fn new() -> Self {
113        Default::default()
114    }
115
116    /// Adds a transaction to the bundle request.
117    ///
118    /// Transactions added to the bundle can either be novel transactions,
119    /// i.e. transactions that you have crafted, or they can be from
120    /// one of the mempool APIs.
121    pub fn push_transaction<T: Into<BundleTransaction>>(mut self, tx: T) -> Self {
122        self.transactions.push(tx.into());
123        self
124    }
125
126    /// Adds a transaction to the bundle request.
127    ///
128    /// This function takes a mutable reference to `self` and adds the specified
129    /// transaction to the `transactions` vector. The added transaction can either
130    /// be a novel transaction that you have crafted, or it can be from one of the
131    /// mempool APIs.
132    pub fn add_transaction<T: Into<BundleTransaction>>(&mut self, tx: T) {
133        self.transactions.push(tx.into());
134    }
135
136    /// Adds a revertible transaction to the bundle request.
137    ///
138    /// This differs from [`BundleRequest::push_transaction`] in that the bundle will still be
139    /// considered valid if the transaction reverts.
140    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    /// Adds a revertible transaction to the bundle request.
154    ///
155    /// This function takes a mutable reference to `self` and adds the specified
156    /// revertible transaction to the `transactions` vector. The added transaction can either
157    /// be a novel transaction that you have crafted, or it can be from one of the
158    /// mempool APIs. Unlike the `push_transaction` method, the bundle will still be considered
159    /// valid even if the added transaction reverts.
160    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    /// Get a reference to the transactions currently in the bundle request.
172    pub fn transactions(&self) -> &Vec<BundleTransaction> {
173        &self.transactions
174    }
175
176    /// Get a list of transaction hashes in the bundle request.
177    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    /// Get a reference to the replacement uuid (if any).
188    pub fn uuid(&self) -> &Option<Uuid> {
189        &self.uuid
190    }
191
192    /// Set the replacement uuid of the bundle.
193    /// This is used for bundle replacements or cancellations using eth_cancelBundle
194    pub fn set_uuid(mut self, uuid: Uuid) -> Self {
195        self.uuid = Some(uuid);
196        self
197    }
198
199    /// Get the target block (if any).
200    pub fn block(&self) -> Option<U64> {
201        self.target_block
202    }
203
204    /// Set the target block of the bundle.
205    pub fn set_block(mut self, block: U64) -> Self {
206        self.target_block = Some(block);
207        self
208    }
209
210    /// Get the block that determines the state for bundle simulation (if any).
211    ///
212    /// See [`eth_callBundle`][fb_call_bundle] in the Flashbots documentation
213    /// for more information on bundle simulations.
214    ///
215    /// [fb_call_bundle]: https://docs.flashbots.net/flashbots-auction/searchers/advanced/rpc-endpoint#eth_callbundle
216    pub fn simulation_block(&self) -> Option<U64> {
217        self.simulation_block
218    }
219
220    /// Set the block that determines the state for bundle simulation.
221    pub fn set_simulation_block(mut self, block: U64) -> Self {
222        self.simulation_block = Some(block);
223        self
224    }
225
226    /// Get the UNIX timestamp used for bundle simulation (if any).
227    ///
228    /// See [`eth_callBundle`][fb_call_bundle] in the Flashbots documentation
229    /// for more information on bundle simulations.
230    ///
231    /// [fb_call_bundle]: https://docs.flashbots.net/flashbots-auction/searchers/advanced/rpc-endpoint#eth_callbundle
232    pub fn simulation_timestamp(&self) -> Option<u64> {
233        self.simulation_timestamp
234    }
235
236    /// Set the UNIX timestamp used for bundle simulation.
237    pub fn set_simulation_timestamp(mut self, timestamp: u64) -> Self {
238        self.simulation_timestamp = Some(timestamp);
239        self
240    }
241
242    /// Get the base gas fee for bundle simulation (if any).
243    ///
244    /// See [`eth_callBundle`][fb_call_bundle] in the Flashbots documentation
245    /// for more information on bundle simulations.
246    ///
247    /// [fb_call_bundle]: https://docs.flashbots.net/flashbots-auction/searchers/advanced/rpc-endpoint#eth_callbundle
248    pub fn simulation_basefee(&self) -> Option<u64> {
249        self.simulation_basefee
250    }
251
252    /// Set the base gas fee for bundle simulation (if any).
253    /// Optional: will default to a value chosen by the node if not specified.
254    pub fn set_simulation_basefee(mut self, basefee: u64) -> Self {
255        self.simulation_basefee = Some(basefee);
256        self
257    }
258
259    /// Get the minimum timestamp for which this bundle is valid (if any),
260    /// in seconds since the UNIX epoch.
261    pub fn min_timestamp(&self) -> Option<u64> {
262        self.min_timestamp
263    }
264
265    /// Set the minimum timestamp for which this bundle is valid (if any),
266    /// in seconds since the UNIX epoch.
267    pub fn set_min_timestamp(mut self, timestamp: u64) -> Self {
268        self.min_timestamp = Some(timestamp);
269        self
270    }
271
272    /// Get the maximum timestamp for which this bundle is valid (if any),
273    /// in seconds since the UNIX epoch.
274    pub fn max_timestamp(&self) -> Option<u64> {
275        self.max_timestamp
276    }
277
278    /// Set the maximum timestamp for which this bundle is valid (if any),
279    /// in seconds since the UNIX epoch.
280    pub fn set_max_timestamp(mut self, timestamp: u64) -> Self {
281        self.max_timestamp = Some(timestamp);
282        self
283    }
284}
285
286/// Details of a simulated transaction.
287///
288/// Details for a transaction that has been simulated as part of
289/// a bundle.
290#[derive(Debug, Clone, Deserialize)]
291pub struct SimulatedTransaction {
292    /// The transaction hash
293    #[serde(rename = "txHash")]
294    pub hash: H256,
295    /// The difference in coinbase's balance due to this transaction.
296    ///
297    /// This includes tips and gas fees for this transaction.
298    #[serde(rename = "coinbaseDiff")]
299    #[serde(deserialize_with = "deserialize_u256")]
300    pub coinbase_diff: U256,
301    /// The amount of Eth sent to coinbase in this transaction.
302    #[serde(rename = "ethSentToCoinbase")]
303    #[serde(deserialize_with = "deserialize_u256")]
304    pub coinbase_tip: U256,
305    /// The gas price.
306    #[serde(rename = "gasPrice")]
307    #[serde(deserialize_with = "deserialize_u256")]
308    pub gas_price: U256,
309    /// The amount of gas used in this transaction.
310    #[serde(rename = "gasUsed")]
311    #[serde(deserialize_with = "deserialize_u256")]
312    pub gas_used: U256,
313    /// The total gas fees for this transaction.
314    #[serde(rename = "gasFees")]
315    #[serde(deserialize_with = "deserialize_u256")]
316    pub gas_fees: U256,
317    /// The origin of this transaction.
318    #[serde(rename = "fromAddress")]
319    pub from: Address,
320    /// The destination of this transaction.
321    ///
322    /// If this is `None`, then the transaction was to a newly
323    /// deployed contract.
324    #[serde(rename = "toAddress")]
325    #[serde(deserialize_with = "deserialize_optional_h160")]
326    pub to: Option<Address>,
327    /// The return value of the transaction.
328    pub value: Option<Bytes>,
329    /// The reason this transaction failed (if it did).
330    pub error: Option<String>,
331    /// The revert reason for this transaction, if available.
332    pub revert: Option<String>,
333}
334
335impl SimulatedTransaction {
336    /// The effective gas price of the transaction,
337    /// i.e. `coinbase_diff / gas_used`.
338    pub fn effective_gas_price(&self) -> U256 {
339        self.coinbase_diff / self.gas_used
340    }
341}
342
343/// Details of a simulated bundle.
344///
345/// The details of a bundle that has been simulated.
346#[derive(Debug, Clone, Deserialize)]
347pub struct SimulatedBundle {
348    /// The bundle's hash.
349    #[serde(rename = "bundleHash")]
350    pub hash: BundleHash,
351    /// The difference in coinbase's balance due to this bundle.
352    ///
353    /// This includes total gas fees and coinbase tips.
354    #[serde(rename = "coinbaseDiff")]
355    #[serde(deserialize_with = "deserialize_u256")]
356    pub coinbase_diff: U256,
357    /// The amount of Eth sent to coinbase in this bundle.
358    #[serde(rename = "ethSentToCoinbase")]
359    #[serde(deserialize_with = "deserialize_u256")]
360    pub coinbase_tip: U256,
361    /// The gas price of the bundle.
362    #[serde(rename = "bundleGasPrice")]
363    #[serde(deserialize_with = "deserialize_u256")]
364    pub gas_price: U256,
365    /// The total amount of gas used in this bundle.
366    #[serde(rename = "totalGasUsed")]
367    #[serde(deserialize_with = "deserialize_u256")]
368    pub gas_used: U256,
369    /// The total amount of gas fees in this bundle.
370    #[serde(rename = "gasFees")]
371    #[serde(deserialize_with = "deserialize_u256")]
372    pub gas_fees: U256,
373    /// The block at which this bundle was simulated.
374    #[serde(rename = "stateBlockNumber")]
375    #[serde(deserialize_with = "deserialize_u64")]
376    pub simulation_block: U64,
377    /// The simulated transactions in this bundle.
378    #[serde(rename = "results")]
379    pub transactions: Vec<SimulatedTransaction>,
380}
381
382impl SimulatedBundle {
383    /// The effective gas price of the transaction,
384    /// i.e. `coinbase_diff / gas_used`.
385    ///
386    /// Note that this is also an approximation of the
387    /// bundle's score.
388    pub fn effective_gas_price(&self) -> U256 {
389        self.coinbase_diff / self.gas_used
390    }
391}
392
393/// Represents stats for a submitted bundle.
394///
395/// See [Flashbots docs][fb_getbundlestats] for more information.
396///
397/// [fb_getbundlestats]: https://docs.flashbots.net/flashbots-auction/searchers/advanced/rpc-endpoint/#flashbots_getbundlestats
398#[derive(Deserialize, Debug)]
399#[serde(rename_all = "camelCase")]
400pub struct BundleStats {
401    /// Whether the bundle is high priority.
402    pub is_high_priority: bool,
403    /// Whether the bundle was simulated.
404    pub is_simulated: bool,
405    /// When the bundle was simulated
406    pub simulated_at: Option<DateTime<Utc>>,
407    /// When the bundle was received by the bundle API.
408    pub received_at: Option<DateTime<Utc>>,
409    /// A list of times at which builders selected the bundle to be included in the target block.
410    #[serde(default = "Vec::new")]
411    pub considered_by_builders_at: Vec<BuilderEntry>,
412    /// A list of times at which builders sealed a block containing the bundle.
413    #[serde(default = "Vec::new")]
414    pub sealed_by_builders_at: Vec<BuilderEntry>,
415}
416
417/// A builder log entry is a pairing of a builder's public key and a timestamp at which they
418/// performed some operation on a bundle.
419#[derive(Deserialize, Debug)]
420pub struct BuilderEntry {
421    /// The public key of the builder.
422    pub pubkey: Bytes,
423    /// The timestamp of this log entry.
424    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}