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/v1";
73
74#[derive(Debug, Parser)]
75pub struct Developer {
76 #[clap(subcommand)]
78 command: DeveloperCommand,
79 #[clap(long, default_value_t=MainnetV0::ID, long, global=true, value_parser = network_id_parser())]
82 network: u16,
83 #[clap(long, global = true)]
85 verbosity: Option<u8>,
86}
87
88#[derive(Debug, Deserialize)]
90struct RestError {
91 error_type: String,
93 message: String,
95 #[serde(default)]
98 chain: Vec<String>,
99}
100
101impl RestError {
102 pub fn parse(self) -> anyhow::Error {
104 let mut error: Option<anyhow::Error> = None;
105 for next in self.chain.into_iter() {
106 if let Some(previous) = error {
107 error = Some(previous.context(next));
108 } else {
109 error = Some(anyhow!(next));
110 }
111 }
112
113 let toplevel = format!("{}: {}", self.error_type, self.message);
114 if let Some(error) = error { error.context(toplevel) } else { anyhow!(toplevel) }
115 }
116}
117
118impl Developer {
119 pub fn parse(self) -> Result<String> {
121 if let Some(verbosity) = self.verbosity {
122 initialize_terminal_logger(verbosity).with_context(|| "Failed to initialize terminal logger")?
123 }
124
125 match self.network {
126 MainnetV0::ID => self.parse_inner::<MainnetV0>(),
127 TestnetV0::ID => self.parse_inner::<TestnetV0>(),
128 CanaryV0::ID => self.parse_inner::<CanaryV0>(),
129 unknown_id => bail!("Unknown network ID ({unknown_id})"),
130 }
131 }
132
133 fn parse_inner<N: Network>(self) -> Result<String> {
135 use DeveloperCommand::*;
136
137 match self.command {
138 Decrypt(decrypt) => decrypt.parse::<N>(),
139 Deploy(deploy) => deploy.parse::<N>(),
140 Execute(execute) => execute.parse::<N>(),
141 Scan(scan) => scan.parse::<N>(),
142 TransferPrivate(transfer_private) => transfer_private.parse::<N>(),
143 }
144 }
145
146 fn parse_package<N: Network>(program_id: ProgramID<N>, path: &Option<String>) -> Result<Package<N>> {
148 let directory = match path {
150 Some(path) => PathBuf::from_str(path)?,
151 None => std::env::current_dir()?,
152 };
153
154 let package = Package::open(&directory)?;
156
157 ensure!(
158 package.program_id() == &program_id,
159 "The program name in the package does not match the specified program name"
160 );
161
162 Ok(package)
164 }
165
166 fn parse_record<N: Network>(private_key: &PrivateKey<N>, record: &str) -> Result<Record<N, Plaintext<N>>> {
168 match record.starts_with("record1") {
169 true => {
170 let ciphertext = Record::<N, Ciphertext<N>>::from_str(record)?;
172 let view_key = ViewKey::try_from(private_key)?;
174 ciphertext.decrypt(&view_key)
176 }
177 false => Record::<N, Plaintext<N>>::from_str(record),
178 }
179 }
180
181 fn build_endpoint<N: Network>(base_url: &http::Uri, route: &str) -> Result<String> {
188 ensure!(!route.starts_with('/'), "path cannot start with a slash");
190
191 let route_has_version_suffix = {
193 let r = base_url.path().trim_end_matches('/');
194 r.ends_with(API_VERSION_V1) || r.ends_with(API_VERSION_V2)
195 };
196
197 let sep = if base_url.path().ends_with('/') { "" } else { "/" };
201
202 let prefix = if route_has_version_suffix {
204 format!("{}/", N::SHORT_NAME)
205 } else {
206 format!("{}/{}/", API_VERSION_V2, N::SHORT_NAME)
207 };
208
209 Ok(format!("{base_url}{sep}{prefix}{route}"))
210 }
211
212 fn handle_ureq_result(result: Result<http::Response<ureq::Body>>) -> Result<Option<ureq::Body>> {
215 let response = result?;
216
217 if response.status().is_success() {
218 Ok(Some(response.into_body()))
219 } else if response.status() == http::StatusCode::NOT_FOUND {
220 Ok(None)
221 } else {
222 let rest_error: RestError =
223 response.into_body().read_json().with_context(|| "Failed to parse error JSON")?;
224
225 Err(rest_error.parse())
226 }
227 }
228
229 fn http_post_json<I: Serialize, O: DeserializeOwned>(path: &str, arg: &I) -> Result<Option<O>> {
231 let result =
232 ureq::post(path).config().http_status_as_error(false).build().send_json(arg).map_err(|err| err.into());
233
234 match Self::handle_ureq_result(result).with_context(|| "HTTP POST request failed")? {
235 Some(mut body) => {
236 let json = body.read_json().with_context(|| "Failed to parse JSON response")?;
237 Ok(Some(json))
238 }
239 None => Ok(None),
240 }
241 }
242
243 fn http_get_json<N: Network, O: DeserializeOwned>(base_url: &http::Uri, route: &str) -> Result<Option<O>> {
245 let endpoint = Self::build_endpoint::<N>(base_url, route)?;
246 let result = ureq::get(&endpoint).config().http_status_as_error(false).build().call().map_err(|err| err.into());
247
248 match Self::handle_ureq_result(result).with_context(|| "HTTP GET request failed")? {
249 Some(mut body) => {
250 let json = body.read_json().with_context(|| "Failed to parse JSON response")?;
251 Ok(Some(json))
252 }
253 None => Ok(None),
254 }
255 }
256
257 fn http_get<N: Network>(base_url: &http::Uri, route: &str) -> Result<Option<ureq::Body>> {
259 let endpoint = Self::build_endpoint::<N>(base_url, route)?;
260 let result = ureq::get(&endpoint).config().http_status_as_error(false).build().call().map_err(|err| err.into());
261
262 Self::handle_ureq_result(result).with_context(|| "HTTP GET request failed")
263 }
264
265 fn wait_for_transaction_confirmation<N: Network>(
267 endpoint: &Uri,
268 transaction_id: &N::TransactionID,
269 timeout_seconds: u64,
270 ) -> Result<()> {
271 let start_time = Instant::now();
272 let timeout_duration = Duration::from_secs(timeout_seconds);
273 let poll_interval = Duration::from_secs(1); while start_time.elapsed() < timeout_duration {
276 let result = Self::http_get::<N>(endpoint, &format!("transaction/{transaction_id}"))
278 .with_context(|| "Failed to check transaction status")?;
279
280 match result {
281 Some(_) => return Ok(()),
282 None => {
283 thread::sleep(poll_interval);
285 }
286 }
287 }
288
289 bail!("❌ Transaction {transaction_id} was not confirmed within {timeout_seconds} seconds");
291 }
292
293 fn get_latest_edition<N: Network>(endpoint: &Uri, program_id: &ProgramID<N>) -> Result<u16> {
295 match Self::http_get_json::<N, _>(endpoint, &format!("program/{program_id}/latest_edition"))? {
296 Some(edition) => Ok(edition),
297 None => bail!("Got unexpected 404 response"),
298 }
299 }
300
301 fn get_public_balance<N: Network>(endpoint: &Uri, address: &Address<N>) -> Result<u64> {
303 let account_mapping = Identifier::<N>::from_str("account")?;
305 let credits = ProgramID::<N>::from_str("credits.aleo")?;
306
307 let result: Option<Value<N>> =
309 Self::http_get_json::<N, _>(endpoint, &format!("program/{credits}/mapping/{account_mapping}/{address}"))?;
310
311 match result {
313 Some(Value::Plaintext(Plaintext::Literal(Literal::<N>::U64(amount), _))) => Ok(*amount),
314 Some(..) => bail!("Failed to deserialize balance for {address}"),
315 None => Ok(0),
316 }
317 }
318
319 #[allow(clippy::too_many_arguments)]
326 fn handle_transaction<N: Network>(
327 endpoint: &Uri,
328 broadcast: &Option<Option<Uri>>,
329 dry_run: bool,
330 store: &Option<String>,
331 store_format: StoreFormat,
332 wait: bool,
333 timeout: u64,
334 transaction: Transaction<N>,
335 operation: String,
336 ) -> Result<String> {
337 let transaction_id = transaction.id();
339
340 ensure!(!transaction.is_fee(), "The transaction is a fee transaction and cannot be broadcast");
342
343 if let Some(path) = store {
345 match PathBuf::from_str(path) {
346 Ok(file_path) => {
347 match store_format {
348 StoreFormat::Bytes => {
349 let transaction_bytes = transaction.to_bytes_le()?;
350 std::fs::write(&file_path, transaction_bytes)?;
351 }
352 StoreFormat::String => {
353 let transaction_string = transaction.to_string();
354 std::fs::write(&file_path, transaction_string)?;
355 }
356 }
357
358 println!(
359 "Transaction {transaction_id} was stored to {} as {:?}",
360 file_path.display(),
361 store_format
362 );
363 }
364 Err(err) => {
365 println!("The transaction was unable to be stored due to: {err}");
366 }
367 }
368 };
369
370 if let Some(broadcast_value) = broadcast {
372 let broadcast_endpoint = if let Some(url) = broadcast_value {
373 url.to_string()
374 } else {
375 Self::build_endpoint::<N>(endpoint, "transaction/broadcast")?
376 };
377
378 let result: Result<String> = match Self::http_post_json(&broadcast_endpoint, &transaction) {
379 Ok(Some(s)) => Ok(s),
380 Ok(None) => Err(anyhow!("Got unexpected 404 error")),
381 Err(err) => Err(err),
382 };
383
384 match result {
385 Ok(response_string) => {
386 ensure!(
387 response_string == transaction_id.to_string(),
388 "The response does not match the transaction id. ({response_string} != {transaction_id})"
389 );
390
391 match transaction {
392 Transaction::Deploy(..) => {
393 println!(
394 "⌛ Deployment {transaction_id} ('{}') has been broadcast to {}.",
395 operation.bold(),
396 broadcast_endpoint
397 )
398 }
399 Transaction::Execute(..) => {
400 println!(
401 "⌛ Execution {transaction_id} ('{}') has been broadcast to {}.",
402 operation.bold(),
403 broadcast_endpoint
404 )
405 }
406 _ => unreachable!(),
407 }
408
409 if wait {
411 println!("⏳ Waiting for transaction confirmation (timeout: {timeout}s)...");
412 Self::wait_for_transaction_confirmation::<N>(endpoint, &transaction_id, timeout)?;
413
414 match transaction {
415 Transaction::Deploy(..) => {
416 println!("✅ Deployment {transaction_id} ('{}') confirmed!", operation.bold())
417 }
418 Transaction::Execute(..) => {
419 println!("✅ Execution {transaction_id} ('{}') confirmed!", operation.bold())
420 }
421 Transaction::Fee(..) => unreachable!(),
422 }
423 }
424 }
425 Err(error) => match transaction {
426 Transaction::Deploy(..) => {
427 return Err(error.context(anyhow!(
428 "Failed to deploy '{op}' to {broadcast_endpoint}",
429 op = operation.bold()
430 )));
431 }
432 Transaction::Execute(..) => {
433 return Err(error.context(anyhow!(
434 "Failed to broadcast execution '{op}' to {broadcast_endpoint}",
435 op = operation.bold()
436 )));
437 }
438 Transaction::Fee(..) => unreachable!(),
439 },
440 };
441
442 Ok(transaction_id.to_string())
444 } else if dry_run {
445 Ok(transaction.to_string())
447 } else {
448 Ok("".to_string())
449 }
450 }
451}