1use alloy_consensus::{Transaction, TxEnvelope, 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, TransactionRequest};
12use alloy_serde::WithOtherFields;
13use eyre::Result;
14use foundry_common_fmt::{UIfmt, UIfmtReceiptExt, get_pretty_receipt_attr};
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<P: Provider<N>>(&mut self, provider: &P) -> Result<()> {
37 self.revert_reason = self.fetch_revert_reason(provider).await?;
38 Ok(())
39 }
40
41 async fn fetch_revert_reason<P: Provider<N>>(&self, provider: &P) -> 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 UIfmt for TransactionMaybeSigned {
96 fn pretty(&self) -> String {
97 match self {
98 Self::Signed { tx, .. } => tx.pretty(),
99 Self::Unsigned(tx) => format!(
100 "
101accessList {}
102chainId {}
103gasLimit {}
104gasPrice {}
105input {}
106maxFeePerBlobGas {}
107maxFeePerGas {}
108maxPriorityFeePerGas {}
109nonce {}
110to {}
111type {}
112value {}",
113 tx.access_list
114 .as_ref()
115 .map(|a| a.iter().collect::<Vec<_>>())
116 .unwrap_or_default()
117 .pretty(),
118 tx.chain_id.pretty(),
119 tx.gas_limit().unwrap_or_default(),
120 tx.gas_price.pretty(),
121 tx.input.input.pretty(),
122 tx.max_fee_per_blob_gas.pretty(),
123 tx.max_fee_per_gas.pretty(),
124 tx.max_priority_fee_per_gas.pretty(),
125 tx.nonce.pretty(),
126 tx.to.as_ref().map(|a| a.to()).unwrap_or_default().pretty(),
127 tx.transaction_type.unwrap_or_default(),
128 tx.value.pretty(),
129 ),
130 }
131 }
132}
133
134fn extract_revert_reason<S: AsRef<str>>(error_string: S) -> Option<String> {
135 let message_substr = "execution reverted: ";
136 error_string
137 .as_ref()
138 .find(message_substr)
139 .map(|index| error_string.as_ref().split_at(index + message_substr.len()).1.to_string())
140}
141
142pub fn get_pretty_receipt_w_reason_attr<N>(
144 receipt: &TransactionReceiptWithRevertReason<N>,
145 attr: &str,
146) -> Option<String>
147where
148 N: Network,
149 N::ReceiptResponse: UIfmtReceiptExt,
150{
151 if matches!(attr, "revertReason" | "revert_reason") {
153 return Some(receipt.revert_reason.pretty());
154 }
155 get_pretty_receipt_attr::<N>(&receipt.receipt, attr)
156}
157
158#[derive(Clone, Debug, Serialize, Deserialize)]
162#[serde(untagged)]
163pub enum TransactionMaybeSigned {
164 Signed {
165 #[serde(flatten)]
166 tx: TxEnvelope,
167 from: Address,
168 },
169 Unsigned(WithOtherFields<TransactionRequest>),
170}
171
172impl TransactionMaybeSigned {
173 pub fn new(tx: WithOtherFields<TransactionRequest>) -> Self {
175 Self::Unsigned(tx)
176 }
177
178 pub fn new_signed(
180 tx: TxEnvelope,
181 ) -> core::result::Result<Self, alloy_consensus::crypto::RecoveryError> {
182 let from = tx.recover_signer()?;
183 Ok(Self::Signed { tx, from })
184 }
185
186 pub fn is_unsigned(&self) -> bool {
187 matches!(self, Self::Unsigned(_))
188 }
189
190 pub fn as_unsigned_mut(&mut self) -> Option<&mut WithOtherFields<TransactionRequest>> {
191 match self {
192 Self::Unsigned(tx) => Some(tx),
193 _ => None,
194 }
195 }
196
197 pub fn from(&self) -> Option<Address> {
198 match self {
199 Self::Signed { from, .. } => Some(*from),
200 Self::Unsigned(tx) => tx.from,
201 }
202 }
203
204 pub fn input(&self) -> Option<&Bytes> {
205 match self {
206 Self::Signed { tx, .. } => Some(tx.input()),
207 Self::Unsigned(tx) => tx.input.input(),
208 }
209 }
210
211 pub fn to(&self) -> Option<TxKind> {
212 match self {
213 Self::Signed { tx, .. } => Some(tx.kind()),
214 Self::Unsigned(tx) => tx.to,
215 }
216 }
217
218 pub fn value(&self) -> Option<U256> {
219 match self {
220 Self::Signed { tx, .. } => Some(tx.value()),
221 Self::Unsigned(tx) => tx.value,
222 }
223 }
224
225 pub fn gas(&self) -> Option<u128> {
226 match self {
227 Self::Signed { tx, .. } => Some(tx.gas_limit() as u128),
228 Self::Unsigned(tx) => tx.gas_limit().map(|g| g as u128),
229 }
230 }
231
232 pub fn nonce(&self) -> Option<u64> {
233 match self {
234 Self::Signed { tx, .. } => Some(tx.nonce()),
235 Self::Unsigned(tx) => tx.nonce,
236 }
237 }
238
239 pub fn authorization_list(&self) -> Option<Vec<SignedAuthorization>> {
240 match self {
241 Self::Signed { tx, .. } => tx.authorization_list().map(|auths| auths.to_vec()),
242 Self::Unsigned(tx) => tx.authorization_list.as_deref().map(|auths| auths.to_vec()),
243 }
244 .filter(|auths| !auths.is_empty())
245 }
246}
247
248impl From<TransactionRequest> for TransactionMaybeSigned {
249 fn from(tx: TransactionRequest) -> Self {
250 Self::new(WithOtherFields::new(tx))
251 }
252}
253
254impl TryFrom<TxEnvelope> for TransactionMaybeSigned {
255 type Error = alloy_consensus::crypto::RecoveryError;
256
257 fn try_from(tx: TxEnvelope) -> core::result::Result<Self, Self::Error> {
258 Self::new_signed(tx)
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 #[test]
267 fn test_extract_revert_reason() {
268 let error_string_1 = "server returned an error response: error code 3: execution reverted: Transaction too old";
269 let error_string_2 = "server returned an error response: error code 3: Invalid signature";
270
271 assert_eq!(extract_revert_reason(error_string_1), Some("Transaction too old".to_string()));
272 assert_eq!(extract_revert_reason(error_string_2), None);
273 }
274}