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