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