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