forge_script/
receipts.rs

1use alloy_chains::Chain;
2use alloy_network::AnyTransactionReceipt;
3use alloy_primitives::{utils::format_units, TxHash, U256};
4use alloy_provider::{PendingTransactionBuilder, PendingTransactionError, Provider, WatchTxError};
5use eyre::{eyre, Result};
6use foundry_common::{provider::RetryProvider, retry, retry::RetryError, shell};
7use std::time::Duration;
8
9/// Convenience enum for internal signalling of transaction status
10pub enum TxStatus {
11    Dropped,
12    Success(AnyTransactionReceipt),
13    Revert(AnyTransactionReceipt),
14}
15
16impl From<AnyTransactionReceipt> for TxStatus {
17    fn from(receipt: AnyTransactionReceipt) -> Self {
18        if !receipt.inner.inner.inner.receipt.status.coerce_status() {
19            Self::Revert(receipt)
20        } else {
21            Self::Success(receipt)
22        }
23    }
24}
25
26/// Checks the status of a txhash by first polling for a receipt, then for
27/// mempool inclusion. Returns the tx hash, and a status
28pub async fn check_tx_status(
29    provider: &RetryProvider,
30    hash: TxHash,
31    timeout: u64,
32) -> (TxHash, Result<TxStatus, eyre::Report>) {
33    let result = retry::Retry::new_no_delay(3)
34        .run_async_until_break(|| async {
35            match PendingTransactionBuilder::new(provider.clone(), hash)
36                .with_timeout(Some(Duration::from_secs(timeout)))
37                .get_receipt()
38                .await
39            {
40                Ok(receipt) => Ok(receipt.into()),
41                Err(e) => match provider.get_transaction_by_hash(hash).await {
42                    Ok(_) => match e {
43                        PendingTransactionError::TxWatcher(WatchTxError::Timeout) => {
44                            Err(RetryError::Continue(eyre!(
45                                "tx is still known to the node, waiting for receipt"
46                            )))
47                        }
48                        _ => Err(RetryError::Retry(e.into())),
49                    },
50                    Err(_) => Ok(TxStatus::Dropped),
51                },
52            }
53        })
54        .await;
55
56    (hash, result)
57}
58
59/// Prints parts of the receipt to stdout
60pub fn format_receipt(chain: Chain, receipt: &AnyTransactionReceipt) -> String {
61    let gas_used = receipt.gas_used;
62    let gas_price = receipt.effective_gas_price;
63    let block_number = receipt.block_number.unwrap_or_default();
64    let success = receipt.inner.inner.inner.receipt.status.coerce_status();
65
66    if shell::is_json() {
67        let _ = sh_println!(
68            "{}",
69            serde_json::json!({
70                "chain": chain,
71                "status": if success {
72                    "success"
73                } else {
74                    "failed"
75                },
76                "tx_hash": receipt.transaction_hash,
77                "contract_address": receipt.contract_address.map(|addr| addr.to_string()),
78                "block_number": block_number,
79                "gas_used": gas_used,
80                "gas_price": gas_price,
81            })
82        );
83
84        String::new()
85    } else {
86        format!(
87            "\n##### {chain}\n{status} Hash: {tx_hash:?}{contract_address}\nBlock: {block_number}\n{gas}\n\n",
88            status = if success { "✅  [Success]" } else { "❌  [Failed]" },
89            tx_hash = receipt.transaction_hash,
90            contract_address = if let Some(addr) = &receipt.contract_address {
91                format!("\nContract Address: {}", addr.to_checksum(None))
92            } else {
93                String::new()
94            },
95            gas = if gas_price == 0 {
96                format!("Gas Used: {gas_used}")
97            } else {
98                let paid = format_units((gas_used as u128).saturating_mul(gas_price), 18)
99                    .unwrap_or_else(|_| "N/A".into());
100                let gas_price =
101                    format_units(U256::from(gas_price), 9).unwrap_or_else(|_| "N/A".into());
102                format!(
103                    "Paid: {} ETH ({gas_used} gas * {} gwei)",
104                    paid.trim_end_matches('0'),
105                    gas_price.trim_end_matches('0').trim_end_matches('.')
106                )
107            },
108        )
109    }
110}