snarkos_cli/commands/developer/
mod.rs1mod 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#[derive(Copy, Clone, Debug, ValueEnum)]
50pub enum StoreFormat {
51 String,
52 Bytes,
53}
54
55#[derive(Debug, Parser)]
57pub enum DeveloperCommand {
58 Decrypt(Decrypt),
60 Deploy(Deploy),
62 Execute(Execute),
64 Scan(Scan),
66 TransferPrivate(TransferPrivate),
68}
69
70const DEFAULT_ENDPOINT: &str = "https://api.explorer.provable.com";
72
73#[derive(Debug, Parser)]
74pub struct Developer {
75 #[clap(subcommand)]
77 command: DeveloperCommand,
78 #[clap(long, default_value_t=MainnetV0::ID, long, global=true, value_parser = network_id_parser())]
81 network: u16,
82 #[clap(long, global = true)]
84 verbosity: Option<u8>,
85}
86
87#[derive(Debug, Deserialize)]
89struct RestError {
90 error_type: String,
92 message: String,
94 #[serde(default)]
97 chain: Vec<String>,
98}
99
100impl RestError {
101 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 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 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 fn parse_package<N: Network>(program_id: ProgramID<N>, path: &Option<String>) -> Result<Package<N>> {
147 let directory = match path {
149 Some(path) => PathBuf::from_str(path)?,
150 None => std::env::current_dir()?,
151 };
152
153 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 Ok(package)
163 }
164
165 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 let ciphertext = Record::<N, Ciphertext<N>>::from_str(record)?;
171 let view_key = ViewKey::try_from(private_key)?;
173 ciphertext.decrypt(&view_key)
175 }
176 false => Record::<N, Plaintext<N>>::from_str(record),
177 }
178 }
179
180 fn build_endpoint<N: Network>(base_url: &http::Uri, route: &str) -> Result<String> {
187 ensure!(!route.starts_with('/'), "path cannot start with a slash");
189
190 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 let sep = if base_url.path().ends_with('/') { "" } else { "/" };
200
201 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 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 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 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 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 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); while start_time.elapsed() < timeout_duration {
275 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 thread::sleep(poll_interval);
284 }
285 }
286 }
287
288 bail!("❌ Transaction {transaction_id} was not confirmed within {timeout_seconds} seconds");
290 }
291
292 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 fn get_public_balance<N: Network>(endpoint: &Uri, address: &Address<N>) -> Result<u64> {
302 let account_mapping = Identifier::<N>::from_str("account")?;
304 let credits = ProgramID::<N>::from_str("credits.aleo")?;
305
306 let result: Option<Value<N>> =
308 Self::http_get_json::<N, _>(endpoint, &format!("program/{credits}/mapping/{account_mapping}/{address}"))?;
309
310 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 #[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 let transaction_id = transaction.id();
338
339 ensure!(!transaction.is_fee(), "The transaction is a fee transaction and cannot be broadcast");
341
342 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 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 {
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 Ok(transaction_id.to_string())
443 } else if dry_run {
444 Ok(transaction.to_string())
446 } else {
447 Ok("".to_string())
448 }
449 }
450}