1use crate::{
2 cache::{check_bundle_tx_list, StateSource},
3 CacheError, SimItemValidity,
4};
5use alloy::{
6 consensus::{
7 transaction::{Recovered, SignerRecoverable},
8 Transaction, TxEnvelope,
9 },
10 primitives::{TxHash, U256},
11};
12use signet_bundle::{RecoveredBundle, SignetEthBundle, TxRequirement};
13use std::{
14 borrow::{Borrow, Cow},
15 hash::Hash,
16 sync::Arc,
17};
18use tracing::{instrument, trace, trace_span};
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum SimItem {
23 Bundle(Arc<RecoveredBundle>),
25 Tx(Arc<Recovered<TxEnvelope>>),
27}
28
29impl TryFrom<SignetEthBundle> for SimItem {
30 type Error = CacheError;
31
32 fn try_from(bundle: SignetEthBundle) -> Result<Self, Self::Error> {
33 bundle.try_into_recovered().map_err(CacheError::BundleRecover).and_then(TryInto::try_into)
34 }
35}
36
37impl TryFrom<RecoveredBundle> for SimItem {
38 type Error = CacheError;
39
40 fn try_from(bundle: RecoveredBundle) -> Result<Self, Self::Error> {
41 if bundle.replacement_uuid().is_some() {
42 Ok(Self::Bundle(bundle.into()))
43 } else {
44 Err(CacheError::BundleWithoutReplacementUuid)
45 }
46 }
47}
48
49impl From<Recovered<TxEnvelope>> for SimItem {
50 fn from(tx: Recovered<TxEnvelope>) -> Self {
51 Self::Tx(tx.into())
52 }
53}
54
55impl TryFrom<TxEnvelope> for SimItem {
56 type Error = CacheError;
57
58 fn try_from(tx: TxEnvelope) -> Result<Self, Self::Error> {
59 tx.try_into_recovered().map_err(Into::into).map(Self::from)
60 }
61}
62
63impl SimItem {
64 pub fn as_bundle(&self) -> Option<&RecoveredBundle> {
66 match self {
67 Self::Bundle(bundle) => Some(bundle),
68 Self::Tx(_) => None,
69 }
70 }
71
72 pub fn as_tx(&self) -> Option<&Recovered<TxEnvelope>> {
74 match self {
75 Self::Bundle(_) => None,
76 Self::Tx(tx) => Some(tx),
77 }
78 }
79
80 pub fn calculate_total_fee(&self, basefee: u64) -> u128 {
83 match self {
84 Self::Bundle(bundle) => {
85 let mut total_tx_fee = 0;
86 for tx in bundle.txs() {
87 total_tx_fee += tx.effective_gas_price(Some(basefee)) * tx.gas_limit() as u128;
88 }
89 total_tx_fee
90 }
91 Self::Tx(tx) => tx.effective_gas_price(Some(basefee)) * tx.gas_limit() as u128,
92 }
93 }
94}
95
96impl SimItem {
98 pub fn identifier(&self) -> SimIdentifier<'_> {
101 match self {
102 Self::Bundle(bundle) => {
103 SimIdentifier::Bundle(Cow::Borrowed(bundle.replacement_uuid().unwrap()))
104 }
105 Self::Tx(tx) => SimIdentifier::Tx(*tx.inner().hash()),
106 }
107 }
108
109 pub fn identifier_owned(&self) -> SimIdentifier<'static> {
111 match self {
112 Self::Bundle(bundle) => {
113 SimIdentifier::Bundle(Cow::Owned(bundle.replacement_uuid().unwrap().to_string()))
114 }
115 Self::Tx(tx) => SimIdentifier::Tx(*tx.inner().hash()),
116 }
117 }
118
119 async fn check_tx<S>(&self, source: &S) -> Result<SimItemValidity, Box<dyn std::error::Error>>
120 where
121 S: StateSource,
122 {
123 let item = self.as_tx().expect("SimItem is not a Tx");
124 let signer = item.signer();
125 let item_nonce = item.nonce();
126 let total = U256::from(item.max_fee_per_gas() * item.gas_limit() as u128) + item.value();
127
128 source
129 .map(&signer, |info| {
130 let _guard = trace_span!(
131 "check_tx",
132 %signer,
133 item_nonce,
134 expected_nonce = info.nonce,
135 )
136 .entered();
137
138 if info.nonce > item_nonce {
141 trace!("nonce too low");
142 return SimItemValidity::Never;
143 }
144 if info.nonce < item_nonce {
146 trace!("nonce too high");
147 return SimItemValidity::Future;
148 }
149 if info.balance < total {
151 trace!(
152 required = %total,
153 available = %info.balance,
154 "insufficient balance",
155 );
156 return SimItemValidity::Future;
157 }
158 SimItemValidity::Now
160 })
161 .await
162 .map_err(Into::into)
163 }
164
165 #[instrument(level = "trace", skip_all)]
166 async fn check_bundle_tx_list_for_rollup<S>(
167 items: impl Iterator<Item = TxRequirement>,
168 source: &S,
169 ) -> Result<SimItemValidity, S::Error>
170 where
171 S: StateSource,
172 {
173 check_bundle_tx_list(items, source).await
174 }
175
176 #[instrument(level = "trace", skip_all)]
177 async fn check_bundle_tx_list_for_host<S>(
178 items: impl Iterator<Item = TxRequirement>,
179 source: &S,
180 ) -> Result<SimItemValidity, S::Error>
181 where
182 S: StateSource,
183 {
184 check_bundle_tx_list(items, source).await
185 }
186
187 async fn check_bundle<S, S2>(
188 &self,
189 source: &S,
190 host_source: &S2,
191 ) -> Result<SimItemValidity, Box<dyn std::error::Error>>
192 where
193 S: StateSource,
194 S2: StateSource,
195 {
196 let item = self.as_bundle().expect("SimItem is not a Bundle");
197
198 let ru_tx = Self::check_bundle_tx_list_for_rollup(item.tx_reqs(), source).await?;
199 let host_tx = Self::check_bundle_tx_list_for_host(item.host_tx_reqs(), host_source).await?;
200
201 Ok(ru_tx.min(host_tx))
203 }
204
205 #[instrument(
210 level = "trace",
211 name = "preflight_check",
212 skip_all,
213 fields(
214 item_identifier = %self.identifier(),
215 item_type = if self.as_bundle().is_some() { "bundle" } else { "tx" },
216 ),
217 ret(level = "debug", Display),
218 err(level = "debug", Display),
219 )]
220 pub async fn check<S, S2>(
221 &self,
222 source: &S,
223 host_source: &S2,
224 ) -> Result<SimItemValidity, Box<dyn std::error::Error>>
225 where
226 S: StateSource,
227 S2: StateSource,
228 {
229 match self {
230 SimItem::Bundle(_) => self.check_bundle(source, host_source).await,
231 SimItem::Tx(_) => self.check_tx(source).await,
232 }
233 }
234}
235
236#[derive(Debug, Clone)]
238pub enum SimIdentifier<'a> {
239 Bundle(Cow<'a, str>),
241 Tx(TxHash),
243}
244
245impl core::fmt::Display for SimIdentifier<'_> {
246 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
247 match self {
248 Self::Bundle(id) => write!(f, "{id}"),
249 Self::Tx(id) => write!(f, "{id}"),
250 }
251 }
252}
253
254impl PartialEq for SimIdentifier<'_> {
255 fn eq(&self, other: &Self) -> bool {
256 self.as_bytes().eq(other.as_bytes())
257 }
258}
259
260impl Eq for SimIdentifier<'_> {}
261
262impl Hash for SimIdentifier<'_> {
263 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
264 self.as_bytes().hash(state);
265 }
266}
267
268impl Borrow<[u8]> for SimIdentifier<'_> {
269 fn borrow(&self) -> &[u8] {
270 self.as_bytes()
271 }
272}
273
274impl From<TxHash> for SimIdentifier<'_> {
275 fn from(tx_hash: TxHash) -> Self {
276 Self::Tx(tx_hash)
277 }
278}
279
280impl SimIdentifier<'_> {
281 pub const fn bundle<'a>(id: Cow<'a, str>) -> SimIdentifier<'a> {
283 SimIdentifier::Bundle(id)
284 }
285
286 pub const fn tx(id: TxHash) -> Self {
288 Self::Tx(id)
289 }
290
291 pub const fn is_bundle(&self) -> bool {
293 matches!(self, Self::Bundle(_))
294 }
295
296 pub const fn is_tx(&self) -> bool {
298 matches!(self, Self::Tx(_))
299 }
300
301 pub fn as_bytes(&self) -> &[u8] {
303 match self {
304 Self::Bundle(id) => id.as_bytes(),
305 Self::Tx(id) => id.as_ref(),
306 }
307 }
308}