signet_bundle/send/
bundle.rs

1//! Signet bundle types.
2use crate::send::SignetEthBundleError;
3use alloy::{
4    consensus::TxEnvelope,
5    eips::Decodable2718,
6    network::Network,
7    primitives::{Bytes, B256},
8    providers::Provider,
9    rlp::Buf,
10    rpc::types::mev::EthSendBundle,
11};
12use serde::{Deserialize, Serialize};
13use signet_types::{SignedFill, SignedPermitError};
14use signet_zenith::HostOrders::HostOrdersInstance;
15use trevm::{revm::Database, BundleError};
16
17/// Bundle of transactions for `signet_sendBundle`.
18///
19/// The Signet bundle contains the following:
20///
21/// - A standard [`EthSendBundle`] with the transactions to simulate.
22/// - A signed permit2 fill to be applied on the Host chain with the bundle.
23///
24/// This is based on the flashbots `eth_sendBundle` 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 SignetEthBundle {
30    /// The bundle of transactions to simulate. Same structure as a Flashbots [`EthSendBundle`] bundle.
31    #[serde(flatten)]
32    pub bundle: EthSendBundle,
33    /// Host fills to be applied with the bundle, represented as a signed
34    /// permit2 fill.
35    #[serde(default)]
36    pub host_fills: Option<SignedFill>,
37}
38
39impl SignetEthBundle {
40    /// Returns the transactions in this bundle.
41    #[allow(clippy::missing_const_for_fn)] // false positive
42    pub fn txs(&self) -> &[Bytes] {
43        &self.bundle.txs
44    }
45
46    /// Returns the block number for this bundle.
47    pub const fn block_number(&self) -> u64 {
48        self.bundle.block_number
49    }
50
51    /// Returns the minimum timestamp for this bundle.
52    pub const fn min_timestamp(&self) -> Option<u64> {
53        self.bundle.min_timestamp
54    }
55
56    /// Returns the maximum timestamp for this bundle.
57    pub const fn max_timestamp(&self) -> Option<u64> {
58        self.bundle.max_timestamp
59    }
60
61    /// Returns the reverting tx hashes for this bundle.
62    pub fn reverting_tx_hashes(&self) -> &[B256] {
63        self.bundle.reverting_tx_hashes.as_slice()
64    }
65
66    /// Returns the replacement uuid for this bundle.
67    pub fn replacement_uuid(&self) -> Option<&str> {
68        self.bundle.replacement_uuid.as_deref()
69    }
70
71    /// Checks if the bundle is valid at a given timestamp.
72    pub fn is_valid_at_timestamp(&self, timestamp: u64) -> bool {
73        let min_timestamp = self.bundle.min_timestamp.unwrap_or(0);
74        let max_timestamp = self.bundle.max_timestamp.unwrap_or(u64::MAX);
75        timestamp >= min_timestamp && timestamp <= max_timestamp
76    }
77
78    /// Checks if the bundle is valid at a given block number.
79    pub const fn is_valid_at_block_number(&self, block_number: u64) -> bool {
80        self.bundle.block_number == block_number
81    }
82
83    /// Decode and validate the transactions in the bundle.
84    pub fn decode_and_validate_txs<Db: Database>(
85        &self,
86    ) -> Result<Vec<TxEnvelope>, BundleError<Db>> {
87        // Decode and validate the transactions in the bundle
88        let txs = self
89            .txs()
90            .iter()
91            .map(|tx| TxEnvelope::decode_2718(&mut tx.chunk()))
92            .collect::<Result<Vec<_>, _>>()
93            .map_err(|err| BundleError::TransactionDecodingError(err))?;
94
95        if txs.iter().any(|tx| tx.is_eip4844()) {
96            return Err(BundleError::UnsupportedTransactionType);
97        }
98
99        Ok(txs)
100    }
101
102    /// Check that this can be syntactically used as a fill.
103    pub fn validate_fills_offchain(&self, timestamp: u64) -> Result<(), SignedPermitError> {
104        if let Some(host_fills) = &self.host_fills {
105            host_fills.validate(timestamp)
106        } else {
107            Ok(())
108        }
109    }
110
111    /// Check that this fill is valid on-chain as of the current block. This
112    /// checks that the tokens can actually be transferred.
113    pub async fn alloy_validate_fills_onchain<Db, P, N>(
114        &self,
115        orders: HostOrdersInstance<P, N>,
116    ) -> Result<(), SignetEthBundleError<Db>>
117    where
118        Db: Database,
119        P: Provider<N>,
120        N: Network,
121    {
122        if let Some(host_fills) = self.host_fills.clone() {
123            orders.try_fill(host_fills.outputs, host_fills.permit).await.map_err(Into::into)
124        } else {
125            Ok(())
126        }
127    }
128}
129
130/// Response for `signet_sendBundle`.
131///
132/// This is based on the flashbots `eth_sendBundle` response. See [their docs].
133///
134/// [their docs]: https://docs.flashbots.net/flashbots-auction/advanced/rpc-endpoint
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
136#[serde(rename_all = "camelCase")]
137pub struct SignetEthBundleResponse {
138    /// The bundle hash of the sent bundle.
139    ///
140    /// This is calculated as keccak256(tx_hashes) where tx_hashes are the
141    /// concatenated transaction hashes.
142    pub bundle_hash: B256,
143}
144
145#[cfg(test)]
146mod test {
147    use super::*;
148    use alloy::primitives::{Address, U256};
149    use signet_zenith::HostOrders::{
150        Output, Permit2Batch, PermitBatchTransferFrom, TokenPermissions,
151    };
152
153    #[test]
154    fn send_bundle_ser_roundtrip() {
155        let bundle = SignetEthBundle {
156            bundle: EthSendBundle {
157                txs: vec![b"tx1".into(), b"tx2".into()],
158                block_number: 1,
159                min_timestamp: Some(2),
160                max_timestamp: Some(3),
161                reverting_tx_hashes: vec![B256::repeat_byte(4), B256::repeat_byte(5)],
162                replacement_uuid: Some("uuid".to_owned()),
163                ..Default::default()
164            },
165            host_fills: Some(SignedFill {
166                permit: Permit2Batch {
167                    permit: PermitBatchTransferFrom {
168                        permitted: vec![TokenPermissions {
169                            token: Address::repeat_byte(66),
170                            amount: U256::from(17),
171                        }],
172                        nonce: U256::from(18),
173                        deadline: U256::from(19),
174                    },
175                    owner: Address::repeat_byte(77),
176                    signature: Bytes::from(b"abcd"),
177                },
178                outputs: vec![Output {
179                    token: Address::repeat_byte(88),
180                    amount: U256::from(20),
181                    recipient: Address::repeat_byte(99),
182                    chainId: 100,
183                }],
184            }),
185        };
186
187        let serialized = serde_json::to_string(&bundle).unwrap();
188        let deserialized: SignetEthBundle = serde_json::from_str(&serialized).unwrap();
189
190        assert_eq!(bundle, deserialized);
191    }
192
193    #[test]
194    fn send_bundle_resp_ser_roundtrip() {
195        let resp = SignetEthBundleResponse { bundle_hash: B256::repeat_byte(1) };
196
197        let serialized = serde_json::to_string(&resp).unwrap();
198        let deserialized: SignetEthBundleResponse = serde_json::from_str(&serialized).unwrap();
199
200        assert_eq!(resp, deserialized);
201    }
202}