snarkos_cli/commands/developer/
mod.rs

1// Copyright (c) 2019-2025 Provable Inc.
2// This file is part of the snarkOS library.
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at:
7
8// http://www.apache.org/licenses/LICENSE-2.0
9
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16mod decrypt;
17pub use decrypt::*;
18
19mod deploy;
20pub use deploy::*;
21
22mod execute;
23pub use execute::*;
24
25mod scan;
26pub use scan::*;
27
28mod transfer_private;
29pub use transfer_private::*;
30
31use crate::helpers::{args::network_id_parser, logger::initialize_terminal_logger};
32
33use snarkos_node_rest::{API_VERSION_V1, API_VERSION_V2};
34use snarkvm::{package::Package, prelude::*};
35
36use anyhow::{Context, Result, anyhow, bail, ensure};
37use clap::{Parser, ValueEnum};
38use colored::Colorize;
39use serde::{Serialize, de::DeserializeOwned};
40use std::{
41    path::PathBuf,
42    str::FromStr,
43    thread,
44    time::{Duration, Instant},
45};
46use ureq::http::{self, Uri};
47
48/// The format to store a generated transaction as.
49#[derive(Copy, Clone, Debug, ValueEnum)]
50pub enum StoreFormat {
51    String,
52    Bytes,
53}
54
55/// Commands to deploy and execute transactions
56#[derive(Debug, Parser)]
57pub enum DeveloperCommand {
58    /// Decrypt a ciphertext.
59    Decrypt(Decrypt),
60    /// Deploy a program.
61    Deploy(Deploy),
62    /// Execute a program function.
63    Execute(Execute),
64    /// Scan the node for records.
65    Scan(Scan),
66    /// Execute the `credits.aleo/transfer_private` function.
67    TransferPrivate(TransferPrivate),
68}
69
70/// Use the Provable explorer's API by default.
71const DEFAULT_ENDPOINT: &str = "https://api.explorer.provable.com";
72
73#[derive(Debug, Parser)]
74pub struct Developer {
75    /// The specific developer command to run.
76    #[clap(subcommand)]
77    command: DeveloperCommand,
78    /// Specify the network to create an execution for.
79    /// [options: 0 = mainnet, 1 = testnet, 2 = canary]
80    #[clap(long, default_value_t=MainnetV0::ID, long, global=true, value_parser = network_id_parser())]
81    network: u16,
82    /// Sets verbosity of log output. By default, no logs are shown.
83    #[clap(long, global = true)]
84    verbosity: Option<u8>,
85}
86
87/// The serialized REST error sent over the network.
88#[derive(Debug, Deserialize)]
89struct RestError {
90    /// The type of error (corresponding to the HTTP status code).
91    error_type: String,
92    /// The top-level error message.
93    message: String,
94    /// The chain of errors that led to the top-level error.
95    /// Default to an empty vector if no error chain was given.
96    #[serde(default)]
97    chain: Vec<String>,
98}
99
100impl RestError {
101    /// Converts a `RestError` into an `anyhow::Error`.
102    pub fn parse(self) -> anyhow::Error {
103        let mut error: Option<anyhow::Error> = None;
104        for next in self.chain.into_iter() {
105            if let Some(previous) = error {
106                error = Some(previous.context(next));
107            } else {
108                error = Some(anyhow!(next));
109            }
110        }
111
112        let toplevel = format!("{}: {}", self.error_type, self.message);
113        if let Some(error) = error { error.context(toplevel) } else { anyhow!(toplevel) }
114    }
115}
116
117impl Developer {
118    /// Runs the developer subcommand chosen by the user.
119    pub fn parse(self) -> Result<String> {
120        if let Some(verbosity) = self.verbosity {
121            initialize_terminal_logger(verbosity).with_context(|| "Failed to initialize terminal logger")?
122        }
123
124        match self.network {
125            MainnetV0::ID => self.parse_inner::<MainnetV0>(),
126            TestnetV0::ID => self.parse_inner::<TestnetV0>(),
127            CanaryV0::ID => self.parse_inner::<CanaryV0>(),
128            unknown_id => bail!("Unknown network ID ({unknown_id})"),
129        }
130    }
131
132    /// Internal logic of [`Self::parse`] for each of the different networks.
133    fn parse_inner<N: Network>(self) -> Result<String> {
134        use DeveloperCommand::*;
135
136        match self.command {
137            Decrypt(decrypt) => decrypt.parse::<N>(),
138            Deploy(deploy) => deploy.parse::<N>(),
139            Execute(execute) => execute.parse::<N>(),
140            Scan(scan) => scan.parse::<N>(),
141            TransferPrivate(transfer_private) => transfer_private.parse::<N>(),
142        }
143    }
144
145    /// Parse the package from the directory.
146    fn parse_package<N: Network>(program_id: ProgramID<N>, path: &Option<String>) -> Result<Package<N>> {
147        // Instantiate a path to the directory containing the manifest file.
148        let directory = match path {
149            Some(path) => PathBuf::from_str(path)?,
150            None => std::env::current_dir()?,
151        };
152
153        // Load the package.
154        let package = Package::open(&directory)?;
155
156        ensure!(
157            package.program_id() == &program_id,
158            "The program name in the package does not match the specified program name"
159        );
160
161        // Return the package.
162        Ok(package)
163    }
164
165    /// Parses the record string. If the string is a ciphertext, then attempt to decrypt it.
166    fn parse_record<N: Network>(private_key: &PrivateKey<N>, record: &str) -> Result<Record<N, Plaintext<N>>> {
167        match record.starts_with("record1") {
168            true => {
169                // Parse the ciphertext.
170                let ciphertext = Record::<N, Ciphertext<N>>::from_str(record)?;
171                // Derive the view key.
172                let view_key = ViewKey::try_from(private_key)?;
173                // Decrypt the ciphertext.
174                ciphertext.decrypt(&view_key)
175            }
176            false => Record::<N, Plaintext<N>>::from_str(record),
177        }
178    }
179
180    /// Builds the full endpoint Uri from the base and path. Used internally for all REST API calls (copied from `snarkvm_ledger_query::Query`).
181    /// This will add the API version number and network name to the resulting endpoint URL.
182    ///
183    /// # Arguments
184    ///  - `base_url`: the hostname (and path prefix) of the node to query. this must exclude the network name.
185    ///  - `route`: the route to the endpoint (e.g., `stateRoot/latest`). This cannot start with a slash.
186    fn build_endpoint<N: Network>(base_url: &http::Uri, route: &str) -> Result<String> {
187        // This function is only called internally but check for additional sanity.
188        ensure!(!route.starts_with('/'), "path cannot start with a slash");
189
190        // If the route already ends with a version segment (v1 or v2), don't prepend a version.
191        let route_has_version_suffix = {
192            let r = base_url.path().trim_end_matches('/');
193            r.ends_with(API_VERSION_V1) || r.ends_with(API_VERSION_V2)
194        };
195
196        // Work around a bug in the `http` crate where empty paths will be set to '/'
197        // but other paths are not appended with a slash.
198        // See https://github.com/hyperium/http/issues/507
199        let sep = if base_url.path().ends_with('/') { "" } else { "/" };
200
201        // Build "{base}/{maybe_version}/{network}/{route}"
202        let prefix = if route_has_version_suffix {
203            format!("{}/", N::SHORT_NAME)
204        } else {
205            format!("{}/{}/", API_VERSION_V2, N::SHORT_NAME)
206        };
207
208        Ok(format!("{base_url}{sep}{prefix}{route}"))
209    }
210
211    /// Converts the returned JSON error (if any) into an anyhow Error chain.
212    /// If the error was 404, this simply returns `Ok(None)`.
213    fn handle_ureq_result(result: Result<http::Response<ureq::Body>>) -> Result<Option<ureq::Body>> {
214        let response = result?;
215
216        if response.status().is_success() {
217            Ok(Some(response.into_body()))
218        } else if response.status() == http::StatusCode::NOT_FOUND {
219            Ok(None)
220        } else {
221            let rest_error: RestError =
222                response.into_body().read_json().with_context(|| "Failed to parse error JSON")?;
223
224            Err(rest_error.parse())
225        }
226    }
227
228    /// Helper function to send a POST request with a JSON body to an endpoint and await a JSON response.
229    fn http_post_json<I: Serialize, O: DeserializeOwned>(path: &str, arg: &I) -> Result<Option<O>> {
230        let result =
231            ureq::post(path).config().http_status_as_error(false).build().send_json(arg).map_err(|err| err.into());
232
233        match Self::handle_ureq_result(result).with_context(|| "HTTP POST request failed")? {
234            Some(mut body) => {
235                let json = body.read_json().with_context(|| "Failed to parse JSON response")?;
236                Ok(Some(json))
237            }
238            None => Ok(None),
239        }
240    }
241
242    /// Helper function to send a GET request to an endpoint and await a JSON response.
243    fn http_get_json<N: Network, O: DeserializeOwned>(base_url: &http::Uri, route: &str) -> Result<Option<O>> {
244        let endpoint = Self::build_endpoint::<N>(base_url, route)?;
245        let result = ureq::get(&endpoint).config().http_status_as_error(false).build().call().map_err(|err| err.into());
246
247        match Self::handle_ureq_result(result).with_context(|| "HTTP GET request failed")? {
248            Some(mut body) => {
249                let json = body.read_json().with_context(|| "Failed to parse JSON response")?;
250                Ok(Some(json))
251            }
252            None => Ok(None),
253        }
254    }
255
256    /// Helper function to send a GET request to an endpoint and await the response.
257    fn http_get<N: Network>(base_url: &http::Uri, route: &str) -> Result<Option<ureq::Body>> {
258        let endpoint = Self::build_endpoint::<N>(base_url, route)?;
259        let result = ureq::get(&endpoint).config().http_status_as_error(false).build().call().map_err(|err| err.into());
260
261        Self::handle_ureq_result(result).with_context(|| "HTTP GET request failed")
262    }
263
264    /// Wait for a transaction to be confirmed by the network.
265    fn wait_for_transaction_confirmation<N: Network>(
266        endpoint: &Uri,
267        transaction_id: &N::TransactionID,
268        timeout_seconds: u64,
269    ) -> Result<()> {
270        let start_time = Instant::now();
271        let timeout_duration = Duration::from_secs(timeout_seconds);
272        let poll_interval = Duration::from_secs(1); // Poll every second
273
274        while start_time.elapsed() < timeout_duration {
275            // Check if transaction exists in a confirmed block
276            let result = Self::http_get::<N>(endpoint, &format!("transaction/{transaction_id}"))
277                .with_context(|| "Failed to check transaction status")?;
278
279            match result {
280                Some(_) => return Ok(()),
281                None => {
282                    // Transaction not found yet, continue polling.
283                    thread::sleep(poll_interval);
284                }
285            }
286        }
287
288        // Timeout reached
289        bail!("❌ Transaction {transaction_id} was not confirmed within {timeout_seconds} seconds");
290    }
291
292    /// Gets the latest eidtion of an Aleo program.
293    fn get_latest_edition<N: Network>(endpoint: &Uri, program_id: &ProgramID<N>) -> Result<u16> {
294        match Self::http_get_json::<N, _>(endpoint, &format!("program/{program_id}/latest_edition"))? {
295            Some(edition) => Ok(edition),
296            None => bail!("Got unexpected 404 response"),
297        }
298    }
299
300    /// Gets the public account balance of an Aleo Address (in microcredits).
301    fn get_public_balance<N: Network>(endpoint: &Uri, address: &Address<N>) -> Result<u64> {
302        // Initialize the program id and account identifier.
303        let account_mapping = Identifier::<N>::from_str("account")?;
304        let credits = ProgramID::<N>::from_str("credits.aleo")?;
305
306        // Send a request to the query node.
307        let result: Option<Value<N>> =
308            Self::http_get_json::<N, _>(endpoint, &format!("program/{credits}/mapping/{account_mapping}/{address}"))?;
309
310        // Return the balance in microcredits.
311        match result {
312            Some(Value::Plaintext(Plaintext::Literal(Literal::<N>::U64(amount), _))) => Ok(*amount),
313            Some(..) => bail!("Failed to deserialize balance for {address}"),
314            None => Ok(0),
315        }
316    }
317
318    /// Determine if the transaction should be broadcast or displayed to user.
319    ///
320    /// This function expects that exactly one of `dry_run`, `store`, and `broadcast` are `true` (or `Some`).
321    /// `broadcast` can be set to `Some(None)` to broadcast using the default endpoint.
322    /// Alternatively, it can be set to `Some(Some(url))` to providifferent
323    /// endpoint than that used for querying.
324    #[allow(clippy::too_many_arguments)]
325    fn handle_transaction<N: Network>(
326        endpoint: &Uri,
327        broadcast: &Option<Option<Uri>>,
328        dry_run: bool,
329        store: &Option<String>,
330        store_format: StoreFormat,
331        wait: bool,
332        timeout: u64,
333        transaction: Transaction<N>,
334        operation: String,
335    ) -> Result<String> {
336        // Get the transaction id.
337        let transaction_id = transaction.id();
338
339        // Ensure the transaction is not a fee transaction.
340        ensure!(!transaction.is_fee(), "The transaction is a fee transaction and cannot be broadcast");
341
342        // Determine if the transaction should be stored.
343        if let Some(path) = store {
344            match PathBuf::from_str(path) {
345                Ok(file_path) => {
346                    match store_format {
347                        StoreFormat::Bytes => {
348                            let transaction_bytes = transaction.to_bytes_le()?;
349                            std::fs::write(&file_path, transaction_bytes)?;
350                        }
351                        StoreFormat::String => {
352                            let transaction_string = transaction.to_string();
353                            std::fs::write(&file_path, transaction_string)?;
354                        }
355                    }
356
357                    println!(
358                        "Transaction {transaction_id} was stored to {} as {:?}",
359                        file_path.display(),
360                        store_format
361                    );
362                }
363                Err(err) => {
364                    println!("The transaction was unable to be stored due to: {err}");
365                }
366            }
367        };
368
369        // Determine if the transaction should be broadcast to the network.
370        if let Some(broadcast_value) = broadcast {
371            let broadcast_endpoint = if let Some(url) = broadcast_value {
372                url.to_string()
373            } else {
374                Self::build_endpoint::<N>(endpoint, "transaction/broadcast")?
375            };
376
377            let result: Result<String> = match Self::http_post_json(&broadcast_endpoint, &transaction) {
378                Ok(Some(s)) => Ok(s),
379                Ok(None) => Err(anyhow!("Got unexpected 404 error")),
380                Err(err) => Err(err),
381            };
382
383            match result {
384                Ok(response_string) => {
385                    ensure!(
386                        response_string == transaction_id.to_string(),
387                        "The response does not match the transaction id. ({response_string} != {transaction_id})"
388                    );
389
390                    match transaction {
391                        Transaction::Deploy(..) => {
392                            println!(
393                                "⌛ Deployment {transaction_id} ('{}') has been broadcast to {}.",
394                                operation.bold(),
395                                broadcast_endpoint
396                            )
397                        }
398                        Transaction::Execute(..) => {
399                            println!(
400                                "⌛ Execution {transaction_id} ('{}') has been broadcast to {}.",
401                                operation.bold(),
402                                broadcast_endpoint
403                            )
404                        }
405                        _ => unreachable!(),
406                    }
407
408                    // If wait is enabled, wait for transaction confirmation
409                    if wait {
410                        println!("⏳ Waiting for transaction confirmation (timeout: {timeout}s)...");
411                        Self::wait_for_transaction_confirmation::<N>(endpoint, &transaction_id, timeout)?;
412
413                        match transaction {
414                            Transaction::Deploy(..) => {
415                                println!("✅ Deployment {transaction_id} ('{}') confirmed!", operation.bold())
416                            }
417                            Transaction::Execute(..) => {
418                                println!("✅ Execution {transaction_id} ('{}') confirmed!", operation.bold())
419                            }
420                            Transaction::Fee(..) => unreachable!(),
421                        }
422                    }
423                }
424                Err(error) => match transaction {
425                    Transaction::Deploy(..) => {
426                        return Err(error.context(anyhow!(
427                            "Failed to deploy '{op}' to {broadcast_endpoint}",
428                            op = operation.bold()
429                        )));
430                    }
431                    Transaction::Execute(..) => {
432                        return Err(error.context(anyhow!(
433                            "Failed to broadcast execution '{op}' to {broadcast_endpoint}",
434                            op = operation.bold()
435                        )));
436                    }
437                    Transaction::Fee(..) => unreachable!(),
438                },
439            };
440
441            // Output the transaction id.
442            Ok(transaction_id.to_string())
443        } else if dry_run {
444            // Output the transaction string.
445            Ok(transaction.to_string())
446        } else {
447            Ok("".to_string())
448        }
449    }
450}