Skip to main content

leo_lang/cli/helpers/
check_transaction.rs

1// Copyright (C) 2019-2026 Provable Inc.
2// This file is part of the Leo library.
3
4// The Leo library is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// The Leo library is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
16
17use leo_ast::NetworkName;
18use leo_errors::Result;
19
20use anyhow::anyhow;
21use serde::Deserialize;
22
23#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize)]
24pub enum TransactionStatus {
25    #[serde(rename = "accepted")]
26    Accepted,
27    #[serde(rename = "aborted")]
28    Aborted,
29    #[serde(rename = "rejected")]
30    Rejected,
31}
32
33#[derive(Debug, Deserialize)]
34struct Transaction {
35    id: String,
36}
37
38#[derive(Debug, Deserialize)]
39struct TransactionResult {
40    status: TransactionStatus,
41
42    transaction: Transaction,
43}
44
45#[derive(Debug, Deserialize)]
46struct Block {
47    transactions: Vec<TransactionResult>,
48    aborted_transaction_ids: Vec<String>,
49}
50
51#[derive(Debug, Deserialize)]
52struct Transition {
53    id: String,
54}
55
56#[derive(Debug, Deserialize)]
57struct Fee {
58    transition: Transition,
59}
60
61#[derive(Debug, Deserialize)]
62struct RejectedTransaction {
63    fee: Option<Fee>,
64}
65
66pub fn current_height(endpoint: &str, network: NetworkName, network_retries: u32) -> Result<usize> {
67    let height_url = format!("{endpoint}/{network}/block/height/latest");
68    let height_str = leo_package::fetch_from_network_plain(&height_url, network_retries)?;
69    let height: usize = height_str.parse().map_err(|e| anyhow!("error parsing height: {e}"))?;
70    Ok(height)
71}
72
73fn status_at_height(
74    id: &str,
75    maybe_fee_id: Option<&str>,
76    endpoint: &str,
77    network: NetworkName,
78    height: usize,
79    max_wait: usize,
80    network_retries: u32,
81) -> Result<Option<TransactionStatus>> {
82    // Wait until the block at `height` exists.
83    // Use a fixed retry count of 1 here: this loop polls every second, and
84    // applying the user-supplied `network_retries` backoff per iteration could
85    // stall the wait loop for many seconds per poll with large retry values.
86    for i in 0usize.. {
87        if current_height(endpoint, network, 1)? >= height {
88            break;
89        } else if i >= max_wait {
90            // We've waited too long; give up.
91            return Ok(None);
92        } else {
93            std::thread::sleep(std::time::Duration::from_secs(1));
94        }
95    }
96
97    let block_url = format!("{endpoint}/{network}/block/{height}");
98    let block_str = leo_package::fetch_from_network_plain(&block_url, network_retries)?;
99    let block: Block = serde_json::from_str(&block_str).map_err(|e| anyhow!("Deserialization failure X: {e}."))?;
100    let maybe_this_transaction =
101        block.transactions.iter().find(|transaction_result| transaction_result.transaction.id == id);
102
103    if let Some(transaction_result) = maybe_this_transaction {
104        // We found it.
105        return Ok(Some(transaction_result.status));
106    }
107
108    if block.aborted_transaction_ids.iter().any(|aborted_id| aborted_id == id) {
109        // It was aborted.
110        return Ok(Some(TransactionStatus::Aborted));
111    }
112
113    for rejected in &block.transactions {
114        if rejected.status != TransactionStatus::Rejected {
115            continue;
116        }
117
118        let url = format!("{endpoint}/{network}/transaction/unconfirmed/{}", rejected.transaction.id);
119        let transaction_str = leo_package::fetch_from_network_plain(&url, network_retries)?;
120        let transaction: RejectedTransaction =
121            serde_json::from_str(&transaction_str).map_err(|e| anyhow!("Deserialization failure: {e}"))?;
122        // It's actually the fee that will show up as rejected.
123        if transaction.fee.map(|fee| fee.transition.id).as_deref() == maybe_fee_id {
124            // It was rejected.
125            return Ok(Some(TransactionStatus::Rejected));
126        }
127    }
128
129    Ok(None)
130}
131
132struct CheckedTransaction {
133    blocks_checked: usize,
134    status: Option<TransactionStatus>,
135}
136
137#[allow(clippy::too_many_arguments)]
138fn check_transaction(
139    id: &str,
140    maybe_fee_id: Option<&str>,
141    endpoint: &str,
142    network: NetworkName,
143    start_height: usize,
144    max_wait: usize,
145    blocks_to_check: usize,
146    network_retries: u32,
147) -> Result<CheckedTransaction> {
148    // It appears that the default rate limit for snarkOS is 10 requests per second per IP,
149    // and this value seems to avoid rate limits in practice, so let's go with this.
150    const DELAY_MILLIS: u64 = 201;
151
152    for use_height in start_height..start_height + blocks_to_check {
153        let status = status_at_height(id, maybe_fee_id, endpoint, network, use_height, max_wait, network_retries)?;
154        if status.is_some() {
155            return Ok(CheckedTransaction { blocks_checked: use_height - start_height + 1, status });
156        }
157
158        // Avoid rate limits.
159        std::thread::sleep(std::time::Duration::from_millis(DELAY_MILLIS));
160    }
161
162    Ok(CheckedTransaction { blocks_checked: blocks_to_check, status: None })
163}
164
165/// Check to find the transaction id among new blocks, printing its status (if found)
166/// to the user. Returns `Some(..)` if and only if the transaction was found.
167#[allow(clippy::too_many_arguments)]
168pub fn check_transaction_with_message(
169    id: &str,
170    maybe_fee_id: Option<&str>,
171    endpoint: &str,
172    network: NetworkName,
173    start_height: usize,
174    max_wait: usize,
175    blocks_to_check: usize,
176    network_retries: u32,
177) -> Result<Option<TransactionStatus>> {
178    println!("🔄 Searching up to {blocks_to_check} blocks to confirm transaction (this may take several seconds)...");
179    let checked = crate::cli::check_transaction::check_transaction(
180        id,
181        maybe_fee_id,
182        endpoint,
183        network,
184        start_height,
185        max_wait,
186        blocks_to_check,
187        network_retries,
188    )?;
189    println!("Explored {} blocks.", checked.blocks_checked);
190    match checked.status {
191        Some(TransactionStatus::Accepted) => println!("Transaction accepted."),
192        Some(TransactionStatus::Rejected) => println!("Transaction rejected."),
193        Some(TransactionStatus::Aborted) => println!("Transaction aborted."),
194        None => println!("Could not find the transaction."),
195    }
196    Ok(checked.status)
197}