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