foundry_primitives/transaction/
request.rs1use alloy_consensus::EthereumTypedTransaction;
2use alloy_network::{
3 BuildResult, NetworkWallet, TransactionBuilder, TransactionBuilder4844, TransactionBuilderError,
4};
5use alloy_primitives::{Address, B256, ChainId, TxKind, U256};
6use alloy_rpc_types::{AccessList, TransactionInputKind, TransactionRequest};
7use alloy_serde::{OtherFields, WithOtherFields};
8use derive_more::{AsMut, AsRef, From, Into};
9use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, TxDeposit};
10use op_revm::transaction::deposit::DepositTransactionParts;
11use serde::{Deserialize, Serialize};
12use tempo_primitives::{TEMPO_TX_TYPE_ID, TempoTransaction, transaction::Call};
13
14use super::{FoundryTxEnvelope, FoundryTxType, FoundryTypedTx};
15use crate::FoundryNetwork;
16
17#[derive(Clone, Debug, Default, PartialEq, Eq, From, Into, AsRef, AsMut)]
22pub struct FoundryTransactionRequest(WithOtherFields<TransactionRequest>);
23
24impl Serialize for FoundryTransactionRequest {
25 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
26 where
27 S: serde::Serializer,
28 {
29 self.as_ref().serialize(serializer)
30 }
31}
32
33impl<'de> Deserialize<'de> for FoundryTransactionRequest {
34 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
35 where
36 D: serde::Deserializer<'de>,
37 {
38 WithOtherFields::<TransactionRequest>::deserialize(deserializer).map(Self)
39 }
40}
41
42impl FoundryTransactionRequest {
43 #[inline]
46 pub fn new(inner: WithOtherFields<TransactionRequest>) -> Self {
47 Self(inner)
48 }
49
50 #[inline]
52 pub fn into_inner(self) -> WithOtherFields<TransactionRequest> {
53 self.0
54 }
55
56 #[inline]
58 pub fn is_deposit(&self) -> bool {
59 self.as_ref().transaction_type == Some(DEPOSIT_TX_TYPE_ID)
60 }
61
62 #[inline]
67 pub fn is_tempo(&self) -> bool {
68 self.as_ref().transaction_type == Some(TEMPO_TX_TYPE_ID)
69 || self.as_ref().other.contains_key("feeToken")
70 || self.as_ref().other.contains_key("nonceKey")
71 }
72
73 fn get_tempo_fee_token(&self) -> Option<Address> {
75 self.as_ref().other.get_deserialized::<Address>("feeToken").transpose().ok().flatten()
76 }
77
78 fn get_tempo_nonce_key(&self) -> U256 {
80 self.as_ref()
81 .other
82 .get_deserialized::<U256>("nonceKey")
83 .transpose()
84 .ok()
85 .flatten()
86 .unwrap_or_default()
87 }
88
89 pub fn complete_tempo(&self) -> Result<(), Vec<&'static str>> {
92 let mut missing = Vec::new();
93 if self.chain_id().is_none() {
94 missing.push("chain_id");
95 }
96 if self.gas_limit().is_none() {
97 missing.push("gas_limit");
98 }
99 if self.max_fee_per_gas().is_none() {
100 missing.push("max_fee_per_gas");
101 }
102 if self.max_priority_fee_per_gas().is_none() {
103 missing.push("max_priority_fee_per_gas");
104 }
105 if self.nonce().is_none() {
106 missing.push("nonce");
107 }
108 if missing.is_empty() { Ok(()) } else { Err(missing) }
109 }
110
111 pub fn preferred_type(&self) -> FoundryTxType {
114 if self.is_deposit() {
115 FoundryTxType::Deposit
116 } else if self.is_tempo() {
117 FoundryTxType::Tempo
118 } else {
119 self.as_ref().preferred_type().into()
120 }
121 }
122
123 pub fn complete_4844(&self) -> Result<(), Vec<&'static str>> {
129 match self.as_ref().complete_4844() {
130 Ok(()) => Ok(()),
131 Err(missing) => {
132 let filtered: Vec<_> =
133 missing.into_iter().filter(|&key| key != "sidecar").collect();
134 if filtered.is_empty() { Ok(()) } else { Err(filtered) }
135 }
136 }
137 }
138
139 pub fn complete_deposit(&self) -> Result<(), Vec<&'static str>> {
142 get_deposit_tx_parts(&self.as_ref().other).map(|_| ())
143 }
144
145 pub fn buildable_type(&self) -> Option<FoundryTxType> {
148 let pref = self.preferred_type();
149 match pref {
150 FoundryTxType::Legacy => self.as_ref().complete_legacy().ok(),
151 FoundryTxType::Eip2930 => self.as_ref().complete_2930().ok(),
152 FoundryTxType::Eip1559 => self.as_ref().complete_1559().ok(),
153 FoundryTxType::Eip4844 => self.as_ref().complete_4844().ok(),
154 FoundryTxType::Eip7702 => self.as_ref().complete_7702().ok(),
155 FoundryTxType::Deposit => self.complete_deposit().ok(),
156 FoundryTxType::Tempo => self.complete_tempo().ok(),
157 }?;
158 Some(pref)
159 }
160
161 pub fn missing_keys(&self) -> Result<FoundryTxType, (FoundryTxType, Vec<&'static str>)> {
168 let pref = self.preferred_type();
169 if let Err(missing) = match pref {
170 FoundryTxType::Legacy => self.as_ref().complete_legacy(),
171 FoundryTxType::Eip2930 => self.as_ref().complete_2930(),
172 FoundryTxType::Eip1559 => self.as_ref().complete_1559(),
173 FoundryTxType::Eip4844 => self.complete_4844(),
174 FoundryTxType::Eip7702 => self.as_ref().complete_7702(),
175 FoundryTxType::Deposit => self.complete_deposit(),
176 FoundryTxType::Tempo => self.complete_tempo(),
177 } {
178 Err((pref, missing))
179 } else {
180 Ok(pref)
181 }
182 }
183
184 pub fn build_typed_tx(self) -> Result<FoundryTypedTx, Self> {
189 if let Ok(deposit_tx_parts) = get_deposit_tx_parts(&self.as_ref().other) {
191 Ok(FoundryTypedTx::Deposit(TxDeposit {
192 from: self.from().unwrap_or_default(),
193 source_hash: deposit_tx_parts.source_hash,
194 to: self.kind().unwrap_or_default(),
195 mint: deposit_tx_parts.mint.unwrap_or_default(),
196 value: self.value().unwrap_or_default(),
197 gas_limit: self.gas_limit().unwrap_or_default(),
198 is_system_transaction: deposit_tx_parts.is_system_transaction,
199 input: self.input().cloned().unwrap_or_default(),
200 }))
201 } else if self.is_tempo() {
202 Ok(FoundryTypedTx::Tempo(TempoTransaction {
204 chain_id: self.chain_id().unwrap_or_default(),
205 fee_token: self.get_tempo_fee_token(),
206 max_fee_per_gas: self.max_fee_per_gas().unwrap_or_default(),
207 max_priority_fee_per_gas: self.max_priority_fee_per_gas().unwrap_or_default(),
208 gas_limit: self.gas_limit().unwrap_or_default(),
209 nonce_key: self.get_tempo_nonce_key(),
210 nonce: self.nonce().unwrap_or_default(),
211 calls: vec![Call {
212 to: self.kind().unwrap_or_default(),
213 value: self.value().unwrap_or_default(),
214 input: self.input().cloned().unwrap_or_default(),
215 }],
216 access_list: self.access_list().cloned().unwrap_or_default(),
217 ..Default::default()
218 }))
219 } else if self.as_ref().has_eip4844_fields() && self.as_ref().blob_sidecar().is_none() {
220 self.0
223 .into_inner()
224 .build_4844_without_sidecar()
225 .map_err(|e| Self(e.into_value().into()))
226 .map(|tx| FoundryTypedTx::Eip4844(tx.into()))
227 } else {
228 let typed_tx = self.0.into_inner().build_typed_tx().map_err(|tx| Self(tx.into()))?;
230 Ok(match typed_tx {
232 EthereumTypedTransaction::Legacy(tx) => FoundryTypedTx::Legacy(tx),
233 EthereumTypedTransaction::Eip2930(tx) => FoundryTypedTx::Eip2930(tx),
234 EthereumTypedTransaction::Eip1559(tx) => FoundryTypedTx::Eip1559(tx),
235 EthereumTypedTransaction::Eip4844(tx) => FoundryTypedTx::Eip4844(tx),
236 EthereumTypedTransaction::Eip7702(tx) => FoundryTypedTx::Eip7702(tx),
237 })
238 }
239 }
240}
241
242impl From<FoundryTypedTx> for FoundryTransactionRequest {
243 fn from(tx: FoundryTypedTx) -> Self {
244 match tx {
245 FoundryTypedTx::Legacy(tx) => Self(Into::<TransactionRequest>::into(tx).into()),
246 FoundryTypedTx::Eip2930(tx) => Self(Into::<TransactionRequest>::into(tx).into()),
247 FoundryTypedTx::Eip1559(tx) => Self(Into::<TransactionRequest>::into(tx).into()),
248 FoundryTypedTx::Eip4844(tx) => Self(Into::<TransactionRequest>::into(tx).into()),
249 FoundryTypedTx::Eip7702(tx) => Self(Into::<TransactionRequest>::into(tx).into()),
250 FoundryTypedTx::Deposit(tx) => {
251 let other = OtherFields::from_iter([
252 ("sourceHash", tx.source_hash.to_string().into()),
253 ("mint", tx.mint.to_string().into()),
254 ("isSystemTx", tx.is_system_transaction.to_string().into()),
255 ]);
256 WithOtherFields { inner: Into::<TransactionRequest>::into(tx), other }.into()
257 }
258 FoundryTypedTx::Tempo(tx) => {
259 let mut other = OtherFields::default();
260 if let Some(fee_token) = tx.fee_token {
261 other.insert("feeToken".to_string(), serde_json::to_value(fee_token).unwrap());
262 }
263 other.insert("nonceKey".to_string(), serde_json::to_value(tx.nonce_key).unwrap());
264 let first_call = tx.calls.first();
265 let mut inner = TransactionRequest::default()
266 .with_chain_id(tx.chain_id)
267 .with_nonce(tx.nonce)
268 .with_gas_limit(tx.gas_limit)
269 .with_max_fee_per_gas(tx.max_fee_per_gas)
270 .with_max_priority_fee_per_gas(tx.max_priority_fee_per_gas)
271 .with_kind(first_call.map(|c| c.to).unwrap_or_default())
272 .with_value(first_call.map(|c| c.value).unwrap_or_default())
273 .with_input(first_call.map(|c| c.input.clone()).unwrap_or_default())
274 .with_access_list(tx.access_list);
275 inner.transaction_type = Some(TEMPO_TX_TYPE_ID);
276 WithOtherFields { inner, other }.into()
277 }
278 }
279 }
280}
281
282impl From<FoundryTxEnvelope> for FoundryTransactionRequest {
283 fn from(tx: FoundryTxEnvelope) -> Self {
284 FoundryTypedTx::from(tx).into()
285 }
286}
287
288impl TransactionBuilder<FoundryNetwork> for FoundryTransactionRequest {
290 fn chain_id(&self) -> Option<ChainId> {
291 self.as_ref().chain_id
292 }
293
294 fn set_chain_id(&mut self, chain_id: ChainId) {
295 self.as_mut().chain_id = Some(chain_id);
296 }
297
298 fn nonce(&self) -> Option<u64> {
299 self.as_ref().nonce
300 }
301
302 fn set_nonce(&mut self, nonce: u64) {
303 self.as_mut().nonce = Some(nonce);
304 }
305
306 fn take_nonce(&mut self) -> Option<u64> {
307 self.as_mut().nonce.take()
308 }
309
310 fn input(&self) -> Option<&alloy_primitives::Bytes> {
311 self.as_ref().input.input()
312 }
313
314 fn set_input<T: Into<alloy_primitives::Bytes>>(&mut self, input: T) {
315 self.as_mut().input.input = Some(input.into());
316 }
317
318 fn set_input_kind<T: Into<alloy_primitives::Bytes>>(
319 &mut self,
320 input: T,
321 kind: TransactionInputKind,
322 ) {
323 let inner = self.as_mut();
324 match kind {
325 TransactionInputKind::Input => inner.input.input = Some(input.into()),
326 TransactionInputKind::Data => inner.input.data = Some(input.into()),
327 TransactionInputKind::Both => {
328 let bytes = input.into();
329 inner.input.input = Some(bytes.clone());
330 inner.input.data = Some(bytes);
331 }
332 }
333 }
334
335 fn from(&self) -> Option<Address> {
336 self.as_ref().from
337 }
338
339 fn set_from(&mut self, from: Address) {
340 self.as_mut().from = Some(from);
341 }
342
343 fn kind(&self) -> Option<TxKind> {
344 self.as_ref().to
345 }
346
347 fn clear_kind(&mut self) {
348 self.as_mut().to = None;
349 }
350
351 fn set_kind(&mut self, kind: TxKind) {
352 self.as_mut().to = Some(kind);
353 }
354
355 fn value(&self) -> Option<U256> {
356 self.as_ref().value
357 }
358
359 fn set_value(&mut self, value: U256) {
360 self.as_mut().value = Some(value);
361 }
362
363 fn gas_price(&self) -> Option<u128> {
364 self.as_ref().gas_price
365 }
366
367 fn set_gas_price(&mut self, gas_price: u128) {
368 self.as_mut().gas_price = Some(gas_price);
369 }
370
371 fn max_fee_per_gas(&self) -> Option<u128> {
372 self.as_ref().max_fee_per_gas
373 }
374
375 fn set_max_fee_per_gas(&mut self, max_fee_per_gas: u128) {
376 self.as_mut().max_fee_per_gas = Some(max_fee_per_gas);
377 }
378
379 fn max_priority_fee_per_gas(&self) -> Option<u128> {
380 self.as_ref().max_priority_fee_per_gas
381 }
382
383 fn set_max_priority_fee_per_gas(&mut self, max_priority_fee_per_gas: u128) {
384 self.as_mut().max_priority_fee_per_gas = Some(max_priority_fee_per_gas);
385 }
386
387 fn gas_limit(&self) -> Option<u64> {
388 self.as_ref().gas
389 }
390
391 fn set_gas_limit(&mut self, gas_limit: u64) {
392 self.as_mut().gas = Some(gas_limit);
393 }
394
395 fn access_list(&self) -> Option<&AccessList> {
396 self.as_ref().access_list.as_ref()
397 }
398
399 fn set_access_list(&mut self, access_list: AccessList) {
400 self.as_mut().access_list = Some(access_list);
401 }
402
403 fn complete_type(&self, ty: FoundryTxType) -> Result<(), Vec<&'static str>> {
404 match ty {
405 FoundryTxType::Legacy => self.as_ref().complete_legacy(),
406 FoundryTxType::Eip2930 => self.as_ref().complete_2930(),
407 FoundryTxType::Eip1559 => self.as_ref().complete_1559(),
408 FoundryTxType::Eip4844 => self.as_ref().complete_4844(),
409 FoundryTxType::Eip7702 => self.as_ref().complete_7702(),
410 FoundryTxType::Deposit => self.complete_deposit(),
411 FoundryTxType::Tempo => self.complete_tempo(),
412 }
413 }
414
415 fn can_submit(&self) -> bool {
416 self.from().is_some()
417 }
418
419 fn can_build(&self) -> bool {
420 self.as_ref().can_build()
421 || get_deposit_tx_parts(&self.as_ref().other).is_ok()
422 || self.is_tempo()
423 }
424
425 fn output_tx_type(&self) -> FoundryTxType {
426 self.preferred_type()
427 }
428
429 fn output_tx_type_checked(&self) -> Option<FoundryTxType> {
430 self.buildable_type()
431 }
432
433 fn prep_for_submission(&mut self) {
436 let preferred_type = self.preferred_type();
437 let inner = self.as_mut();
438 inner.transaction_type = Some(preferred_type as u8);
439 inner.gas_limit().is_none().then(|| inner.set_gas_limit(Default::default()));
440 if !matches!(preferred_type, FoundryTxType::Deposit | FoundryTxType::Tempo) {
441 inner.trim_conflicting_keys();
442 inner.populate_blob_hashes();
443 }
444 if preferred_type != FoundryTxType::Deposit {
445 inner.nonce().is_none().then(|| inner.set_nonce(Default::default()));
446 }
447 if matches!(preferred_type, FoundryTxType::Legacy | FoundryTxType::Eip2930) {
448 inner.gas_price().is_none().then(|| inner.set_gas_price(Default::default()));
449 }
450 if preferred_type == FoundryTxType::Eip2930 {
451 inner.access_list().is_none().then(|| inner.set_access_list(Default::default()));
452 }
453 if matches!(
454 preferred_type,
455 FoundryTxType::Eip1559
456 | FoundryTxType::Eip4844
457 | FoundryTxType::Eip7702
458 | FoundryTxType::Tempo
459 ) {
460 inner
461 .max_priority_fee_per_gas()
462 .is_none()
463 .then(|| inner.set_max_priority_fee_per_gas(Default::default()));
464 inner
465 .max_fee_per_gas()
466 .is_none()
467 .then(|| inner.set_max_fee_per_gas(Default::default()));
468 }
469 if preferred_type == FoundryTxType::Eip4844 {
470 inner
471 .as_ref()
472 .max_fee_per_blob_gas()
473 .is_none()
474 .then(|| inner.as_mut().set_max_fee_per_blob_gas(Default::default()));
475 }
476 }
477
478 fn build_unsigned(self) -> BuildResult<FoundryTypedTx, FoundryNetwork> {
479 if let Err((tx_type, missing)) = self.missing_keys() {
480 return Err(TransactionBuilderError::InvalidTransactionRequest(tx_type, missing)
481 .into_unbuilt(self));
482 }
483 Ok(self.build_typed_tx().expect("checked by missing_keys"))
484 }
485
486 async fn build<W: NetworkWallet<FoundryNetwork>>(
487 self,
488 wallet: &W,
489 ) -> Result<FoundryTxEnvelope, TransactionBuilderError<FoundryNetwork>> {
490 Ok(wallet.sign_request(self).await?)
491 }
492}
493
494pub fn get_deposit_tx_parts(
496 other: &OtherFields,
497) -> Result<DepositTransactionParts, Vec<&'static str>> {
498 let mut missing = Vec::new();
499 let source_hash =
500 other.get_deserialized::<B256>("sourceHash").transpose().ok().flatten().unwrap_or_else(
501 || {
502 missing.push("sourceHash");
503 Default::default()
504 },
505 );
506 let mint = other
507 .get_deserialized::<U256>("mint")
508 .transpose()
509 .unwrap_or_else(|_| {
510 missing.push("mint");
511 Default::default()
512 })
513 .map(|value| value.to::<u128>());
514 let is_system_transaction =
515 other.get_deserialized::<bool>("isSystemTx").transpose().ok().flatten().unwrap_or_else(
516 || {
517 missing.push("isSystemTx");
518 Default::default()
519 },
520 );
521 if missing.is_empty() {
522 Ok(DepositTransactionParts { source_hash, mint, is_system_transaction })
523 } else {
524 Err(missing)
525 }
526}