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 snarkvm::{
32    console::network::Network,
33    package::Package,
34    prelude::{
35        Address,
36        Ciphertext,
37        Identifier,
38        Literal,
39        Plaintext,
40        PrivateKey,
41        Program,
42        ProgramID,
43        Record,
44        ToBytes,
45        Value,
46        ViewKey,
47        block::Transaction,
48    },
49};
50
51use anyhow::{Result, bail, ensure};
52use clap::Parser;
53use colored::Colorize;
54use std::{path::PathBuf, str::FromStr};
55
56/// Commands to deploy and execute transactions
57#[derive(Debug, Parser)]
58pub enum Developer {
59    /// Decrypt a ciphertext.
60    Decrypt(Decrypt),
61    /// Deploy a program.
62    Deploy(Deploy),
63    /// Execute a program function.
64    Execute(Execute),
65    /// Scan the node for records.
66    Scan(Scan),
67    /// Execute the `credits.aleo/transfer_private` function.
68    TransferPrivate(TransferPrivate),
69}
70
71impl Developer {
72    pub fn parse(self) -> Result<String> {
73        match self {
74            Self::Decrypt(decrypt) => decrypt.parse(),
75            Self::Deploy(deploy) => deploy.parse(),
76            Self::Execute(execute) => execute.parse(),
77            Self::Scan(scan) => scan.parse(),
78            Self::TransferPrivate(transfer_private) => transfer_private.parse(),
79        }
80    }
81
82    /// Parse the package from the directory.
83    fn parse_package<N: Network>(program_id: ProgramID<N>, path: &Option<String>) -> Result<Package<N>> {
84        // Instantiate a path to the directory containing the manifest file.
85        let directory = match path {
86            Some(path) => PathBuf::from_str(path)?,
87            None => std::env::current_dir()?,
88        };
89
90        // Load the package.
91        let package = Package::open(&directory)?;
92
93        ensure!(
94            package.program_id() == &program_id,
95            "The program name in the package does not match the specified program name"
96        );
97
98        // Return the package.
99        Ok(package)
100    }
101
102    /// Parses the record string. If the string is a ciphertext, then attempt to decrypt it.
103    fn parse_record<N: Network>(private_key: &PrivateKey<N>, record: &str) -> Result<Record<N, Plaintext<N>>> {
104        match record.starts_with("record1") {
105            true => {
106                // Parse the ciphertext.
107                let ciphertext = Record::<N, Ciphertext<N>>::from_str(record)?;
108                // Derive the view key.
109                let view_key = ViewKey::try_from(private_key)?;
110                // Decrypt the ciphertext.
111                ciphertext.decrypt(&view_key)
112            }
113            false => Record::<N, Plaintext<N>>::from_str(record),
114        }
115    }
116
117    /// Fetch the program from the given endpoint.
118    fn fetch_program<N: Network>(program_id: &ProgramID<N>, endpoint: &str) -> Result<Program<N>> {
119        // Get the network being used.
120        let network = match N::ID {
121            snarkvm::console::network::MainnetV0::ID => "mainnet",
122            snarkvm::console::network::TestnetV0::ID => "testnet",
123            snarkvm::console::network::CanaryV0::ID => "canary",
124            unknown_id => bail!("Unknown network ID ({unknown_id})"),
125        };
126
127        // Send a request to the query node.
128        let response = ureq::get(&format!("{endpoint}/{network}/program/{program_id}")).call();
129
130        // Deserialize the program.
131        match response {
132            Ok(response) => response.into_json().map_err(|err| err.into()),
133            Err(err) => match err {
134                ureq::Error::Status(_status, response) => {
135                    // Debug formatting displays more useful info, especially if the response body is empty.
136                    bail!("Failed to fetch program {program_id}: {response:?}")
137                }
138                err => bail!(err),
139            },
140        }
141    }
142
143    /// Fetch the public balance in microcredits associated with the address from the given endpoint.
144    fn get_public_balance<N: Network>(address: &Address<N>, endpoint: &str) -> Result<u64> {
145        // Initialize the program id and account identifier.
146        let credits = ProgramID::<N>::from_str("credits.aleo")?;
147        let account_mapping = Identifier::<N>::from_str("account")?;
148
149        // Get the network being used.
150        let network = match N::ID {
151            snarkvm::console::network::MainnetV0::ID => "mainnet",
152            snarkvm::console::network::TestnetV0::ID => "testnet",
153            snarkvm::console::network::CanaryV0::ID => "canary",
154            unknown_id => bail!("Unknown network ID ({unknown_id})"),
155        };
156
157        // Send a request to the query node.
158        let response =
159            ureq::get(&format!("{endpoint}/{network}/program/{credits}/mapping/{account_mapping}/{address}")).call();
160
161        // Deserialize the balance.
162        let balance: Result<Option<Value<N>>> = match response {
163            Ok(response) => response.into_json().map_err(|err| err.into()),
164            Err(err) => match err {
165                ureq::Error::Status(_status, response) => {
166                    bail!(response.into_string().unwrap_or("Response too large!".to_owned()))
167                }
168                err => bail!(err),
169            },
170        };
171
172        // Return the balance in microcredits.
173        match balance {
174            Ok(Some(Value::Plaintext(Plaintext::Literal(Literal::<N>::U64(amount), _)))) => Ok(*amount),
175            Ok(None) => Ok(0),
176            Ok(Some(..)) => bail!("Failed to deserialize balance for {address}"),
177            Err(err) => bail!("Failed to fetch balance for {address}: {err}"),
178        }
179    }
180
181    /// Determine if the transaction should be broadcast or displayed to user.
182    fn handle_transaction<N: Network>(
183        broadcast: &Option<String>,
184        dry_run: bool,
185        store: &Option<String>,
186        transaction: Transaction<N>,
187        operation: String,
188    ) -> Result<String> {
189        // Get the transaction id.
190        let transaction_id = transaction.id();
191
192        // Ensure the transaction is not a fee transaction.
193        ensure!(!transaction.is_fee(), "The transaction is a fee transaction and cannot be broadcast");
194
195        // Determine if the transaction should be stored.
196        if let Some(path) = store {
197            match PathBuf::from_str(path) {
198                Ok(file_path) => {
199                    let transaction_bytes = transaction.to_bytes_le()?;
200                    std::fs::write(&file_path, transaction_bytes)?;
201                    println!("Transaction {transaction_id} was stored to {}", file_path.display());
202                }
203                Err(err) => {
204                    println!("The transaction was unable to be stored due to: {err}");
205                }
206            }
207        };
208
209        // Determine if the transaction should be broadcast to the network.
210        if let Some(endpoint) = broadcast {
211            // Send the deployment request to the local development node.
212            match ureq::post(endpoint).send_json(&transaction) {
213                Ok(id) => {
214                    // Remove the quotes from the response.
215                    let response_string = id.into_string()?.trim_matches('\"').to_string();
216                    ensure!(
217                        response_string == transaction_id.to_string(),
218                        "The response does not match the transaction id. ({response_string} != {transaction_id})"
219                    );
220
221                    match transaction {
222                        Transaction::Deploy(..) => {
223                            println!(
224                                "⌛ Deployment {transaction_id} ('{}') has been broadcast to {}.",
225                                operation.bold(),
226                                endpoint
227                            )
228                        }
229                        Transaction::Execute(..) => {
230                            println!(
231                                "⌛ Execution {transaction_id} ('{}') has been broadcast to {}.",
232                                operation.bold(),
233                                endpoint
234                            )
235                        }
236                        Transaction::Fee(..) => {
237                            println!("❌ Failed to broadcast fee '{}' to the {}.", operation.bold(), endpoint)
238                        }
239                    }
240                }
241                Err(error) => {
242                    let error_message = match error {
243                        ureq::Error::Status(code, response) => {
244                            format!("(status code {code}: {:?})", response.into_string()?)
245                        }
246                        ureq::Error::Transport(err) => format!("({err})"),
247                    };
248
249                    match transaction {
250                        Transaction::Deploy(..) => {
251                            bail!("❌ Failed to deploy '{}' to {}: {}", operation.bold(), &endpoint, error_message)
252                        }
253                        Transaction::Execute(..) => {
254                            bail!(
255                                "❌ Failed to broadcast execution '{}' to {}: {}",
256                                operation.bold(),
257                                &endpoint,
258                                error_message
259                            )
260                        }
261                        Transaction::Fee(..) => {
262                            bail!(
263                                "❌ Failed to broadcast fee '{}' to {}: {}",
264                                operation.bold(),
265                                &endpoint,
266                                error_message
267                            )
268                        }
269                    }
270                }
271            };
272
273            // Output the transaction id.
274            Ok(transaction_id.to_string())
275        } else if dry_run {
276            // Output the transaction string.
277            Ok(transaction.to_string())
278        } else {
279            Ok("".to_string())
280        }
281    }
282}