Skip to main content

foundry_common/
transactions.rs

1//! Wrappers for transactions.
2
3use 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/// Helper type to carry a transaction along with an optional revert reason
18#[derive(Clone, Debug, Serialize, Deserialize)]
19pub struct TransactionReceiptWithRevertReason<N: Network> {
20    /// The underlying transaction receipt
21    #[serde(flatten)]
22    pub receipt: N::ReceiptResponse,
23
24    /// The revert reason string if the transaction status is failed
25    #[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    /// Updates the revert reason field using `eth_call` and returns an Err variant if the revert
35    /// reason was not successfully updated
36    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 the transaction succeeded, there is no revert reason to fetch
43        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
146/// Returns the `UiFmt::pretty()` formatted attribute of the transaction receipt with revert reason
147pub 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    // Handle revert reason first, then delegate to the receipt formatting function
156    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/// Used for broadcasting transactions
163/// A transaction can either be a `TransactionRequest` waiting to be signed
164/// or a `TxEnvelope`, already signed
165#[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    /// Creates a new (unsigned) transaction for broadcast
178    pub fn new(tx: N::TransactionRequest) -> Self {
179        Self::Unsigned(tx)
180    }
181
182    /// Creates a new signed transaction for broadcast.
183    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}