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};
12
13use super::{FoundryTxEnvelope, FoundryTxType, FoundryTypedTx};
14use crate::FoundryNetwork;
15
16#[derive(Clone, Debug, Default, PartialEq, Eq, From, Into, AsRef, AsMut)]
21pub struct FoundryTransactionRequest(WithOtherFields<TransactionRequest>);
22
23impl Serialize for FoundryTransactionRequest {
24 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
25 where
26 S: serde::Serializer,
27 {
28 self.as_ref().serialize(serializer)
29 }
30}
31
32impl<'de> Deserialize<'de> for FoundryTransactionRequest {
33 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
34 where
35 D: serde::Deserializer<'de>,
36 {
37 WithOtherFields::<TransactionRequest>::deserialize(deserializer).map(Self)
38 }
39}
40
41impl FoundryTransactionRequest {
42 #[inline]
45 pub fn new(inner: WithOtherFields<TransactionRequest>) -> Self {
46 Self(inner)
47 }
48
49 #[inline]
51 pub fn is_deposit(&self) -> bool {
52 self.as_ref().transaction_type == Some(DEPOSIT_TX_TYPE_ID)
53 }
54
55 pub fn preferred_type(&self) -> FoundryTxType {
58 if self.is_deposit() {
59 FoundryTxType::Deposit
60 } else {
61 self.as_ref().preferred_type().into()
62 }
63 }
64
65 pub fn complete_4844(&self) -> Result<(), Vec<&'static str>> {
71 match self.as_ref().complete_4844() {
72 Ok(()) => Ok(()),
73 Err(missing) => {
74 let filtered: Vec<_> =
75 missing.into_iter().filter(|&key| key != "sidecar").collect();
76 if filtered.is_empty() { Ok(()) } else { Err(filtered) }
77 }
78 }
79 }
80
81 pub fn complete_deposit(&self) -> Result<(), Vec<&'static str>> {
84 get_deposit_tx_parts(&self.as_ref().other).map(|_| ())
85 }
86
87 pub fn buildable_type(&self) -> Option<FoundryTxType> {
90 let pref = self.preferred_type();
91 match pref {
92 FoundryTxType::Legacy => self.as_ref().complete_legacy().ok(),
93 FoundryTxType::Eip2930 => self.as_ref().complete_2930().ok(),
94 FoundryTxType::Eip1559 => self.as_ref().complete_1559().ok(),
95 FoundryTxType::Eip4844 => self.as_ref().complete_4844().ok(),
96 FoundryTxType::Eip7702 => self.as_ref().complete_7702().ok(),
97 FoundryTxType::Deposit => self.complete_deposit().ok(),
98 FoundryTxType::Tempo => None,
100 }?;
101 Some(pref)
102 }
103
104 pub fn missing_keys(&self) -> Result<FoundryTxType, (FoundryTxType, Vec<&'static str>)> {
111 let pref = self.preferred_type();
112 if let Err(missing) = match pref {
113 FoundryTxType::Legacy => self.as_ref().complete_legacy(),
114 FoundryTxType::Eip2930 => self.as_ref().complete_2930(),
115 FoundryTxType::Eip1559 => self.as_ref().complete_1559(),
116 FoundryTxType::Eip4844 => self.complete_4844(),
117 FoundryTxType::Eip7702 => self.as_ref().complete_7702(),
118 FoundryTxType::Deposit => self.complete_deposit(),
119 FoundryTxType::Tempo => Err(vec!["tempo transaction building not yet supported"]),
121 } {
122 Err((pref, missing))
123 } else {
124 Ok(pref)
125 }
126 }
127
128 pub fn build_typed_tx(self) -> Result<FoundryTypedTx, Self> {
133 if let Ok(deposit_tx_parts) = get_deposit_tx_parts(&self.as_ref().other) {
135 Ok(FoundryTypedTx::Deposit(TxDeposit {
136 from: self.from().unwrap_or_default(),
137 source_hash: deposit_tx_parts.source_hash,
138 to: self.kind().unwrap_or_default(),
139 mint: deposit_tx_parts.mint.unwrap_or_default(),
140 value: self.value().unwrap_or_default(),
141 gas_limit: self.gas_limit().unwrap_or_default(),
142 is_system_transaction: deposit_tx_parts.is_system_transaction,
143 input: self.input().cloned().unwrap_or_default(),
144 }))
145 } else if self.as_ref().has_eip4844_fields() && self.as_ref().blob_sidecar().is_none() {
146 self.0
149 .into_inner()
150 .build_4844_without_sidecar()
151 .map_err(|e| Self(e.into_value().into()))
152 .map(|tx| FoundryTypedTx::Eip4844(tx.into()))
153 } else {
154 let typed_tx = self.0.into_inner().build_typed_tx().map_err(|tx| Self(tx.into()))?;
156 Ok(match typed_tx {
158 EthereumTypedTransaction::Legacy(tx) => FoundryTypedTx::Legacy(tx),
159 EthereumTypedTransaction::Eip2930(tx) => FoundryTypedTx::Eip2930(tx),
160 EthereumTypedTransaction::Eip1559(tx) => FoundryTypedTx::Eip1559(tx),
161 EthereumTypedTransaction::Eip4844(tx) => FoundryTypedTx::Eip4844(tx),
162 EthereumTypedTransaction::Eip7702(tx) => FoundryTypedTx::Eip7702(tx),
163 })
164 }
165 }
166}
167
168impl From<FoundryTypedTx> for FoundryTransactionRequest {
169 fn from(tx: FoundryTypedTx) -> Self {
170 match tx {
171 FoundryTypedTx::Legacy(tx) => Self(Into::<TransactionRequest>::into(tx).into()),
172 FoundryTypedTx::Eip2930(tx) => Self(Into::<TransactionRequest>::into(tx).into()),
173 FoundryTypedTx::Eip1559(tx) => Self(Into::<TransactionRequest>::into(tx).into()),
174 FoundryTypedTx::Eip4844(tx) => Self(Into::<TransactionRequest>::into(tx).into()),
175 FoundryTypedTx::Eip7702(tx) => Self(Into::<TransactionRequest>::into(tx).into()),
176 FoundryTypedTx::Deposit(tx) => {
177 let other = OtherFields::from_iter([
178 ("sourceHash", tx.source_hash.to_string().into()),
179 ("mint", tx.mint.to_string().into()),
180 ("isSystemTx", tx.is_system_transaction.to_string().into()),
181 ]);
182 WithOtherFields { inner: Into::<TransactionRequest>::into(tx), other }.into()
183 }
184 FoundryTypedTx::Tempo(_) => unimplemented!("tempo transaction request conversion"),
186 }
187 }
188}
189
190impl From<FoundryTxEnvelope> for FoundryTransactionRequest {
191 fn from(tx: FoundryTxEnvelope) -> Self {
192 FoundryTypedTx::from(tx).into()
193 }
194}
195
196impl TransactionBuilder<FoundryNetwork> for FoundryTransactionRequest {
198 fn chain_id(&self) -> Option<ChainId> {
199 self.as_ref().chain_id
200 }
201
202 fn set_chain_id(&mut self, chain_id: ChainId) {
203 self.as_mut().chain_id = Some(chain_id);
204 }
205
206 fn nonce(&self) -> Option<u64> {
207 self.as_ref().nonce
208 }
209
210 fn set_nonce(&mut self, nonce: u64) {
211 self.as_mut().nonce = Some(nonce);
212 }
213
214 fn take_nonce(&mut self) -> Option<u64> {
215 self.as_mut().nonce.take()
216 }
217
218 fn input(&self) -> Option<&alloy_primitives::Bytes> {
219 self.as_ref().input.input()
220 }
221
222 fn set_input<T: Into<alloy_primitives::Bytes>>(&mut self, input: T) {
223 self.as_mut().input.input = Some(input.into());
224 }
225
226 fn set_input_kind<T: Into<alloy_primitives::Bytes>>(
227 &mut self,
228 input: T,
229 kind: TransactionInputKind,
230 ) {
231 let inner = self.as_mut();
232 match kind {
233 TransactionInputKind::Input => inner.input.input = Some(input.into()),
234 TransactionInputKind::Data => inner.input.data = Some(input.into()),
235 TransactionInputKind::Both => {
236 let bytes = input.into();
237 inner.input.input = Some(bytes.clone());
238 inner.input.data = Some(bytes);
239 }
240 }
241 }
242
243 fn from(&self) -> Option<Address> {
244 self.as_ref().from
245 }
246
247 fn set_from(&mut self, from: Address) {
248 self.as_mut().from = Some(from);
249 }
250
251 fn kind(&self) -> Option<TxKind> {
252 self.as_ref().to
253 }
254
255 fn clear_kind(&mut self) {
256 self.as_mut().to = None;
257 }
258
259 fn set_kind(&mut self, kind: TxKind) {
260 self.as_mut().to = Some(kind);
261 }
262
263 fn value(&self) -> Option<U256> {
264 self.as_ref().value
265 }
266
267 fn set_value(&mut self, value: U256) {
268 self.as_mut().value = Some(value);
269 }
270
271 fn gas_price(&self) -> Option<u128> {
272 self.as_ref().gas_price
273 }
274
275 fn set_gas_price(&mut self, gas_price: u128) {
276 self.as_mut().gas_price = Some(gas_price);
277 }
278
279 fn max_fee_per_gas(&self) -> Option<u128> {
280 self.as_ref().max_fee_per_gas
281 }
282
283 fn set_max_fee_per_gas(&mut self, max_fee_per_gas: u128) {
284 self.as_mut().max_fee_per_gas = Some(max_fee_per_gas);
285 }
286
287 fn max_priority_fee_per_gas(&self) -> Option<u128> {
288 self.as_ref().max_priority_fee_per_gas
289 }
290
291 fn set_max_priority_fee_per_gas(&mut self, max_priority_fee_per_gas: u128) {
292 self.as_mut().max_priority_fee_per_gas = Some(max_priority_fee_per_gas);
293 }
294
295 fn gas_limit(&self) -> Option<u64> {
296 self.as_ref().gas
297 }
298
299 fn set_gas_limit(&mut self, gas_limit: u64) {
300 self.as_mut().gas = Some(gas_limit);
301 }
302
303 fn access_list(&self) -> Option<&AccessList> {
304 self.as_ref().access_list.as_ref()
305 }
306
307 fn set_access_list(&mut self, access_list: AccessList) {
308 self.as_mut().access_list = Some(access_list);
309 }
310
311 fn complete_type(&self, ty: FoundryTxType) -> Result<(), Vec<&'static str>> {
312 match ty {
313 FoundryTxType::Legacy => self.as_ref().complete_legacy(),
314 FoundryTxType::Eip2930 => self.as_ref().complete_2930(),
315 FoundryTxType::Eip1559 => self.as_ref().complete_1559(),
316 FoundryTxType::Eip4844 => self.as_ref().complete_4844(),
317 FoundryTxType::Eip7702 => self.as_ref().complete_7702(),
318 FoundryTxType::Deposit => self.complete_deposit(),
319 FoundryTxType::Tempo => Err(vec!["tempo transaction building not yet supported"]),
321 }
322 }
323
324 fn can_submit(&self) -> bool {
325 self.from().is_some()
326 }
327
328 fn can_build(&self) -> bool {
329 self.as_ref().can_build() || get_deposit_tx_parts(&self.as_ref().other).is_ok()
330 }
331
332 fn output_tx_type(&self) -> FoundryTxType {
333 self.preferred_type()
334 }
335
336 fn output_tx_type_checked(&self) -> Option<FoundryTxType> {
337 self.buildable_type()
338 }
339
340 fn prep_for_submission(&mut self) {
343 let preferred_type = self.preferred_type();
344 let inner = self.as_mut();
345 inner.transaction_type = Some(preferred_type as u8);
346 inner.gas_limit().is_none().then(|| inner.set_gas_limit(Default::default()));
347 if preferred_type != FoundryTxType::Deposit {
348 inner.trim_conflicting_keys();
349 inner.populate_blob_hashes();
350 inner.nonce().is_none().then(|| inner.set_nonce(Default::default()));
351 }
352 if matches!(preferred_type, FoundryTxType::Legacy | FoundryTxType::Eip2930) {
353 inner.gas_price().is_none().then(|| inner.set_gas_price(Default::default()));
354 }
355 if preferred_type == FoundryTxType::Eip2930 {
356 inner.access_list().is_none().then(|| inner.set_access_list(Default::default()));
357 }
358 if matches!(
359 preferred_type,
360 FoundryTxType::Eip1559 | FoundryTxType::Eip4844 | FoundryTxType::Eip7702
361 ) {
362 inner
363 .max_priority_fee_per_gas()
364 .is_none()
365 .then(|| inner.set_max_priority_fee_per_gas(Default::default()));
366 inner
367 .max_fee_per_gas()
368 .is_none()
369 .then(|| inner.set_max_fee_per_gas(Default::default()));
370 }
371 if preferred_type == FoundryTxType::Eip4844 {
372 inner
373 .as_ref()
374 .max_fee_per_blob_gas()
375 .is_none()
376 .then(|| inner.as_mut().set_max_fee_per_blob_gas(Default::default()));
377 }
378 }
379
380 fn build_unsigned(self) -> BuildResult<FoundryTypedTx, FoundryNetwork> {
381 if let Err((tx_type, missing)) = self.missing_keys() {
382 return Err(TransactionBuilderError::InvalidTransactionRequest(tx_type, missing)
383 .into_unbuilt(self));
384 }
385 Ok(self.build_typed_tx().expect("checked by missing_keys"))
386 }
387
388 async fn build<W: NetworkWallet<FoundryNetwork>>(
389 self,
390 wallet: &W,
391 ) -> Result<FoundryTxEnvelope, TransactionBuilderError<FoundryNetwork>> {
392 Ok(wallet.sign_request(self).await?)
393 }
394}
395
396pub fn get_deposit_tx_parts(
398 other: &OtherFields,
399) -> Result<DepositTransactionParts, Vec<&'static str>> {
400 let mut missing = Vec::new();
401 let source_hash =
402 other.get_deserialized::<B256>("sourceHash").transpose().ok().flatten().unwrap_or_else(
403 || {
404 missing.push("sourceHash");
405 Default::default()
406 },
407 );
408 let mint = other
409 .get_deserialized::<U256>("mint")
410 .transpose()
411 .unwrap_or_else(|_| {
412 missing.push("mint");
413 Default::default()
414 })
415 .map(|value| value.to::<u128>());
416 let is_system_transaction =
417 other.get_deserialized::<bool>("isSystemTx").transpose().ok().flatten().unwrap_or_else(
418 || {
419 missing.push("isSystemTx");
420 Default::default()
421 },
422 );
423 if missing.is_empty() {
424 Ok(DepositTransactionParts { source_hash, mint, is_system_transaction })
425 } else {
426 Err(missing)
427 }
428}