1use alloy_consensus::{Transaction, TxEnvelope};
4use alloy_eips::eip7702::SignedAuthorization;
5use alloy_network::AnyTransactionReceipt;
6use alloy_primitives::{Address, TxKind, U256};
7use alloy_provider::{
8 network::{AnyNetwork, ReceiptResponse, TransactionBuilder},
9 Provider,
10};
11use alloy_rpc_types::{BlockId, TransactionRequest};
12use alloy_serde::WithOtherFields;
13use eyre::Result;
14use foundry_common_fmt::UIfmt;
15use serde::{Deserialize, Serialize};
16
17#[derive(Clone, Debug, Serialize, Deserialize)]
19pub struct TransactionReceiptWithRevertReason {
20 #[serde(flatten)]
22 pub receipt: AnyTransactionReceipt,
23
24 #[serde(skip_serializing_if = "Option::is_none", rename = "revertReason")]
26 pub revert_reason: Option<String>,
27}
28
29impl TransactionReceiptWithRevertReason {
30 pub fn is_failure(&self) -> bool {
32 !self.receipt.inner.inner.inner.receipt.status.coerce_status()
33 }
34
35 pub async fn update_revert_reason<P: Provider<AnyNetwork>>(
38 &mut self,
39 provider: &P,
40 ) -> Result<()> {
41 self.revert_reason = self.fetch_revert_reason(provider).await?;
42 Ok(())
43 }
44
45 async fn fetch_revert_reason<P: Provider<AnyNetwork>>(
46 &self,
47 provider: &P,
48 ) -> Result<Option<String>> {
49 if !self.is_failure() {
50 return Ok(None)
51 }
52
53 let transaction = provider
54 .get_transaction_by_hash(self.receipt.transaction_hash)
55 .await
56 .map_err(|err| eyre::eyre!("unable to fetch transaction: {err}"))?
57 .ok_or_else(|| eyre::eyre!("transaction not found"))?;
58
59 if let Some(block_hash) = self.receipt.block_hash {
60 let mut call_request: WithOtherFields<TransactionRequest> =
61 transaction.inner.inner.clone_inner().into();
62 call_request.set_from(transaction.inner.inner.signer());
63 match provider.call(call_request).block(BlockId::Hash(block_hash.into())).await {
64 Err(e) => return Ok(extract_revert_reason(e.to_string())),
65 Ok(_) => eyre::bail!("no revert reason as transaction succeeded"),
66 }
67 }
68 eyre::bail!("unable to fetch block_hash")
69 }
70}
71
72impl From<AnyTransactionReceipt> for TransactionReceiptWithRevertReason {
73 fn from(receipt: AnyTransactionReceipt) -> Self {
74 Self { receipt, revert_reason: None }
75 }
76}
77
78impl From<TransactionReceiptWithRevertReason> for AnyTransactionReceipt {
79 fn from(receipt_with_reason: TransactionReceiptWithRevertReason) -> Self {
80 receipt_with_reason.receipt
81 }
82}
83
84impl UIfmt for TransactionReceiptWithRevertReason {
85 fn pretty(&self) -> String {
86 if let Some(revert_reason) = &self.revert_reason {
87 format!(
88 "{}
89revertReason {}",
90 self.receipt.pretty(),
91 revert_reason
92 )
93 } else {
94 self.receipt.pretty()
95 }
96 }
97}
98
99impl UIfmt for TransactionMaybeSigned {
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.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.as_ref().map(|a| a.to()).unwrap_or_default().pretty(),
131 tx.transaction_type.unwrap_or_default(),
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_tx_receipt_attr(
148 receipt: &TransactionReceiptWithRevertReason,
149 attr: &str,
150) -> Option<String> {
151 match attr {
152 "blockHash" | "block_hash" => Some(receipt.receipt.block_hash.pretty()),
153 "blockNumber" | "block_number" => Some(receipt.receipt.block_number.pretty()),
154 "contractAddress" | "contract_address" => Some(receipt.receipt.contract_address.pretty()),
155 "cumulativeGasUsed" | "cumulative_gas_used" => {
156 Some(receipt.receipt.inner.inner.inner.receipt.cumulative_gas_used.pretty())
157 }
158 "effectiveGasPrice" | "effective_gas_price" => {
159 Some(receipt.receipt.effective_gas_price.to_string())
160 }
161 "gasUsed" | "gas_used" => Some(receipt.receipt.gas_used.to_string()),
162 "logs" => Some(receipt.receipt.inner.inner.inner.receipt.logs.as_slice().pretty()),
163 "logsBloom" | "logs_bloom" => Some(receipt.receipt.inner.inner.inner.logs_bloom.pretty()),
164 "root" | "stateRoot" | "state_root " => Some(receipt.receipt.state_root().pretty()),
165 "status" | "statusCode" | "status_code" => {
166 Some(receipt.receipt.inner.inner.inner.receipt.status.pretty())
167 }
168 "transactionHash" | "transaction_hash" => Some(receipt.receipt.transaction_hash.pretty()),
169 "transactionIndex" | "transaction_index" => {
170 Some(receipt.receipt.transaction_index.pretty())
171 }
172 "type" | "transaction_type" => Some(receipt.receipt.inner.inner.r#type.to_string()),
173 "revertReason" | "revert_reason" => Some(receipt.revert_reason.pretty()),
174 _ => None,
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 #[test]
183 fn test_extract_revert_reason() {
184 let error_string_1 = "server returned an error response: error code 3: execution reverted: Transaction too old";
185 let error_string_2 = "server returned an error response: error code 3: Invalid signature";
186
187 assert_eq!(extract_revert_reason(error_string_1), Some("Transaction too old".to_string()));
188 assert_eq!(extract_revert_reason(error_string_2), None);
189 }
190}
191
192#[derive(Clone, Debug, Serialize, Deserialize)]
196#[serde(untagged)]
197pub enum TransactionMaybeSigned {
198 Signed {
199 #[serde(flatten)]
200 tx: TxEnvelope,
201 from: Address,
202 },
203 Unsigned(WithOtherFields<TransactionRequest>),
204}
205
206impl TransactionMaybeSigned {
207 pub fn new(tx: WithOtherFields<TransactionRequest>) -> Self {
209 Self::Unsigned(tx)
210 }
211
212 pub fn new_signed(
214 tx: TxEnvelope,
215 ) -> core::result::Result<Self, alloy_primitives::SignatureError> {
216 let from = tx.recover_signer()?;
217 Ok(Self::Signed { tx, from })
218 }
219
220 pub fn is_unsigned(&self) -> bool {
221 matches!(self, Self::Unsigned(_))
222 }
223
224 pub fn as_unsigned_mut(&mut self) -> Option<&mut WithOtherFields<TransactionRequest>> {
225 match self {
226 Self::Unsigned(tx) => Some(tx),
227 _ => None,
228 }
229 }
230
231 pub fn from(&self) -> Option<Address> {
232 match self {
233 Self::Signed { from, .. } => Some(*from),
234 Self::Unsigned(tx) => tx.from,
235 }
236 }
237
238 pub fn input(&self) -> Option<&[u8]> {
239 match self {
240 Self::Signed { tx, .. } => Some(tx.input()),
241 Self::Unsigned(tx) => tx.input.input().map(|i| i.as_ref()),
242 }
243 }
244
245 pub fn to(&self) -> Option<TxKind> {
246 match self {
247 Self::Signed { tx, .. } => Some(tx.kind()),
248 Self::Unsigned(tx) => tx.to,
249 }
250 }
251
252 pub fn value(&self) -> Option<U256> {
253 match self {
254 Self::Signed { tx, .. } => Some(tx.value()),
255 Self::Unsigned(tx) => tx.value,
256 }
257 }
258
259 pub fn gas(&self) -> Option<u128> {
260 match self {
261 Self::Signed { tx, .. } => Some(tx.gas_limit() as u128),
262 Self::Unsigned(tx) => tx.gas_limit().map(|g| g as u128),
263 }
264 }
265
266 pub fn nonce(&self) -> Option<u64> {
267 match self {
268 Self::Signed { tx, .. } => Some(tx.nonce()),
269 Self::Unsigned(tx) => tx.nonce,
270 }
271 }
272
273 pub fn authorization_list(&self) -> Option<Vec<SignedAuthorization>> {
274 match self {
275 Self::Signed { tx, .. } => tx.authorization_list().map(|auths| auths.to_vec()),
276 Self::Unsigned(tx) => tx.authorization_list.as_deref().map(|auths| auths.to_vec()),
277 }
278 .filter(|auths| !auths.is_empty())
279 }
280}
281
282impl From<TransactionRequest> for TransactionMaybeSigned {
283 fn from(tx: TransactionRequest) -> Self {
284 Self::new(WithOtherFields::new(tx))
285 }
286}
287
288impl TryFrom<TxEnvelope> for TransactionMaybeSigned {
289 type Error = alloy_primitives::SignatureError;
290
291 fn try_from(tx: TxEnvelope) -> core::result::Result<Self, Self::Error> {
292 Self::new_signed(tx)
293 }
294}