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