1use alloy_chains::{Chain, NamedChain};
2use alloy_network::AnyTransactionReceipt;
3use alloy_primitives::{TxHash, U256, utils::format_units};
4use alloy_provider::{PendingTransactionBuilder, PendingTransactionError, Provider, WatchTxError};
5use eyre::{Result, eyre};
6use foundry_common::{provider::RetryProvider, retry, retry::RetryError, shell};
7use std::time::Duration;
8
9#[derive(Debug, thiserror::Error)]
11#[error(
12 "Received a pending receipt for {tx_hash}, but transaction is still known to the node, retrying"
13)]
14pub struct PendingReceiptError {
15 pub tx_hash: TxHash,
16}
17
18pub enum TxStatus {
20 Dropped,
21 Success(AnyTransactionReceipt),
22 Revert(AnyTransactionReceipt),
23}
24
25impl From<AnyTransactionReceipt> for TxStatus {
26 fn from(receipt: AnyTransactionReceipt) -> Self {
27 if !receipt.inner.inner.inner.receipt.status.coerce_status() {
28 Self::Revert(receipt)
29 } else {
30 Self::Success(receipt)
31 }
32 }
33}
34
35pub async fn check_tx_status(
38 provider: &RetryProvider,
39 hash: TxHash,
40 timeout: u64,
41) -> (TxHash, Result<TxStatus, eyre::Report>) {
42 let result = retry::Retry::new_no_delay(3)
43 .run_async_until_break(|| async {
44 match PendingTransactionBuilder::new(provider.clone(), hash)
45 .with_timeout(Some(Duration::from_secs(timeout)))
46 .get_receipt()
47 .await
48 {
49 Ok(receipt) => {
50 let is_pending = receipt.block_number.is_none()
52 || receipt.block_hash.is_none()
53 || receipt.transaction_index.is_none();
54
55 if !is_pending {
56 return Ok(receipt.into());
57 }
58
59 match provider.get_transaction_by_hash(hash).await {
61 Ok(_) => {
62 tokio::time::sleep(Duration::from_millis(500)).await;
64 Err(RetryError::Retry(PendingReceiptError { tx_hash: hash }.into()))
66 }
67 Err(_) => {
68 Ok(TxStatus::Dropped)
70 }
71 }
72 }
73 Err(e) => match provider.get_transaction_by_hash(hash).await {
74 Ok(_) => match e {
75 PendingTransactionError::TxWatcher(WatchTxError::Timeout) => {
76 Err(RetryError::Continue(eyre!(
77 "tx is still known to the node, waiting for receipt"
78 )))
79 }
80 _ => Err(RetryError::Retry(e.into())),
81 },
82 Err(_) => Ok(TxStatus::Dropped),
83 },
84 }
85 })
86 .await;
87
88 (hash, result)
89}
90
91pub fn format_receipt(chain: Chain, receipt: &AnyTransactionReceipt) -> String {
93 let gas_used = receipt.gas_used;
94 let gas_price = receipt.effective_gas_price;
95 let block_number = receipt.block_number.unwrap_or_default();
96 let success = receipt.inner.inner.inner.receipt.status.coerce_status();
97
98 if shell::is_json() {
99 let _ = sh_println!(
100 "{}",
101 serde_json::json!({
102 "chain": chain,
103 "status": if success {
104 "success"
105 } else {
106 "failed"
107 },
108 "tx_hash": receipt.transaction_hash,
109 "contract_address": receipt.contract_address.map(|addr| addr.to_string()),
110 "block_number": block_number,
111 "gas_used": gas_used,
112 "gas_price": gas_price,
113 })
114 );
115
116 String::new()
117 } else {
118 format!(
119 "\n##### {chain}\n{status} Hash: {tx_hash:?}{contract_address}\nBlock: {block_number}\n{gas}\n\n",
120 status = if success { "✅ [Success]" } else { "❌ [Failed]" },
121 tx_hash = receipt.transaction_hash,
122 contract_address = if let Some(addr) = &receipt.contract_address {
123 format!("\nContract Address: {}", addr.to_checksum(None))
124 } else {
125 String::new()
126 },
127 gas = if gas_price == 0 {
128 format!("Gas Used: {gas_used}")
129 } else {
130 let paid = format_units((gas_used as u128).saturating_mul(gas_price), 18)
131 .unwrap_or_else(|_| "N/A".into());
132 let gas_price =
133 format_units(U256::from(gas_price), 9).unwrap_or_else(|_| "N/A".into());
134 let token_symbol = NamedChain::try_from(chain)
135 .unwrap_or_default()
136 .native_currency_symbol()
137 .unwrap_or("ETH");
138 format!(
139 "Paid: {} {} ({gas_used} gas * {} gwei)",
140 paid.trim_end_matches('0'),
141 token_symbol,
142 gas_price.trim_end_matches('0').trim_end_matches('.')
143 )
144 },
145 )
146 }
147}