kabu_broadcast_flashbots/client/
bundle.rs

1use std::fmt::{Display, Formatter};
2
3use alloy_consensus::TxEnvelope;
4use alloy_network::eip2718::Encodable2718;
5use alloy_network::TransactionResponse;
6use alloy_primitives::{keccak256, Address, Bytes, TxHash, U256, U64};
7use alloy_rpc_types::{AccessList, Log, Transaction};
8use eyre::Result;
9use serde::ser::Error as SerdeError;
10use serde::{Deserialize, Serialize, Serializer};
11
12use crate::client::utils::{deserialize_optional_h160, deserialize_u256, deserialize_u64};
13
14/// A bundle hash.
15pub type BundleHash = TxHash;
16
17/// A transaction that can be added to a bundle.
18#[derive(Debug, Clone)]
19pub enum BundleTransaction {
20    /// A pre-signed transaction.
21    Signed(Box<Transaction>),
22    /// An RLP encoded signed transaction.
23    Raw(Bytes),
24}
25
26impl From<TxEnvelope> for BundleTransaction {
27    fn from(tx: TxEnvelope) -> Self {
28        let rlp = tx.encoded_2718();
29        //Self::Signed(Bytes))
30        Self::Raw(Bytes::from(rlp))
31    }
32}
33
34impl From<Bytes> for BundleTransaction {
35    fn from(tx: Bytes) -> Self {
36        Self::Raw(tx)
37    }
38}
39
40/// A bundle that can be submitted to a Flashbots relay.
41///
42/// The bundle can include your own transactions and transactions from
43/// the mempool.
44///
45/// Additionally, this bundle can be simulated through a relay if simulation
46/// parameters are provided using [`BundleRequest::set_simulation_block`] and
47/// [`BundleRequest::set_simulation_timestamp`].
48///
49/// Please note that some parameters are required, and submitting a bundle
50/// without them will get it rejected pre-flight. The required parameters
51/// include:
52///
53/// - At least one transaction ([`BundleRequest::push_transaction`])
54/// - A target block ([`BundleRequest::set_target_block`])
55#[derive(Clone, Debug, Default, Serialize)]
56#[serde(rename_all = "camelCase")]
57pub struct BundleRequest {
58    #[serde(rename = "txs")]
59    #[serde(serialize_with = "serialize_txs")]
60    transactions: Vec<BundleTransaction>,
61    #[serde(rename = "revertingTxHashes")]
62    #[serde(skip_serializing_if = "Vec::is_empty")]
63    revertible_transaction_hashes: Vec<TxHash>,
64
65    #[serde(rename = "accessListHashList")]
66    #[serde(skip_serializing_if = "Option::is_none")]
67    access_list_hashes: Option<Vec<TxHash>>,
68
69    #[serde(rename = "blockNumber")]
70    #[serde(skip_serializing_if = "Option::is_none")]
71    target_block: Option<U64>,
72
73    #[serde(skip_serializing_if = "Option::is_none")]
74    min_timestamp: Option<u64>,
75
76    #[serde(skip_serializing_if = "Option::is_none")]
77    max_timestamp: Option<u64>,
78
79    #[serde(rename = "stateBlockNumber")]
80    #[serde(skip_serializing_if = "Option::is_none")]
81    simulation_block: Option<U64>,
82
83    #[serde(skip_serializing_if = "Option::is_none")]
84    #[serde(rename = "timestamp")]
85    simulation_timestamp: Option<u64>,
86
87    #[serde(skip_serializing_if = "Option::is_none")]
88    #[serde(rename = "baseFee")]
89    simulation_basefee: Option<u64>,
90}
91
92pub fn serialize_txs<S>(txs: &[BundleTransaction], s: S) -> Result<S::Ok, S::Error>
93where
94    S: Serializer,
95{
96    let raw_txs: Result<Vec<Bytes>> = txs
97        .iter()
98        .map(|tx| match tx {
99            BundleTransaction::Signed(inner) => {
100                let tx = inner.as_ref().clone();
101                Ok(Bytes::from(tx.inner.encoded_2718()))
102            }
103            BundleTransaction::Raw(inner) => Ok(inner.clone()),
104        })
105        .collect();
106
107    raw_txs.map_err(S::Error::custom)?.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: TxHash = match tx {
145            BundleTransaction::Signed(inner) => inner.tx_hash(),
146            BundleTransaction::Raw(inner) => keccak256(inner),
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: TxHash = match tx {
165            BundleTransaction::Signed(inner) => inner.tx_hash(),
166            BundleTransaction::Raw(inner) => keccak256(inner),
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    /*
177    /// Get a list of transaction hashes in the bundle request.
178    pub fn transaction_hashes(&self) -> Vec<TxHash> {
179        self.transactions
180            .iter()
181            .map(|tx| match tx {
182                BundleTransaction::Signed(inner) => keccak256(inner.as_ref().rlp()).into(),
183                BundleTransaction::Raw(inner) => keccak256(inner).into(),
184            })
185            .collect()
186    }
187
188     */
189
190    /// Get the target block (if any).
191    pub fn target_block(&self) -> Option<U64> {
192        self.target_block
193    }
194
195    /// Set the target block of the bundle.
196    pub fn set_target_block(mut self, target_block: U64) -> Self {
197        self.target_block = Some(target_block);
198        self
199    }
200
201    /// Get the block that determines the state for bundle simulation (if any).
202    ///
203    /// See [`eth_callBundle`][fb_call_bundle] in the Flashbots documentation
204    /// for more information on bundle simulations.
205    ///
206    /// [fb_call_bundle]: https://docs.flashbots.net/flashbots-auction/searchers/advanced/rpc-endpoint#eth_callbundle
207    pub fn simulation_block(&self) -> Option<U64> {
208        self.simulation_block
209    }
210
211    /// Set the block that determines the state for bundle simulation.
212    pub fn set_simulation_block(mut self, block: U64) -> Self {
213        self.simulation_block = Some(block);
214        self
215    }
216
217    pub fn set_access_list_hashes(mut self, hashes: Option<Vec<TxHash>>) -> Self {
218        self.access_list_hashes = hashes;
219        self
220    }
221
222    /// Get the UNIX timestamp used for bundle simulation (if any).
223    ///
224    /// See [`eth_callBundle`][fb_call_bundle] in the Flashbots documentation
225    /// for more information on bundle simulations.
226    ///
227    /// [fb_call_bundle]: https://docs.flashbots.net/flashbots-auction/searchers/advanced/rpc-endpoint#eth_callbundle
228    pub fn simulation_timestamp(&self) -> Option<u64> {
229        self.simulation_timestamp
230    }
231
232    /// Set the UNIX timestamp used for bundle simulation.
233    pub fn set_simulation_timestamp(mut self, timestamp: u64) -> Self {
234        self.simulation_timestamp = Some(timestamp);
235        self
236    }
237
238    /// Get the base gas fee for bundle simulation (if any).
239    ///
240    /// See [`eth_callBundle`][fb_call_bundle] in the Flashbots documentation
241    /// for more information on bundle simulations.
242    ///
243    /// [fb_call_bundle]: https://docs.flashbots.net/flashbots-auction/searchers/advanced/rpc-endpoint#eth_callbundle
244    pub fn simulation_basefee(&self) -> Option<u64> {
245        self.simulation_basefee
246    }
247
248    /// Set the base gas fee for bundle simulation (if any).
249    /// Optional: will default to a value chosen by the node if not specified.
250    pub fn set_simulation_basefee(mut self, basefee: u64) -> Self {
251        self.simulation_basefee = Some(basefee);
252        self
253    }
254
255    /// Get the minimum timestamp for which this bundle is valid (if any),
256    /// in seconds since the UNIX epoch.
257    pub fn min_timestamp(&self) -> Option<u64> {
258        self.min_timestamp
259    }
260
261    /// Set the minimum timestamp for which this bundle is valid (if any),
262    /// in seconds since the UNIX epoch.
263    pub fn set_min_timestamp(mut self, timestamp: u64) -> Self {
264        self.min_timestamp = Some(timestamp);
265        self
266    }
267
268    /// Get the maximum timestamp for which this bundle is valid (if any),
269    /// in seconds since the UNIX epoch.
270    pub fn max_timestamp(&self) -> Option<u64> {
271        self.max_timestamp
272    }
273
274    /// Set the maximum timestamp for which this bundle is valid (if any),
275    /// in seconds since the UNIX epoch.
276    pub fn set_max_timestamp(mut self, timestamp: u64) -> Self {
277        self.max_timestamp = Some(timestamp);
278        self
279    }
280}
281
282/// Details of a simulated transaction.
283///
284/// Details for a transaction that has been simulated as part of
285/// a bundle.
286#[derive(Debug, Clone, Deserialize, Serialize)]
287pub struct SimulatedTransaction {
288    /// The transaction hash
289    #[serde(rename = "txHash")]
290    pub hash: TxHash,
291    /// The difference in coinbase's balance due to this transaction.
292    ///
293    /// This includes tips and gas fees for this transaction.
294    #[serde(rename = "coinbaseDiff")]
295    #[serde(deserialize_with = "deserialize_u256")]
296    pub coinbase_diff: U256,
297    /// The amount of Eth sent to coinbase in this transaction.
298    #[serde(rename = "ethSentToCoinbase")]
299    #[serde(deserialize_with = "deserialize_u256")]
300    pub coinbase_tip: U256,
301    /// The gas price.
302    #[serde(rename = "gasPrice")]
303    #[serde(deserialize_with = "deserialize_u256")]
304    pub gas_price: U256,
305    /// The amount of gas used in this transaction.
306    #[serde(rename = "gasUsed")]
307    #[serde(deserialize_with = "deserialize_u256")]
308    pub gas_used: U256,
309    /// The total gas fees for this transaction.
310    #[serde(rename = "gasFees")]
311    #[serde(deserialize_with = "deserialize_u256")]
312    pub gas_fees: U256,
313    /// The origin of this transaction.
314    #[serde(rename = "fromAddress")]
315    pub from: Address,
316    /// The destination of this transaction.
317    ///
318    /// If this is `None`, then the transaction was to a newly
319    /// deployed contract.
320    #[serde(rename = "toAddress")]
321    #[serde(deserialize_with = "deserialize_optional_h160")]
322    pub to: Option<Address>,
323    /// The return value of the transaction.
324    pub value: Option<Bytes>,
325    /// The reason this transaction failed (if it did).
326    pub error: Option<String>,
327    /// The revert reason for this transaction, if available.
328    pub revert: Option<String>,
329
330    #[serde(rename = "accessList")]
331    pub access_list: Option<AccessList>,
332    #[serde(rename = "logs")]
333    pub logs: Option<Vec<Log>>,
334}
335
336impl Display for SimulatedTransaction {
337    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
338        write!(
339            f,
340            "{:#20x}->{:20x} {} Gas : {}  CB : {} {}",
341            self.from,
342            self.to.unwrap_or_default(),
343            self.hash,
344            self.gas_used,
345            self.coinbase_tip,
346            self.coinbase_diff,
347        )
348    }
349}
350
351impl SimulatedTransaction {
352    /// The effective gas price of the transaction,
353    /// i.e. `coinbase_diff / gas_used`.
354    pub fn effective_gas_price(&self) -> U256 {
355        self.coinbase_diff / self.gas_used
356    }
357}
358
359/// Details of a simulated bundle.
360///
361/// The details of a bundle that has been simulated.
362#[derive(Debug, Clone, Deserialize, Serialize)]
363pub struct SimulatedBundle {
364    /// The bundle's hash.
365    #[serde(rename = "bundleHash")]
366    pub hash: BundleHash,
367    /// The difference in coinbase's balance due to this bundle.
368    ///
369    /// This includes total gas fees and coinbase tips.
370    #[serde(rename = "coinbaseDiff")]
371    #[serde(deserialize_with = "deserialize_u256")]
372    pub coinbase_diff: U256,
373    /// The amount of Eth sent to coinbase in this bundle.
374    #[serde(rename = "ethSentToCoinbase")]
375    #[serde(deserialize_with = "deserialize_u256")]
376    pub coinbase_tip: U256,
377    /// The gas price of the bundle.
378    #[serde(rename = "bundleGasPrice")]
379    #[serde(deserialize_with = "deserialize_u256")]
380    pub gas_price: U256,
381    /// The total amount of gas used in this bundle.
382    #[serde(rename = "totalGasUsed")]
383    #[serde(deserialize_with = "deserialize_u256")]
384    pub gas_used: U256,
385    /// The total amount of gas fees in this bundle.
386    #[serde(rename = "gasFees")]
387    #[serde(deserialize_with = "deserialize_u256")]
388    pub gas_fees: U256,
389    /// The block at which this bundle was simulated.
390    #[serde(rename = "stateBlockNumber")]
391    #[serde(deserialize_with = "deserialize_u64")]
392    pub simulation_block: U64,
393    /// The simulated transactions in this bundle.
394    #[serde(rename = "results")]
395    pub transactions: Vec<SimulatedTransaction>,
396}
397
398impl SimulatedBundle {
399    /// The effective gas price of the transaction,
400    /// i.e. `coinbase_diff / gas_used`.
401    ///
402    /// Note that this is also an approximation of the
403    /// bundle's score.
404    pub fn effective_gas_price(&self) -> U256 {
405        self.coinbase_diff / self.gas_used
406    }
407
408    pub fn find_tx(&self, tx_hash: TxHash) -> Option<&SimulatedTransaction> {
409        self.transactions.iter().find(|&item| item.hash == tx_hash)
410    }
411}
412
413#[cfg(test)]
414mod tests {
415    use std::str::FromStr;
416
417    use super::*;
418
419    #[test]
420    fn bundle_serialize() {
421        let bundle = BundleRequest::new()
422            .push_transaction(Bytes::from(vec![0x1]))
423            .push_revertible_transaction(Bytes::from(vec![0x2]))
424            .set_target_block(U64::from(2))
425            .set_min_timestamp(1000)
426            .set_max_timestamp(2000)
427            .set_simulation_timestamp(1000)
428            .set_simulation_block(U64::from(1))
429            .set_simulation_basefee(333333);
430
431        assert_eq!(
432            &serde_json::to_string(&bundle).unwrap(),
433            r#"{"txs":["0x01","0x02"],"revertingTxHashes":["0xf2ee15ea639b73fa3db9b34a245bdfa015c260c598b211bf05a1ecc4b3e3b4f2"],"blockNumber":"0x2","minTimestamp":1000,"maxTimestamp":2000,"stateBlockNumber":"0x1","timestamp":1000,"baseFee":333333}"#
434        );
435    }
436
437    #[test]
438    fn bundle_serialize_add_transactions() {
439        let mut bundle = BundleRequest::new()
440            .push_transaction(Bytes::from(vec![0x1]))
441            .push_revertible_transaction(Bytes::from(vec![0x2]))
442            .set_target_block(U64::from(2))
443            .set_min_timestamp(1000)
444            .set_max_timestamp(2000)
445            .set_simulation_timestamp(1000)
446            .set_simulation_block(U64::from(1))
447            .set_simulation_basefee(333333);
448
449        bundle.add_transaction(Bytes::from(vec![0x3]));
450        bundle.add_revertible_transaction(Bytes::from(vec![0x4]));
451
452        assert_eq!(
453            &serde_json::to_string(&bundle).unwrap(),
454            r#"{"txs":["0x01","0x02","0x03","0x04"],"revertingTxHashes":["0xf2ee15ea639b73fa3db9b34a245bdfa015c260c598b211bf05a1ecc4b3e3b4f2","0xf343681465b9efe82c933c3e8748c70cb8aa06539c361de20f72eac04e766393"],"blockNumber":"0x2","minTimestamp":1000,"maxTimestamp":2000,"stateBlockNumber":"0x1","timestamp":1000,"baseFee":333333}"#
455        );
456    }
457
458    #[test]
459    fn simulated_bundle_deserialize() {
460        let simulated_bundle: SimulatedBundle = serde_json::from_str(
461            r#"{
462    "bundleGasPrice": "476190476193",
463    "bundleHash": "0x73b1e258c7a42fd0230b2fd05529c5d4b6fcb66c227783f8bece8aeacdd1db2e",
464    "coinbaseDiff": "20000000000126000",
465    "ethSentToCoinbase": "20000000000000000",
466    "gasFees": "126000",
467    "results": [
468      {
469        "coinbaseDiff": "10000000000063000",
470        "ethSentToCoinbase": "10000000000000000",
471        "fromAddress": "0x02A727155aeF8609c9f7F2179b2a1f560B39F5A0",
472        "gasFees": "63000",
473        "gasPrice": "476190476193",
474        "gasUsed": 21000,
475        "toAddress": "0x73625f59CAdc5009Cb458B751b3E7b6b48C06f2C",
476        "txHash": "0x669b4704a7d993a946cdd6e2f95233f308ce0c4649d2e04944e8299efcaa098a",
477        "value": "0x",
478        "error": "execution reverted"
479      },
480      {
481        "coinbaseDiff": "10000000000063000",
482        "ethSentToCoinbase": "10000000000000000",
483        "fromAddress": "0x02A727155aeF8609c9f7F2179b2a1f560B39F5A0",
484        "gasFees": "63000",
485        "gasPrice": "476190476193",
486        "gasUsed": 21000,
487        "toAddress": "0x73625f59CAdc5009Cb458B751b3E7b6b48C06f2C",
488        "txHash": "0xa839ee83465657cac01adc1d50d96c1b586ed498120a84a64749c0034b4f19fa",
489        "value": "0x01"
490      },
491      {
492        "coinbaseDiff": "10000000000063000",
493        "ethSentToCoinbase": "10000000000000000",
494        "fromAddress": "0x02A727155aeF8609c9f7F2179b2a1f560B39F5A0",
495        "gasFees": "63000",
496        "gasPrice": "476190476193",
497        "gasUsed": 21000,
498        "toAddress": "0x",
499        "txHash": "0xa839ee83465657cac01adc1d50d96c1b586ed498120a84a64749c0034b4f19fa",
500        "value": "0x"
501      }
502    ],
503    "stateBlockNumber": 5221585,
504    "totalGasUsed": 42000
505  }"#,
506        )
507        .unwrap();
508
509        assert_eq!(
510            simulated_bundle.hash,
511            TxHash::from_str("0x73b1e258c7a42fd0230b2fd05529c5d4b6fcb66c227783f8bece8aeacdd1db2e").expect("could not deserialize hash")
512        );
513        assert_eq!(simulated_bundle.coinbase_diff, U256::from(20000000000126000u64));
514        assert_eq!(simulated_bundle.coinbase_tip, U256::from(20000000000000000u64));
515        assert_eq!(simulated_bundle.gas_price, U256::from(476190476193u64));
516        assert_eq!(simulated_bundle.gas_used, U256::from(42000));
517        assert_eq!(simulated_bundle.gas_fees, U256::from(126000));
518        assert_eq!(simulated_bundle.simulation_block, U64::from(5221585));
519        assert_eq!(simulated_bundle.transactions.len(), 3);
520        assert_eq!(simulated_bundle.transactions[0].value, Some(Bytes::from(vec![])));
521        assert_eq!(simulated_bundle.transactions[0].error, Some("execution reverted".into()));
522        assert_eq!(simulated_bundle.transactions[1].error, None);
523        assert_eq!(simulated_bundle.transactions[1].value, Some(Bytes::from(vec![0x1])));
524        assert_eq!(simulated_bundle.transactions[2].to, None);
525    }
526
527    #[test]
528    fn simulated_transaction_deserialize() {
529        let tx: SimulatedTransaction = serde_json::from_str(
530            r#"{
531        "coinbaseDiff": "10000000000063000",
532        "ethSentToCoinbase": "10000000000000000",
533        "fromAddress": "0x02A727155aeF8609c9f7F2179b2a1f560B39F5A0",
534        "gasFees": "63000",
535        "gasPrice": "476190476193",
536        "gasUsed": 21000,
537        "toAddress": "0x",
538        "txHash": "0xa839ee83465657cac01adc1d50d96c1b586ed498120a84a64749c0034b4f19fa",
539        "error": "execution reverted"
540      }"#,
541        )
542        .unwrap();
543        assert_eq!(tx.error, Some("execution reverted".into()));
544
545        let tx: SimulatedTransaction = serde_json::from_str(
546            r#"{
547        "coinbaseDiff": "10000000000063000",
548        "ethSentToCoinbase": "10000000000000000",
549        "fromAddress": "0x02A727155aeF8609c9f7F2179b2a1f560B39F5A0",
550        "gasFees": "63000",
551        "gasPrice": "476190476193",
552        "gasUsed": 21000,
553        "toAddress": "0x",
554        "txHash": "0xa839ee83465657cac01adc1d50d96c1b586ed498120a84a64749c0034b4f19fa",
555        "error": "execution reverted",
556        "revert": "transfer failed"
557      }"#,
558        )
559        .unwrap();
560
561        assert_eq!(tx.error, Some("execution reverted".into()));
562        assert_eq!(tx.revert, Some("transfer failed".into()));
563    }
564}