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::{
16    inspectors::{Layered, TimeLimit},
17    revm::{inspector::NoOpInspector, Database},
18    BundleError,
19};
20
21/// The inspector type required by the Signet bundle driver.
22pub type BundleInspector<I = NoOpInspector> = Layered<TimeLimit, I>;
23
24/// Bundle of transactions for `signet_sendBundle`.
25///
26/// The Signet bundle contains the following:
27///
28/// - A standard [`EthSendBundle`] with the transactions to simulate.
29/// - A signed permit2 fill to be applied on the Host chain with the bundle.
30///
31/// This is based on the flashbots `eth_sendBundle` bundle. See [their docs].
32///
33/// [their docs]: https://docs.flashbots.net/flashbots-auction/advanced/rpc-endpoint
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "camelCase")]
36pub struct SignetEthBundle {
37    /// The bundle of transactions to simulate. Same structure as a Flashbots [`EthSendBundle`] bundle.
38    #[serde(flatten)]
39    pub bundle: EthSendBundle,
40    /// Host fills to be applied with the bundle, represented as a signed
41    /// permit2 fill.
42    #[serde(default, skip_serializing_if = "Option::is_none")]
43    pub host_fills: Option<SignedFill>,
44
45    /// Host transactions to be included in the host bundle.
46    #[serde(default, skip_serializing_if = "Vec::is_empty")]
47    pub host_txs: Vec<Bytes>,
48}
49
50impl SignetEthBundle {
51    /// Returns the transactions in this bundle.
52    #[allow(clippy::missing_const_for_fn)] // false positive
53    pub fn txs(&self) -> &[Bytes] {
54        &self.bundle.txs
55    }
56
57    /// Returns the block number for this bundle.
58    pub const fn block_number(&self) -> u64 {
59        self.bundle.block_number
60    }
61
62    /// Returns the minimum timestamp for this bundle.
63    pub const fn min_timestamp(&self) -> Option<u64> {
64        self.bundle.min_timestamp
65    }
66
67    /// Returns the maximum timestamp for this bundle.
68    pub const fn max_timestamp(&self) -> Option<u64> {
69        self.bundle.max_timestamp
70    }
71
72    /// Returns the reverting tx hashes for this bundle.
73    pub fn reverting_tx_hashes(&self) -> &[B256] {
74        self.bundle.reverting_tx_hashes.as_slice()
75    }
76
77    /// Returns the replacement uuid for this bundle.
78    pub fn replacement_uuid(&self) -> Option<&str> {
79        self.bundle.replacement_uuid.as_deref()
80    }
81
82    /// Checks if the bundle is valid at a given timestamp.
83    pub fn is_valid_at_timestamp(&self, timestamp: u64) -> bool {
84        let min_timestamp = self.bundle.min_timestamp.unwrap_or(0);
85        let max_timestamp = self.bundle.max_timestamp.unwrap_or(u64::MAX);
86        timestamp >= min_timestamp && timestamp <= max_timestamp
87    }
88
89    /// Checks if the bundle is valid at a given block number.
90    pub const fn is_valid_at_block_number(&self, block_number: u64) -> bool {
91        self.bundle.block_number == block_number
92    }
93
94    /// Decode and validate the transactions in the bundle.
95    pub fn decode_and_validate_txs<Db: Database>(
96        &self,
97    ) -> Result<Vec<TxEnvelope>, BundleError<Db>> {
98        // Decode and validate the transactions in the bundle
99        let txs = self
100            .txs()
101            .iter()
102            .map(|tx| TxEnvelope::decode_2718(&mut tx.chunk()))
103            .collect::<Result<Vec<_>, _>>()
104            .map_err(|err| BundleError::TransactionDecodingError(err))?;
105
106        if txs.iter().any(|tx| tx.is_eip4844()) {
107            return Err(BundleError::UnsupportedTransactionType);
108        }
109
110        Ok(txs)
111    }
112
113    /// Check that this can be syntactically used as a fill.
114    pub fn validate_fills_offchain(&self, timestamp: u64) -> Result<(), SignedPermitError> {
115        if let Some(host_fills) = &self.host_fills {
116            host_fills.validate(timestamp)
117        } else {
118            Ok(())
119        }
120    }
121
122    /// Check that this fill is valid on-chain as of the current block. This
123    /// checks that the tokens can actually be transferred.
124    ///
125    /// # WARNING:
126    ///
127    /// This function will send an RPC request to the provider containing the
128    /// fills. It MUST NOT be used with an untrusted provider.
129    pub async fn alloy_validate_fills_onchain<Db, P, N>(
130        &self,
131        orders: HostOrdersInstance<P, N>,
132    ) -> Result<(), SignetEthBundleError<Db>>
133    where
134        Db: Database,
135        P: Provider<N>,
136        N: Network,
137    {
138        if let Some(host_fills) = self.host_fills.clone() {
139            orders.try_fill(host_fills.outputs, host_fills.permit).await.map_err(Into::into)
140        } else {
141            Ok(())
142        }
143    }
144}
145
146/// Response for `signet_sendBundle`.
147///
148/// This is based on the flashbots `eth_sendBundle` response. See [their docs].
149///
150/// [their docs]: https://docs.flashbots.net/flashbots-auction/advanced/rpc-endpoint
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
152#[serde(rename_all = "camelCase")]
153pub struct SignetEthBundleResponse {
154    /// The bundle hash of the sent bundle.
155    ///
156    /// This is calculated as keccak256(tx_hashes) where tx_hashes are the
157    /// concatenated transaction hashes.
158    pub bundle_hash: B256,
159}
160
161#[cfg(test)]
162mod test {
163    use super::*;
164    use alloy::primitives::{Address, U256};
165    use signet_zenith::HostOrders::{
166        Output, Permit2Batch, PermitBatchTransferFrom, TokenPermissions,
167    };
168
169    #[test]
170    fn send_bundle_ser_roundtrip() {
171        let bundle = SignetEthBundle {
172            bundle: EthSendBundle {
173                txs: vec![b"tx1".into(), b"tx2".into()],
174                block_number: 1,
175                min_timestamp: Some(2),
176                max_timestamp: Some(3),
177                reverting_tx_hashes: vec![B256::repeat_byte(4), B256::repeat_byte(5)],
178                replacement_uuid: Some("uuid".to_owned()),
179                ..Default::default()
180            },
181            host_fills: Some(SignedFill {
182                permit: Permit2Batch {
183                    permit: PermitBatchTransferFrom {
184                        permitted: vec![TokenPermissions {
185                            token: Address::repeat_byte(66),
186                            amount: U256::from(17),
187                        }],
188                        nonce: U256::from(18),
189                        deadline: U256::from(19),
190                    },
191                    owner: Address::repeat_byte(77),
192                    signature: Bytes::from(b"abcd"),
193                },
194                outputs: vec![Output {
195                    token: Address::repeat_byte(88),
196                    amount: U256::from(20),
197                    recipient: Address::repeat_byte(99),
198                    chainId: 100,
199                }],
200            }),
201            host_txs: vec![b"host_tx1".into(), b"host_tx2".into()],
202        };
203
204        let serialized = serde_json::to_string(&bundle).unwrap();
205        let deserialized: SignetEthBundle = serde_json::from_str(&serialized).unwrap();
206
207        assert_eq!(bundle, deserialized);
208    }
209
210    #[test]
211    fn send_bundle_ser_roundtrip_no_host_no_fills() {
212        let bundle = SignetEthBundle {
213            bundle: EthSendBundle {
214                txs: vec![b"tx1".into(), b"tx2".into()],
215                block_number: 1,
216                min_timestamp: Some(2),
217                max_timestamp: Some(3),
218                reverting_tx_hashes: vec![B256::repeat_byte(4), B256::repeat_byte(5)],
219                replacement_uuid: Some("uuid".to_owned()),
220                ..Default::default()
221            },
222            host_fills: None,
223            host_txs: vec![],
224        };
225
226        let serialized = serde_json::to_string(&bundle).unwrap();
227        let deserialized: SignetEthBundle = serde_json::from_str(&serialized).unwrap();
228
229        assert_eq!(bundle, deserialized);
230    }
231
232    #[test]
233    fn test_deser_bundle_no_host_no_fills() {
234        let json = r#"
235        {"txs":["0x747831","0x747832"],"blockNumber":"0x1","minTimestamp":2,"maxTimestamp":3,"revertingTxHashes":["0x0404040404040404040404040404040404040404040404040404040404040404","0x0505050505050505050505050505050505050505050505050505050505050505"],"replacementUuid":"uuid"}"#;
236
237        let deserialized: SignetEthBundle = serde_json::from_str(json).unwrap();
238
239        assert!(deserialized.host_fills.is_none());
240        assert!(deserialized.host_txs.is_empty());
241    }
242
243    #[test]
244    fn send_bundle_resp_ser_roundtrip() {
245        let resp = SignetEthBundleResponse { bundle_hash: B256::repeat_byte(1) };
246
247        let serialized = serde_json::to_string(&resp).unwrap();
248        let deserialized: SignetEthBundleResponse = serde_json::from_str(&serialized).unwrap();
249
250        assert_eq!(resp, deserialized);
251    }
252}