leo_lang/cli/helpers/
check_transaction.rs

1// Copyright (C) 2019-2025 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_errors::Result;
18use leo_package::NetworkName;
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) -> Result<usize> {
67    let height_url = format!("{endpoint}/{network}/block/height/latest");
68    let height_str = leo_package::fetch_from_network_plain(&height_url)?;
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) -> Result<Option<TransactionStatus>> {
81    // Wait until the block at `height` exists.
82    for i in 0usize.. {
83        if current_height(endpoint, network)? >= height {
84            break;
85        } else if i >= max_wait {
86            // We've waited too long; give up.
87            return Ok(None);
88        } else {
89            std::thread::sleep(std::time::Duration::from_secs(1));
90        }
91    }
92
93    let block_url = format!("{endpoint}/{network}/block/{height}");
94    let block_str = leo_package::fetch_from_network_plain(&block_url)?;
95    let block: Block = serde_json::from_str(&block_str).map_err(|e| anyhow!("Deserialization failure X: {e}."))?;
96    let maybe_this_transaction =
97        block.transactions.iter().find(|transaction_result| transaction_result.transaction.id == id);
98
99    if let Some(transaction_result) = maybe_this_transaction {
100        // We found it.
101        return Ok(Some(transaction_result.status));
102    }
103
104    if block.aborted_transaction_ids.iter().any(|aborted_id| aborted_id == id) {
105        // It was aborted.
106        return Ok(Some(TransactionStatus::Aborted));
107    }
108
109    for rejected in &block.transactions {
110        if rejected.status != TransactionStatus::Rejected {
111            continue;
112        }
113
114        let url = format!("{endpoint}/{network}/transaction/unconfirmed/{}", rejected.transaction.id);
115        let transaction_str = leo_package::fetch_from_network_plain(&url)?;
116        let transaction: RejectedTransaction =
117            serde_json::from_str(&transaction_str).map_err(|e| anyhow!("Deserialization failure: {e}"))?;
118        // It's actually the fee that will show up as rejected.
119        if transaction.fee.map(|fee| fee.transition.id).as_deref() == maybe_fee_id {
120            // It was rejected.
121            return Ok(Some(TransactionStatus::Rejected));
122        }
123    }
124
125    Ok(None)
126}
127
128struct CheckedTransaction {
129    blocks_checked: usize,
130    status: Option<TransactionStatus>,
131}
132
133fn check_transaction(
134    id: &str,
135    maybe_fee_id: Option<&str>,
136    endpoint: &str,
137    network: NetworkName,
138    start_height: usize,
139    max_wait: usize,
140    blocks_to_check: usize,
141) -> Result<CheckedTransaction> {
142    // It appears that the default rate limit for snarkOS is 10 requests per second per IP,
143    // and this value seems to avoid rate limits in practice, so let's go with this.
144    const DELAY_MILLIS: u64 = 101;
145
146    for use_height in start_height..start_height + blocks_to_check {
147        let status = status_at_height(id, maybe_fee_id, endpoint, network, use_height, max_wait)?;
148        if status.is_some() {
149            return Ok(CheckedTransaction { blocks_checked: use_height - start_height + 1, status });
150        }
151
152        // Avoid rate limits.
153        std::thread::sleep(std::time::Duration::from_millis(DELAY_MILLIS));
154    }
155
156    Ok(CheckedTransaction { blocks_checked: blocks_to_check, status: None })
157}
158
159/// Check to find the transaction id among new blocks, printing its status (if found)
160/// to the user. Returns `Some(..)` if and only if the transaction was found.
161pub fn check_transaction_with_message(
162    id: &str,
163    maybe_fee_id: Option<&str>,
164    endpoint: &str,
165    network: NetworkName,
166    start_height: usize,
167    max_wait: usize,
168    blocks_to_check: usize,
169) -> Result<Option<TransactionStatus>> {
170    println!("Searching up to {blocks_to_check} blocks to find transaction (this may take several seconds)...");
171    let checked = crate::cli::check_transaction::check_transaction(
172        id,
173        maybe_fee_id,
174        endpoint,
175        network,
176        start_height,
177        max_wait,
178        blocks_to_check,
179    )?;
180    println!("Explored {} blocks.", checked.blocks_checked);
181    match checked.status {
182        Some(TransactionStatus::Accepted) => println!("Transaction accepted."),
183        Some(TransactionStatus::Rejected) => println!("Transaction rejected."),
184        Some(TransactionStatus::Aborted) => println!("Transaction aborted."),
185        None => println!("Couldn't find the transaction."),
186    }
187    Ok(checked.status)
188}