alloy_network/any/
unknowns.rs

1use core::fmt;
2use std::sync::OnceLock;
3
4use alloy_consensus::{TxType, Typed2718};
5use alloy_eips::{eip2718::Eip2718Error, eip7702::SignedAuthorization};
6use alloy_primitives::{Address, Bytes, ChainId, TxKind, B256, U128, U256, U64, U8};
7use alloy_rpc_types_eth::AccessList;
8use alloy_serde::OtherFields;
9
10/// Transaction type for a catch-all network.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12#[doc(alias = "AnyTransactionType")]
13pub struct AnyTxType(pub u8);
14
15impl fmt::Display for AnyTxType {
16    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
17        write!(f, "AnyTxType({})", self.0)
18    }
19}
20
21impl TryFrom<u8> for AnyTxType {
22    type Error = Eip2718Error;
23
24    fn try_from(value: u8) -> Result<Self, Self::Error> {
25        Ok(Self(value))
26    }
27}
28
29impl From<&AnyTxType> for u8 {
30    fn from(value: &AnyTxType) -> Self {
31        value.0
32    }
33}
34
35impl From<AnyTxType> for u8 {
36    fn from(value: AnyTxType) -> Self {
37        value.0
38    }
39}
40
41impl serde::Serialize for AnyTxType {
42    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
43    where
44        S: serde::Serializer,
45    {
46        U8::from(self.0).serialize(serializer)
47    }
48}
49
50impl<'de> serde::Deserialize<'de> for AnyTxType {
51    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
52    where
53        D: serde::Deserializer<'de>,
54    {
55        U8::deserialize(deserializer).map(|t| Self(t.to::<u8>()))
56    }
57}
58
59impl TryFrom<AnyTxType> for TxType {
60    type Error = Eip2718Error;
61
62    fn try_from(value: AnyTxType) -> Result<Self, Self::Error> {
63        value.0.try_into()
64    }
65}
66
67impl From<TxType> for AnyTxType {
68    fn from(value: TxType) -> Self {
69        Self(value as u8)
70    }
71}
72
73impl Typed2718 for AnyTxType {
74    fn ty(&self) -> u8 {
75        self.0
76    }
77}
78
79/// Memoization for deserialization of [`UnknownTxEnvelope`],
80/// [`UnknownTypedTransaction`] [`AnyTxEnvelope`], [`AnyTypedTransaction`].
81/// Setting these manually is discouraged, however the fields are left public
82/// for power users :)
83///
84/// [`AnyTxEnvelope`]: crate::AnyTxEnvelope
85/// [`AnyTypedTransaction`]: crate::AnyTypedTransaction
86#[derive(Debug, Clone, PartialEq, Eq, Default)]
87#[expect(unnameable_types)]
88pub struct DeserMemo {
89    pub input: OnceLock<Bytes>,
90    pub access_list: OnceLock<AccessList>,
91    pub blob_versioned_hashes: OnceLock<Vec<B256>>,
92    pub authorization_list: OnceLock<Vec<SignedAuthorization>>,
93}
94
95/// A typed transaction of an unknown Network
96#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
97#[doc(alias = "UnknownTypedTx")]
98pub struct UnknownTypedTransaction {
99    #[serde(rename = "type")]
100    /// Transaction type.
101    pub ty: AnyTxType,
102
103    /// Additional fields.
104    #[serde(flatten)]
105    pub fields: OtherFields,
106
107    /// Memoization for deserialization.
108    #[serde(skip, default)]
109    pub memo: DeserMemo,
110}
111
112impl alloy_consensus::Transaction for UnknownTypedTransaction {
113    #[inline]
114    fn chain_id(&self) -> Option<ChainId> {
115        self.fields.get_deserialized::<U64>("chainId").and_then(Result::ok).map(|v| v.to())
116    }
117
118    #[inline]
119    fn nonce(&self) -> u64 {
120        self.fields.get_deserialized::<U64>("nonce").and_then(Result::ok).unwrap_or_default().to()
121    }
122
123    #[inline]
124    fn gas_limit(&self) -> u64 {
125        self.fields.get_deserialized::<U64>("gas").and_then(Result::ok).unwrap_or_default().to()
126    }
127
128    #[inline]
129    fn gas_price(&self) -> Option<u128> {
130        self.fields.get_deserialized::<U128>("gasPrice").and_then(Result::ok).map(|v| v.to())
131    }
132
133    #[inline]
134    fn max_fee_per_gas(&self) -> u128 {
135        self.fields
136            .get_deserialized::<U128>("maxFeePerGas")
137            .and_then(Result::ok)
138            .unwrap_or_default()
139            .to()
140    }
141
142    #[inline]
143    fn max_priority_fee_per_gas(&self) -> Option<u128> {
144        self.fields
145            .get_deserialized::<U128>("maxPriorityFeePerGas")
146            .and_then(Result::ok)
147            .map(|v| v.to())
148    }
149
150    #[inline]
151    fn max_fee_per_blob_gas(&self) -> Option<u128> {
152        self.fields
153            .get_deserialized::<U128>("maxFeePerBlobGas")
154            .and_then(Result::ok)
155            .map(|v| v.to())
156    }
157
158    #[inline]
159    fn priority_fee_or_price(&self) -> u128 {
160        self.gas_price().or(self.max_priority_fee_per_gas()).unwrap_or_default()
161    }
162
163    fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
164        if let Some(gas_price) = self.gas_price() {
165            return gas_price;
166        }
167
168        base_fee.map_or(self.max_fee_per_gas(), |base_fee| {
169            // if the tip is greater than the max priority fee per gas, set it to the max
170            // priority fee per gas + base fee
171            let max_fee = self.max_fee_per_gas();
172            if max_fee == 0 {
173                return 0;
174            }
175            let Some(max_prio_fee) = self.max_priority_fee_per_gas() else { return max_fee };
176            let tip = max_fee.saturating_sub(base_fee as u128);
177            if tip > max_prio_fee {
178                max_prio_fee + base_fee as u128
179            } else {
180                // otherwise return the max fee per gas
181                max_fee
182            }
183        })
184    }
185
186    #[inline]
187    fn is_dynamic_fee(&self) -> bool {
188        self.fields.get_deserialized::<U128>("maxFeePerGas").is_some()
189            || self.fields.get_deserialized::<U128>("maxFeePerBlobGas").is_some()
190    }
191
192    #[inline]
193    fn kind(&self) -> TxKind {
194        self.fields
195            .get("to")
196            .or(Some(&serde_json::Value::Null))
197            .and_then(|v| {
198                if v.is_null() {
199                    Some(TxKind::Create)
200                } else {
201                    v.as_str().and_then(|v| v.parse::<Address>().ok().map(Into::into))
202                }
203            })
204            .unwrap_or_default()
205    }
206
207    #[inline]
208    fn is_create(&self) -> bool {
209        self.fields.get("to").is_none_or(|v| v.is_null())
210    }
211
212    #[inline]
213    fn value(&self) -> U256 {
214        self.fields.get_deserialized("value").and_then(Result::ok).unwrap_or_default()
215    }
216
217    #[inline]
218    fn input(&self) -> &Bytes {
219        self.memo.input.get_or_init(|| {
220            self.fields.get_deserialized("input").and_then(Result::ok).unwrap_or_default()
221        })
222    }
223
224    #[inline]
225    fn access_list(&self) -> Option<&AccessList> {
226        if self.fields.contains_key("accessList") {
227            Some(self.memo.access_list.get_or_init(|| {
228                self.fields.get_deserialized("accessList").and_then(Result::ok).unwrap_or_default()
229            }))
230        } else {
231            None
232        }
233    }
234
235    #[inline]
236    fn blob_versioned_hashes(&self) -> Option<&[B256]> {
237        if self.fields.contains_key("blobVersionedHashes") {
238            Some(self.memo.blob_versioned_hashes.get_or_init(|| {
239                self.fields
240                    .get_deserialized("blobVersionedHashes")
241                    .and_then(Result::ok)
242                    .unwrap_or_default()
243            }))
244        } else {
245            None
246        }
247    }
248
249    #[inline]
250    fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
251        if self.fields.contains_key("authorizationList") {
252            Some(self.memo.authorization_list.get_or_init(|| {
253                self.fields
254                    .get_deserialized("authorizationList")
255                    .and_then(Result::ok)
256                    .unwrap_or_default()
257            }))
258        } else {
259            None
260        }
261    }
262}
263
264impl Typed2718 for UnknownTxEnvelope {
265    fn ty(&self) -> u8 {
266        self.inner.ty.0
267    }
268}
269
270impl Typed2718 for UnknownTypedTransaction {
271    fn ty(&self) -> u8 {
272        self.ty.0
273    }
274}
275
276/// A transaction envelope from an unknown network.
277#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
278#[doc(alias = "UnknownTransactionEnvelope")]
279pub struct UnknownTxEnvelope {
280    /// Transaction hash.
281    pub hash: B256,
282
283    /// Transaction type.
284    #[serde(flatten)]
285    pub inner: UnknownTypedTransaction,
286}
287
288impl AsRef<UnknownTypedTransaction> for UnknownTxEnvelope {
289    fn as_ref(&self) -> &UnknownTypedTransaction {
290        &self.inner
291    }
292}
293
294impl alloy_consensus::Transaction for UnknownTxEnvelope {
295    #[inline]
296    fn chain_id(&self) -> Option<ChainId> {
297        self.inner.chain_id()
298    }
299
300    #[inline]
301    fn nonce(&self) -> u64 {
302        self.inner.nonce()
303    }
304
305    #[inline]
306    fn gas_limit(&self) -> u64 {
307        self.inner.gas_limit()
308    }
309
310    #[inline]
311    fn gas_price(&self) -> Option<u128> {
312        self.inner.gas_price()
313    }
314
315    #[inline]
316    fn max_fee_per_gas(&self) -> u128 {
317        self.inner.max_fee_per_gas()
318    }
319
320    #[inline]
321    fn max_priority_fee_per_gas(&self) -> Option<u128> {
322        self.inner.max_priority_fee_per_gas()
323    }
324
325    #[inline]
326    fn max_fee_per_blob_gas(&self) -> Option<u128> {
327        self.inner.max_fee_per_blob_gas()
328    }
329
330    #[inline]
331    fn priority_fee_or_price(&self) -> u128 {
332        self.inner.priority_fee_or_price()
333    }
334
335    fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
336        self.inner.effective_gas_price(base_fee)
337    }
338
339    #[inline]
340    fn is_dynamic_fee(&self) -> bool {
341        self.inner.is_dynamic_fee()
342    }
343
344    #[inline]
345    fn kind(&self) -> TxKind {
346        self.inner.kind()
347    }
348
349    #[inline]
350    fn is_create(&self) -> bool {
351        self.inner.is_create()
352    }
353
354    #[inline]
355    fn value(&self) -> U256 {
356        self.inner.value()
357    }
358
359    #[inline]
360    fn input(&self) -> &Bytes {
361        self.inner.input()
362    }
363
364    #[inline]
365    fn access_list(&self) -> Option<&AccessList> {
366        self.inner.access_list()
367    }
368
369    #[inline]
370    fn blob_versioned_hashes(&self) -> Option<&[B256]> {
371        self.inner.blob_versioned_hashes()
372    }
373
374    #[inline]
375    fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
376        self.inner.authorization_list()
377    }
378}
379
380#[cfg(test)]
381mod tests {
382    use alloy_consensus::Transaction;
383
384    use crate::{AnyRpcTransaction, AnyTxEnvelope};
385
386    use super::*;
387
388    #[test]
389    fn test_serde_anytype() {
390        let ty = AnyTxType(126);
391        assert_eq!(serde_json::to_string(&ty).unwrap(), "\"0x7e\"");
392    }
393
394    #[test]
395    fn test_serde_op_deposit() {
396        let input = r#"{
397            "blockHash": "0xef664d656f841b5ad6a2b527b963f1eb48b97d7889d742f6cbff6950388e24cd",
398            "blockNumber": "0x73a78fd",
399            "depositReceiptVersion": "0x1",
400            "from": "0x36bde71c97b33cc4729cf772ae268934f7ab70b2",
401            "gas": "0xc27a8",
402            "gasPrice": "0x521",
403            "hash": "0x0bf1845c5d7a82ec92365d5027f7310793d53004f3c86aa80965c67bf7e7dc80",
404            "input": "0xd764ad0b000100000000000000000000000000000000000000000000000000000001cf5400000000000000000000000099c9fc46f92e8a1c0dec1b1747d010903e884be100000000000000000000000042000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007a12000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000e40166a07a0000000000000000000000000994206dfe8de6ec6920ff4d779b0d950605fb53000000000000000000000000d533a949740bb3306d119cc777fa900ba034cd52000000000000000000000000ca74f404e0c7bfa35b13b511097df966d5a65597000000000000000000000000ca74f404e0c7bfa35b13b511097df966d5a65597000000000000000000000000000000000000000000000216614199391dbba2ba00000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
405            "mint": "0x0",
406            "nonce": "0x74060",
407            "r": "0x0",
408            "s": "0x0",
409            "sourceHash": "0x074adb22f2e6ed9bdd31c52eefc1f050e5db56eb85056450bccd79a6649520b3",
410            "to": "0x4200000000000000000000000000000000000007",
411            "transactionIndex": "0x1",
412            "type": "0x7e",
413            "v": "0x0",
414            "value": "0x0"
415        }"#;
416
417        let tx: AnyRpcTransaction = serde_json::from_str(input).unwrap();
418
419        let AnyTxEnvelope::Unknown(inner) = tx.inner.inner.inner().clone() else {
420            panic!("expected unknown envelope");
421        };
422
423        assert_eq!(inner.inner.ty, AnyTxType(126));
424        assert!(inner.inner.fields.contains_key("input"));
425        assert!(inner.inner.fields.contains_key("mint"));
426        assert!(inner.inner.fields.contains_key("sourceHash"));
427        assert_eq!(inner.gas_limit(), 796584);
428        assert_eq!(inner.gas_price(), Some(1313));
429        assert_eq!(inner.nonce(), 475232);
430
431        let roundrip_tx: AnyRpcTransaction =
432            serde_json::from_str(&serde_json::to_string(&tx).unwrap()).unwrap();
433
434        assert_eq!(tx, roundrip_tx);
435    }
436}