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