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