alloy_network/any/
unknowns.rs1use 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#[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#[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#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
97#[doc(alias = "UnknownTypedTx")]
98pub struct UnknownTypedTransaction {
99 #[serde(rename = "type")]
100 pub ty: AnyTxType,
102
103 #[serde(flatten)]
105 pub fields: OtherFields,
106
107 #[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 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 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#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
278#[doc(alias = "UnknownTransactionEnvelope")]
279pub struct UnknownTxEnvelope {
280 pub hash: B256,
282
283 #[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}