Skip to main content

foundry_common/transactions/
receipt.rs

1use alloy_network::{AnyNetwork, AnyTransactionReceipt, Network, TransactionResponse};
2use alloy_primitives::Address;
3use alloy_provider::{
4    Provider,
5    network::{ReceiptResponse, TransactionBuilder},
6};
7use alloy_rpc_types::{BlockId, TransactionReceipt};
8use eyre::Result;
9use foundry_common_fmt::{UIfmt, UIfmtReceiptExt, get_pretty_receipt_attr};
10use serde::{Deserialize, Serialize};
11use tempo_alloy::rpc::TempoTransactionReceipt;
12
13#[cfg(feature = "base")]
14use base_common_rpc_types::BaseTransactionReceipt;
15
16#[cfg(feature = "optimism")]
17use op_alloy_rpc_types::OpTransactionReceipt;
18
19/// Helper trait providing `contract_address` setter for generic `ReceiptResponse`
20pub trait FoundryReceiptResponse {
21    /// Sets address of the created contract, or `None` if the transaction was not a deployment.
22    fn set_contract_address(&mut self, contract_address: Address);
23}
24
25impl FoundryReceiptResponse for TransactionReceipt {
26    fn set_contract_address(&mut self, contract_address: Address) {
27        self.contract_address = Some(contract_address);
28    }
29}
30
31#[cfg(feature = "base")]
32impl FoundryReceiptResponse for BaseTransactionReceipt {
33    fn set_contract_address(&mut self, contract_address: Address) {
34        self.inner.contract_address = Some(contract_address);
35    }
36}
37
38#[cfg(feature = "optimism")]
39impl FoundryReceiptResponse for OpTransactionReceipt {
40    fn set_contract_address(&mut self, contract_address: Address) {
41        self.inner.contract_address = Some(contract_address);
42    }
43}
44
45impl FoundryReceiptResponse for TempoTransactionReceipt {
46    fn set_contract_address(&mut self, contract_address: Address) {
47        self.contract_address = Some(contract_address);
48    }
49}
50
51/// Helper type to carry a transaction along with an optional revert reason
52#[derive(Clone, Debug, Serialize, Deserialize)]
53pub struct TransactionReceiptWithRevertReason<N: Network> {
54    /// The underlying transaction receipt
55    #[serde(flatten)]
56    pub receipt: N::ReceiptResponse,
57
58    /// The revert reason string if the transaction status is failed
59    #[serde(skip_serializing_if = "Option::is_none", rename = "revertReason")]
60    pub revert_reason: Option<String>,
61}
62
63impl<N: Network> TransactionReceiptWithRevertReason<N>
64where
65    N::TxEnvelope: Clone,
66    N::ReceiptResponse: UIfmtReceiptExt,
67{
68    /// Updates the revert reason field using `eth_call` and returns an Err variant if the revert
69    /// reason was not successfully updated
70    pub async fn update_revert_reason(&mut self, provider: &dyn Provider<N>) -> Result<()> {
71        self.revert_reason = self.fetch_revert_reason(provider).await?;
72        Ok(())
73    }
74
75    async fn fetch_revert_reason(&self, provider: &dyn Provider<N>) -> Result<Option<String>> {
76        // If the transaction succeeded, there is no revert reason to fetch
77        if self.receipt.status() {
78            return Ok(None);
79        }
80
81        let transaction = provider
82            .get_transaction_by_hash(self.receipt.transaction_hash())
83            .await
84            .map_err(|err| eyre::eyre!("unable to fetch transaction: {err}"))?
85            .ok_or_else(|| eyre::eyre!("transaction not found"))?;
86
87        if let Some(block_hash) = self.receipt.block_hash() {
88            let mut call_request: N::TransactionRequest = transaction.as_ref().clone().into();
89            call_request.set_from(transaction.from());
90            match provider.call(call_request).block(BlockId::Hash(block_hash.into())).await {
91                Err(e) => return Ok(extract_revert_reason(e.to_string())),
92                Ok(_) => {
93                    eyre::bail!("no revert reason as transaction succeeded");
94                }
95            }
96        }
97        eyre::bail!("unable to fetch block_hash");
98    }
99}
100
101impl From<AnyTransactionReceipt> for TransactionReceiptWithRevertReason<AnyNetwork> {
102    fn from(receipt: AnyTransactionReceipt) -> Self {
103        Self { receipt, revert_reason: None }
104    }
105}
106
107impl From<TransactionReceiptWithRevertReason<AnyNetwork>> for AnyTransactionReceipt {
108    fn from(receipt_with_reason: TransactionReceiptWithRevertReason<AnyNetwork>) -> Self {
109        receipt_with_reason.receipt
110    }
111}
112
113impl<N: Network> UIfmt for TransactionReceiptWithRevertReason<N>
114where
115    N::ReceiptResponse: UIfmt,
116{
117    fn pretty(&self) -> String {
118        if let Some(revert_reason) = &self.revert_reason {
119            format!(
120                "{}
121revertReason         {}",
122                self.receipt.pretty(),
123                revert_reason
124            )
125        } else {
126            self.receipt.pretty()
127        }
128    }
129}
130
131fn extract_revert_reason<S: AsRef<str>>(error_string: S) -> Option<String> {
132    let message_substr = "execution reverted: ";
133    error_string
134        .as_ref()
135        .find(message_substr)
136        .map(|index| error_string.as_ref().split_at(index + message_substr.len()).1.to_string())
137}
138
139/// Returns the `UiFmt::pretty()` formatted attribute of the transaction receipt with revert reason
140pub fn get_pretty_receipt_w_reason_attr<N>(
141    receipt: &TransactionReceiptWithRevertReason<N>,
142    attr: &str,
143) -> Option<String>
144where
145    N: Network,
146    N::ReceiptResponse: UIfmtReceiptExt,
147{
148    // Handle revert reason first, then delegate to the receipt formatting function
149    if matches!(attr, "revertReason" | "revert_reason") {
150        return Some(receipt.revert_reason.pretty());
151    }
152    get_pretty_receipt_attr::<N>(&receipt.receipt, attr)
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn test_extract_revert_reason() {
161        let error_string_1 = "server returned an error response: error code 3: execution reverted: Transaction too old";
162        let error_string_2 = "server returned an error response: error code 3: Invalid signature";
163
164        assert_eq!(extract_revert_reason(error_string_1), Some("Transaction too old".to_string()));
165        assert_eq!(extract_revert_reason(error_string_2), None);
166    }
167}