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
73#[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#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
91#[doc(alias = "UnknownTypedTx")]
92pub struct UnknownTypedTransaction {
93 #[serde(rename = "type")]
94 pub ty: AnyTxType,
96
97 #[serde(flatten)]
99 pub fields: OtherFields,
100
101 #[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 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 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#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
272#[doc(alias = "UnknownTransactionEnvelope")]
273pub struct UnknownTxEnvelope {
274 pub hash: B256,
276
277 #[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}