tempo_primitives/transaction/
fee_token.rs1use alloy_consensus::{
2 SignableTransaction, Signed, Transaction,
3 transaction::{RlpEcdsaDecodableTx, RlpEcdsaEncodableTx},
4};
5use alloy_eips::{Typed2718, eip2930::AccessList, eip7702::SignedAuthorization};
6use alloy_primitives::{Address, B256, Bytes, ChainId, Signature, TxKind, U256, keccak256};
7use alloy_rlp::{Buf, BufMut, Decodable, EMPTY_STRING_CODE, Encodable};
8use core::mem;
9
10pub const FEE_TOKEN_TX_TYPE_ID: u8 = 0x77;
12
13pub const FEE_PAYER_SIGNATURE_MAGIC_BYTE: u8 = 0x78;
15
16#[derive(Clone, Debug, PartialEq, Eq, Hash)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25#[cfg_attr(feature = "reth-codec", derive(reth_codecs::Compact))]
26#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
27#[doc(alias = "FeeTokenTransaction", alias = "TransactionFeeToken")]
28#[cfg_attr(test, reth_codecs::add_arbitrary_tests(compact, rlp))]
29pub struct TxFeeToken {
30 #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
32 pub chain_id: ChainId,
33
34 #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
36 pub nonce: u64,
37
38 pub fee_token: Option<Address>,
40
41 #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
43 pub max_priority_fee_per_gas: u128,
44
45 #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
47 pub max_fee_per_gas: u128,
48
49 #[cfg_attr(
51 feature = "serde",
52 serde(with = "alloy_serde::quantity", rename = "gas", alias = "gasLimit")
53 )]
54 pub gas_limit: u64,
55
56 pub to: TxKind,
58
59 pub value: U256,
61
62 pub access_list: AccessList,
64
65 pub authorization_list: Vec<SignedAuthorization>,
67
68 pub fee_payer_signature: Option<Signature>,
70
71 pub input: Bytes,
74}
75
76impl Default for TxFeeToken {
77 fn default() -> Self {
78 Self {
79 chain_id: 0,
80 nonce: 0,
81 fee_token: None,
82 max_priority_fee_per_gas: 0,
83 max_fee_per_gas: 0,
84 gas_limit: 0,
85 to: TxKind::Create,
86 value: U256::ZERO,
87 access_list: AccessList::default(),
88 authorization_list: Vec::new(),
89 fee_payer_signature: None,
90 input: Bytes::new(),
91 }
92 }
93}
94
95impl TxFeeToken {
96 #[doc(alias = "transaction_type")]
98 pub const fn tx_type() -> u8 {
99 FEE_TOKEN_TX_TYPE_ID
100 }
101
102 pub fn validate(&self) -> Result<(), &'static str> {
104 if !self.authorization_list.is_empty() && self.to.is_create() {
106 return Err("to field cannot be Create when authorization_list is non-empty");
107 }
108 Ok(())
109 }
110
111 #[inline]
113 pub fn size(&self) -> usize {
114 mem::size_of::<ChainId>() + mem::size_of::<u64>() + mem::size_of::<Option<Address>>() + mem::size_of::<u128>() + mem::size_of::<u128>() + mem::size_of::<u64>() + mem::size_of::<TxKind>() + mem::size_of::<U256>() + self.access_list.size() + self.authorization_list.len() * mem::size_of::<SignedAuthorization>() + self.input.len() }
126
127 pub fn into_signed(self, signature: Signature) -> Signed<Self> {
129 let tx_hash = self.tx_hash(&signature);
130 Signed::new_unchecked(self, signature, tx_hash)
131 }
132
133 fn rlp_encoded_fields_length(
135 &self,
136 signature_length: impl FnOnce(&Option<Signature>) -> usize,
137 skip_fee_token: bool,
138 ) -> usize {
139 self.chain_id.length() +
140 self.nonce.length() +
141 self.max_priority_fee_per_gas.length() +
142 self.max_fee_per_gas.length() +
143 self.gas_limit.length() +
144 self.to.length() +
145 self.value.length() +
146 self.input.length() +
147 self.access_list.length() +
148 self.authorization_list.length() +
149 if !skip_fee_token && let Some(addr) = self.fee_token {
151 addr.length()
152 } else {
153 1 } +
155 signature_length(&self.fee_payer_signature)
156 }
157
158 fn rlp_encode_fields(
159 &self,
160 out: &mut dyn BufMut,
161 encode_signature: impl FnOnce(&Option<Signature>, &mut dyn BufMut),
162 skip_fee_token: bool,
163 ) {
164 self.chain_id.encode(out);
165 self.nonce.encode(out);
166 self.max_priority_fee_per_gas.encode(out);
167 self.max_fee_per_gas.encode(out);
168 self.gas_limit.encode(out);
169 self.to.encode(out);
170 self.value.encode(out);
171 self.input.encode(out);
172 self.access_list.encode(out);
173 self.authorization_list.encode(out);
174 if !skip_fee_token && let Some(addr) = self.fee_token {
176 addr.encode(out);
177 } else {
178 out.put_u8(EMPTY_STRING_CODE);
179 }
180 encode_signature(&self.fee_payer_signature, out);
181 }
182
183 pub fn fee_payer_signature_hash(&self, sender: Address) -> B256 {
184 let rlp_header = alloy_rlp::Header {
185 list: true,
186 payload_length: self.rlp_encoded_fields_length(|_| sender.length(), false),
187 };
188 let mut buf = Vec::with_capacity(rlp_header.length_with_payload());
189 buf.put_u8(FEE_PAYER_SIGNATURE_MAGIC_BYTE);
190 rlp_header.encode(&mut buf);
191 self.rlp_encode_fields(
192 &mut buf,
193 |_, out| {
194 sender.encode(out);
195 },
196 false,
197 );
198
199 keccak256(&buf)
200 }
201}
202
203impl RlpEcdsaEncodableTx for TxFeeToken {
204 fn rlp_encoded_fields_length(&self) -> usize {
206 self.rlp_encoded_fields_length(
207 |signature| {
208 signature.map_or(1, |s| {
209 alloy_rlp::Header {
210 list: true,
211 payload_length: s.rlp_rs_len() + s.v().length(),
212 }
213 .length_with_payload()
214 })
215 },
216 false,
217 )
218 }
219
220 fn rlp_encode_fields(&self, out: &mut dyn alloy_rlp::BufMut) {
222 self.rlp_encode_fields(
223 out,
224 |signature, out| {
225 if let Some(signature) = signature {
226 let payload_length = signature.rlp_rs_len() + signature.v().length();
227 alloy_rlp::Header {
228 list: true,
229 payload_length,
230 }
231 .encode(out);
232 signature.write_rlp_vrs(out, signature.v());
233 } else {
234 out.put_u8(EMPTY_STRING_CODE);
235 }
236 },
237 false,
238 );
239 }
240}
241
242impl RlpEcdsaDecodableTx for TxFeeToken {
243 const DEFAULT_TX_TYPE: u8 = FEE_TOKEN_TX_TYPE_ID;
244
245 fn rlp_decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
247 let chain_id = Decodable::decode(buf)?;
248 let nonce = Decodable::decode(buf)?;
249
250 let tx = Self {
251 chain_id,
252 nonce,
253 max_priority_fee_per_gas: Decodable::decode(buf)?,
254 max_fee_per_gas: Decodable::decode(buf)?,
255 gas_limit: Decodable::decode(buf)?,
256 to: Decodable::decode(buf)?,
257 value: Decodable::decode(buf)?,
258 input: Decodable::decode(buf)?,
259 access_list: Decodable::decode(buf)?,
260 authorization_list: Decodable::decode(buf)?,
261 fee_token: TxKind::decode(buf)?.into_to(),
263 fee_payer_signature: if let Some(first) = buf.first() {
264 if *first == EMPTY_STRING_CODE {
265 buf.advance(1);
266 None
267 } else {
268 let header = alloy_rlp::Header::decode(buf)?;
269 if buf.len() < header.payload_length {
270 return Err(alloy_rlp::Error::InputTooShort);
271 }
272 if !header.list {
273 return Err(alloy_rlp::Error::UnexpectedString);
274 }
275 Some(Signature::decode_rlp_vrs(buf, bool::decode)?)
276 }
277 } else {
278 return Err(alloy_rlp::Error::InputTooShort);
279 },
280 };
281
282 tx.validate().map_err(alloy_rlp::Error::Custom)?;
284
285 Ok(tx)
286 }
287}
288
289impl Transaction for TxFeeToken {
290 #[inline]
291 fn chain_id(&self) -> Option<ChainId> {
292 Some(self.chain_id)
293 }
294
295 #[inline]
296 fn nonce(&self) -> u64 {
297 self.nonce
298 }
299
300 #[inline]
301 fn gas_limit(&self) -> u64 {
302 self.gas_limit
303 }
304
305 #[inline]
306 fn gas_price(&self) -> Option<u128> {
307 None
308 }
309
310 #[inline]
311 fn max_fee_per_gas(&self) -> u128 {
312 self.max_fee_per_gas
313 }
314
315 #[inline]
316 fn max_priority_fee_per_gas(&self) -> Option<u128> {
317 Some(self.max_priority_fee_per_gas)
318 }
319
320 #[inline]
321 fn max_fee_per_blob_gas(&self) -> Option<u128> {
322 None
323 }
324
325 #[inline]
326 fn priority_fee_or_price(&self) -> u128 {
327 self.max_priority_fee_per_gas
328 }
329
330 fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
331 alloy_eips::eip1559::calc_effective_gas_price(
332 self.max_fee_per_gas,
333 self.max_priority_fee_per_gas,
334 base_fee,
335 )
336 }
337
338 #[inline]
339 fn is_dynamic_fee(&self) -> bool {
340 true
341 }
342
343 #[inline]
344 fn kind(&self) -> TxKind {
345 self.to
346 }
347
348 #[inline]
349 fn is_create(&self) -> bool {
350 self.to.is_create()
351 }
352
353 #[inline]
354 fn value(&self) -> U256 {
355 self.value
356 }
357
358 #[inline]
359 fn input(&self) -> &Bytes {
360 &self.input
361 }
362
363 #[inline]
364 fn access_list(&self) -> Option<&AccessList> {
365 Some(&self.access_list)
366 }
367
368 #[inline]
369 fn blob_versioned_hashes(&self) -> Option<&[B256]> {
370 None
371 }
372
373 #[inline]
374 fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
375 Some(&self.authorization_list)
376 }
377}
378
379impl Typed2718 for TxFeeToken {
380 fn ty(&self) -> u8 {
381 FEE_TOKEN_TX_TYPE_ID
382 }
383}
384
385impl SignableTransaction<Signature> for TxFeeToken {
386 fn set_chain_id(&mut self, chain_id: ChainId) {
387 self.chain_id = chain_id;
388 }
389
390 fn encode_for_signing(&self, out: &mut dyn alloy_rlp::BufMut) {
391 let skip_fee_token = self.fee_payer_signature.is_some();
394 out.put_u8(Self::tx_type());
396 let payload_length = self.rlp_encoded_fields_length(|_| 1, skip_fee_token);
397 alloy_rlp::Header {
398 list: true,
399 payload_length,
400 }
401 .encode(out);
402 self.rlp_encode_fields(
403 out,
404 |signature, out| {
405 if signature.is_some() {
406 out.put_u8(0);
407 } else {
408 out.put_u8(EMPTY_STRING_CODE);
409 }
410 },
411 skip_fee_token,
412 );
413 }
414
415 fn payload_len_for_signature(&self) -> usize {
416 let payload_length =
417 self.rlp_encoded_fields_length(|_| 1, self.fee_payer_signature.is_some());
418 1 + alloy_rlp::Header {
419 list: true,
420 payload_length,
421 }
422 .length_with_payload()
423 }
424}
425
426impl Encodable for TxFeeToken {
427 fn encode(&self, out: &mut dyn BufMut) {
428 self.rlp_encode(out);
429 }
430
431 fn length(&self) -> usize {
432 self.rlp_encoded_length()
433 }
434}
435
436impl Decodable for TxFeeToken {
437 fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
438 Self::rlp_decode(buf)
439 }
440}
441
442impl reth_primitives_traits::InMemorySize for TxFeeToken {
443 fn size(&self) -> usize {
444 Self::size(self)
445 }
446}
447
448#[cfg(feature = "serde-bincode-compat")]
449impl reth_primitives_traits::serde_bincode_compat::RlpBincode for TxFeeToken {}
450
451#[cfg(any(test, feature = "arbitrary"))]
452impl<'a> arbitrary::Arbitrary<'a> for TxFeeToken {
453 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
454 let authorization_list: Vec<SignedAuthorization> = u.arbitrary()?;
456
457 let to = if !authorization_list.is_empty() {
459 TxKind::Call(u.arbitrary()?)
461 } else {
462 u.arbitrary()?
464 };
465
466 Ok(Self {
467 chain_id: u.arbitrary()?,
468 nonce: u.arbitrary()?,
469 fee_token: u.arbitrary()?,
470 max_priority_fee_per_gas: u.arbitrary()?,
471 max_fee_per_gas: u.arbitrary()?,
472 gas_limit: u.arbitrary()?,
473 to,
474 value: u.arbitrary()?,
475 access_list: u.arbitrary()?,
476 authorization_list,
477 fee_payer_signature: u.arbitrary()?,
478 input: u.arbitrary()?,
479 })
480 }
481}
482
483#[cfg(test)]
484mod tests {
485 use super::*;
486 use alloy_eips::eip7702::{Authorization, SignedAuthorization};
487 use alloy_primitives::{Address, U256};
488
489 #[test]
490 fn test_tx_fee_token_validation() {
491 let tx1 = TxFeeToken {
493 to: TxKind::Create,
494 authorization_list: vec![],
495 ..Default::default()
496 };
497 assert!(tx1.validate().is_ok());
498
499 let tx2 = TxFeeToken {
501 to: TxKind::Call(Address::ZERO),
502 authorization_list: vec![SignedAuthorization::new_unchecked(
503 Authorization {
504 chain_id: U256::from(1),
505 address: Address::ZERO,
506 nonce: 0,
507 },
508 0,
509 U256::ZERO,
510 U256::ZERO,
511 )],
512 ..Default::default()
513 };
514 assert!(tx2.validate().is_ok());
515
516 let tx3 = TxFeeToken {
518 to: TxKind::Create,
519 authorization_list: vec![SignedAuthorization::new_unchecked(
520 Authorization {
521 chain_id: U256::from(1),
522 address: Address::ZERO,
523 nonce: 0,
524 },
525 0,
526 U256::ZERO,
527 U256::ZERO,
528 )],
529 ..Default::default()
530 };
531 assert!(tx3.validate().is_err());
532 }
533
534 #[test]
535 fn test_tx_type() {
536 assert_eq!(TxFeeToken::tx_type(), 0x77);
537 }
538}