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