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