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