foundry_common/
transactions.rs

1//! Wrappers for transactions.
2
3use 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/// Helper type to carry a transaction along with an optional revert reason
18#[derive(Clone, Debug, Serialize, Deserialize)]
19pub struct TransactionReceiptWithRevertReason {
20    /// The underlying transaction receipt
21    #[serde(flatten)]
22    pub receipt: AnyTransactionReceipt,
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 TransactionReceiptWithRevertReason {
30    /// Returns if the status of the transaction is 0 (failure)
31    pub fn is_failure(&self) -> bool {
32        !self.receipt.inner.inner.inner.receipt.status.coerce_status()
33    }
34
35    /// Updates the revert reason field using `eth_call` and returns an Err variant if the revert
36    /// reason was not successfully updated
37    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
146/// Returns the `UiFmt::pretty()` formatted attribute of the transaction receipt
147pub 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/// Used for broadcasting transactions
193/// A transaction can either be a [`TransactionRequest`] waiting to be signed
194/// or a [`TxEnvelope`], already signed
195#[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    /// Creates a new (unsigned) transaction for broadcast
208    pub fn new(tx: WithOtherFields<TransactionRequest>) -> Self {
209        Self::Unsigned(tx)
210    }
211
212    /// Creates a new signed transaction for broadcast.
213    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}