1use super::{CIP64_TX_TYPE, FoundryTxEnvelope, FoundryTxType, FoundryTypedTx, TxCip64};
2use crate::FoundryNetwork;
3use alloy_consensus::{BlobTransactionSidecarVariant, EthereumTypedTransaction};
4use alloy_network::{
5 BuildResult, NetworkTransactionBuilder, NetworkWallet, TransactionBuilder,
6 TransactionBuilder4844, TransactionBuilderError,
7};
8use alloy_primitives::{Address, ChainId, TxKind, U256};
9use alloy_rpc_types::{AccessList, TransactionInputKind, TransactionRequest};
10use alloy_serde::WithOtherFields;
11use core::num::NonZeroU64;
12use serde::{Deserialize, Serialize};
13use tempo_primitives::{
14 SignatureType, TEMPO_TX_TYPE_ID, TempoTxType,
15 transaction::{Call, SignedKeyAuthorization, TempoSignedAuthorization},
16};
17
18#[cfg(all(feature = "base", not(feature = "optimism")))]
19use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, TxDeposit};
20
21#[cfg(any(feature = "base", feature = "optimism"))]
22use super::get_deposit_tx_parts;
23#[cfg(any(feature = "base", feature = "optimism"))]
24use op_revm::transaction::deposit::DepositTransactionParts;
25
26#[cfg(any(test, feature = "base", feature = "optimism"))]
27use alloy_serde::OtherFields;
28
29#[cfg(feature = "base")]
30use base_common_evm::EIP8130_TRANSACTION_TYPE;
31#[cfg(feature = "base")]
32use base_common_rpc_types::BaseTransactionRequest;
33
34#[cfg(feature = "optimism")]
35use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, POST_EXEC_TX_TYPE_ID, TxDeposit};
36
37pub use tempo_alloy::rpc::TempoTransactionRequest;
38
39#[derive(Clone, Debug, PartialEq, Eq)]
50#[allow(clippy::large_enum_variant)]
51pub enum FoundryTransactionRequest {
52 Ethereum(TransactionRequest),
53 #[cfg(feature = "base")]
54 Base(BaseTransactionRequest),
55 #[cfg(any(feature = "base", feature = "optimism"))]
56 Op(WithOtherFields<TransactionRequest>),
57 Tempo(Box<TempoTransactionRequest>),
58 Celo(Cip64TransactionRequest),
60}
61
62#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
64#[serde(rename_all = "camelCase")]
65pub struct Cip64TransactionRequest {
66 #[serde(flatten)]
68 pub inner: TransactionRequest,
69 pub fee_currency: Option<Address>,
71}
72
73const TEMPO_REQUEST_FIELDS: &[&str] = &[
74 "feeToken",
75 "nonceKey",
76 "calls",
77 "keyType",
78 "keyData",
79 "keyId",
80 "aaAuthorizationList",
81 "keyAuthorization",
82 "validBefore",
83 "validAfter",
84 "feePayerSignature",
85];
86
87impl FoundryTransactionRequest {
88 pub const fn is_ethereum(&self) -> bool {
90 matches!(self, Self::Ethereum(_))
91 }
92
93 #[cfg(feature = "base")]
95 pub const fn is_base(&self) -> bool {
96 matches!(self, Self::Base(_))
97 }
98
99 #[cfg(feature = "base")]
101 pub const fn as_base(&self) -> Option<&BaseTransactionRequest> {
102 match self {
103 Self::Base(request) => Some(request),
104 _ => None,
105 }
106 }
107
108 #[cfg(any(feature = "base", feature = "optimism"))]
110 pub const fn is_op(&self) -> bool {
111 matches!(self, Self::Op(_))
112 }
113
114 pub const fn is_tempo(&self) -> bool {
116 matches!(self, Self::Tempo(_))
117 }
118
119 #[inline]
122 pub fn new(inner: WithOtherFields<TransactionRequest>) -> serde_json::Result<Self> {
123 inner.try_into()
124 }
125
126 pub fn into_inner(self) -> TransactionRequest {
128 match self {
129 Self::Ethereum(tx) => tx,
130 Self::Celo(tx) => tx.inner,
131 #[cfg(feature = "base")]
132 Self::Base(tx) => tx.into(),
133 #[cfg(any(feature = "base", feature = "optimism"))]
134 Self::Op(tx) => tx.inner,
135 Self::Tempo(tx) => tx.inner,
136 }
137 }
138
139 #[cfg(any(feature = "base", feature = "optimism"))]
146 pub fn get_deposit_tx_parts(&self) -> Result<DepositTransactionParts, Vec<&'static str>> {
147 match self {
148 Self::Op(tx) => get_deposit_tx_parts(&tx.other),
149 _ => Err(vec!["sourceHash", "mint", "isSystemTx"]),
152 }
153 }
154
155 pub fn preferred_type(&self) -> FoundryTxType {
158 match self {
159 Self::Ethereum(tx) => tx.preferred_type().into(),
160 #[cfg(feature = "base")]
161 Self::Base(_) => FoundryTxType::Eip8130,
162 #[cfg(feature = "optimism")]
163 Self::Op(tx) if tx.inner.transaction_type == Some(POST_EXEC_TX_TYPE_ID) => {
164 FoundryTxType::PostExec
165 }
166 #[cfg(any(feature = "base", feature = "optimism"))]
167 Self::Op(_) => FoundryTxType::Deposit,
168 Self::Tempo(_) => FoundryTxType::Tempo,
169 Self::Celo(_) => FoundryTxType::Celo,
170 }
171 }
172
173 pub fn complete_4844(&self) -> Result<(), Vec<&'static str>> {
179 match self.as_ref().complete_4844() {
180 Ok(()) => Ok(()),
181 Err(missing) => {
182 let filtered: Vec<_> =
183 missing.into_iter().filter(|&key| key != "sidecar").collect();
184 if filtered.is_empty() { Ok(()) } else { Err(filtered) }
185 }
186 }
187 }
188
189 #[cfg(any(feature = "base", feature = "optimism"))]
192 pub fn complete_deposit(&self) -> Result<(), Vec<&'static str>> {
193 self.get_deposit_tx_parts().map(|_| ())
194 }
195
196 pub fn complete_tempo(&self) -> Result<(), Vec<&'static str>> {
199 match self {
200 Self::Tempo(tx) => tx.complete_type(TempoTxType::AA).map(|_| ()),
201 _ => Err(vec!["feeToken", "nonceKey"]),
203 }
204 }
205
206 fn check_type(&self, ty: FoundryTxType) -> Result<(), Vec<&'static str>> {
207 match ty {
208 FoundryTxType::Legacy => self.as_ref().complete_legacy(),
209 FoundryTxType::Eip2930 => self.as_ref().complete_2930(),
210 FoundryTxType::Eip1559 => self.as_ref().complete_1559(),
211 FoundryTxType::Eip4844 => self.as_ref().complete_4844(),
212 FoundryTxType::Eip7702 => self.as_ref().complete_7702(),
213 #[cfg(any(feature = "base", feature = "optimism"))]
214 FoundryTxType::Deposit => self.complete_deposit(),
215 #[cfg(feature = "optimism")]
216 FoundryTxType::PostExec => Err(vec!["not implemented for post-exec tx"]),
217 #[cfg(feature = "base")]
218 FoundryTxType::Eip8130 => {
219 Err(vec!["EIP-8130 requires a signed raw transaction envelope"])
220 }
221 FoundryTxType::Tempo => self.complete_tempo(),
222 FoundryTxType::Celo => self.as_ref().complete_1559(),
223 }
224 }
225
226 pub fn missing_keys(&self) -> Result<FoundryTxType, (FoundryTxType, Vec<&'static str>)> {
233 let pref = self.preferred_type();
234 let result = if pref.is_eip4844() { self.complete_4844() } else { self.check_type(pref) };
235 if let Err(missing) = result { Err((pref, missing)) } else { Ok(pref) }
236 }
237
238 pub fn build_typed_tx(self) -> Result<FoundryTypedTx, Self> {
243 if let Self::Celo(request) = self {
244 let fee_currency = request.fee_currency;
245 return request
246 .inner
247 .clone()
248 .build_1559()
249 .map(|inner| FoundryTypedTx::Celo(TxCip64 { inner, fee_currency }))
250 .map_err(|_| Self::Celo(request));
251 }
252 #[cfg(feature = "base")]
253 if self.is_base() {
254 return Err(self);
255 }
256 #[cfg(any(feature = "base", feature = "optimism"))]
257 if let Ok(deposit_tx_parts) = self.get_deposit_tx_parts() {
258 return Ok(FoundryTypedTx::Deposit(TxDeposit {
260 from: self.from().unwrap_or_default(),
261 source_hash: deposit_tx_parts.source_hash,
262 to: self.kind().unwrap_or_default(),
263 mint: deposit_tx_parts.mint.unwrap_or_default(),
264 value: self.value().unwrap_or_default(),
265 gas_limit: self.gas_limit().unwrap_or_default(),
266 is_system_transaction: deposit_tx_parts.is_system_transaction,
267 input: self.input().cloned().unwrap_or_default(),
268 }));
269 }
270 if self.complete_tempo().is_ok()
271 && let Self::Tempo(tx_req) = self
272 {
273 Ok(FoundryTypedTx::Tempo(
275 tx_req.build_aa().map_err(|e| Self::Tempo(Box::new(e.into_value())))?,
276 ))
277 } else if self.as_ref().has_eip4844_fields() && self.blob_sidecar().is_none() {
278 self.into_inner()
281 .build_4844_without_sidecar()
282 .map_err(|e| Self::Ethereum(e.into_value()))
283 .map(|tx| FoundryTypedTx::Eip4844(tx.into()))
284 } else {
285 let typed_tx = self.into_inner().build_typed_tx().map_err(Self::Ethereum)?;
287 Ok(match typed_tx {
289 EthereumTypedTransaction::Legacy(tx) => FoundryTypedTx::Legacy(tx),
290 EthereumTypedTransaction::Eip2930(tx) => FoundryTypedTx::Eip2930(tx),
291 EthereumTypedTransaction::Eip1559(tx) => FoundryTypedTx::Eip1559(tx),
292 EthereumTypedTransaction::Eip4844(tx) => FoundryTypedTx::Eip4844(tx),
293 EthereumTypedTransaction::Eip7702(tx) => FoundryTypedTx::Eip7702(tx),
294 })
295 }
296 }
297}
298
299impl Default for FoundryTransactionRequest {
300 fn default() -> Self {
301 Self::Ethereum(TransactionRequest::default())
302 }
303}
304
305impl Serialize for FoundryTransactionRequest {
306 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
307 where
308 S: serde::Serializer,
309 {
310 match self {
311 Self::Ethereum(tx) => tx.serialize(serializer),
312 #[cfg(feature = "base")]
313 Self::Base(tx) => tx.serialize(serializer),
314 #[cfg(any(feature = "base", feature = "optimism"))]
315 Self::Op(tx) => tx.serialize(serializer),
316 Self::Tempo(tx) => tx.serialize(serializer),
317 Self::Celo(tx) => tx.serialize(serializer),
318 }
319 }
320}
321
322impl<'de> Deserialize<'de> for FoundryTransactionRequest {
323 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
324 where
325 D: serde::Deserializer<'de>,
326 {
327 Self::new(WithOtherFields::<TransactionRequest>::deserialize(deserializer)?)
328 .map_err(serde::de::Error::custom)
329 }
330}
331
332impl AsRef<TransactionRequest> for FoundryTransactionRequest {
333 fn as_ref(&self) -> &TransactionRequest {
334 match self {
335 Self::Ethereum(tx) => tx,
336 #[cfg(feature = "base")]
337 Self::Base(tx) => tx.as_ref(),
338 #[cfg(any(feature = "base", feature = "optimism"))]
339 Self::Op(tx) => tx,
340 Self::Tempo(tx) => tx.as_ref(),
341 Self::Celo(tx) => &tx.inner,
342 }
343 }
344}
345
346impl AsMut<TransactionRequest> for FoundryTransactionRequest {
347 fn as_mut(&mut self) -> &mut TransactionRequest {
348 match self {
349 Self::Ethereum(tx) => tx,
350 #[cfg(feature = "base")]
351 Self::Base(tx) => tx.as_mut(),
352 #[cfg(any(feature = "base", feature = "optimism"))]
353 Self::Op(tx) => tx,
354 Self::Tempo(tx) => tx.as_mut(),
355 Self::Celo(tx) => &mut tx.inner,
356 }
357 }
358}
359
360impl TryFrom<WithOtherFields<TransactionRequest>> for FoundryTransactionRequest {
361 type Error = serde_json::Error;
362
363 fn try_from(tx: WithOtherFields<TransactionRequest>) -> Result<Self, Self::Error> {
364 if tx.transaction_type == Some(CIP64_TX_TYPE)
365 || tx.other.get("feeCurrency").is_some_and(|value| !value.is_null())
366 {
367 if tx.transaction_type.is_some_and(|ty| ty != CIP64_TX_TYPE)
368 || tx.gas_price.is_some()
369 || tx.has_eip4844_fields()
370 || tx.authorization_list.is_some()
371 || TEMPO_REQUEST_FIELDS.iter().any(|field| tx.other.contains_key(*field))
372 || [
373 "accountChanges",
374 "metadata",
375 "sender",
376 "senderAuth",
377 "payer",
378 "payerAuth",
379 "sourceHash",
380 "mint",
381 "isSystemTx",
382 ]
383 .iter()
384 .any(|field| tx.other.get(*field).is_some_and(|value| !value.is_null()))
385 {
386 return Err(serde::de::Error::custom("conflicting CIP-64 transaction fields"));
387 }
388 let fee_currency =
389 tx.other.get_deserialized::<Option<Address>>("feeCurrency").transpose()?.flatten();
390 let mut inner = tx.inner;
391 inner.transaction_type = Some(CIP64_TX_TYPE);
392 return Ok(Self::Celo(Cip64TransactionRequest { inner, fee_currency }));
393 }
394 #[derive(Deserialize)]
395 struct NonZeroQuantity(#[serde(with = "alloy_serde::quantity::opt")] Option<NonZeroU64>);
396
397 #[cfg(feature = "base")]
398 {
399 let present = |field: &str| tx.other.get(field).is_some_and(|value| !value.is_null());
400 let base_fields =
401 ["accountChanges", "metadata", "sender", "senderAuth", "payer", "payerAuth"]
402 .iter()
403 .any(|field| present(field));
404 let phased_calls = tx
405 .other
406 .get("calls")
407 .and_then(serde_json::Value::as_array)
408 .is_some_and(|calls| calls.first().is_some_and(serde_json::Value::is_array));
409 let explicit_base = tx.transaction_type == Some(EIP8130_TRANSACTION_TYPE);
410 if base_fields || phased_calls || explicit_base {
411 if tx.transaction_type.is_some() && !explicit_base {
412 return Err(serde::de::Error::custom(
413 "Base fields conflict with transaction type",
414 ));
415 }
416 if TEMPO_REQUEST_FIELDS.iter().any(|field| {
417 !matches!(*field, "nonceKey" | "calls" | "validBefore" | "validAfter")
418 && present(field)
419 }) {
420 return Err(serde::de::Error::custom(
421 "conflicting Base and Tempo request fields",
422 ));
423 }
424 let mut base =
425 serde_json::from_value::<BaseTransactionRequest>(serde_json::to_value(&tx)?)?;
426 base.as_mut().transaction_type = Some(EIP8130_TRANSACTION_TYPE);
427 return Ok(Self::Base(base));
428 }
429 }
430
431 if tx.transaction_type == Some(TEMPO_TX_TYPE_ID)
432 || TEMPO_REQUEST_FIELDS.iter().any(|field| {
433 tx.other.get(*field).is_some_and(|value| {
434 !value.is_null()
435 && !matches!(
436 (*field, value),
437 (
438 "calls" | "aaAuthorizationList",
439 serde_json::Value::Array(values)
440 ) if values.is_empty()
441 )
442 })
443 })
444 {
445 let mut tempo_tx_req: TempoTransactionRequest = tx.inner.into();
446 tempo_tx_req.fee_token =
447 tx.other.get_deserialized::<Option<Address>>("feeToken").transpose()?.flatten();
448 tempo_tx_req.nonce_key =
449 tx.other.get_deserialized::<Option<U256>>("nonceKey").transpose()?.flatten();
450 tempo_tx_req.calls =
451 tx.other.get_deserialized::<Vec<Call>>("calls").transpose()?.unwrap_or_default();
452 tempo_tx_req.key_type = tx
453 .other
454 .get_deserialized::<Option<SignatureType>>("keyType")
455 .transpose()?
456 .flatten();
457 tempo_tx_req.key_data =
458 tx.other.get_deserialized::<Option<_>>("keyData").transpose()?.flatten();
459 tempo_tx_req.key_id =
460 tx.other.get_deserialized::<Option<_>>("keyId").transpose()?.flatten();
461 tempo_tx_req.tempo_authorization_list = tx
462 .other
463 .get_deserialized::<Vec<TempoSignedAuthorization>>("aaAuthorizationList")
464 .transpose()?
465 .unwrap_or_default();
466 tempo_tx_req.key_authorization = tx
467 .other
468 .get_deserialized::<Option<SignedKeyAuthorization>>("keyAuthorization")
469 .transpose()?
470 .flatten();
471 tempo_tx_req.valid_before = tx
472 .other
473 .get_deserialized::<NonZeroQuantity>("validBefore")
474 .transpose()?
475 .and_then(|value| value.0);
476 tempo_tx_req.valid_after = tx
477 .other
478 .get_deserialized::<NonZeroQuantity>("validAfter")
479 .transpose()?
480 .and_then(|value| value.0);
481 tempo_tx_req.fee_payer_signature =
482 tx.other.get_deserialized::<Option<_>>("feePayerSignature").transpose()?.flatten();
483 return Ok(Self::Tempo(Box::new(tempo_tx_req)));
484 }
485 #[cfg(all(feature = "base", not(feature = "optimism")))]
486 if tx.transaction_type == Some(DEPOSIT_TX_TYPE_ID)
487 || get_deposit_tx_parts(&tx.other).is_ok()
488 {
489 return Ok(Self::Op(tx));
490 }
491 #[cfg(feature = "optimism")]
492 if tx.transaction_type == Some(DEPOSIT_TX_TYPE_ID)
493 || tx.transaction_type == Some(POST_EXEC_TX_TYPE_ID)
494 || get_deposit_tx_parts(&tx.other).is_ok()
495 {
496 return Ok(Self::Op(tx));
497 }
498 Ok(Self::Ethereum(tx.into_inner()))
499 }
500}
501
502impl From<TransactionRequest> for FoundryTransactionRequest {
503 fn from(tx: TransactionRequest) -> Self {
504 Self::Ethereum(tx)
505 }
506}
507
508impl From<FoundryTypedTx> for FoundryTransactionRequest {
509 fn from(tx: FoundryTypedTx) -> Self {
510 match tx {
511 FoundryTypedTx::Legacy(tx) => Self::Ethereum(Into::<TransactionRequest>::into(tx)),
512 FoundryTypedTx::Eip2930(tx) => Self::Ethereum(Into::<TransactionRequest>::into(tx)),
513 FoundryTypedTx::Eip1559(tx) => Self::Ethereum(Into::<TransactionRequest>::into(tx)),
514 FoundryTypedTx::Eip4844(tx) => Self::Ethereum(Into::<TransactionRequest>::into(tx)),
515 FoundryTypedTx::Eip7702(tx) => Self::Ethereum(Into::<TransactionRequest>::into(tx)),
516 #[cfg(any(feature = "base", feature = "optimism"))]
517 FoundryTypedTx::Deposit(tx) => {
518 let other = OtherFields::from_iter([
519 ("sourceHash", serde_json::to_value(tx.source_hash).unwrap()),
520 ("mint", serde_json::to_value(U256::from(tx.mint)).unwrap()),
521 ("isSystemTx", serde_json::to_value(tx.is_system_transaction).unwrap()),
522 ]);
523 WithOtherFields { inner: Into::<TransactionRequest>::into(tx), other }
524 .try_into()
525 .expect("valid deposit transaction request")
526 }
527 #[cfg(feature = "optimism")]
528 FoundryTypedTx::PostExec(tx) => Self::new(WithOtherFields {
529 inner: Into::<TransactionRequest>::into(tx),
530 other: OtherFields::default(),
531 })
532 .expect("valid OP post-exec transaction request"),
533 #[cfg(feature = "base")]
534 FoundryTypedTx::Eip8130(tx) => {
535 Self::Base(super::base::simulation_request(tx, None, None, None))
536 }
537 FoundryTypedTx::Tempo(tx) => Self::Tempo(Box::new(tx.into())),
538 FoundryTypedTx::Celo(tx) => {
539 let mut inner: TransactionRequest = tx.inner.into();
540 inner.transaction_type = Some(CIP64_TX_TYPE);
541 Self::Celo(Cip64TransactionRequest { inner, fee_currency: tx.fee_currency })
542 }
543 }
544 }
545}
546
547impl From<FoundryTxEnvelope> for FoundryTransactionRequest {
548 fn from(tx: FoundryTxEnvelope) -> Self {
549 #[cfg(feature = "base")]
550 if let FoundryTxEnvelope::Eip8130(tx) = tx {
551 let from = tx.recover_sender().ok();
552 let sender_auth = Some(tx.sender_auth().clone());
553 let payer_auth = Some(tx.payer_auth().clone());
554 return Self::Base(super::base::simulation_request(
555 tx.into_tx(),
556 from,
557 sender_auth,
558 payer_auth,
559 ));
560 }
561 FoundryTypedTx::from(tx).into()
562 }
563}
564
565#[cfg(not(feature = "optimism"))]
566impl From<alloy_rpc_types_eth::Transaction<FoundryTxEnvelope>> for FoundryTransactionRequest {
567 fn from(tx: alloy_rpc_types_eth::Transaction<FoundryTxEnvelope>) -> Self {
568 tx.inner.into_inner().into()
569 }
570}
571
572impl TransactionBuilder for FoundryTransactionRequest {
574 fn chain_id(&self) -> Option<ChainId> {
575 self.as_ref().chain_id
576 }
577
578 fn set_chain_id(&mut self, chain_id: ChainId) {
579 self.as_mut().chain_id = Some(chain_id);
580 }
581
582 fn nonce(&self) -> Option<u64> {
583 self.as_ref().nonce
584 }
585
586 fn set_nonce(&mut self, nonce: u64) {
587 self.as_mut().nonce = Some(nonce);
588 }
589
590 fn take_nonce(&mut self) -> Option<u64> {
591 self.as_mut().nonce.take()
592 }
593
594 fn input(&self) -> Option<&alloy_primitives::Bytes> {
595 self.as_ref().input.input()
596 }
597
598 fn set_input<T: Into<alloy_primitives::Bytes>>(&mut self, input: T) {
599 self.as_mut().input.input = Some(input.into());
600 }
601
602 fn set_input_kind<T: Into<alloy_primitives::Bytes>>(
603 &mut self,
604 input: T,
605 kind: TransactionInputKind,
606 ) {
607 let inner = self.as_mut();
608 match kind {
609 TransactionInputKind::Input => inner.input.input = Some(input.into()),
610 TransactionInputKind::Data => inner.input.data = Some(input.into()),
611 TransactionInputKind::Both => {
612 let bytes = input.into();
613 inner.input.input = Some(bytes.clone());
614 inner.input.data = Some(bytes);
615 }
616 }
617 }
618
619 fn from(&self) -> Option<Address> {
620 self.as_ref().from
621 }
622
623 fn set_from(&mut self, from: Address) {
624 self.as_mut().from = Some(from);
625 }
626
627 fn kind(&self) -> Option<TxKind> {
628 self.as_ref().to
629 }
630
631 fn clear_kind(&mut self) {
632 self.as_mut().to = None;
633 }
634
635 fn set_kind(&mut self, kind: TxKind) {
636 self.as_mut().to = Some(kind);
637 }
638
639 fn value(&self) -> Option<U256> {
640 self.as_ref().value
641 }
642
643 fn set_value(&mut self, value: U256) {
644 self.as_mut().value = Some(value);
645 }
646
647 fn gas_price(&self) -> Option<u128> {
648 self.as_ref().gas_price
649 }
650
651 fn set_gas_price(&mut self, gas_price: u128) {
652 self.as_mut().gas_price = Some(gas_price);
653 }
654
655 fn max_fee_per_gas(&self) -> Option<u128> {
656 self.as_ref().max_fee_per_gas
657 }
658
659 fn set_max_fee_per_gas(&mut self, max_fee_per_gas: u128) {
660 self.as_mut().max_fee_per_gas = Some(max_fee_per_gas);
661 }
662
663 fn max_priority_fee_per_gas(&self) -> Option<u128> {
664 self.as_ref().max_priority_fee_per_gas
665 }
666
667 fn set_max_priority_fee_per_gas(&mut self, max_priority_fee_per_gas: u128) {
668 self.as_mut().max_priority_fee_per_gas = Some(max_priority_fee_per_gas);
669 }
670
671 fn gas_limit(&self) -> Option<u64> {
672 self.as_ref().gas
673 }
674
675 fn set_gas_limit(&mut self, gas_limit: u64) {
676 self.as_mut().gas = Some(gas_limit);
677 }
678
679 fn access_list(&self) -> Option<&AccessList> {
680 self.as_ref().access_list.as_ref()
681 }
682
683 fn set_access_list(&mut self, access_list: AccessList) {
684 self.as_mut().access_list = Some(access_list);
685 }
686}
687
688impl NetworkTransactionBuilder<FoundryNetwork> for FoundryTransactionRequest {
689 fn complete_type(&self, ty: FoundryTxType) -> Result<(), Vec<&'static str>> {
690 self.check_type(ty)
691 }
692
693 fn can_submit(&self) -> bool {
694 self.from().is_some()
695 }
696
697 fn can_build(&self) -> bool {
698 #[cfg(feature = "base")]
699 if self.is_base() {
700 return false;
701 }
702 if let Self::Celo(_) = self {
703 return self.check_type(FoundryTxType::Celo).is_ok();
704 }
705 if self.as_ref().can_build() || self.complete_tempo().is_ok() {
706 return true;
707 }
708 #[cfg(any(feature = "base", feature = "optimism"))]
709 if self.complete_deposit().is_ok() {
710 return true;
711 }
712 false
713 }
714
715 fn output_tx_type(&self) -> FoundryTxType {
716 self.preferred_type()
717 }
718
719 fn output_tx_type_checked(&self) -> Option<FoundryTxType> {
720 let pref = self.preferred_type();
721 self.check_type(pref).ok()?;
722 Some(pref)
723 }
724
725 fn prep_for_submission(&mut self) {
728 #[cfg(feature = "base")]
729 if self.is_base() {
730 return;
731 }
732 let preferred_type = self.preferred_type();
733 let inner = self.as_mut();
734 inner.transaction_type = Some(preferred_type as u8);
735 inner.gas.is_none().then(|| inner.set_gas_limit(Default::default()));
736 let is_deposit = {
737 #[cfg(any(feature = "base", feature = "optimism"))]
738 {
739 preferred_type.is_deposit()
740 }
741 #[cfg(not(any(feature = "base", feature = "optimism")))]
742 {
743 false
744 }
745 };
746 if !is_deposit && !preferred_type.is_tempo() && !preferred_type.is_celo() {
747 inner.trim_conflicting_keys();
748 inner.populate_blob_hashes();
749 }
750 if !is_deposit {
751 inner.nonce.is_none().then(|| inner.set_nonce(Default::default()));
752 }
753 if preferred_type.is_legacy() || preferred_type.is_eip2930() {
754 inner.gas_price.is_none().then(|| inner.set_gas_price(Default::default()));
755 }
756 if preferred_type.is_eip2930() {
757 inner.access_list.is_none().then(|| inner.set_access_list(Default::default()));
758 }
759 if preferred_type.is_eip1559()
760 || preferred_type.is_eip4844()
761 || preferred_type.is_eip7702()
762 || preferred_type.is_tempo()
763 || preferred_type.is_celo()
764 {
765 inner
766 .max_priority_fee_per_gas
767 .is_none()
768 .then(|| inner.set_max_priority_fee_per_gas(Default::default()));
769 inner.max_fee_per_gas.is_none().then(|| inner.set_max_fee_per_gas(Default::default()));
770 }
771 if preferred_type.is_eip4844() {
772 inner
773 .as_ref()
774 .max_fee_per_blob_gas()
775 .is_none()
776 .then(|| inner.as_mut().set_max_fee_per_blob_gas(Default::default()));
777 }
778 }
779
780 fn build_unsigned(self) -> BuildResult<FoundryTypedTx, FoundryNetwork> {
781 if let Err((tx_type, missing)) = self.missing_keys() {
782 return Err(TransactionBuilderError::InvalidTransactionRequest(tx_type, missing)
783 .into_unbuilt(self));
784 }
785 Ok(self.build_typed_tx().expect("checked by missing_keys"))
786 }
787
788 async fn build<W: NetworkWallet<FoundryNetwork>>(
789 self,
790 wallet: &W,
791 ) -> Result<FoundryTxEnvelope, TransactionBuilderError<FoundryNetwork>> {
792 Ok(wallet.sign_request(self).await?)
793 }
794}
795
796impl TransactionBuilder4844 for FoundryTransactionRequest {
797 fn max_fee_per_blob_gas(&self) -> Option<u128> {
798 self.as_ref().max_fee_per_blob_gas()
799 }
800
801 fn set_max_fee_per_blob_gas(&mut self, max_fee_per_blob_gas: u128) {
802 self.as_mut().set_max_fee_per_blob_gas(max_fee_per_blob_gas);
803 }
804
805 fn blob_sidecar(&self) -> Option<&BlobTransactionSidecarVariant> {
806 self.as_ref().blob_sidecar()
807 }
808
809 fn set_blob_sidecar(&mut self, sidecar: BlobTransactionSidecarVariant) {
810 self.as_mut().set_blob_sidecar(sidecar);
811 }
812}
813
814#[cfg(test)]
815mod tests {
816 use super::*;
817 use alloy_primitives::{B256, Bytes, Signature};
818 use tempo_primitives::{
819 TempoSignature, TempoTransaction,
820 transaction::{Authorization, KeyAuthorization, PrimitiveSignature},
821 };
822
823 fn default_tx_req() -> TransactionRequest {
824 TransactionRequest::default()
825 .with_to(Address::random())
826 .with_nonce(1)
827 .with_value(U256::from(1000000))
828 .with_gas_limit(1000000)
829 .with_max_fee_per_gas(1000000)
830 .with_max_priority_fee_per_gas(1000000)
831 }
832
833 #[test]
834 fn request_predicates() {
835 let ethereum = FoundryTransactionRequest::default();
836 assert!(ethereum.is_ethereum());
837 assert!(!ethereum.is_tempo());
838
839 let tempo = FoundryTransactionRequest::Tempo(Box::default());
840 assert!(tempo.is_tempo());
841 assert!(!tempo.is_ethereum());
842
843 #[cfg(any(feature = "base", feature = "optimism"))]
844 {
845 let op = FoundryTransactionRequest::Op(WithOtherFields::default());
846 assert!(op.is_op());
847 assert!(!op.is_ethereum());
848 assert!(!op.is_tempo());
849 }
850 }
851
852 #[test]
853 fn test_routing_ethereum_default() {
854 let tx = default_tx_req();
855 let req: FoundryTransactionRequest = WithOtherFields::new(tx).try_into().unwrap();
856
857 assert!(req.is_ethereum());
858 assert!(matches!(req.build_unsigned(), Ok(FoundryTypedTx::Eip1559(_))));
859 }
860
861 #[test]
862 fn test_routing_tempo_by_fee_token() {
863 let tx = default_tx_req();
864 let mut other = OtherFields::default();
865 other.insert("feeToken".to_string(), serde_json::to_value(Address::random()).unwrap());
866
867 let req: FoundryTransactionRequest =
868 FoundryTransactionRequest::new(WithOtherFields { inner: tx, other }).unwrap();
869
870 assert!(req.is_tempo());
871 assert!(matches!(req.build_unsigned(), Ok(FoundryTypedTx::Tempo(_))));
872 }
873
874 #[cfg(feature = "base")]
875 #[test]
876 fn base_and_tempo_request_routing() {
877 let call = serde_json::json!({ "to": Address::ZERO, "data": "0x", "value": "0x0" });
878 for (value, expected_type) in [
879 (serde_json::json!({"calls": [call]}), FoundryTxType::Tempo),
880 (serde_json::json!({"nonceKey": "0x1"}), FoundryTxType::Tempo),
881 (serde_json::json!({"validBefore": "0x123"}), FoundryTxType::Tempo),
882 (serde_json::json!({"calls": []}), FoundryTxType::Eip1559),
883 (serde_json::json!({"calls": [[call]], "validBefore": 123}), FoundryTxType::Eip8130),
884 (serde_json::json!({"type": "0x79"}), FoundryTxType::Eip8130),
885 (serde_json::json!({"type": "0x79", "nonceKey": "0x1"}), FoundryTxType::Eip8130),
886 (serde_json::json!({"type": "0x76", "calls": [call]}), FoundryTxType::Tempo),
887 (
888 serde_json::json!({"sender": Address::ZERO, "feeToken": null}),
889 FoundryTxType::Eip8130,
890 ),
891 ] {
892 let request: FoundryTransactionRequest = serde_json::from_value(value.clone())
893 .unwrap_or_else(|err| panic!("{value}: {err}"));
894 assert_eq!(request.preferred_type(), expected_type, "{value}");
895 if expected_type.is_eip8130() {
896 assert!(request.is_base(), "{value}");
897 assert!(!request.can_build(), "{value}");
898 }
899 let roundtrip: FoundryTransactionRequest =
900 serde_json::from_value(serde_json::to_value(&request).unwrap()).unwrap();
901 assert_eq!(request, roundtrip);
902 }
903 for value in [
904 serde_json::json!({"type": "0x76", "calls": [[call]]}),
905 serde_json::json!({"type": "0x79", "feeToken": Address::ZERO}),
906 serde_json::json!({"type": "0x2", "sender": Address::ZERO}),
907 serde_json::json!({"type": "0x79", "calls": [call]}),
908 ] {
909 assert!(
910 serde_json::from_value::<FoundryTransactionRequest>(value.clone()).is_err(),
911 "{value}"
912 );
913 }
914 }
915
916 #[test]
917 fn test_routing_serialized_non_aa_tempo_request_to_ethereum() {
918 let request = TempoTransactionRequest { inner: default_tx_req(), ..Default::default() };
919 let request = serde_json::from_value::<WithOtherFields<TransactionRequest>>(
920 serde_json::to_value(request).unwrap(),
921 )
922 .unwrap();
923
924 let request = FoundryTransactionRequest::try_from(request).unwrap();
925
926 assert!(request.is_ethereum());
927 assert!(matches!(request.build_unsigned(), Ok(FoundryTypedTx::Eip1559(_))));
928 }
929
930 #[test]
931 #[cfg(any(feature = "base", feature = "optimism"))]
932 fn test_routing_op_by_deposit_fields() {
933 let tx = default_tx_req();
934 let mut other = OtherFields::default();
935 other.insert("sourceHash".to_string(), serde_json::to_value(B256::ZERO).unwrap());
936 other.insert("mint".to_string(), serde_json::to_value(U256::from(1000)).unwrap());
937 other.insert("isSystemTx".to_string(), serde_json::to_value(false).unwrap());
938
939 let req: FoundryTransactionRequest =
940 FoundryTransactionRequest::new(WithOtherFields { inner: tx, other }).unwrap();
941
942 assert!(req.is_op());
943 assert!(matches!(req.build_unsigned(), Ok(FoundryTypedTx::Deposit(_))));
944 }
945
946 #[test]
947 fn test_op_incomplete_routes_to_ethereum() {
948 let tx = default_tx_req();
949 let mut other = OtherFields::default();
950 other.insert("sourceHash".to_string(), serde_json::to_value(B256::ZERO).unwrap());
952 other.insert("mint".to_string(), serde_json::to_value(U256::from(1000)).unwrap());
953
954 let req: FoundryTransactionRequest =
955 FoundryTransactionRequest::new(WithOtherFields { inner: tx, other }).unwrap();
956
957 assert!(req.is_ethereum());
958 assert!(matches!(req.build_unsigned(), Ok(FoundryTypedTx::Eip1559(_))));
959 }
960
961 #[test]
962 fn test_ethereum_with_unrelated_other_fields() {
963 let tx = default_tx_req();
964 let mut other = OtherFields::default();
965 other.insert("anotherField".to_string(), serde_json::to_value(123).unwrap());
966
967 let req: FoundryTransactionRequest =
968 FoundryTransactionRequest::new(WithOtherFields { inner: tx, other }).unwrap();
969
970 assert!(req.is_ethereum());
971 assert!(matches!(req.build_unsigned(), Ok(FoundryTypedTx::Eip1559(_))));
972 }
973
974 #[test]
975 fn test_serialization_ethereum() {
976 let tx = default_tx_req();
977 let original: FoundryTransactionRequest = WithOtherFields::new(tx).try_into().unwrap();
978
979 let serialized = serde_json::to_string(&original).unwrap();
980 let deserialized: FoundryTransactionRequest = serde_json::from_str(&serialized).unwrap();
981
982 assert!(deserialized.is_ethereum());
983 }
984
985 #[test]
986 #[cfg(any(feature = "base", feature = "optimism"))]
987 fn test_serialization_op() {
988 let tx = default_tx_req();
989 let mut other = OtherFields::default();
990 other.insert("sourceHash".to_string(), serde_json::to_value(B256::ZERO).unwrap());
991 other.insert("mint".to_string(), serde_json::to_value(U256::from(1000)).unwrap());
992 other.insert("isSystemTx".to_string(), serde_json::to_value(false).unwrap());
993
994 let original: FoundryTransactionRequest =
995 FoundryTransactionRequest::new(WithOtherFields { inner: tx, other }).unwrap();
996
997 let serialized = serde_json::to_string(&original).unwrap();
998 let deserialized: FoundryTransactionRequest = serde_json::from_str(&serialized).unwrap();
999
1000 assert!(deserialized.is_op());
1001 }
1002
1003 #[test]
1004 fn test_serialization_tempo() {
1005 let tx = default_tx_req();
1006 let mut other = OtherFields::default();
1007 other.insert("feeToken".to_string(), serde_json::to_value(Address::ZERO).unwrap());
1008 other.insert("nonceKey".to_string(), serde_json::to_value(U256::from(42)).unwrap());
1009
1010 let original: FoundryTransactionRequest =
1011 FoundryTransactionRequest::new(WithOtherFields { inner: tx, other }).unwrap();
1012
1013 let serialized = serde_json::to_string(&original).unwrap();
1014 let deserialized: FoundryTransactionRequest = serde_json::from_str(&serialized).unwrap();
1015
1016 assert!(deserialized.is_tempo());
1017 }
1018
1019 #[test]
1020 fn test_tempo_request_decodes_all_extension_fields() {
1021 let signature = Signature::test_signature();
1022 let expected = TempoTransactionRequest {
1023 inner: TransactionRequest {
1024 transaction_type: Some(TEMPO_TX_TYPE_ID),
1025 ..default_tx_req()
1026 },
1027 fee_token: Some(Address::repeat_byte(0x11)),
1028 nonce_key: Some(U256::from(42)),
1029 calls: vec![Call {
1030 to: TxKind::Call(Address::repeat_byte(0x22)),
1031 value: U256::from(7),
1032 input: Bytes::from_static(&[0xde, 0xad]),
1033 }],
1034 key_type: Some(SignatureType::WebAuthn),
1035 key_data: Some(Bytes::from_static(&[0xbe, 0xef])),
1036 key_id: Some(Address::repeat_byte(0x33)),
1037 tempo_authorization_list: vec![TempoSignedAuthorization::new_unchecked(
1038 Authorization {
1039 chain_id: U256::from(4217),
1040 address: Address::repeat_byte(0x44),
1041 nonce: 3,
1042 },
1043 TempoSignature::default(),
1044 )],
1045 key_authorization: Some(
1046 KeyAuthorization::unrestricted(
1047 4217,
1048 SignatureType::Secp256k1,
1049 Address::repeat_byte(0x55),
1050 )
1051 .into_signed(PrimitiveSignature::Secp256k1(signature)),
1052 ),
1053 valid_before: NonZeroU64::new(100),
1054 valid_after: NonZeroU64::new(10),
1055 fee_payer_signature: Some(signature),
1056 };
1057 let request = serde_json::from_value::<WithOtherFields<TransactionRequest>>(
1058 serde_json::to_value(&expected).unwrap(),
1059 )
1060 .unwrap();
1061
1062 let decoded = FoundryTransactionRequest::try_from(request).unwrap();
1063
1064 let FoundryTransactionRequest::Tempo(decoded) = decoded else { panic!() };
1065 assert_eq!(*decoded, expected);
1066 }
1067
1068 #[test]
1069 fn test_malformed_tempo_field_is_rejected() {
1070 let mut other = OtherFields::default();
1071 other.insert("nonceKey".to_string(), serde_json::json!("not a quantity"));
1072
1073 let result =
1074 FoundryTransactionRequest::new(WithOtherFields { inner: default_tx_req(), other });
1075
1076 assert!(result.is_err());
1077 }
1078
1079 #[test]
1080 fn test_tempo_validity_uses_rpc_quantities() {
1081 let request = serde_json::from_value::<FoundryTransactionRequest>(serde_json::json!({
1082 "type": "0x76",
1083 "validBefore": "0x64",
1084 "validAfter": "0xa",
1085 }))
1086 .unwrap();
1087
1088 let FoundryTransactionRequest::Tempo(request) = request else { panic!() };
1089 assert_eq!(request.valid_before, NonZeroU64::new(100));
1090 assert_eq!(request.valid_after, NonZeroU64::new(10));
1091 }
1092
1093 #[test]
1094 fn test_tempo_typed_request_roundtrip_preserves_fields() {
1095 let tx = TempoTransaction {
1096 chain_id: 4217,
1097 nonce: 7,
1098 fee_payer_signature: None,
1099 valid_before: NonZeroU64::new(100),
1100 valid_after: NonZeroU64::new(10),
1101 gas_limit: 100_000,
1102 max_fee_per_gas: 20,
1103 max_priority_fee_per_gas: 2,
1104 fee_token: Some(Address::random()),
1105 access_list: Default::default(),
1106 calls: vec![Call {
1107 to: TxKind::Call(Address::random()),
1108 value: U256::from(42),
1109 input: vec![1, 2, 3].into(),
1110 }],
1111 tempo_authorization_list: Vec::new(),
1112 nonce_key: U256::from(9),
1113 key_authorization: None,
1114 };
1115
1116 let request: FoundryTransactionRequest = FoundryTypedTx::Tempo(tx.clone()).into();
1117 let rebuilt = request.build_unsigned().unwrap();
1118
1119 assert_eq!(rebuilt, FoundryTypedTx::Tempo(tx));
1120 }
1121
1122 #[test]
1123 #[cfg(any(feature = "base", feature = "optimism"))]
1124 fn test_deposit_typed_tx_roundtrip() {
1125 let deposit_tx = TxDeposit {
1126 from: Address::random(),
1127 source_hash: B256::random(),
1128 to: TxKind::Call(Address::random()),
1129 mint: 1000u128,
1130 value: U256::from(500),
1131 gas_limit: 21000,
1132 is_system_transaction: true,
1133 input: Default::default(),
1134 };
1135
1136 let req: FoundryTransactionRequest = FoundryTypedTx::Deposit(deposit_tx.clone()).into();
1137
1138 assert!(req.is_op());
1139
1140 let parts = req.get_deposit_tx_parts().expect("should parse deposit parts");
1141 assert_eq!(parts.source_hash, deposit_tx.source_hash);
1142 assert_eq!(parts.mint, Some(deposit_tx.mint));
1143 assert_eq!(parts.is_system_transaction, deposit_tx.is_system_transaction);
1144 }
1145}