1use alloy::{
3 consensus::{transaction::SignerRecoverable, Transaction, TxEnvelope},
4 eips::{eip2718::Encodable2718, BlockNumberOrTag, Decodable2718},
5 primitives::{keccak256, Bytes, B256, U256},
6 rlp::Buf,
7 rpc::types::mev::{EthCallBundle, EthCallBundleResponse, EthCallBundleTransactionResult},
8};
9use serde::{Deserialize, Serialize};
10use signet_types::{AggregateFills, AggregateOrders};
11use trevm::{
12 revm::{context::result::ExecutionResult, Database},
13 BundleError,
14};
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "camelCase")]
27pub struct SignetCallBundle {
28 #[serde(flatten)]
31 pub bundle: EthCallBundle,
32}
33
34impl SignetCallBundle {
35 #[allow(clippy::missing_const_for_fn)] pub fn txs(&self) -> &[Bytes] {
38 &self.bundle.txs
39 }
40
41 pub const fn block_number(&self) -> u64 {
43 self.bundle.block_number
44 }
45
46 pub const fn state_block_number(&self) -> BlockNumberOrTag {
48 self.bundle.state_block_number
49 }
50
51 pub const fn timestamp(&self) -> Option<u64> {
53 self.bundle.timestamp
54 }
55
56 pub const fn gas_limit(&self) -> Option<u64> {
58 self.bundle.gas_limit
59 }
60
61 pub const fn difficulty(&self) -> Option<U256> {
63 self.bundle.difficulty
64 }
65
66 pub const fn base_fee(&self) -> Option<u128> {
68 self.bundle.base_fee
69 }
70
71 pub fn append_2718_tx(self, tx: impl Encodable2718) -> Self {
73 self.append_raw_tx(tx.encoded_2718())
74 }
75
76 pub fn append_raw_tx(mut self, tx: impl Into<Bytes>) -> Self {
78 self.bundle.txs.push(tx.into());
79 self
80 }
81
82 pub fn extend_2718_txs<I, T>(self, tx: I) -> Self
84 where
85 I: IntoIterator<Item = T>,
86 T: Encodable2718,
87 {
88 self.extend_raw_txs(tx.into_iter().map(|tx| tx.encoded_2718()))
89 }
90
91 pub fn extend_raw_txs<I, T>(mut self, txs: I) -> Self
93 where
94 I: IntoIterator<Item = T>,
95 T: Into<Bytes>,
96 {
97 self.bundle.txs.extend(txs.into_iter().map(Into::into));
98 self
99 }
100
101 pub const fn with_block_number(mut self, block_number: u64) -> Self {
103 self.bundle.block_number = block_number;
104 self
105 }
106
107 pub fn with_state_block_number(
109 mut self,
110 state_block_number: impl Into<BlockNumberOrTag>,
111 ) -> Self {
112 self.bundle.state_block_number = state_block_number.into();
113 self
114 }
115
116 pub const fn with_timestamp(mut self, timestamp: u64) -> Self {
118 self.bundle.timestamp = Some(timestamp);
119 self
120 }
121
122 pub const fn with_gas_limit(mut self, gas_limit: u64) -> Self {
124 self.bundle.gas_limit = Some(gas_limit);
125 self
126 }
127
128 pub const fn with_difficulty(mut self, difficulty: U256) -> Self {
130 self.bundle.difficulty = Some(difficulty);
131 self
132 }
133
134 pub const fn with_base_fee(mut self, base_fee: u128) -> Self {
136 self.bundle.base_fee = Some(base_fee);
137 self
138 }
139
140 pub fn bundle_hash(&self) -> B256 {
146 let mut hasher = alloy::primitives::Keccak256::new();
147
148 for tx in self.bundle.txs.iter() {
150 hasher.update(keccak256(tx).as_slice());
152 }
153 hasher.finalize()
154 }
155
156 pub fn decode_and_validate_txs<Db: Database>(
158 &self,
159 ) -> Result<Vec<TxEnvelope>, BundleError<Db>> {
160 let txs = self
161 .txs()
162 .iter()
163 .map(|tx| TxEnvelope::decode_2718(&mut tx.chunk()))
164 .collect::<Result<Vec<_>, _>>()
165 .map_err(|err| BundleError::TransactionDecodingError(err))?;
166
167 if txs.iter().any(|tx| tx.is_eip4844()) {
168 return Err(BundleError::UnsupportedTransactionType);
169 }
170
171 Ok(txs)
172 }
173}
174
175#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
186pub struct SignetCallBundleResponse {
187 #[serde(flatten)]
188 inner: EthCallBundleResponse,
189 pub orders: AggregateOrders,
199 pub fills: AggregateFills,
203}
204
205impl core::ops::Deref for SignetCallBundleResponse {
206 type Target = EthCallBundleResponse;
207
208 fn deref(&self) -> &Self::Target {
209 &self.inner
210 }
211}
212
213impl core::ops::DerefMut for SignetCallBundleResponse {
214 fn deref_mut(&mut self) -> &mut Self::Target {
215 &mut self.inner
216 }
217}
218
219impl AsRef<EthCallBundleResponse> for SignetCallBundleResponse {
220 fn as_ref(&self) -> &EthCallBundleResponse {
221 &self.inner
222 }
223}
224
225impl AsMut<EthCallBundleResponse> for SignetCallBundleResponse {
226 fn as_mut(&mut self) -> &mut EthCallBundleResponse {
227 &mut self.inner
228 }
229}
230
231impl From<EthCallBundleResponse> for SignetCallBundleResponse {
232 fn from(inner: EthCallBundleResponse) -> Self {
233 Self { inner, orders: Default::default(), fills: Default::default() }
234 }
235}
236
237impl From<SignetCallBundleResponse> for EthCallBundleResponse {
238 fn from(this: SignetCallBundleResponse) -> Self {
239 this.inner
240 }
241}
242
243impl SignetCallBundleResponse {
244 fn accumulate_tx_result(&mut self, tx_result: EthCallBundleTransactionResult) {
246 self.inner.total_gas_used += tx_result.gas_used;
247 self.inner.gas_fees += tx_result.gas_fees;
248 self.inner.results.push(tx_result);
249 }
250
251 pub fn accumulate_tx<Db: Database>(
253 &mut self,
254 tx: &TxEnvelope,
255 coinbase_diff: U256,
256 base_fee: u64,
257 execution_result: ExecutionResult,
258 ) -> Result<(), BundleError<Db>> {
259 if let TxEnvelope::Eip4844(_) = tx {
260 return Err(BundleError::UnsupportedTransactionType);
261 }
262
263 let mut result = EthCallBundleTransactionResult::default();
265
266 result.from_address =
267 tx.recover_signer().map_err(|e| BundleError::TransactionSenderRecoveryError(e))?;
268
269 result.gas_price = U256::from(tx.effective_gas_price(Some(base_fee)));
271 result.gas_used = execution_result.gas_used();
272 result.gas_fees = result.gas_price * U256::from(result.gas_used);
273
274 if execution_result.is_success() {
276 result.value = Some(execution_result.into_output().unwrap_or_default());
277 } else {
278 result.revert = Some(execution_result.into_output().unwrap_or_default());
279 };
280
281 result.coinbase_diff = coinbase_diff;
283 result.eth_sent_to_coinbase = result.coinbase_diff.saturating_sub(result.gas_fees);
284
285 self.accumulate_tx_result(result);
287 Ok(())
288 }
289}
290
291#[cfg(test)]
292mod test {
293 use super::*;
294 use alloy::{
295 eips::BlockNumberOrTag,
296 primitives::{Address, U256},
297 rpc::types::mev::{EthCallBundle, EthCallBundleTransactionResult},
298 };
299
300 #[test]
301 fn call_bundle_ser_roundtrip() {
302 let bundle = SignetCallBundle {
303 bundle: EthCallBundle {
304 txs: vec![b"tx1".into(), b"tx2".into()],
305 block_number: 1,
306 state_block_number: BlockNumberOrTag::Number(2),
307 timestamp: Some(3),
308 gas_limit: Some(4),
309 difficulty: Some(alloy::primitives::U256::from(5)),
310 base_fee: Some(6),
311 transaction_index: Some(7.into()),
312 coinbase: Some(Address::repeat_byte(8)),
313 timeout: Some(9),
314 },
315 };
316
317 let serialized = serde_json::to_string(&bundle).unwrap();
318 let deserialized: SignetCallBundle = serde_json::from_str(&serialized).unwrap();
319
320 assert_eq!(bundle, deserialized);
321 }
322
323 #[test]
324 fn call_bundle_resp_ser_roundtrip() {
325 let resp: SignetCallBundleResponse = EthCallBundleResponse {
326 bundle_hash: B256::repeat_byte(1),
327 bundle_gas_price: U256::from(2),
328 coinbase_diff: U256::from(3),
329 eth_sent_to_coinbase: U256::from(4),
330 gas_fees: U256::from(5),
331 results: vec![EthCallBundleTransactionResult {
332 coinbase_diff: U256::from(6),
333 eth_sent_to_coinbase: U256::from(7),
334 from_address: Address::repeat_byte(8),
335 gas_fees: U256::from(9),
336 gas_price: U256::from(10),
337 gas_used: 11,
338 to_address: Some(Address::repeat_byte(12)),
339 tx_hash: B256::repeat_byte(13),
340 value: Some(Bytes::from(b"value")),
341 revert: Some(Bytes::from(b"revert")),
342 }],
343 state_block_number: 14,
344 total_gas_used: 15,
345 }
346 .into();
347
348 let serialized = serde_json::to_string(&resp).unwrap();
349 let deserialized: SignetCallBundleResponse = serde_json::from_str(&serialized).unwrap();
350
351 assert_eq!(resp, deserialized);
352 }
353
354 #[test]
358 #[ignore]
359 fn generate_call_bundle_vectors() {
360 let vectors = vec![
361 (
362 "minimal",
363 SignetCallBundle {
364 bundle: EthCallBundle {
365 txs: vec![b"\x02\xf8test_tx_1".into()],
366 block_number: 12345678,
367 state_block_number: BlockNumberOrTag::Number(12345677),
368 ..Default::default()
369 },
370 },
371 ),
372 (
373 "with_overrides",
374 SignetCallBundle {
375 bundle: EthCallBundle {
376 txs: vec![b"\x02\xf8test_tx_1".into()],
377 block_number: 12345678,
378 state_block_number: BlockNumberOrTag::Number(12345677),
379 timestamp: Some(1700000000),
380 gas_limit: Some(30000000),
381 base_fee: Some(1000000000),
382 ..Default::default()
383 },
384 },
385 ),
386 (
387 "with_coinbase",
388 SignetCallBundle {
389 bundle: EthCallBundle {
390 txs: vec![b"\x02\xf8test_tx_1".into()],
391 block_number: 12345678,
392 state_block_number: BlockNumberOrTag::Latest,
393 coinbase: Some(Address::repeat_byte(0x42)),
394 timeout: Some(5),
395 ..Default::default()
396 },
397 },
398 ),
399 ];
400
401 let output: Vec<_> = vectors
402 .into_iter()
403 .map(|(name, bundle)| {
404 serde_json::json!({
405 "name": name,
406 "bundle": bundle,
407 })
408 })
409 .collect();
410
411 println!("// SignetCallBundle vectors\n{}", serde_json::to_string_pretty(&output).unwrap());
412
413 let response_vectors = vec![
415 (
416 "minimal_response",
417 SignetCallBundleResponse::from(EthCallBundleResponse {
418 bundle_hash: B256::repeat_byte(0xaa),
419 bundle_gas_price: U256::from(1000000000u64),
420 coinbase_diff: U256::from(100000000000000u64),
421 eth_sent_to_coinbase: U256::from(50000000000000u64),
422 gas_fees: U256::from(50000000000000u64),
423 results: vec![EthCallBundleTransactionResult {
424 coinbase_diff: U256::from(100000000000000u64),
425 eth_sent_to_coinbase: U256::from(50000000000000u64),
426 from_address: Address::repeat_byte(0x11),
427 gas_fees: U256::from(50000000000000u64),
428 gas_price: U256::from(1000000000u64),
429 gas_used: 21000,
430 to_address: Some(Address::repeat_byte(0x22)),
431 tx_hash: B256::repeat_byte(0xbb),
432 value: Some(Bytes::from(b"result_data")),
433 revert: None,
434 }],
435 state_block_number: 12345677,
436 total_gas_used: 21000,
437 }),
438 ),
439 (
440 "reverted_response",
441 SignetCallBundleResponse::from(EthCallBundleResponse {
442 bundle_hash: B256::repeat_byte(0xcc),
443 bundle_gas_price: U256::from(1000000000u64),
444 coinbase_diff: U256::from(0u64),
445 eth_sent_to_coinbase: U256::from(0u64),
446 gas_fees: U256::from(21000000000000u64),
447 results: vec![EthCallBundleTransactionResult {
448 coinbase_diff: U256::from(0u64),
449 eth_sent_to_coinbase: U256::from(0u64),
450 from_address: Address::repeat_byte(0x33),
451 gas_fees: U256::from(21000000000000u64),
452 gas_price: U256::from(1000000000u64),
453 gas_used: 21000,
454 to_address: Some(Address::repeat_byte(0x44)),
455 tx_hash: B256::repeat_byte(0xdd),
456 value: None,
457 revert: Some(Bytes::from(b"execution reverted")),
458 }],
459 state_block_number: 12345677,
460 total_gas_used: 21000,
461 }),
462 ),
463 ];
464
465 let response_output: Vec<_> = response_vectors
466 .into_iter()
467 .map(|(name, resp)| {
468 serde_json::json!({
469 "name": name,
470 "response": resp,
471 })
472 })
473 .collect();
474
475 println!(
476 "\n// SignetCallBundleResponse vectors\n{}",
477 serde_json::to_string_pretty(&response_output).unwrap()
478 );
479 }
480}