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