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