1use alloy_consensus::{Transaction, transaction::SignerRecoverable};
4use alloy_eips::eip7702::SignedAuthorization;
5use alloy_network::{AnyTransactionReceipt, Network, TransactionResponse};
6use alloy_primitives::{Address, Bytes, TxKind, U256};
7use alloy_provider::{
8 Provider,
9 network::{AnyNetwork, ReceiptResponse, TransactionBuilder},
10};
11use alloy_rpc_types::BlockId;
12use eyre::Result;
13use foundry_common_fmt::{UIfmt, UIfmtReceiptExt, get_pretty_receipt_attr};
14use foundry_primitives::FoundryTransactionBuilder;
15use serde::{Deserialize, Serialize};
16
17#[derive(Clone, Debug, Serialize, Deserialize)]
19pub struct TransactionReceiptWithRevertReason<N: Network> {
20 #[serde(flatten)]
22 pub receipt: N::ReceiptResponse,
23
24 #[serde(skip_serializing_if = "Option::is_none", rename = "revertReason")]
26 pub revert_reason: Option<String>,
27}
28
29impl<N: Network> TransactionReceiptWithRevertReason<N>
30where
31 N::TxEnvelope: Clone,
32 N::ReceiptResponse: UIfmtReceiptExt,
33{
34 pub async fn update_revert_reason(&mut self, provider: &dyn Provider<N>) -> Result<()> {
37 self.revert_reason = self.fetch_revert_reason(provider).await?;
38 Ok(())
39 }
40
41 async fn fetch_revert_reason(&self, provider: &dyn Provider<N>) -> Result<Option<String>> {
42 if self.receipt.status() {
44 return Ok(None);
45 }
46
47 let transaction = provider
48 .get_transaction_by_hash(self.receipt.transaction_hash())
49 .await
50 .map_err(|err| eyre::eyre!("unable to fetch transaction: {err}"))?
51 .ok_or_else(|| eyre::eyre!("transaction not found"))?;
52
53 if let Some(block_hash) = self.receipt.block_hash() {
54 let mut call_request: N::TransactionRequest = transaction.as_ref().clone().into();
55 call_request.set_from(transaction.from());
56 match provider.call(call_request).block(BlockId::Hash(block_hash.into())).await {
57 Err(e) => return Ok(extract_revert_reason(e.to_string())),
58 Ok(_) => eyre::bail!("no revert reason as transaction succeeded"),
59 }
60 }
61 eyre::bail!("unable to fetch block_hash")
62 }
63}
64
65impl From<AnyTransactionReceipt> for TransactionReceiptWithRevertReason<AnyNetwork> {
66 fn from(receipt: AnyTransactionReceipt) -> Self {
67 Self { receipt, revert_reason: None }
68 }
69}
70
71impl From<TransactionReceiptWithRevertReason<AnyNetwork>> for AnyTransactionReceipt {
72 fn from(receipt_with_reason: TransactionReceiptWithRevertReason<AnyNetwork>) -> Self {
73 receipt_with_reason.receipt
74 }
75}
76
77impl<N: Network> UIfmt for TransactionReceiptWithRevertReason<N>
78where
79 N::ReceiptResponse: UIfmt,
80{
81 fn pretty(&self) -> String {
82 if let Some(revert_reason) = &self.revert_reason {
83 format!(
84 "{}
85revertReason {}",
86 self.receipt.pretty(),
87 revert_reason
88 )
89 } else {
90 self.receipt.pretty()
91 }
92 }
93}
94
95impl<N: Network> UIfmt for TransactionMaybeSigned<N>
96where
97 N::TxEnvelope: UIfmt,
98 N::TransactionRequest: FoundryTransactionBuilder<N>,
99{
100 fn pretty(&self) -> String {
101 match self {
102 Self::Signed { tx, .. } => tx.pretty(),
103 Self::Unsigned(tx) => format!(
104 "
105accessList {}
106chainId {}
107gasLimit {}
108gasPrice {}
109input {}
110maxFeePerBlobGas {}
111maxFeePerGas {}
112maxPriorityFeePerGas {}
113nonce {}
114to {}
115type {}
116value {}",
117 tx.access_list()
118 .as_ref()
119 .map(|a| a.iter().collect::<Vec<_>>())
120 .unwrap_or_default()
121 .pretty(),
122 tx.chain_id().pretty(),
123 tx.gas_limit().unwrap_or_default(),
124 tx.gas_price().pretty(),
125 tx.input().pretty(),
126 tx.max_fee_per_blob_gas().pretty(),
127 tx.max_fee_per_gas().pretty(),
128 tx.max_priority_fee_per_gas().pretty(),
129 tx.nonce().pretty(),
130 tx.to().pretty(),
131 tx.output_tx_type(),
132 tx.value().pretty(),
133 ),
134 }
135 }
136}
137
138fn extract_revert_reason<S: AsRef<str>>(error_string: S) -> Option<String> {
139 let message_substr = "execution reverted: ";
140 error_string
141 .as_ref()
142 .find(message_substr)
143 .map(|index| error_string.as_ref().split_at(index + message_substr.len()).1.to_string())
144}
145
146pub fn get_pretty_receipt_w_reason_attr<N>(
148 receipt: &TransactionReceiptWithRevertReason<N>,
149 attr: &str,
150) -> Option<String>
151where
152 N: Network,
153 N::ReceiptResponse: UIfmtReceiptExt,
154{
155 if matches!(attr, "revertReason" | "revert_reason") {
157 return Some(receipt.revert_reason.pretty());
158 }
159 get_pretty_receipt_attr::<N>(&receipt.receipt, attr)
160}
161
162#[derive(Clone, Debug, Serialize, Deserialize)]
166#[serde(untagged)]
167pub enum TransactionMaybeSigned<N: Network> {
168 Signed {
169 #[serde(flatten)]
170 tx: N::TxEnvelope,
171 from: Address,
172 },
173 Unsigned(N::TransactionRequest),
174}
175
176impl<N: Network> TransactionMaybeSigned<N> {
177 pub fn new(tx: N::TransactionRequest) -> Self {
179 Self::Unsigned(tx)
180 }
181
182 pub fn new_signed(
184 tx: N::TxEnvelope,
185 ) -> core::result::Result<Self, alloy_consensus::crypto::RecoveryError>
186 where
187 N::TxEnvelope: SignerRecoverable,
188 {
189 let from = tx.recover_signer()?;
190 Ok(Self::Signed { tx, from })
191 }
192
193 pub fn is_unsigned(&self) -> bool {
194 matches!(self, Self::Unsigned(_))
195 }
196
197 pub fn as_unsigned_mut(&mut self) -> Option<&mut N::TransactionRequest> {
198 match self {
199 Self::Unsigned(tx) => Some(tx),
200 _ => None,
201 }
202 }
203
204 pub fn from(&self) -> Option<Address> {
205 match self {
206 Self::Signed { from, .. } => Some(*from),
207 Self::Unsigned(tx) => tx.from(),
208 }
209 }
210
211 pub fn input(&self) -> Option<&Bytes> {
212 match self {
213 Self::Signed { tx, .. } => Some(tx.input()),
214 Self::Unsigned(tx) => tx.input(),
215 }
216 }
217
218 pub fn to(&self) -> Option<TxKind> {
219 match self {
220 Self::Signed { tx, .. } => Some(tx.kind()),
221 Self::Unsigned(tx) => tx.kind(),
222 }
223 }
224
225 pub fn value(&self) -> Option<U256> {
226 match self {
227 Self::Signed { tx, .. } => Some(tx.value()),
228 Self::Unsigned(tx) => tx.value(),
229 }
230 }
231
232 pub fn gas(&self) -> Option<u128> {
233 match self {
234 Self::Signed { tx, .. } => Some(tx.gas_limit() as u128),
235 Self::Unsigned(tx) => tx.gas_limit().map(|g| g as u128),
236 }
237 }
238
239 pub fn nonce(&self) -> Option<u64> {
240 match self {
241 Self::Signed { tx, .. } => Some(tx.nonce()),
242 Self::Unsigned(tx) => tx.nonce(),
243 }
244 }
245
246 pub fn authorization_list(&self) -> Option<Vec<SignedAuthorization>>
247 where
248 N::TransactionRequest: FoundryTransactionBuilder<N>,
249 {
250 match self {
251 Self::Signed { tx, .. } => tx.authorization_list().map(|auths| auths.to_vec()),
252 Self::Unsigned(tx) => tx.authorization_list().map(|auths| auths.to_vec()),
253 }
254 .filter(|auths| !auths.is_empty())
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
263 fn test_extract_revert_reason() {
264 let error_string_1 = "server returned an error response: error code 3: execution reverted: Transaction too old";
265 let error_string_2 = "server returned an error response: error code 3: Invalid signature";
266
267 assert_eq!(extract_revert_reason(error_string_1), Some("Transaction too old".to_string()));
268 assert_eq!(extract_revert_reason(error_string_2), None);
269 }
270}