1use alloy::consensus::{
4 SignableTransaction, Signed, Transaction, Typed2718, transaction::RlpEcdsaEncodableTx,
5};
6use alloy::primitives::Signature;
7use alloy::primitives::{Address, Bytes, ChainId, TxKind, U256, keccak256};
8use alloy::rlp::{BufMut, Decodable, Encodable, Header};
9use alloy::rpc::types::TransactionInput;
10use serde::{Deserialize, Serialize};
11
12use crate::network::tx_type::TxType;
13
14pub use self::meta::{Eip712Meta, PaymasterParams};
15pub use self::utils::{BytecodeHashError, hash_bytecode};
16
17mod meta;
18mod signing;
19mod utils;
20
21#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
26#[serde(rename_all = "camelCase")]
27#[doc(
28 alias = "Eip712Transaction",
29 alias = "TransactionEip712",
30 alias = "Eip712Tx"
31)]
32pub struct TxEip712 {
33 #[serde(with = "alloy::serde::quantity")]
35 pub chain_id: ChainId,
36 pub nonce: U256,
39 #[serde(with = "alloy::serde::quantity")]
45 pub gas: u64,
46 #[serde(with = "alloy::serde::quantity")]
58 pub max_fee_per_gas: u128,
59 #[serde(with = "alloy::serde::quantity")]
67 pub max_priority_fee_per_gas: u128, pub to: Address,
73 pub from: Address,
75 pub value: U256,
80 pub input: Bytes,
86 #[serde(flatten)]
88 pub eip712_meta: Option<Eip712Meta>,
89}
90
91impl TxEip712 {
92 pub const fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
94 match base_fee {
95 None => self.max_fee_per_gas,
96 Some(base_fee) => {
97 let tip = self.max_fee_per_gas.saturating_sub(base_fee as u128);
100 if tip > self.max_priority_fee_per_gas {
101 self.max_priority_fee_per_gas + base_fee as u128
102 } else {
103 self.max_fee_per_gas
105 }
106 }
107 }
108 }
109
110 pub(crate) fn encode_with_signature(
113 &self,
114 signature: &Signature,
115 out: &mut dyn BufMut,
116 ) {
118 out.put_u8(self.tx_type() as u8);
133 self.encode_with_signature_fields(signature, out);
134 }
135
136 #[doc(hidden)]
143 pub fn decode_signed_fields(buf: &mut &[u8]) -> alloy::rlp::Result<Signed<Self>> {
144 let header = Header::decode(buf)?;
145 if !header.list {
146 return Err(alloy::rlp::Error::UnexpectedString);
147 }
148
149 let original_len = buf.len();
151
152 let nonce = Decodable::decode(buf)?;
153 let max_priority_fee_per_gas = Decodable::decode(buf)?;
154 let max_fee_per_gas = Decodable::decode(buf)?;
155 let gas = Decodable::decode(buf)?;
156 let to = Decodable::decode(buf)?;
157 let value = Decodable::decode(buf)?;
158 let input = Decodable::decode(buf)?;
159 let signature = Signature::decode_rlp_vrs(buf, bool::decode)?;
160 let chain_id = Decodable::decode(buf)?;
161 let from = Decodable::decode(buf)?;
162 let eip712_meta = Decodable::decode(buf)?;
163
164 let tx = Self {
165 chain_id,
166 nonce,
167 gas,
168 max_fee_per_gas,
169 max_priority_fee_per_gas,
170 to,
171 from,
172 value,
173 input,
174 eip712_meta: Some(eip712_meta),
175 };
176
177 let signed = tx.into_signed(signature);
179
180 if buf.len() + header.payload_length != original_len {
181 return Err(alloy::rlp::Error::ListLengthMismatch {
182 expected: header.payload_length,
183 got: original_len - buf.len(),
184 });
185 }
186
187 Ok(signed)
188 }
189
190 pub(crate) fn fields_len(&self) -> usize {
191 self.nonce.length()
192 + self.max_priority_fee_per_gas.length()
193 + self.max_fee_per_gas.length()
194 + self.gas.length()
195 + self.to.length()
196 + self.value.length()
197 + self.input.length()
198 + self.chain_id.length()
199 + self.from.length()
200 + self
201 .eip712_meta
202 .as_ref()
203 .map(|m| m.length())
204 .unwrap_or_default()
205 }
206
207 pub(crate) fn encode_with_signature_fields(&self, signature: &Signature, out: &mut dyn BufMut) {
212 let payload_length = self.fields_len() + signature.rlp_rs_len() + signature.v().length();
213 let header = Header {
214 list: true,
215 payload_length,
216 };
217 header.encode(out);
218
219 self.nonce.encode(out);
220 self.max_priority_fee_per_gas.encode(out);
221 self.max_fee_per_gas.encode(out);
222 self.gas.encode(out);
223 self.to.encode(out);
224 self.value.encode(out);
225 self.input.0.encode(out);
226 signature.write_rlp_vrs(out, signature.v());
227 self.chain_id.encode(out);
228 self.from.encode(out);
229 if let Some(eip712_meta) = &self.eip712_meta {
230 eip712_meta.encode(out);
231 }
232 }
233
234 pub(crate) fn encoded_length(&self, signature: &Signature) -> usize {
236 let payload_length = self.fields_len() + signature.rlp_rs_len() + signature.v().length();
237 alloy::rlp::length_of_length(payload_length) + payload_length
238 }
239
240 #[doc(alias = "transaction_type")]
242 pub(crate) const fn tx_type(&self) -> TxType {
243 TxType::Eip712
244 }
245
246 }
259
260impl Typed2718 for TxEip712 {
261 fn ty(&self) -> u8 {
262 self.tx_type() as u8
263 }
264}
265
266impl Transaction for TxEip712 {
267 fn chain_id(&self) -> Option<ChainId> {
268 Some(self.chain_id)
269 }
270
271 fn nonce(&self) -> u64 {
272 (self.nonce % U256::from(u64::MAX)).try_into().unwrap()
274 }
275
276 fn gas_limit(&self) -> u64 {
277 self.gas
278 }
279
280 fn gas_price(&self) -> Option<u128> {
281 None
282 }
283
284 fn to(&self) -> Option<Address> {
285 self.to.into()
286 }
287
288 fn is_create(&self) -> bool {
289 matches!(self.kind(), TxKind::Create)
290 }
291
292 fn value(&self) -> U256 {
293 self.value
294 }
295
296 fn input(&self) -> &Bytes {
297 &self.input
298 }
299
300 fn max_fee_per_gas(&self) -> u128 {
301 self.max_fee_per_gas
302 }
303
304 fn max_priority_fee_per_gas(&self) -> Option<u128> {
305 Some(self.max_priority_fee_per_gas)
306 }
307
308 fn max_fee_per_blob_gas(&self) -> Option<u128> {
309 None
310 }
311
312 fn priority_fee_or_price(&self) -> u128 {
313 todo!()
314 }
315
316 fn access_list(&self) -> Option<&alloy::rpc::types::AccessList> {
317 None
318 }
319
320 fn blob_versioned_hashes(&self) -> Option<&[alloy::primitives::B256]> {
321 None
322 }
323
324 fn authorization_list(&self) -> Option<&[alloy::eips::eip7702::SignedAuthorization]> {
325 None
326 }
327
328 fn kind(&self) -> TxKind {
329 self.to.into()
330 }
331
332 fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
333 self.effective_gas_price(base_fee)
334 }
335
336 fn is_dynamic_fee(&self) -> bool {
337 false
338 }
339}
340
341impl SignableTransaction<Signature> for TxEip712 {
343 fn set_chain_id(&mut self, chain_id: ChainId) {
344 self.chain_id = chain_id;
345 }
346
347 fn encode_for_signing(&self, out: &mut dyn alloy::rlp::BufMut) {
348 out.put_u8(0x19);
350 out.put_u8(0x01);
351 out.put_slice(self.domain_hash().as_slice());
352 out.put_slice(self.eip712_hash_struct().as_slice());
353 }
354
355 fn payload_len_for_signature(&self) -> usize {
356 2 + 32 + 32
358 }
359
360 fn into_signed(self, signature: Signature) -> Signed<Self> {
361 let mut buf = [0u8; 64];
362 buf[..32].copy_from_slice(self.signature_hash().as_slice());
363 buf[32..].copy_from_slice(keccak256(signature.as_bytes()).as_slice());
364 let hash = keccak256(buf);
365
366 Signed::new_unchecked(self, signature, hash)
367 }
368}
369
370impl RlpEcdsaEncodableTx for TxEip712 {
371 #[doc = " Calculate the encoded length of the transaction\'s fields, without a RLP"]
372 #[doc = " header."]
373 fn rlp_encoded_fields_length(&self) -> usize {
374 self.rlp_encoded_length()
375 }
376
377 #[doc = " Encodes only the transaction\'s fields into the desired buffer, without"]
378 #[doc = " a RLP header."]
379 fn rlp_encode_fields(&self, out: &mut dyn BufMut) {
380 self.rlp_encode(out);
381 }
382}
383
384impl From<TxEip712> for alloy::rpc::types::transaction::TransactionRequest {
421 fn from(tx: TxEip712) -> Self {
422 Self {
423 transaction_type: Some(tx.tx_type() as u8),
424 chain_id: Some(tx.chain_id),
425 nonce: Some((tx.nonce % U256::from(u64::MAX)).try_into().unwrap()), gas: Some(tx.gas),
427 max_fee_per_gas: Some(tx.max_fee_per_gas),
428 max_priority_fee_per_gas: Some(tx.max_priority_fee_per_gas),
429 to: Some(tx.to.into()),
430 from: Some(tx.from),
431 value: Some(tx.value),
432 input: TransactionInput::new(tx.input),
433 access_list: None,
434 blob_versioned_hashes: None,
435 max_fee_per_blob_gas: None,
436 gas_price: None,
437 sidecar: None,
438 authorization_list: None,
439 }
440 }
441}
442
443#[cfg(test)]
444mod tests {
445 use std::str::FromStr;
446
447 use crate::network::unsigned_tx::eip712::{Eip712Meta, PaymasterParams};
448
449 use super::TxEip712;
450 use alloy::consensus::SignableTransaction;
451 use alloy::hex::FromHex;
452 use alloy::primitives::{Address, B256, Bytes, FixedBytes, Signature, U256, address, hex};
453
454 #[test]
455 fn decode_eip712_tx() {
456 let encoded = hex::decode("f8b701800b0c940754b07d1ea3071c3ec9bd86b2aa6f1a59a514980a8301020380a0635f9ee3a1523de15fc8b72a0eea12f5247c6b6e2369ed158274587af6496599a030f7c66d1ed24fca92527e6974b85b07ec30fdd5c2d41eae46966224add965f982010e9409a6aa96b9a17d7f7ba3e3b19811c082aba9f1e304e1a0020202020202020202020202020202020202020202020202020202020202020283010203d694000000000000000000000000000000000000000080").unwrap();
458 let signed_tx = TxEip712::decode_signed_fields(&mut &encoded[..]).unwrap();
459 let tx = signed_tx.tx();
460 assert_eq!(tx.chain_id, 270);
461 assert_eq!(tx.nonce, U256::from(1));
462 assert_eq!(tx.gas, 12);
463 assert_eq!(tx.max_fee_per_gas, 11);
464 assert_eq!(tx.max_priority_fee_per_gas, 0);
465 assert_eq!(tx.to, address!("0754b07d1ea3071c3ec9bd86b2aa6f1a59a51498"));
466 assert_eq!(
467 tx.from,
468 address!("09a6aa96b9a17d7f7ba3e3b19811c082aba9f1e3")
469 );
470 assert_eq!(tx.value, U256::from(10));
471 assert_eq!(tx.input, Bytes::from_hex("0x010203").unwrap());
472 assert_eq!(
473 tx.eip712_meta,
474 Some(Eip712Meta {
475 gas_per_pubdata: U256::from(4),
476 factory_deps: vec![
477 Bytes::from_hex(
478 "0x0202020202020202020202020202020202020202020202020202020202020202"
479 )
480 .unwrap()
481 ],
482 custom_signature: Some(Bytes::from_hex("0x010203").unwrap()),
483 paymaster_params: Some(PaymasterParams {
484 paymaster: address!("0000000000000000000000000000000000000000"),
485 paymaster_input: Bytes::from_hex("0x").unwrap()
486 }),
487 })
488 );
489 }
490
491 #[test]
492 fn decode_eip712_tx_with_paymaster() {
493 let encoded = hex::decode("f9036580843b9aca00843b9aca0083989680949c1a3d7c98dbf89c7f5d167f2219c29c2fe775a780b903045abef77a000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000002e0000000000000000000000000000000000000000000000000000000000000008000000000000000000000000051ef809ffd89cf8056d4c17f0aff1b6f8257eb6000000000000000000000000000000000000000000000000000000000000001f4000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000001e10100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000010f0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000078cad996530109838eb016619f5931a03250489a000000000000000000000000aaf5f437fb0524492886fba64d703df15bf619ae000000000000000000000000000000000000000000000000000000000000010f00000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000064a41368620000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000568656c6c6f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080808082010f94b2a6e81272904caf680008078feb36336d9376b482c350c080d89499e12239cbf8112fbb3f7fd473d0558031abcbb5821234").unwrap();
496 let signed_tx = TxEip712::decode_signed_fields(&mut &encoded[..]).unwrap();
497 let tx = signed_tx.tx();
498 assert_eq!(tx.chain_id, 271);
499 assert_eq!(tx.nonce, U256::from(0));
500 assert_eq!(tx.gas, 10000000);
501 assert_eq!(tx.max_fee_per_gas, 1000000000);
502 assert_eq!(tx.max_priority_fee_per_gas, 1000000000);
503 assert_eq!(tx.to, address!("9c1a3d7c98dbf89c7f5d167f2219c29c2fe775a7"));
504 assert_eq!(
505 tx.from,
506 address!("b2a6e81272904caf680008078feb36336d9376b4")
507 );
508 assert_eq!(tx.value, U256::from(0));
509 assert_eq!(tx.input, Bytes::from_hex("5abef77a000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000002e0000000000000000000000000000000000000000000000000000000000000008000000000000000000000000051ef809ffd89cf8056d4c17f0aff1b6f8257eb6000000000000000000000000000000000000000000000000000000000000001f4000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000001e10100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000010f0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000078cad996530109838eb016619f5931a03250489a000000000000000000000000aaf5f437fb0524492886fba64d703df15bf619ae000000000000000000000000000000000000000000000000000000000000010f00000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000064a41368620000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000568656c6c6f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").unwrap());
510
511 assert_eq!(
512 tx.eip712_meta,
513 Some(Eip712Meta {
514 gas_per_pubdata: U256::from(50000),
515 factory_deps: vec![],
516 custom_signature: Some(Bytes::from_hex("0x").unwrap()),
517 paymaster_params: Some(PaymasterParams {
518 paymaster: address!("99E12239CBf8112fBB3f7Fd473d0558031abcbb5"),
519 paymaster_input: Bytes::from_hex("0x1234").unwrap()
520 }),
521 })
522 );
523 }
524
525 #[test]
526 fn test_eip712_tx() {
527 let eip712_meta = Eip712Meta {
528 gas_per_pubdata: U256::from(4),
529 factory_deps: vec![vec![2; 32].into()],
530 custom_signature: Some(vec![].into()),
531 paymaster_params: None,
532 };
533 let tx = TxEip712 {
534 chain_id: 270,
535 from: Address::from_str("0xe30f4fb40666753a7596d315f2f1f1d140d1508b").unwrap(),
536 to: Address::from_str("0x82112600a140ceaa9d7da373bb65453f7d99af4b").unwrap(),
537 nonce: U256::from(1),
538 value: U256::from(10),
539 gas: 12,
540 max_fee_per_gas: 11,
541 max_priority_fee_per_gas: 0,
542 input: vec![0x01, 0x02, 0x03].into(),
543 eip712_meta: Some(eip712_meta),
544 };
545 let expected_signature_hash = FixedBytes::<32>::from_str(
546 "0xfc76820a67d9b1b351f2ac661e6d2bcca1c67508ae4930e036f540fa135875fe",
547 )
548 .unwrap();
549 assert_eq!(tx.signature_hash(), expected_signature_hash);
550
551 let signature = Signature::from_str("0x3faf83b5451ad3001f96f577b0bb5dfcaa7769ab11908f281dc6b15c45a3986f0325197832aac9a7ab2f5a83873834d457e0d22c1e72377d45364c6968f8ac3b1c").unwrap();
552 let recovered_signer = signature
553 .recover_address_from_prehash(&tx.signature_hash())
554 .unwrap();
555 assert_eq!(recovered_signer, tx.from);
556
557 let mut buf = Vec::new();
558 tx.encode_with_signature_fields(&signature, &mut buf);
559 let decoded = TxEip712::decode_signed_fields(&mut &buf[..]).unwrap();
560 assert_eq!(decoded, tx.into_signed(signature));
561
562 let expected_hash =
563 B256::from_str("0xb85668399db249d62d06bbc59eace82e01364602fb7159e161ca810ff6ddbbf4")
564 .unwrap();
565 assert_eq!(*decoded.hash(), expected_hash);
566 }
567
568 #[test]
569 fn test_eip712_tx_encode_decode_with_paymaster() {
570 let eip712_meta = Eip712Meta {
571 gas_per_pubdata: U256::from(4),
572 factory_deps: vec![vec![2; 32].into()],
573 custom_signature: Some(vec![].into()),
574 paymaster_params: Some(PaymasterParams {
575 paymaster: address!("99E12239CBf8112fBB3f7Fd473d0558031abcbb5"),
576 paymaster_input: Bytes::from_hex("0x112233").unwrap(),
577 }),
578 };
579 let tx = TxEip712 {
580 chain_id: 270,
581 from: Address::from_str("0xe30f4fb40666753a7596d315f2f1f1d140d1508b").unwrap(),
582 to: Address::from_str("0x82112600a140ceaa9d7da373bb65453f7d99af4b").unwrap(),
583 nonce: U256::from(1),
584 value: U256::from(10),
585 gas: 12,
586 max_fee_per_gas: 11,
587 max_priority_fee_per_gas: 0,
588 input: vec![0x01, 0x02, 0x03].into(),
589 eip712_meta: Some(eip712_meta),
590 };
591
592 let signature = Signature::from_str("0x3faf83b5451ad3001f96f577b0bb5dfcaa7769ab11908f281dc6b15c45a3986f0325197832aac9a7ab2f5a83873834d457e0d22c1e72377d45364c6968f8ac3b1c").unwrap();
594
595 let mut buf = Vec::new();
596 tx.encode_with_signature_fields(&signature, &mut buf);
597 let decoded = TxEip712::decode_signed_fields(&mut &buf[..]).unwrap();
598 assert_eq!(decoded, tx.into_signed(signature));
600 }
601
602 }