leo_lang/cli/commands/common/
query.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 super::*;
18use crate::cli::query::{LeoBlock, LeoProgram};
19
20use leo_ast::NetworkName;
21use leo_package::ProgramData;
22use leo_span::Symbol;
23use snarkvm::prelude::{Program, ProgramID};
24
25use indexmap::IndexSet;
26use std::collections::HashMap;
27
28/// A helper function to query the public balance of an address.
29pub fn get_public_balance<N: Network>(
30    private_key: &PrivateKey<N>,
31    endpoint: &str,
32    network: NetworkName,
33    context: &Context,
34) -> Result<u64> {
35    // Derive the account address.
36    let address = Address::<N>::try_from(ViewKey::try_from(private_key)?)?;
37    // Query the public balance of the address on the `account` mapping from `credits.aleo`.
38    let mut public_balance = LeoQuery {
39        env_override: EnvOptions { endpoint: Some(endpoint.to_string()), network: Some(network), ..Default::default() },
40        command: QueryCommands::Program {
41            command: LeoProgram {
42                name: "credits".to_string(),
43                edition: None,
44                mappings: false,
45                mapping_value: Some(vec!["account".to_string(), address.to_string()]),
46            },
47        },
48    }
49    .execute(Context::new(context.path.clone(), context.home.clone(), true)?)?;
50    // Remove the last 3 characters since they represent the `u64` suffix.
51    public_balance.truncate(public_balance.len() - 3);
52    // Make sure the balance is valid.
53    public_balance.parse::<u64>().map_err(|_| CliError::invalid_balance(address).into())
54}
55
56// A helper function to query for the latest block height.
57#[allow(dead_code)]
58pub fn get_latest_block_height(endpoint: &str, network: NetworkName, context: &Context) -> Result<u32> {
59    // Query the latest block height.
60    let height = LeoQuery {
61        env_override: EnvOptions { endpoint: Some(endpoint.to_string()), network: Some(network), ..Default::default() },
62        command: QueryCommands::Block {
63            command: LeoBlock {
64                id: None,
65                latest: false,
66                latest_hash: false,
67                latest_height: true,
68                range: None,
69                transactions: false,
70                to_height: false,
71            },
72        },
73    }
74    .execute(Context::new(context.path.clone(), context.home.clone(), true)?)?;
75    // Parse the height.
76    let height = height.parse::<u32>().map_err(CliError::string_parse_error)?;
77    Ok(height)
78}
79
80/// Determine if the transaction should be broadcast or displayed to user.
81///
82/// Returns (body, status code).
83pub fn handle_broadcast<N: Network>(
84    endpoint: &str,
85    transaction: &Transaction<N>,
86    operation: &str,
87) -> Result<(String, u16)> {
88    // Send the deployment request to the endpoint.
89    let mut response = ureq::Agent::config_builder()
90        .max_redirects(0)
91        .build()
92        .new_agent()
93        .post(endpoint)
94        .query("check_transaction", "true")
95        .header("X-Leo-Version", env!("CARGO_PKG_VERSION"))
96        .send_json(transaction)
97        .map_err(|err| CliError::broadcast_error(err.to_string()))?;
98    match response.status().as_u16() {
99        200..=299 => {
100            println!(
101                "✉️ Broadcasted transaction with:\n  - transaction ID: '{}'",
102                transaction.id().to_string().bold().yellow(),
103            );
104            if let Some(fee) = transaction.fee_transition() {
105                // Most transactions will have fees, but some, like credits.aleo/upgrade executions, may not.
106                println!("  - fee ID: '{}'", fee.id().to_string().bold().yellow());
107                // Print the fee as a transaction.
108                println!("  - fee transaction ID: '{}'", Transaction::from_fee(fee)?.id().to_string().bold().yellow());
109                println!("    (use this to check for rejected transactions)\n");
110            }
111            Ok((response.body_mut().read_to_string().unwrap(), response.status().as_u16()))
112        }
113        301 => {
114            let msg = format!(
115                "⚠️ The endpoint `{endpoint}` has been permanently moved. Try using `https://api.explorer.provable.com/v1` in your `.env` file or via the `--endpoint` flag."
116            );
117            Err(CliError::broadcast_error(msg).into())
118        }
119        _ => {
120            let code = response.status();
121            let error_message = match response.body_mut().read_to_string() {
122                Ok(response) => format!("(status code {code}: {response:?})"),
123                Err(err) => format!("({err})"),
124            };
125
126            let msg = match transaction {
127                Transaction::Deploy(..) => {
128                    format!("❌ Failed to deploy '{}' to {}: {}", operation.bold(), &endpoint, error_message)
129                }
130                Transaction::Execute(..) => {
131                    format!(
132                        "❌ Failed to broadcast execution '{}' to {}: {}",
133                        operation.bold(),
134                        &endpoint,
135                        error_message
136                    )
137                }
138                Transaction::Fee(..) => {
139                    format!("❌ Failed to broadcast fee '{}' to {}: {}", operation.bold(), &endpoint, error_message)
140                }
141            };
142
143            Err(CliError::broadcast_error(msg).into())
144        }
145    }
146}
147
148/// Loads the latest edition of a program and all its imports from the network, using an iterative DFS.
149pub fn load_latest_programs_from_network<N: Network>(
150    context: &Context,
151    program_id: ProgramID<N>,
152    network: NetworkName,
153    endpoint: &str,
154) -> Result<Vec<(Program<N>, Option<u16>)>> {
155    use snarkvm::prelude::Program;
156    use std::collections::HashSet;
157
158    // A cache for loaded programs, mapping a program ID to its bytecode and edition.
159    let mut programs = HashMap::new();
160    // The ordered set of programs.
161    let mut ordered_programs = IndexSet::new();
162    // Stack of program IDs to process (DFS traversal).
163    let mut stack = vec![(program_id, false)];
164
165    // Loop until all programs and their dependencies are visited.
166    while let Some((current_id, seen)) = stack.pop() {
167        // If the program has already been seen, then all its imports have been processed.
168        // Add it to the ordered set and continue.
169        if seen {
170            ordered_programs.insert(current_id);
171        }
172        // Otherwise, fetch it and schedule its imports for processing.
173        else {
174            // If the program is already in the cache, skip it.
175            if programs.contains_key(&current_id) {
176                continue;
177            }
178            // Fetch the program source from the network.
179            let program = leo_package::Program::fetch(
180                Symbol::intern(&current_id.name().to_string()),
181                None,
182                &context.home()?,
183                network,
184                endpoint,
185                true,
186            )
187            .map_err(|_| CliError::custom(format!("Failed to fetch program source for ID: {current_id}")))?;
188            let ProgramData::Bytecode(program_src) = program.data else {
189                panic!("Expected bytecode when fetching a remote program");
190            };
191
192            // Parse the program source into a Program object.
193            let bytecode = Program::<N>::from_str(&program_src)
194                .map_err(|_| CliError::custom(format!("Failed to parse program source for ID: {current_id}")))?;
195
196            // Get the imports of the program.
197            let imports = bytecode.imports().keys().cloned().collect::<HashSet<_>>();
198
199            // Add the program to the cache.
200            programs.insert(current_id, (bytecode, program.edition));
201
202            // Mark the program as seen.
203            stack.push((current_id, true));
204
205            // Queue all imported programs for processing.
206            for import_id in imports {
207                stack.push((import_id, false));
208            }
209        }
210    }
211
212    // Return all loaded programs in insertion order.
213    Ok(ordered_programs
214        .iter()
215        .map(|program_id| programs.remove(program_id).expect("Program not found in cache"))
216        .collect())
217}