alloy_consensus/transaction/
eip7702.rs1use crate::{SignableTransaction, Transaction, TxType};
2use alloc::vec::Vec;
3use alloy_eips::{
4 eip2718::IsTyped2718,
5 eip2930::AccessList,
6 eip7702::{constants::EIP7702_TX_TYPE_ID, SignedAuthorization},
7 Typed2718,
8};
9use alloy_primitives::{Address, Bytes, ChainId, Signature, TxKind, B256, U256};
10use alloy_rlp::{BufMut, Decodable, Encodable};
11use core::mem;
12
13use super::{RlpEcdsaDecodableTx, RlpEcdsaEncodableTx};
14
15#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
17#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))]
20#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
21#[doc(alias = "Eip7702Transaction", alias = "TransactionEip7702", alias = "Eip7702Tx")]
22pub struct TxEip7702 {
23 #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
25 pub chain_id: ChainId,
26 #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
28 pub nonce: u64,
29 #[cfg_attr(
35 feature = "serde",
36 serde(with = "alloy_serde::quantity", rename = "gas", alias = "gasLimit")
37 )]
38 pub gas_limit: u64,
39 #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
51 pub max_fee_per_gas: u128,
52 #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
60 pub max_priority_fee_per_gas: u128,
61 pub to: Address,
63 pub value: U256,
68 pub access_list: AccessList,
74 pub authorization_list: Vec<SignedAuthorization>,
78 pub input: Bytes,
81}
82
83impl TxEip7702 {
84 #[doc(alias = "transaction_type")]
86 pub const fn tx_type() -> TxType {
87 TxType::Eip7702
88 }
89
90 #[inline]
92 pub fn size(&self) -> usize {
93 mem::size_of::<ChainId>() + mem::size_of::<u64>() + mem::size_of::<u64>() + mem::size_of::<u128>() + mem::size_of::<u128>() + mem::size_of::<Address>() + mem::size_of::<U256>() + self.access_list.size() + self.input.len() + self.authorization_list.capacity() * mem::size_of::<SignedAuthorization>()
103 }
105}
106
107impl RlpEcdsaEncodableTx for TxEip7702 {
108 #[doc(hidden)]
110 fn rlp_encoded_fields_length(&self) -> usize {
111 self.chain_id.length()
112 + self.nonce.length()
113 + self.max_priority_fee_per_gas.length()
114 + self.max_fee_per_gas.length()
115 + self.gas_limit.length()
116 + self.to.length()
117 + self.value.length()
118 + self.input.0.length()
119 + self.access_list.length()
120 + self.authorization_list.length()
121 }
122
123 fn rlp_encode_fields(&self, out: &mut dyn alloy_rlp::BufMut) {
124 self.chain_id.encode(out);
125 self.nonce.encode(out);
126 self.max_priority_fee_per_gas.encode(out);
127 self.max_fee_per_gas.encode(out);
128 self.gas_limit.encode(out);
129 self.to.encode(out);
130 self.value.encode(out);
131 self.input.0.encode(out);
132 self.access_list.encode(out);
133 self.authorization_list.encode(out);
134 }
135}
136
137impl RlpEcdsaDecodableTx for TxEip7702 {
138 const DEFAULT_TX_TYPE: u8 = { Self::tx_type() as u8 };
139
140 fn rlp_decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
141 Ok(Self {
142 chain_id: Decodable::decode(buf)?,
143 nonce: Decodable::decode(buf)?,
144 max_priority_fee_per_gas: Decodable::decode(buf)?,
145 max_fee_per_gas: Decodable::decode(buf)?,
146 gas_limit: Decodable::decode(buf)?,
147 to: Decodable::decode(buf)?,
148 value: Decodable::decode(buf)?,
149 input: Decodable::decode(buf)?,
150 access_list: Decodable::decode(buf)?,
151 authorization_list: Decodable::decode(buf)?,
152 })
153 }
154}
155
156impl Transaction for TxEip7702 {
157 #[inline]
158 fn chain_id(&self) -> Option<ChainId> {
159 Some(self.chain_id)
160 }
161
162 #[inline]
163 fn nonce(&self) -> u64 {
164 self.nonce
165 }
166
167 #[inline]
168 fn gas_limit(&self) -> u64 {
169 self.gas_limit
170 }
171
172 #[inline]
173 fn gas_price(&self) -> Option<u128> {
174 None
175 }
176
177 #[inline]
178 fn max_fee_per_gas(&self) -> u128 {
179 self.max_fee_per_gas
180 }
181
182 #[inline]
183 fn max_priority_fee_per_gas(&self) -> Option<u128> {
184 Some(self.max_priority_fee_per_gas)
185 }
186
187 #[inline]
188 fn max_fee_per_blob_gas(&self) -> Option<u128> {
189 None
190 }
191
192 #[inline]
193 fn priority_fee_or_price(&self) -> u128 {
194 self.max_priority_fee_per_gas
195 }
196
197 fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
198 alloy_eips::eip1559::calc_effective_gas_price(
199 self.max_fee_per_gas,
200 self.max_priority_fee_per_gas,
201 base_fee,
202 )
203 }
204
205 #[inline]
206 fn is_dynamic_fee(&self) -> bool {
207 true
208 }
209
210 #[inline]
211 fn kind(&self) -> TxKind {
212 self.to.into()
213 }
214
215 #[inline]
216 fn is_create(&self) -> bool {
217 false
218 }
219
220 #[inline]
221 fn value(&self) -> U256 {
222 self.value
223 }
224
225 #[inline]
226 fn input(&self) -> &Bytes {
227 &self.input
228 }
229
230 #[inline]
231 fn access_list(&self) -> Option<&AccessList> {
232 Some(&self.access_list)
233 }
234
235 #[inline]
236 fn blob_versioned_hashes(&self) -> Option<&[B256]> {
237 None
238 }
239
240 #[inline]
241 fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
242 Some(&self.authorization_list)
243 }
244}
245
246impl SignableTransaction<Signature> for TxEip7702 {
247 fn set_chain_id(&mut self, chain_id: ChainId) {
248 self.chain_id = chain_id;
249 }
250
251 fn encode_for_signing(&self, out: &mut dyn alloy_rlp::BufMut) {
252 out.put_u8(EIP7702_TX_TYPE_ID);
253 self.encode(out)
254 }
255
256 fn payload_len_for_signature(&self) -> usize {
257 self.length() + 1
258 }
259}
260
261impl Typed2718 for TxEip7702 {
262 fn ty(&self) -> u8 {
263 TxType::Eip7702 as u8
264 }
265}
266
267impl IsTyped2718 for TxEip7702 {
268 fn is_type(type_id: u8) -> bool {
269 matches!(type_id, 0x04)
270 }
271}
272
273impl Encodable for TxEip7702 {
274 fn encode(&self, out: &mut dyn BufMut) {
275 self.rlp_encode(out);
276 }
277
278 fn length(&self) -> usize {
279 self.rlp_encoded_length()
280 }
281}
282
283impl Decodable for TxEip7702 {
284 fn decode(data: &mut &[u8]) -> alloy_rlp::Result<Self> {
285 Self::rlp_decode(data)
286 }
287}
288
289#[cfg(all(feature = "serde", feature = "serde-bincode-compat"))]
291pub(super) mod serde_bincode_compat {
292 use alloc::{borrow::Cow, vec::Vec};
293 use alloy_eips::{eip2930::AccessList, eip7702::serde_bincode_compat::SignedAuthorization};
294 use alloy_primitives::{Address, Bytes, ChainId, U256};
295 use serde::{Deserialize, Deserializer, Serialize, Serializer};
296 use serde_with::{DeserializeAs, SerializeAs};
297
298 #[derive(Debug, Serialize, Deserialize)]
314 pub struct TxEip7702<'a> {
315 chain_id: ChainId,
316 nonce: u64,
317 gas_limit: u64,
318 max_fee_per_gas: u128,
319 max_priority_fee_per_gas: u128,
320 to: Address,
321 value: U256,
322 access_list: Cow<'a, AccessList>,
323 authorization_list: Vec<SignedAuthorization<'a>>,
324 input: Cow<'a, Bytes>,
325 }
326
327 impl<'a> From<&'a super::TxEip7702> for TxEip7702<'a> {
328 fn from(value: &'a super::TxEip7702) -> Self {
329 Self {
330 chain_id: value.chain_id,
331 nonce: value.nonce,
332 gas_limit: value.gas_limit,
333 max_fee_per_gas: value.max_fee_per_gas,
334 max_priority_fee_per_gas: value.max_priority_fee_per_gas,
335 to: value.to,
336 value: value.value,
337 access_list: Cow::Borrowed(&value.access_list),
338 authorization_list: value.authorization_list.iter().map(Into::into).collect(),
339 input: Cow::Borrowed(&value.input),
340 }
341 }
342 }
343
344 impl<'a> From<TxEip7702<'a>> for super::TxEip7702 {
345 fn from(value: TxEip7702<'a>) -> Self {
346 Self {
347 chain_id: value.chain_id,
348 nonce: value.nonce,
349 gas_limit: value.gas_limit,
350 max_fee_per_gas: value.max_fee_per_gas,
351 max_priority_fee_per_gas: value.max_priority_fee_per_gas,
352 to: value.to,
353 value: value.value,
354 access_list: value.access_list.into_owned(),
355 authorization_list: value.authorization_list.into_iter().map(Into::into).collect(),
356 input: value.input.into_owned(),
357 }
358 }
359 }
360
361 impl SerializeAs<super::TxEip7702> for TxEip7702<'_> {
362 fn serialize_as<S>(source: &super::TxEip7702, serializer: S) -> Result<S::Ok, S::Error>
363 where
364 S: Serializer,
365 {
366 TxEip7702::from(source).serialize(serializer)
367 }
368 }
369
370 impl<'de> DeserializeAs<'de, super::TxEip7702> for TxEip7702<'de> {
371 fn deserialize_as<D>(deserializer: D) -> Result<super::TxEip7702, D::Error>
372 where
373 D: Deserializer<'de>,
374 {
375 TxEip7702::deserialize(deserializer).map(Into::into)
376 }
377 }
378
379 #[cfg(test)]
380 mod tests {
381 use arbitrary::Arbitrary;
382 use bincode::config;
383 use rand::Rng;
384 use serde::{Deserialize, Serialize};
385 use serde_with::serde_as;
386
387 use super::super::{serde_bincode_compat, TxEip7702};
388
389 #[test]
390 fn test_tx_eip7702_bincode_roundtrip() {
391 #[serde_as]
392 #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
393 struct Data {
394 #[serde_as(as = "serde_bincode_compat::TxEip7702")]
395 transaction: TxEip7702,
396 }
397
398 let mut bytes = [0u8; 1024];
399 rand::thread_rng().fill(bytes.as_mut_slice());
400 let data = Data {
401 transaction: TxEip7702::arbitrary(&mut arbitrary::Unstructured::new(&bytes))
402 .unwrap(),
403 };
404
405 let encoded = bincode::serde::encode_to_vec(&data, config::legacy()).unwrap();
406 let (decoded, _) =
407 bincode::serde::decode_from_slice::<Data, _>(&encoded, config::legacy()).unwrap();
408 assert_eq!(decoded, data);
409 }
410 }
411}
412
413#[cfg(all(test, feature = "k256"))]
414mod tests {
415 use super::*;
416 use crate::SignableTransaction;
417 use alloy_eips::eip2930::AccessList;
418 use alloy_primitives::{address, b256, hex, Address, Signature, U256};
419
420 #[test]
421 fn encode_decode_eip7702() {
422 let tx = TxEip7702 {
423 chain_id: 1,
424 nonce: 0x42,
425 gas_limit: 44386,
426 to: address!("6069a6c32cf691f5982febae4faf8a6f3ab2f0f6"),
427 value: U256::from(0_u64),
428 input: hex!("a22cb4650000000000000000000000005eee75727d804a2b13038928d36f8b188945a57a0000000000000000000000000000000000000000000000000000000000000000").into(),
429 max_fee_per_gas: 0x4a817c800,
430 max_priority_fee_per_gas: 0x3b9aca00,
431 access_list: AccessList::default(),
432 authorization_list: vec![],
433 };
434
435 let sig = Signature::from_scalars_and_parity(
436 b256!("840cfc572845f5786e702984c2a582528cad4b49b2a10b9db1be7fca90058565"),
437 b256!("25e7109ceb98168d95b09b18bbf6b685130e0562f233877d492b94eee0c5b6d1"),
438 false,
439 );
440
441 let mut buf = vec![];
442 tx.rlp_encode_signed(&sig, &mut buf);
443 let decoded = TxEip7702::rlp_decode_signed(&mut &buf[..]).unwrap();
444 assert_eq!(decoded, tx.into_signed(sig));
445 }
446
447 #[test]
448 fn test_decode_create() {
449 let tx = TxEip7702 {
451 chain_id: 1u64,
452 nonce: 0,
453 max_fee_per_gas: 0x4a817c800,
454 max_priority_fee_per_gas: 0x3b9aca00,
455 gas_limit: 2,
456 to: Address::default(),
457 value: U256::ZERO,
458 input: vec![1, 2].into(),
459 access_list: Default::default(),
460 authorization_list: Default::default(),
461 };
462 let sig = Signature::from_scalars_and_parity(
463 b256!("840cfc572845f5786e702984c2a582528cad4b49b2a10b9db1be7fca90058565"),
464 b256!("25e7109ceb98168d95b09b18bbf6b685130e0562f233877d492b94eee0c5b6d1"),
465 false,
466 );
467 let mut buf = vec![];
468 tx.rlp_encode_signed(&sig, &mut buf);
469 let decoded = TxEip7702::rlp_decode_signed(&mut &buf[..]).unwrap();
470 assert_eq!(decoded, tx.into_signed(sig));
471 }
472
473 #[test]
474 fn test_decode_call() {
475 let tx = TxEip7702 {
476 chain_id: 1u64,
477 nonce: 0,
478 max_fee_per_gas: 0x4a817c800,
479 max_priority_fee_per_gas: 0x3b9aca00,
480 gas_limit: 2,
481 to: Address::default(),
482 value: U256::ZERO,
483 input: vec![1, 2].into(),
484 access_list: Default::default(),
485 authorization_list: Default::default(),
486 };
487
488 let sig = Signature::from_scalars_and_parity(
489 b256!("840cfc572845f5786e702984c2a582528cad4b49b2a10b9db1be7fca90058565"),
490 b256!("25e7109ceb98168d95b09b18bbf6b685130e0562f233877d492b94eee0c5b6d1"),
491 false,
492 );
493
494 let mut buf = vec![];
495 tx.rlp_encode_signed(&sig, &mut buf);
496 let decoded = TxEip7702::rlp_decode_signed(&mut &buf[..]).unwrap();
497 assert_eq!(decoded, tx.into_signed(sig));
498 }
499}