Skip to main content

signet_bundle/call/
ty.rs

1//! Signet bundle types.
2use 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/// Bundle of transactions for `signet_callBundle`.
17///
18/// The Signet bundle contains the following:
19///
20/// - A standard [`EthCallBundle`] with the transactions to simulate.
21///
22/// This is based on the flashbots `eth_callBundle` bundle. See [their docs].
23///
24/// [their docs]: https://docs.flashbots.net/flashbots-auction/advanced/rpc-endpoint
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "camelCase")]
27pub struct SignetCallBundle {
28    /// The bundle of transactions to simulate. Same structure as a Flashbots
29    /// [`EthCallBundle`] bundle.
30    #[serde(flatten)]
31    pub bundle: EthCallBundle,
32}
33
34impl SignetCallBundle {
35    /// Returns the transactions in this bundle.
36    #[allow(clippy::missing_const_for_fn)] // false positive
37    pub fn txs(&self) -> &[Bytes] {
38        &self.bundle.txs
39    }
40
41    /// Returns the block number for this bundle.
42    pub const fn block_number(&self) -> u64 {
43        self.bundle.block_number
44    }
45
46    /// Returns the state block number for this bundle.
47    pub const fn state_block_number(&self) -> BlockNumberOrTag {
48        self.bundle.state_block_number
49    }
50
51    /// Returns the timestamp for this bundle.
52    pub const fn timestamp(&self) -> Option<u64> {
53        self.bundle.timestamp
54    }
55
56    /// Returns the gas limit for this bundle.
57    pub const fn gas_limit(&self) -> Option<u64> {
58        self.bundle.gas_limit
59    }
60
61    /// Returns the difficulty for this bundle.
62    pub const fn difficulty(&self) -> Option<U256> {
63        self.bundle.difficulty
64    }
65
66    /// Returns the base fee for this bundle.
67    pub const fn base_fee(&self) -> Option<u128> {
68        self.bundle.base_fee
69    }
70
71    /// Adds an [`Encodable2718`] transaction to the bundle.
72    pub fn append_2718_tx(self, tx: impl Encodable2718) -> Self {
73        self.append_raw_tx(tx.encoded_2718())
74    }
75
76    /// Adds an EIP-2718 envelope to the bundle.
77    pub fn append_raw_tx(mut self, tx: impl Into<Bytes>) -> Self {
78        self.bundle.txs.push(tx.into());
79        self
80    }
81
82    /// Adds multiple [`Encodable2718`] transactions to the bundle.
83    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    /// Adds multiple calls to the block.
92    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    /// Sets the block number for the bundle.
102    pub const fn with_block_number(mut self, block_number: u64) -> Self {
103        self.bundle.block_number = block_number;
104        self
105    }
106
107    /// Sets the state block number for the bundle.
108    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    /// Sets the timestamp for the bundle.
117    pub const fn with_timestamp(mut self, timestamp: u64) -> Self {
118        self.bundle.timestamp = Some(timestamp);
119        self
120    }
121
122    /// Sets the gas limit for the bundle.
123    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    /// Sets the difficulty for the bundle.
129    pub const fn with_difficulty(mut self, difficulty: U256) -> Self {
130        self.bundle.difficulty = Some(difficulty);
131        self
132    }
133
134    /// Sets the base fee for the bundle.
135    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    /// Calculate the bundle hash for this bundle.
141    ///
142    /// The hash is calculated as
143    /// `keccak256(tx_hash1 || tx_hash2 || ... || tx_hashn)` where `||` is the
144    /// concatenation operator.
145    pub fn bundle_hash(&self) -> B256 {
146        let mut hasher = alloy::primitives::Keccak256::new();
147
148        // Concatenate the transaction hashes, to then hash them. This is the tx_preimage.
149        for tx in self.bundle.txs.iter() {
150            // Calculate the tx hash (keccak256(encoded_signed_tx)) and append it to the tx_bytes.
151            hasher.update(keccak256(tx).as_slice());
152        }
153        hasher.finalize()
154    }
155
156    /// Decode and validate the transactions in the bundle.
157    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/// Response for `signet_callBundle`.
176///
177/// The response contains the following:
178/// - The inner [`EthCallBundleResponse`] response.
179/// - Aggregate orders produced by the bundle.
180/// - Fills produced by the bundle.
181///
182/// The aggregate orders contains both the net outputs the filler can expect to
183/// receive from this bundle and the net inputs the filler must provide to
184/// ensure this bundle is valid.
185#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
186pub struct SignetCallBundleResponse {
187    #[serde(flatten)]
188    inner: EthCallBundleResponse,
189    /// Aggregate orders produced by the bundle.
190    ///
191    /// This mapping will contain all outputs and required inputs, collapsed
192    /// into a single entry per asset. For the bundle to be valid, there must
193    /// be fills for all the inputs in this mapping. Which is to say, this type
194    /// indicates the following:
195    ///
196    /// - The net outputs the filler can expect to receive from this bundle.
197    /// - The net inputs the filler must provide to ensure this bundle is valid.
198    pub orders: AggregateOrders,
199    /// Fills produced by the bundle. This will contain the net fills produced
200    /// by the transaction. These can be deducted from the net inputs required
201    /// by the orders to ensure the bundle is valid.
202    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    /// Accumulate a transaction result into the response.
245    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    /// Accumulate the result of transaction execution into the response.
252    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        // we'll incrementally populate this result.
264        let mut result = EthCallBundleTransactionResult::default();
265
266        result.from_address =
267            tx.recover_signer().map_err(|e| BundleError::TransactionSenderRecoveryError(e))?;
268
269        // Calculate the gas price and fees
270        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        // set the return data for the response
275        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        // Calculate the coinbase diff and the eth sent to coinbase
282        result.coinbase_diff = coinbase_diff;
283        result.eth_sent_to_coinbase = result.coinbase_diff.saturating_sub(result.gas_fees);
284
285        // Accumulate the result
286        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    /// Generate test vectors for TypeScript SDK.
355    ///
356    /// Run with: `cargo t -p signet-bundle -- --ignored --nocapture`
357    #[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        // Also generate response vectors
414        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}