1use alloy_consensus::{
3 Receipt, ReceiptEnvelope, ReceiptWithBloom, Signed, Transaction, TxEip1559, TxEip2930,
4 TxEnvelope, TxLegacy, TxReceipt, Typed2718,
5 transaction::{
6 Recovered, TxEip7702,
7 eip4844::{TxEip4844, TxEip4844Variant, TxEip4844WithSidecar},
8 },
9};
10
11use alloy_eips::eip2718::Encodable2718;
12use alloy_network::{AnyReceiptEnvelope, AnyTransactionReceipt};
13use alloy_primitives::{Address, B256, Bloom, Bytes, TxHash, TxKind, U64, U256};
14use alloy_rlp::{Decodable, Encodable};
15use alloy_rpc_types::{
16 Transaction as RpcTransaction, TransactionReceipt, request::TransactionRequest,
17 trace::otterscan::OtsReceipt,
18};
19use alloy_serde::{OtherFields, WithOtherFields};
20use bytes::BufMut;
21use foundry_evm::traces::CallTraceNode;
22use foundry_primitives::{FoundryTxEnvelope, FoundryTypedTx};
23use op_alloy_consensus::{OpDepositReceipt, OpDepositReceiptWithBloom, TxDeposit};
24use revm::interpreter::InstructionResult;
25use serde::{Deserialize, Serialize};
26use std::ops::Deref;
27
28pub fn transaction_request_to_typed(
31 tx: WithOtherFields<TransactionRequest>,
32) -> Option<FoundryTypedTx> {
33 let WithOtherFields::<TransactionRequest> {
34 inner:
35 TransactionRequest {
36 from,
37 to,
38 gas_price,
39 max_fee_per_gas,
40 max_priority_fee_per_gas,
41 max_fee_per_blob_gas,
42 blob_versioned_hashes,
43 gas,
44 value,
45 input,
46 nonce,
47 access_list,
48 sidecar,
49 transaction_type,
50 authorization_list,
51 chain_id: _,
52 },
53 other,
54 } = tx;
55
56 if transaction_type == Some(0x7E) || has_optimism_fields(&other) {
58 let mint = other.get_deserialized::<U256>("mint")?.map(|m| m.to::<u128>()).ok()?;
59
60 return Some(FoundryTypedTx::Deposit(TxDeposit {
61 from: from.unwrap_or_default(),
62 source_hash: other.get_deserialized::<B256>("sourceHash")?.ok()?,
63 to: to.unwrap_or_default(),
64 mint,
65 value: value.unwrap_or_default(),
66 gas_limit: gas.unwrap_or_default(),
67 is_system_transaction: other.get_deserialized::<bool>("isSystemTx")?.ok()?,
68 input: input.into_input().unwrap_or_default(),
69 }));
70 }
71
72 if transaction_type == Some(4) || authorization_list.is_some() {
74 return Some(FoundryTypedTx::Eip7702(TxEip7702 {
75 nonce: nonce.unwrap_or_default(),
76 max_fee_per_gas: max_fee_per_gas.unwrap_or_default(),
77 max_priority_fee_per_gas: max_priority_fee_per_gas.unwrap_or_default(),
78 gas_limit: gas.unwrap_or_default(),
79 value: value.unwrap_or(U256::ZERO),
80 input: input.into_input().unwrap_or_default(),
81 to: to?.into_to()?,
83 chain_id: 0,
84 access_list: access_list.unwrap_or_default(),
85 authorization_list: authorization_list.unwrap_or_default(),
86 }));
87 }
88
89 match (
90 transaction_type,
91 gas_price,
92 max_fee_per_gas,
93 max_priority_fee_per_gas,
94 access_list.as_ref(),
95 max_fee_per_blob_gas,
96 blob_versioned_hashes.as_ref(),
97 sidecar.as_ref(),
98 to,
99 ) {
100 (Some(0), _, None, None, None, None, None, None, _)
102 | (None, Some(_), None, None, None, None, None, None, _) => {
103 Some(FoundryTypedTx::Legacy(TxLegacy {
104 nonce: nonce.unwrap_or_default(),
105 gas_price: gas_price.unwrap_or_default(),
106 gas_limit: gas.unwrap_or_default(),
107 value: value.unwrap_or(U256::ZERO),
108 input: input.into_input().unwrap_or_default(),
109 to: to.unwrap_or_default(),
110 chain_id: None,
111 }))
112 }
113 (Some(1), _, None, None, _, None, None, None, _)
115 | (None, _, None, None, Some(_), None, None, None, _) => {
116 Some(FoundryTypedTx::Eip2930(TxEip2930 {
117 nonce: nonce.unwrap_or_default(),
118 gas_price: gas_price.unwrap_or_default(),
119 gas_limit: gas.unwrap_or_default(),
120 value: value.unwrap_or(U256::ZERO),
121 input: input.into_input().unwrap_or_default(),
122 to: to.unwrap_or_default(),
123 chain_id: 0,
124 access_list: access_list.unwrap_or_default(),
125 }))
126 }
127 (Some(2), None, _, _, _, _, None, None, _)
129 | (None, None, Some(_), _, _, _, None, None, _)
130 | (None, None, _, Some(_), _, _, None, None, _)
131 | (None, None, None, None, None, _, None, None, _) => {
132 Some(FoundryTypedTx::Eip1559(TxEip1559 {
134 nonce: nonce.unwrap_or_default(),
135 max_fee_per_gas: max_fee_per_gas.unwrap_or_default(),
136 max_priority_fee_per_gas: max_priority_fee_per_gas.unwrap_or_default(),
137 gas_limit: gas.unwrap_or_default(),
138 value: value.unwrap_or(U256::ZERO),
139 input: input.into_input().unwrap_or_default(),
140 to: to.unwrap_or_default(),
141 chain_id: 0,
142 access_list: access_list.unwrap_or_default(),
143 }))
144 }
145 (Some(3), None, _, _, _, _, Some(_), _, to) => {
147 let tx = TxEip4844 {
148 nonce: nonce.unwrap_or_default(),
149 max_fee_per_gas: max_fee_per_gas.unwrap_or_default(),
150 max_priority_fee_per_gas: max_priority_fee_per_gas.unwrap_or_default(),
151 max_fee_per_blob_gas: max_fee_per_blob_gas.unwrap_or_default(),
152 gas_limit: gas.unwrap_or_default(),
153 value: value.unwrap_or(U256::ZERO),
154 input: input.into_input().unwrap_or_default(),
155 to: match to.unwrap_or(TxKind::Create) {
156 TxKind::Call(to) => to,
157 TxKind::Create => Address::ZERO,
158 },
159 chain_id: 0,
160 access_list: access_list.unwrap_or_default(),
161 blob_versioned_hashes: blob_versioned_hashes.unwrap_or_default(),
162 };
163
164 if let Some(sidecar) = sidecar {
165 Some(FoundryTypedTx::Eip4844(TxEip4844Variant::TxEip4844WithSidecar(
166 TxEip4844WithSidecar::from_tx_and_sidecar(tx, sidecar),
167 )))
168 } else {
169 Some(FoundryTypedTx::Eip4844(TxEip4844Variant::TxEip4844(tx)))
170 }
171 }
172 _ => None,
173 }
174}
175
176pub fn has_optimism_fields(other: &OtherFields) -> bool {
177 other.contains_key("sourceHash")
178 && other.contains_key("mint")
179 && other.contains_key("isSystemTx")
180}
181
182#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
187pub struct MaybeImpersonatedTransaction {
188 transaction: FoundryTxEnvelope,
189 impersonated_sender: Option<Address>,
190}
191
192impl Typed2718 for MaybeImpersonatedTransaction {
193 fn ty(&self) -> u8 {
194 self.transaction.ty()
195 }
196}
197
198impl MaybeImpersonatedTransaction {
199 pub fn new(transaction: FoundryTxEnvelope) -> Self {
201 Self { transaction, impersonated_sender: None }
202 }
203
204 pub fn impersonated(transaction: FoundryTxEnvelope, impersonated_sender: Address) -> Self {
206 Self { transaction, impersonated_sender: Some(impersonated_sender) }
207 }
208
209 pub fn recover(&self) -> Result<Address, alloy_primitives::SignatureError> {
211 if let Some(sender) = self.impersonated_sender {
212 return Ok(sender);
213 }
214 self.transaction.recover()
215 }
216
217 pub fn is_impersonated(&self) -> bool {
219 self.impersonated_sender.is_some()
220 }
221
222 pub fn hash(&self) -> B256 {
224 if let Some(sender) = self.impersonated_sender {
225 return self.transaction.impersonated_hash(sender);
226 }
227 self.transaction.hash()
228 }
229
230 pub fn into_rpc_transaction(self) -> RpcTransaction {
232 let hash = self.hash();
233 let from = self.recover().unwrap_or_default();
234 let envelope = self.transaction.try_into_eth().expect("cant build deposit transactions");
235
236 let inner_envelope = match envelope {
239 TxEnvelope::Legacy(t) => {
240 let (tx, sig, _) = t.into_parts();
241 TxEnvelope::Legacy(Signed::new_unchecked(tx, sig, hash))
242 }
243 TxEnvelope::Eip2930(t) => {
244 let (tx, sig, _) = t.into_parts();
245 TxEnvelope::Eip2930(Signed::new_unchecked(tx, sig, hash))
246 }
247 TxEnvelope::Eip1559(t) => {
248 let (tx, sig, _) = t.into_parts();
249 TxEnvelope::Eip1559(Signed::new_unchecked(tx, sig, hash))
250 }
251 TxEnvelope::Eip4844(t) => {
252 let (tx, sig, _) = t.into_parts();
253 TxEnvelope::Eip4844(Signed::new_unchecked(tx, sig, hash))
254 }
255 TxEnvelope::Eip7702(t) => {
256 let (tx, sig, _) = t.into_parts();
257 TxEnvelope::Eip7702(Signed::new_unchecked(tx, sig, hash))
258 }
259 };
260
261 RpcTransaction {
262 block_hash: None,
263 block_number: None,
264 transaction_index: None,
265 effective_gas_price: None,
266 inner: Recovered::new_unchecked(inner_envelope, from),
267 }
268 }
269}
270
271impl Encodable2718 for MaybeImpersonatedTransaction {
272 fn encode_2718_len(&self) -> usize {
273 self.transaction.encode_2718_len()
274 }
275
276 fn encode_2718(&self, out: &mut dyn BufMut) {
277 self.transaction.encode_2718(out)
278 }
279}
280
281impl Encodable for MaybeImpersonatedTransaction {
282 fn encode(&self, out: &mut dyn bytes::BufMut) {
283 self.transaction.encode(out)
284 }
285}
286
287impl From<MaybeImpersonatedTransaction> for FoundryTxEnvelope {
288 fn from(value: MaybeImpersonatedTransaction) -> Self {
289 value.transaction
290 }
291}
292
293impl From<FoundryTxEnvelope> for MaybeImpersonatedTransaction {
294 fn from(value: FoundryTxEnvelope) -> Self {
295 Self::new(value)
296 }
297}
298
299impl Decodable for MaybeImpersonatedTransaction {
300 fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
301 FoundryTxEnvelope::decode(buf).map(Self::new)
302 }
303}
304
305impl AsRef<FoundryTxEnvelope> for MaybeImpersonatedTransaction {
306 fn as_ref(&self) -> &FoundryTxEnvelope {
307 &self.transaction
308 }
309}
310
311impl Deref for MaybeImpersonatedTransaction {
312 type Target = FoundryTxEnvelope;
313
314 fn deref(&self) -> &Self::Target {
315 &self.transaction
316 }
317}
318
319impl From<MaybeImpersonatedTransaction> for RpcTransaction {
320 fn from(value: MaybeImpersonatedTransaction) -> Self {
321 value.into_rpc_transaction()
322 }
323}
324
325#[derive(Clone, Debug, PartialEq, Eq)]
327pub struct PendingTransaction {
328 pub transaction: MaybeImpersonatedTransaction,
330 sender: Address,
332 hash: TxHash,
334}
335
336impl PendingTransaction {
337 pub fn new(transaction: FoundryTxEnvelope) -> Result<Self, alloy_primitives::SignatureError> {
338 let sender = transaction.recover()?;
339 let hash = transaction.hash();
340 Ok(Self { transaction: MaybeImpersonatedTransaction::new(transaction), sender, hash })
341 }
342
343 pub fn with_impersonated(transaction: FoundryTxEnvelope, sender: Address) -> Self {
344 let hash = transaction.impersonated_hash(sender);
345 Self {
346 transaction: MaybeImpersonatedTransaction::impersonated(transaction, sender),
347 sender,
348 hash,
349 }
350 }
351
352 pub fn from_maybe_impersonated(
354 transaction: MaybeImpersonatedTransaction,
355 ) -> Result<Self, alloy_primitives::SignatureError> {
356 if let Some(impersonated) = transaction.impersonated_sender {
357 Ok(Self::with_impersonated(transaction.transaction, impersonated))
358 } else {
359 Self::new(transaction.transaction)
360 }
361 }
362
363 pub fn nonce(&self) -> u64 {
364 self.transaction.nonce()
365 }
366
367 pub fn hash(&self) -> &TxHash {
368 &self.hash
369 }
370
371 pub fn sender(&self) -> &Address {
372 &self.sender
373 }
374}
375
376#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
378pub struct TransactionInfo {
379 pub transaction_hash: B256,
380 pub transaction_index: u64,
381 pub from: Address,
382 pub to: Option<Address>,
383 pub contract_address: Option<Address>,
384 pub traces: Vec<CallTraceNode>,
385 pub exit: InstructionResult,
386 pub out: Option<Bytes>,
387 pub nonce: u64,
388 pub gas_used: u64,
389}
390
391#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
393#[serde(tag = "type")]
394pub enum TypedReceiptRpc {
395 #[serde(rename = "0x0", alias = "0x00")]
396 Legacy(ReceiptWithBloom<Receipt<alloy_rpc_types::Log>>),
397 #[serde(rename = "0x1", alias = "0x01")]
398 Eip2930(ReceiptWithBloom<Receipt<alloy_rpc_types::Log>>),
399 #[serde(rename = "0x2", alias = "0x02")]
400 Eip1559(ReceiptWithBloom<Receipt<alloy_rpc_types::Log>>),
401 #[serde(rename = "0x3", alias = "0x03")]
402 Eip4844(ReceiptWithBloom<Receipt<alloy_rpc_types::Log>>),
403 #[serde(rename = "0x4", alias = "0x04")]
404 Eip7702(ReceiptWithBloom<Receipt<alloy_rpc_types::Log>>),
405 #[serde(rename = "0x7E", alias = "0x7e")]
406 Deposit(OpDepositReceiptWithBloom),
407}
408
409impl TypedReceiptRpc {
410 pub fn as_receipt_with_bloom(&self) -> &ReceiptWithBloom<Receipt<alloy_rpc_types::Log>> {
411 match self {
412 Self::Legacy(r)
413 | Self::Eip1559(r)
414 | Self::Eip2930(r)
415 | Self::Eip4844(r)
416 | Self::Eip7702(r) => r,
417 Self::Deposit(_) => unreachable!("use variant-specific helpers for deposit"),
418 }
419 }
420
421 pub fn logs_bloom(&self) -> &Bloom {
422 match self {
423 Self::Legacy(r)
424 | Self::Eip1559(r)
425 | Self::Eip2930(r)
426 | Self::Eip4844(r)
427 | Self::Eip7702(r) => &r.logs_bloom,
428 Self::Deposit(r) => &r.logs_bloom,
429 }
430 }
431
432 pub fn logs(&self) -> &[alloy_rpc_types::Log] {
433 match self {
434 Self::Legacy(r)
435 | Self::Eip1559(r)
436 | Self::Eip2930(r)
437 | Self::Eip4844(r)
438 | Self::Eip7702(r) => &r.receipt.logs,
439 Self::Deposit(_) => unreachable!("use variant-specific helpers for deposit"),
440 }
441 }
442
443 pub fn cumulative_gas_used(&self) -> u64 {
444 self.as_receipt_with_bloom().cumulative_gas_used()
445 }
446}
447
448impl From<TypedReceiptRpc> for ReceiptWithBloom<Receipt<alloy_rpc_types::Log>> {
450 fn from(value: TypedReceiptRpc) -> Self {
451 match value {
452 TypedReceiptRpc::Legacy(r)
453 | TypedReceiptRpc::Eip1559(r)
454 | TypedReceiptRpc::Eip2930(r)
455 | TypedReceiptRpc::Eip4844(r)
456 | TypedReceiptRpc::Eip7702(r) => r,
457 TypedReceiptRpc::Deposit(r) => {
458 let receipt = Receipt::<alloy_rpc_types::Log> {
460 status: r.receipt.inner.status,
461 cumulative_gas_used: r.receipt.inner.cumulative_gas_used,
462 logs: r
463 .receipt
464 .inner
465 .logs
466 .into_iter()
467 .map(|l| alloy_rpc_types::Log {
468 inner: l,
469 block_hash: None,
470 block_number: None,
471 block_timestamp: None,
472 transaction_hash: None,
473 transaction_index: None,
474 log_index: None,
475 removed: false,
476 })
477 .collect(),
478 };
479 Self { receipt, logs_bloom: r.logs_bloom }
480 }
481 }
482 }
483}
484
485impl From<TypedReceiptRpc> for OtsReceipt {
486 fn from(value: TypedReceiptRpc) -> Self {
487 let r#type = match value {
488 TypedReceiptRpc::Legacy(_) => 0x00,
489 TypedReceiptRpc::Eip2930(_) => 0x01,
490 TypedReceiptRpc::Eip1559(_) => 0x02,
491 TypedReceiptRpc::Eip4844(_) => 0x03,
492 TypedReceiptRpc::Eip7702(_) => 0x04,
493 TypedReceiptRpc::Deposit(_) => 0x7E,
494 } as u8;
495 let receipt = ReceiptWithBloom::<Receipt<alloy_rpc_types::Log>>::from(value);
496 let status = receipt.status();
497 let cumulative_gas_used = receipt.cumulative_gas_used();
498 let logs = receipt.logs().to_vec();
499 let logs_bloom = receipt.logs_bloom;
500
501 Self { status, cumulative_gas_used, logs: Some(logs), logs_bloom: Some(logs_bloom), r#type }
502 }
503}
504
505impl From<ReceiptEnvelope<alloy_rpc_types::Log>> for TypedReceiptRpc {
506 fn from(value: ReceiptEnvelope<alloy_rpc_types::Log>) -> Self {
507 match value {
508 ReceiptEnvelope::Legacy(r) => Self::Legacy(r),
509 ReceiptEnvelope::Eip2930(r) => Self::Eip2930(r),
510 ReceiptEnvelope::Eip1559(r) => Self::Eip1559(r),
511 ReceiptEnvelope::Eip4844(r) => Self::Eip4844(r),
512 ReceiptEnvelope::Eip7702(r) => Self::Eip7702(r),
513 }
514 }
515}
516
517pub type ReceiptResponse = WithOtherFields<TransactionReceipt<TypedReceiptRpc>>;
518
519pub fn convert_to_anvil_receipt(receipt: AnyTransactionReceipt) -> Option<ReceiptResponse> {
520 let WithOtherFields {
521 inner:
522 TransactionReceipt {
523 transaction_hash,
524 transaction_index,
525 block_hash,
526 block_number,
527 gas_used,
528 contract_address,
529 effective_gas_price,
530 from,
531 to,
532 blob_gas_price,
533 blob_gas_used,
534 inner: AnyReceiptEnvelope { inner: receipt_with_bloom, r#type },
535 },
536 other,
537 } = receipt;
538
539 Some(WithOtherFields {
540 inner: TransactionReceipt {
541 transaction_hash,
542 transaction_index,
543 block_hash,
544 block_number,
545 gas_used,
546 contract_address,
547 effective_gas_price,
548 from,
549 to,
550 blob_gas_price,
551 blob_gas_used,
552 inner: match r#type {
553 0x00 => TypedReceiptRpc::Legacy(receipt_with_bloom),
554 0x01 => TypedReceiptRpc::Eip2930(receipt_with_bloom),
555 0x02 => TypedReceiptRpc::Eip1559(receipt_with_bloom),
556 0x03 => TypedReceiptRpc::Eip4844(receipt_with_bloom),
557 0x04 => TypedReceiptRpc::Eip7702(receipt_with_bloom),
558 0x7E => TypedReceiptRpc::Deposit(OpDepositReceiptWithBloom {
559 receipt: OpDepositReceipt {
560 inner: Receipt {
561 status: alloy_consensus::Eip658Value::Eip658(
562 receipt_with_bloom.status(),
563 ),
564 cumulative_gas_used: receipt_with_bloom.cumulative_gas_used(),
565 logs: receipt_with_bloom
566 .receipt
567 .logs
568 .into_iter()
569 .map(|l| l.inner)
570 .collect(),
571 },
572 deposit_nonce: other
573 .get_deserialized::<U64>("depositNonce")
574 .transpose()
575 .ok()?
576 .map(|v| v.to()),
577 deposit_receipt_version: other
578 .get_deserialized::<U64>("depositReceiptVersion")
579 .transpose()
580 .ok()?
581 .map(|v| v.to()),
582 },
583 logs_bloom: receipt_with_bloom.logs_bloom,
584 }),
585 _ => return None,
586 },
587 },
588 other,
589 })
590}
591
592#[cfg(test)]
593mod tests {
594 use super::*;
595
596 #[test]
598 fn test_receipt_convert() {
599 let s = r#"{"type":"0x4","status":"0x1","cumulativeGasUsed":"0x903fd1","logs":[{"address":"0x0000d9fcd47bf761e7287d8ee09917d7e2100000","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000234ce51365b9c417171b6dad280f49143e1b0547"],"data":"0x00000000000000000000000000000000000000000000032139b42c3431700000","blockHash":"0xd26b59c1d8b5bfa9362d19eb0da3819dfe0b367987a71f6d30908dd45e0d7a60","blockNumber":"0x159663e","blockTimestamp":"0x68411f7b","transactionHash":"0x17a6af73d1317e69cfc3cac9221bd98261d40f24815850a44dbfbf96652ae52a","transactionIndex":"0x22","logIndex":"0x158","removed":false}],"logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000008100000000000000000000000000000000000000000000000020000200000000000000800000000800000000000000010000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000","transactionHash":"0x17a6af73d1317e69cfc3cac9221bd98261d40f24815850a44dbfbf96652ae52a","transactionIndex":"0x22","blockHash":"0xd26b59c1d8b5bfa9362d19eb0da3819dfe0b367987a71f6d30908dd45e0d7a60","blockNumber":"0x159663e","gasUsed":"0x28ee7","effectiveGasPrice":"0x4bf02090","from":"0x234ce51365b9c417171b6dad280f49143e1b0547","to":"0x234ce51365b9c417171b6dad280f49143e1b0547","contractAddress":null}"#;
600 let receipt: AnyTransactionReceipt = serde_json::from_str(s).unwrap();
601 let _converted = convert_to_anvil_receipt(receipt).unwrap();
602 }
603}