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)]
28#[serde(rename_all = "camelCase")]
29pub struct SignetCallBundle {
30 #[serde(flatten)]
33 pub bundle: EthCallBundle,
34}
35
36impl SignetCallBundle {
37 #[allow(clippy::missing_const_for_fn)] pub fn txs(&self) -> &[Bytes] {
40 &self.bundle.txs
41 }
42
43 pub const fn block_number(&self) -> u64 {
45 self.bundle.block_number
46 }
47
48 pub const fn state_block_number(&self) -> BlockNumberOrTag {
50 self.bundle.state_block_number
51 }
52
53 pub const fn timestamp(&self) -> Option<u64> {
55 self.bundle.timestamp
56 }
57
58 pub const fn gas_limit(&self) -> Option<u64> {
60 self.bundle.gas_limit
61 }
62
63 pub const fn difficulty(&self) -> Option<U256> {
65 self.bundle.difficulty
66 }
67
68 pub const fn base_fee(&self) -> Option<u128> {
70 self.bundle.base_fee
71 }
72
73 pub fn append_2718_tx(self, tx: impl Encodable2718) -> Self {
75 self.append_raw_tx(tx.encoded_2718())
76 }
77
78 pub fn append_raw_tx(mut self, tx: impl Into<Bytes>) -> Self {
80 self.bundle.txs.push(tx.into());
81 self
82 }
83
84 pub fn extend_2718_txs<I, T>(self, tx: I) -> Self
86 where
87 I: IntoIterator<Item = T>,
88 T: Encodable2718,
89 {
90 self.extend_raw_txs(tx.into_iter().map(|tx| tx.encoded_2718()))
91 }
92
93 pub fn extend_raw_txs<I, T>(mut self, txs: I) -> Self
95 where
96 I: IntoIterator<Item = T>,
97 T: Into<Bytes>,
98 {
99 self.bundle.txs.extend(txs.into_iter().map(Into::into));
100 self
101 }
102
103 pub const fn with_block_number(mut self, block_number: u64) -> Self {
105 self.bundle.block_number = block_number;
106 self
107 }
108
109 pub fn with_state_block_number(
111 mut self,
112 state_block_number: impl Into<BlockNumberOrTag>,
113 ) -> Self {
114 self.bundle.state_block_number = state_block_number.into();
115 self
116 }
117
118 pub const fn with_timestamp(mut self, timestamp: u64) -> Self {
120 self.bundle.timestamp = Some(timestamp);
121 self
122 }
123
124 pub const fn with_gas_limit(mut self, gas_limit: u64) -> Self {
126 self.bundle.gas_limit = Some(gas_limit);
127 self
128 }
129
130 pub const fn with_difficulty(mut self, difficulty: U256) -> Self {
132 self.bundle.difficulty = Some(difficulty);
133 self
134 }
135
136 pub const fn with_base_fee(mut self, base_fee: u128) -> Self {
138 self.bundle.base_fee = Some(base_fee);
139 self
140 }
141
142 pub fn bundle_hash(&self) -> B256 {
148 let mut hasher = alloy::primitives::Keccak256::new();
149
150 for tx in self.bundle.txs.iter() {
152 hasher.update(keccak256(tx).as_slice());
154 }
155 hasher.finalize()
156 }
157
158 pub fn decode_and_validate_txs<Db: trevm::revm::Database>(
160 &self,
161 ) -> Result<Vec<TxEnvelope>, BundleError<Db>> {
162 let txs = self
163 .txs()
164 .iter()
165 .map(|tx| TxEnvelope::decode_2718(&mut tx.chunk()))
166 .collect::<Result<Vec<_>, _>>()
167 .map_err(|err| BundleError::TransactionDecodingError(err))?;
168
169 if txs.iter().any(|tx| tx.is_eip4844()) {
170 return Err(BundleError::UnsupportedTransactionType);
171 }
172
173 Ok(txs)
174 }
175}
176
177#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
188pub struct SignetCallBundleResponse {
189 #[serde(flatten)]
190 inner: EthCallBundleResponse,
191 pub orders: AggregateOrders,
201 pub fills: AggregateFills,
205}
206
207impl core::ops::Deref for SignetCallBundleResponse {
208 type Target = EthCallBundleResponse;
209
210 fn deref(&self) -> &Self::Target {
211 &self.inner
212 }
213}
214
215impl core::ops::DerefMut for SignetCallBundleResponse {
216 fn deref_mut(&mut self) -> &mut Self::Target {
217 &mut self.inner
218 }
219}
220
221impl AsRef<EthCallBundleResponse> for SignetCallBundleResponse {
222 fn as_ref(&self) -> &EthCallBundleResponse {
223 &self.inner
224 }
225}
226
227impl AsMut<EthCallBundleResponse> for SignetCallBundleResponse {
228 fn as_mut(&mut self) -> &mut EthCallBundleResponse {
229 &mut self.inner
230 }
231}
232
233impl From<EthCallBundleResponse> for SignetCallBundleResponse {
234 fn from(inner: EthCallBundleResponse) -> Self {
235 Self { inner, orders: Default::default(), fills: Default::default() }
236 }
237}
238
239impl From<SignetCallBundleResponse> for EthCallBundleResponse {
240 fn from(this: SignetCallBundleResponse) -> Self {
241 this.inner
242 }
243}
244
245impl SignetCallBundleResponse {
246 fn accumulate_tx_result(&mut self, tx_result: EthCallBundleTransactionResult) {
248 self.inner.total_gas_used += tx_result.gas_used;
249 self.inner.gas_fees += tx_result.gas_fees;
250 self.inner.results.push(tx_result);
251 }
252
253 pub fn accumulate_tx<Db: Database>(
255 &mut self,
256 tx: &TxEnvelope,
257 coinbase_diff: U256,
258 base_fee: u64,
259 execution_result: ExecutionResult,
260 ) -> Result<(), BundleError<Db>> {
261 if let TxEnvelope::Eip4844(_) = tx {
262 return Err(BundleError::UnsupportedTransactionType);
263 }
264
265 let mut result = EthCallBundleTransactionResult::default();
267
268 result.from_address =
269 tx.recover_signer().map_err(|e| BundleError::TransactionSenderRecoveryError(e))?;
270
271 result.gas_price = U256::from(tx.effective_gas_price(Some(base_fee)));
273 result.gas_used = execution_result.gas_used();
274 result.gas_fees = result.gas_price * U256::from(result.gas_used);
275
276 if execution_result.is_success() {
278 result.value = Some(execution_result.into_output().unwrap_or_default());
279 } else {
280 result.revert = Some(execution_result.into_output().unwrap_or_default());
281 };
282
283 result.coinbase_diff = coinbase_diff;
285 result.eth_sent_to_coinbase = result.coinbase_diff.saturating_sub(result.gas_fees);
286
287 self.accumulate_tx_result(result);
289 Ok(())
290 }
291}
292
293#[cfg(test)]
294mod test {
295 use super::*;
296 use alloy::{
297 eips::BlockNumberOrTag,
298 primitives::{Address, U256},
299 rpc::types::mev::{EthCallBundle, EthCallBundleTransactionResult},
300 };
301
302 #[test]
303 fn call_bundle_ser_roundtrip() {
304 let bundle = SignetCallBundle {
305 bundle: EthCallBundle {
306 txs: vec![b"tx1".into(), b"tx2".into()],
307 block_number: 1,
308 state_block_number: BlockNumberOrTag::Number(2),
309 timestamp: Some(3),
310 gas_limit: Some(4),
311 difficulty: Some(alloy::primitives::U256::from(5)),
312 base_fee: Some(6),
313 transaction_index: Some(7.into()),
314 coinbase: Some(Address::repeat_byte(8)),
315 timeout: Some(9),
316 },
317 };
318
319 let serialized = serde_json::to_string(&bundle).unwrap();
320 let deserialized: SignetCallBundle = serde_json::from_str(&serialized).unwrap();
321
322 assert_eq!(bundle, deserialized);
323 }
324
325 #[test]
326 fn call_bundle_resp_ser_roundtrip() {
327 let resp: SignetCallBundleResponse = EthCallBundleResponse {
328 bundle_hash: B256::repeat_byte(1),
329 bundle_gas_price: U256::from(2),
330 coinbase_diff: U256::from(3),
331 eth_sent_to_coinbase: U256::from(4),
332 gas_fees: U256::from(5),
333 results: vec![EthCallBundleTransactionResult {
334 coinbase_diff: U256::from(6),
335 eth_sent_to_coinbase: U256::from(7),
336 from_address: Address::repeat_byte(8),
337 gas_fees: U256::from(9),
338 gas_price: U256::from(10),
339 gas_used: 11,
340 to_address: Some(Address::repeat_byte(12)),
341 tx_hash: B256::repeat_byte(13),
342 value: Some(Bytes::from(b"value")),
343 revert: Some(Bytes::from(b"revert")),
344 }],
345 state_block_number: 14,
346 total_gas_used: 15,
347 }
348 .into();
349
350 let serialized = serde_json::to_string(&resp).unwrap();
351 let deserialized: SignetCallBundleResponse = serde_json::from_str(&serialized).unwrap();
352
353 assert_eq!(resp, deserialized);
354 }
355}