forge_script/
receipts.rs

1use alloy_chains::{Chain, NamedChain};
2use alloy_network::AnyTransactionReceipt;
3use alloy_primitives::{TxHash, U256, utils::format_units};
4use alloy_provider::{PendingTransactionBuilder, PendingTransactionError, Provider, WatchTxError};
5use eyre::{Result, eyre};
6use foundry_common::{provider::RetryProvider, retry, retry::RetryError, shell};
7use std::time::Duration;
8
9/// Marker error type for pending receipts
10#[derive(Debug, thiserror::Error)]
11#[error(
12    "Received a pending receipt for {tx_hash}, but transaction is still known to the node, retrying"
13)]
14pub struct PendingReceiptError {
15    pub tx_hash: TxHash,
16}
17
18/// Convenience enum for internal signalling of transaction status
19pub enum TxStatus {
20    Dropped,
21    Success(AnyTransactionReceipt),
22    Revert(AnyTransactionReceipt),
23}
24
25impl From<AnyTransactionReceipt> for TxStatus {
26    fn from(receipt: AnyTransactionReceipt) -> Self {
27        if !receipt.inner.inner.inner.receipt.status.coerce_status() {
28            Self::Revert(receipt)
29        } else {
30            Self::Success(receipt)
31        }
32    }
33}
34
35/// Checks the status of a txhash by first polling for a receipt, then for
36/// mempool inclusion. Returns the tx hash, and a status
37pub async fn check_tx_status(
38    provider: &RetryProvider,
39    hash: TxHash,
40    timeout: u64,
41) -> (TxHash, Result<TxStatus, eyre::Report>) {
42    let result = retry::Retry::new_no_delay(3)
43        .run_async_until_break(|| async {
44            match PendingTransactionBuilder::new(provider.clone(), hash)
45                .with_timeout(Some(Duration::from_secs(timeout)))
46                .get_receipt()
47                .await
48            {
49                Ok(receipt) => {
50                    // Check if the receipt is pending (missing block information)
51                    let is_pending = receipt.block_number.is_none()
52                        || receipt.block_hash.is_none()
53                        || receipt.transaction_index.is_none();
54
55                    if !is_pending {
56                        return Ok(receipt.into());
57                    }
58
59                    // Receipt is pending, try to sleep and retry a few times
60                    match provider.get_transaction_by_hash(hash).await {
61                        Ok(_) => {
62                            // Sleep for a short time to allow the transaction to be mined
63                            tokio::time::sleep(Duration::from_millis(500)).await;
64                            // Transaction is still known to the node, retry
65                            Err(RetryError::Retry(PendingReceiptError { tx_hash: hash }.into()))
66                        }
67                        Err(_) => {
68                            // Transaction is not known to the node, mark it as dropped
69                            Ok(TxStatus::Dropped)
70                        }
71                    }
72                }
73                Err(e) => match provider.get_transaction_by_hash(hash).await {
74                    Ok(_) => match e {
75                        PendingTransactionError::TxWatcher(WatchTxError::Timeout) => {
76                            Err(RetryError::Continue(eyre!(
77                                "tx is still known to the node, waiting for receipt"
78                            )))
79                        }
80                        _ => Err(RetryError::Retry(e.into())),
81                    },
82                    Err(_) => Ok(TxStatus::Dropped),
83                },
84            }
85        })
86        .await;
87
88    (hash, result)
89}
90
91/// Prints parts of the receipt to stdout
92pub fn format_receipt(chain: Chain, receipt: &AnyTransactionReceipt) -> String {
93    let gas_used = receipt.gas_used;
94    let gas_price = receipt.effective_gas_price;
95    let block_number = receipt.block_number.unwrap_or_default();
96    let success = receipt.inner.inner.inner.receipt.status.coerce_status();
97
98    if shell::is_json() {
99        let _ = sh_println!(
100            "{}",
101            serde_json::json!({
102                "chain": chain,
103                "status": if success {
104                    "success"
105                } else {
106                    "failed"
107                },
108                "tx_hash": receipt.transaction_hash,
109                "contract_address": receipt.contract_address.map(|addr| addr.to_string()),
110                "block_number": block_number,
111                "gas_used": gas_used,
112                "gas_price": gas_price,
113            })
114        );
115
116        String::new()
117    } else {
118        format!(
119            "\n##### {chain}\n{status} Hash: {tx_hash:?}{contract_address}\nBlock: {block_number}\n{gas}\n\n",
120            status = if success { "✅  [Success]" } else { "❌  [Failed]" },
121            tx_hash = receipt.transaction_hash,
122            contract_address = if let Some(addr) = &receipt.contract_address {
123                format!("\nContract Address: {}", addr.to_checksum(None))
124            } else {
125                String::new()
126            },
127            gas = if gas_price == 0 {
128                format!("Gas Used: {gas_used}")
129            } else {
130                let paid = format_units((gas_used as u128).saturating_mul(gas_price), 18)
131                    .unwrap_or_else(|_| "N/A".into());
132                let gas_price =
133                    format_units(U256::from(gas_price), 9).unwrap_or_else(|_| "N/A".into());
134                let token_symbol = NamedChain::try_from(chain)
135                    .unwrap_or_default()
136                    .native_currency_symbol()
137                    .unwrap_or("ETH");
138                format!(
139                    "Paid: {} {} ({gas_used} gas * {} gwei)",
140                    paid.trim_end_matches('0'),
141                    token_symbol,
142                    gas_price.trim_end_matches('0').trim_end_matches('.')
143                )
144            },
145        )
146    }
147}