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