kabu_broadcast_flashbots/client/
bundle.rs1use 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
14pub type BundleHash = TxHash;
16
17#[derive(Debug, Clone)]
19pub enum BundleTransaction {
20 Signed(Box<Transaction>),
22 Raw(Bytes),
24}
25
26impl From<TxEnvelope> for BundleTransaction {
27 fn from(tx: TxEnvelope) -> Self {
28 let rlp = tx.encoded_2718();
29 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#[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 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: 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 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 pub fn transactions(&self) -> &Vec<BundleTransaction> {
173 &self.transactions
174 }
175
176 pub fn target_block(&self) -> Option<U64> {
192 self.target_block
193 }
194
195 pub fn set_target_block(mut self, target_block: U64) -> Self {
197 self.target_block = Some(target_block);
198 self
199 }
200
201 pub fn simulation_block(&self) -> Option<U64> {
208 self.simulation_block
209 }
210
211 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 pub fn simulation_timestamp(&self) -> Option<u64> {
229 self.simulation_timestamp
230 }
231
232 pub fn set_simulation_timestamp(mut self, timestamp: u64) -> Self {
234 self.simulation_timestamp = Some(timestamp);
235 self
236 }
237
238 pub fn simulation_basefee(&self) -> Option<u64> {
245 self.simulation_basefee
246 }
247
248 pub fn set_simulation_basefee(mut self, basefee: u64) -> Self {
251 self.simulation_basefee = Some(basefee);
252 self
253 }
254
255 pub fn min_timestamp(&self) -> Option<u64> {
258 self.min_timestamp
259 }
260
261 pub fn set_min_timestamp(mut self, timestamp: u64) -> Self {
264 self.min_timestamp = Some(timestamp);
265 self
266 }
267
268 pub fn max_timestamp(&self) -> Option<u64> {
271 self.max_timestamp
272 }
273
274 pub fn set_max_timestamp(mut self, timestamp: u64) -> Self {
277 self.max_timestamp = Some(timestamp);
278 self
279 }
280}
281
282#[derive(Debug, Clone, Deserialize, Serialize)]
287pub struct SimulatedTransaction {
288 #[serde(rename = "txHash")]
290 pub hash: TxHash,
291 #[serde(rename = "coinbaseDiff")]
295 #[serde(deserialize_with = "deserialize_u256")]
296 pub coinbase_diff: U256,
297 #[serde(rename = "ethSentToCoinbase")]
299 #[serde(deserialize_with = "deserialize_u256")]
300 pub coinbase_tip: U256,
301 #[serde(rename = "gasPrice")]
303 #[serde(deserialize_with = "deserialize_u256")]
304 pub gas_price: U256,
305 #[serde(rename = "gasUsed")]
307 #[serde(deserialize_with = "deserialize_u256")]
308 pub gas_used: U256,
309 #[serde(rename = "gasFees")]
311 #[serde(deserialize_with = "deserialize_u256")]
312 pub gas_fees: U256,
313 #[serde(rename = "fromAddress")]
315 pub from: Address,
316 #[serde(rename = "toAddress")]
321 #[serde(deserialize_with = "deserialize_optional_h160")]
322 pub to: Option<Address>,
323 pub value: Option<Bytes>,
325 pub error: Option<String>,
327 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 pub fn effective_gas_price(&self) -> U256 {
355 self.coinbase_diff / self.gas_used
356 }
357}
358
359#[derive(Debug, Clone, Deserialize, Serialize)]
363pub struct SimulatedBundle {
364 #[serde(rename = "bundleHash")]
366 pub hash: BundleHash,
367 #[serde(rename = "coinbaseDiff")]
371 #[serde(deserialize_with = "deserialize_u256")]
372 pub coinbase_diff: U256,
373 #[serde(rename = "ethSentToCoinbase")]
375 #[serde(deserialize_with = "deserialize_u256")]
376 pub coinbase_tip: U256,
377 #[serde(rename = "bundleGasPrice")]
379 #[serde(deserialize_with = "deserialize_u256")]
380 pub gas_price: U256,
381 #[serde(rename = "totalGasUsed")]
383 #[serde(deserialize_with = "deserialize_u256")]
384 pub gas_used: U256,
385 #[serde(rename = "gasFees")]
387 #[serde(deserialize_with = "deserialize_u256")]
388 pub gas_fees: U256,
389 #[serde(rename = "stateBlockNumber")]
391 #[serde(deserialize_with = "deserialize_u64")]
392 pub simulation_block: U64,
393 #[serde(rename = "results")]
395 pub transactions: Vec<SimulatedTransaction>,
396}
397
398impl SimulatedBundle {
399 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}