alloy_consensus/transaction/
eip2930.rs1use crate::{SignableTransaction, Transaction, TxType};
2use alloy_eips::{
3 eip2718::IsTyped2718, eip2930::AccessList, eip7702::SignedAuthorization, Typed2718,
4};
5use alloy_primitives::{Bytes, ChainId, Signature, TxKind, B256, U256};
6use alloy_rlp::{BufMut, Decodable, Encodable};
7use core::mem;
8
9use super::{RlpEcdsaDecodableTx, RlpEcdsaEncodableTx};
10
11#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
13#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
16#[doc(alias = "Eip2930Transaction", alias = "TransactionEip2930", alias = "Eip2930Tx")]
17pub struct TxEip2930 {
18 #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
20 pub chain_id: ChainId,
21 #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
23 pub nonce: u64,
24 #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
32 pub gas_price: u128,
33 #[cfg_attr(
39 feature = "serde",
40 serde(with = "alloy_serde::quantity", rename = "gas", alias = "gasLimit")
41 )]
42 pub gas_limit: u64,
43 #[cfg_attr(feature = "serde", serde(default))]
46 pub to: TxKind,
47 pub value: U256,
52 pub access_list: AccessList,
58 pub input: Bytes,
64}
65
66impl TxEip2930 {
67 #[doc(alias = "transaction_type")]
69 pub const fn tx_type() -> TxType {
70 TxType::Eip2930
71 }
72
73 #[inline]
75 pub fn size(&self) -> usize {
76 mem::size_of::<ChainId>() + mem::size_of::<u64>() + mem::size_of::<u128>() + mem::size_of::<u64>() + self.to.size() + mem::size_of::<U256>() + self.access_list.size() + self.input.len() }
85}
86
87impl RlpEcdsaEncodableTx for TxEip2930 {
88 fn rlp_encoded_fields_length(&self) -> usize {
90 self.chain_id.length()
91 + self.nonce.length()
92 + self.gas_price.length()
93 + self.gas_limit.length()
94 + self.to.length()
95 + self.value.length()
96 + self.input.0.length()
97 + self.access_list.length()
98 }
99
100 fn rlp_encode_fields(&self, out: &mut dyn alloy_rlp::BufMut) {
101 self.chain_id.encode(out);
102 self.nonce.encode(out);
103 self.gas_price.encode(out);
104 self.gas_limit.encode(out);
105 self.to.encode(out);
106 self.value.encode(out);
107 self.input.0.encode(out);
108 self.access_list.encode(out);
109 }
110}
111
112impl RlpEcdsaDecodableTx for TxEip2930 {
113 const DEFAULT_TX_TYPE: u8 = { Self::tx_type() as u8 };
114
115 fn rlp_decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
116 Ok(Self {
117 chain_id: Decodable::decode(buf)?,
118 nonce: Decodable::decode(buf)?,
119 gas_price: Decodable::decode(buf)?,
120 gas_limit: Decodable::decode(buf)?,
121 to: Decodable::decode(buf)?,
122 value: Decodable::decode(buf)?,
123 input: Decodable::decode(buf)?,
124 access_list: Decodable::decode(buf)?,
125 })
126 }
127}
128
129impl Transaction for TxEip2930 {
130 #[inline]
131 fn chain_id(&self) -> Option<ChainId> {
132 Some(self.chain_id)
133 }
134
135 #[inline]
136 fn nonce(&self) -> u64 {
137 self.nonce
138 }
139
140 #[inline]
141 fn gas_limit(&self) -> u64 {
142 self.gas_limit
143 }
144
145 #[inline]
146 fn gas_price(&self) -> Option<u128> {
147 Some(self.gas_price)
148 }
149
150 #[inline]
151 fn max_fee_per_gas(&self) -> u128 {
152 self.gas_price
153 }
154
155 #[inline]
156 fn max_priority_fee_per_gas(&self) -> Option<u128> {
157 None
158 }
159
160 #[inline]
161 fn max_fee_per_blob_gas(&self) -> Option<u128> {
162 None
163 }
164
165 #[inline]
166 fn priority_fee_or_price(&self) -> u128 {
167 self.gas_price
168 }
169
170 fn effective_gas_price(&self, _base_fee: Option<u64>) -> u128 {
171 self.gas_price
172 }
173
174 #[inline]
175 fn is_dynamic_fee(&self) -> bool {
176 false
177 }
178
179 #[inline]
180 fn kind(&self) -> TxKind {
181 self.to
182 }
183
184 #[inline]
185 fn is_create(&self) -> bool {
186 self.to.is_create()
187 }
188
189 #[inline]
190 fn value(&self) -> U256 {
191 self.value
192 }
193
194 #[inline]
195 fn input(&self) -> &Bytes {
196 &self.input
197 }
198
199 #[inline]
200 fn access_list(&self) -> Option<&AccessList> {
201 Some(&self.access_list)
202 }
203
204 #[inline]
205 fn blob_versioned_hashes(&self) -> Option<&[B256]> {
206 None
207 }
208
209 #[inline]
210 fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
211 None
212 }
213}
214
215impl Typed2718 for TxEip2930 {
216 fn ty(&self) -> u8 {
217 TxType::Eip2930 as u8
218 }
219}
220
221impl IsTyped2718 for TxEip2930 {
222 fn is_type(type_id: u8) -> bool {
223 matches!(type_id, 0x01)
224 }
225}
226
227impl SignableTransaction<Signature> for TxEip2930 {
228 fn set_chain_id(&mut self, chain_id: ChainId) {
229 self.chain_id = chain_id;
230 }
231
232 fn encode_for_signing(&self, out: &mut dyn BufMut) {
233 out.put_u8(Self::tx_type() as u8);
234 self.encode(out);
235 }
236
237 fn payload_len_for_signature(&self) -> usize {
238 self.length() + 1
239 }
240}
241
242impl Encodable for TxEip2930 {
243 fn encode(&self, out: &mut dyn BufMut) {
244 self.rlp_encode(out);
245 }
246
247 fn length(&self) -> usize {
248 self.rlp_encoded_length()
249 }
250}
251
252impl Decodable for TxEip2930 {
253 fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
254 Self::rlp_decode(buf)
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261 use crate::{SignableTransaction, TxEnvelope};
262 use alloy_primitives::{Address, Signature, TxKind, U256};
263 use alloy_rlp::{Decodable, Encodable};
264
265 #[test]
266 fn test_decode_create() {
267 let tx = TxEip2930 {
269 chain_id: 1u64,
270 nonce: 0,
271 gas_price: 1,
272 gas_limit: 2,
273 to: TxKind::Create,
274 value: U256::from(3_u64),
275 input: vec![1, 2].into(),
276 access_list: Default::default(),
277 };
278 let signature = Signature::test_signature();
279
280 let mut encoded = Vec::new();
281 tx.rlp_encode_signed(&signature, &mut encoded);
282
283 let decoded = TxEip2930::rlp_decode_signed(&mut &*encoded).unwrap();
284 assert_eq!(decoded, tx.into_signed(signature));
285 }
286
287 #[test]
288 fn test_decode_call() {
289 let request = TxEip2930 {
290 chain_id: 1u64,
291 nonce: 0,
292 gas_price: 1,
293 gas_limit: 2,
294 to: Address::default().into(),
295 value: U256::from(3_u64),
296 input: vec![1, 2].into(),
297 access_list: Default::default(),
298 };
299
300 let signature = Signature::test_signature();
301
302 let tx = request.into_signed(signature);
303
304 let envelope = TxEnvelope::Eip2930(tx);
305
306 let mut encoded = Vec::new();
307 envelope.encode(&mut encoded);
308 assert_eq!(encoded.len(), envelope.length());
309
310 assert_eq!(
311 alloy_primitives::hex::encode(&encoded),
312 "b86401f8610180010294000000000000000000000000000000000000000003820102c080a0840cfc572845f5786e702984c2a582528cad4b49b2a10b9db1be7fca90058565a025e7109ceb98168d95b09b18bbf6b685130e0562f233877d492b94eee0c5b6d1"
313 );
314
315 let decoded = TxEnvelope::decode(&mut encoded.as_ref()).unwrap();
316 assert_eq!(decoded, envelope);
317 }
318}
319
320#[cfg(all(feature = "serde", feature = "serde-bincode-compat"))]
322pub(super) mod serde_bincode_compat {
323 use alloc::borrow::Cow;
324 use alloy_eips::eip2930::AccessList;
325 use alloy_primitives::{Bytes, ChainId, TxKind, U256};
326 use serde::{Deserialize, Deserializer, Serialize, Serializer};
327 use serde_with::{DeserializeAs, SerializeAs};
328
329 #[derive(Debug, Serialize, Deserialize)]
345 pub struct TxEip2930<'a> {
346 chain_id: ChainId,
347 nonce: u64,
348 gas_price: u128,
349 gas_limit: u64,
350 #[serde(default)]
351 to: TxKind,
352 value: U256,
353 access_list: Cow<'a, AccessList>,
354 input: Cow<'a, Bytes>,
355 }
356
357 impl<'a> From<&'a super::TxEip2930> for TxEip2930<'a> {
358 fn from(value: &'a super::TxEip2930) -> Self {
359 Self {
360 chain_id: value.chain_id,
361 nonce: value.nonce,
362 gas_price: value.gas_price,
363 gas_limit: value.gas_limit,
364 to: value.to,
365 value: value.value,
366 access_list: Cow::Borrowed(&value.access_list),
367 input: Cow::Borrowed(&value.input),
368 }
369 }
370 }
371
372 impl<'a> From<TxEip2930<'a>> for super::TxEip2930 {
373 fn from(value: TxEip2930<'a>) -> Self {
374 Self {
375 chain_id: value.chain_id,
376 nonce: value.nonce,
377 gas_price: value.gas_price,
378 gas_limit: value.gas_limit,
379 to: value.to,
380 value: value.value,
381 access_list: value.access_list.into_owned(),
382 input: value.input.into_owned(),
383 }
384 }
385 }
386
387 impl SerializeAs<super::TxEip2930> for TxEip2930<'_> {
388 fn serialize_as<S>(source: &super::TxEip2930, serializer: S) -> Result<S::Ok, S::Error>
389 where
390 S: Serializer,
391 {
392 TxEip2930::from(source).serialize(serializer)
393 }
394 }
395
396 impl<'de> DeserializeAs<'de, super::TxEip2930> for TxEip2930<'de> {
397 fn deserialize_as<D>(deserializer: D) -> Result<super::TxEip2930, D::Error>
398 where
399 D: Deserializer<'de>,
400 {
401 TxEip2930::deserialize(deserializer).map(Into::into)
402 }
403 }
404
405 #[cfg(test)]
406 mod tests {
407 use arbitrary::Arbitrary;
408 use bincode::config;
409 use rand::Rng;
410 use serde::{Deserialize, Serialize};
411 use serde_with::serde_as;
412
413 use super::super::{serde_bincode_compat, TxEip2930};
414
415 #[test]
416 fn test_tx_eip2930_bincode_roundtrip() {
417 #[serde_as]
418 #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
419 struct Data {
420 #[serde_as(as = "serde_bincode_compat::TxEip2930")]
421 transaction: TxEip2930,
422 }
423
424 let mut bytes = [0u8; 1024];
425 rand::thread_rng().fill(bytes.as_mut_slice());
426 let data = Data {
427 transaction: TxEip2930::arbitrary(&mut arbitrary::Unstructured::new(&bytes))
428 .unwrap(),
429 };
430
431 let encoded = bincode::serde::encode_to_vec(&data, config::legacy()).unwrap();
432 let (decoded, _) =
433 bincode::serde::decode_from_slice::<Data, _>(&encoded, config::legacy()).unwrap();
434 assert_eq!(decoded, data);
435 }
436 }
437}