1use 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
21pub type BundleInspector<I = NoOpInspector> = Layered<TimeLimit, I>;
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "camelCase")]
36pub struct SignetEthBundle {
37 #[serde(flatten)]
39 pub bundle: EthSendBundle,
40 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub host_fills: Option<SignedFill>,
44
45 #[serde(default, skip_serializing_if = "Vec::is_empty")]
47 pub host_txs: Vec<Bytes>,
48}
49
50impl SignetEthBundle {
51 #[allow(clippy::missing_const_for_fn)] pub fn txs(&self) -> &[Bytes] {
54 &self.bundle.txs
55 }
56
57 pub const fn block_number(&self) -> u64 {
59 self.bundle.block_number
60 }
61
62 pub const fn min_timestamp(&self) -> Option<u64> {
64 self.bundle.min_timestamp
65 }
66
67 pub const fn max_timestamp(&self) -> Option<u64> {
69 self.bundle.max_timestamp
70 }
71
72 pub fn reverting_tx_hashes(&self) -> &[B256] {
74 self.bundle.reverting_tx_hashes.as_slice()
75 }
76
77 pub fn replacement_uuid(&self) -> Option<&str> {
79 self.bundle.replacement_uuid.as_deref()
80 }
81
82 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 pub const fn is_valid_at_block_number(&self, block_number: u64) -> bool {
91 self.bundle.block_number == block_number
92 }
93
94 pub fn decode_and_validate_txs<Db: Database>(
96 &self,
97 ) -> Result<Vec<TxEnvelope>, BundleError<Db>> {
98 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
152#[serde(rename_all = "camelCase")]
153pub struct SignetEthBundleResponse {
154 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}