1mod 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 tracing::debug;
47use ureq::http::{self, Uri};
48
49#[derive(Copy, Clone, Debug, ValueEnum)]
51pub enum StoreFormat {
52 String,
53 Bytes,
54}
55
56#[derive(Copy, Clone, Debug, PartialEq, Eq)]
58pub enum ApiVersion {
59 V1,
60 V2,
61}
62
63#[derive(Debug, Parser)]
65pub enum DeveloperCommand {
66 Decrypt(Decrypt),
68 Deploy(Deploy),
70 Execute(Execute),
72 Scan(Scan),
74 TransferPrivate(TransferPrivate),
76}
77
78const DEFAULT_ENDPOINT: &str = "https://api.explorer.provable.com/v2";
81
82#[derive(Debug, Parser)]
83pub struct Developer {
84 #[clap(subcommand)]
86 command: DeveloperCommand,
87 #[clap(long, default_value_t=MainnetV0::ID, long, global=true, value_parser = network_id_parser())]
90 network: u16,
91 #[clap(long, global = true)]
93 verbosity: Option<u8>,
94}
95
96#[derive(Debug, Deserialize)]
98struct RestError {
99 error_type: String,
101 message: String,
103 #[serde(default)]
106 chain: Vec<String>,
107}
108
109impl RestError {
110 pub fn parse(self) -> anyhow::Error {
112 let mut error: Option<anyhow::Error> = None;
113 for next in self.chain.into_iter() {
114 if let Some(previous) = error {
115 error = Some(previous.context(next));
116 } else {
117 error = Some(anyhow!(next));
118 }
119 }
120
121 let toplevel = format!("{}: {}", self.error_type, self.message);
122 if let Some(error) = error { error.context(toplevel) } else { anyhow!(toplevel) }
123 }
124}
125
126impl Developer {
127 pub fn parse(self) -> Result<String> {
129 if let Some(verbosity) = self.verbosity {
130 initialize_terminal_logger(verbosity).with_context(|| "Failed to initialize terminal logger")?
131 }
132
133 match self.network {
134 MainnetV0::ID => self.parse_inner::<MainnetV0>(),
135 TestnetV0::ID => self.parse_inner::<TestnetV0>(),
136 CanaryV0::ID => self.parse_inner::<CanaryV0>(),
137 unknown_id => bail!("Unknown network ID ({unknown_id})"),
138 }
139 }
140
141 fn parse_inner<N: Network>(self) -> Result<String> {
143 use DeveloperCommand::*;
144
145 match self.command {
146 Decrypt(decrypt) => decrypt.parse::<N>(),
147 Deploy(deploy) => deploy.parse::<N>(),
148 Execute(execute) => execute.parse::<N>(),
149 Scan(scan) => scan.parse::<N>(),
150 TransferPrivate(transfer_private) => transfer_private.parse::<N>(),
151 }
152 }
153
154 fn parse_package<N: Network>(program_id: ProgramID<N>, path: &Option<String>) -> Result<Package<N>> {
156 let directory = match path {
158 Some(path) => PathBuf::from_str(path)?,
159 None => std::env::current_dir()?,
160 };
161
162 let package = Package::open(&directory)?;
164
165 ensure!(
166 package.program_id() == &program_id,
167 "The program name in the package does not match the specified program name"
168 );
169
170 Ok(package)
172 }
173
174 fn parse_record<N: Network>(private_key: &PrivateKey<N>, record: &str) -> Result<Record<N, Plaintext<N>>> {
176 match record.starts_with("record1") {
177 true => {
178 let ciphertext = Record::<N, Ciphertext<N>>::from_str(record)?;
180 let view_key = ViewKey::try_from(private_key)?;
182 ciphertext.decrypt(&view_key)
184 }
185 false => Record::<N, Plaintext<N>>::from_str(record),
186 }
187 }
188
189 fn build_endpoint<N: Network>(base_url: &http::Uri, route: &str) -> Result<(String, ApiVersion)> {
199 ensure!(!route.starts_with('/'), "path cannot start with a slash");
201
202 let api_version = {
204 let r = base_url.path().trim_end_matches('/');
205
206 if r.ends_with(API_VERSION_V1) {
207 ApiVersion::V1
208 } else if r.ends_with(API_VERSION_V2) {
209 ApiVersion::V2
210 } else {
211 ApiVersion::V1
214 }
215 };
216
217 let sep = if base_url.path().ends_with('/') { "" } else { "/" };
221
222 let full_uri = format!("{base_url}{sep}{network}/{route}", network = N::SHORT_NAME);
224 Ok((full_uri, api_version))
225 }
226
227 fn handle_ureq_result(result: Result<http::Response<ureq::Body>>) -> Result<Option<ureq::Body>> {
230 let response = result?;
231
232 if response.status().is_success() {
233 Ok(Some(response.into_body()))
234 } else if response.status() == http::StatusCode::NOT_FOUND {
235 Ok(None)
236 } else {
237 let is_json = response
239 .headers()
240 .get(http::header::CONTENT_TYPE)
241 .and_then(|h| h.to_str().ok())
242 .map(|ct| ct.contains("json"))
243 .unwrap_or(false);
244
245 if is_json {
246 let rest_error: RestError =
247 response.into_body().read_json().with_context(|| "Failed to parse error JSON")?;
248
249 Err(rest_error.parse())
250 } else {
251 let err_msg = response.into_body().read_to_string()?;
253 Err(anyhow!(err_msg))
254 }
255 }
256 }
257
258 fn parse_custom_endpoint<N: Network>(url: &Uri) -> (String, ApiVersion) {
260 if let Some(pq) = url.path_and_query()
262 && pq.path().ends_with(&format!("{API_VERSION_V2}/{}/transaction/broadcast", N::SHORT_NAME))
263 {
264 (url.to_string(), ApiVersion::V2)
265 } else {
266 (url.to_string(), ApiVersion::V1)
267 }
268 }
269
270 fn http_post_json<I: Serialize, O: DeserializeOwned>(path: &str, arg: &I) -> Result<Option<O>> {
272 debug!("Issuing POST request to \"{path}\"");
273
274 let result =
275 ureq::post(path).config().http_status_as_error(false).build().send_json(arg).map_err(|err| err.into());
276
277 match Self::handle_ureq_result(result).with_context(|| format!("HTTP POST request to {path} failed"))? {
278 Some(mut body) => {
279 let json = body.read_json().with_context(|| format!("Failed to parse JSON response from {path}"))?;
280 Ok(Some(json))
281 }
282 None => Ok(None),
283 }
284 }
285
286 fn http_get_json<N: Network, O: DeserializeOwned>(base_url: &http::Uri, route: &str) -> Result<Option<O>> {
288 let (endpoint, _api_version) = Self::build_endpoint::<N>(base_url, route)?;
289 debug!("Issuing GET request to \"{endpoint}\"");
290
291 let result = ureq::get(&endpoint).config().http_status_as_error(false).build().call().map_err(|err| err.into());
292
293 match Self::handle_ureq_result(result).with_context(|| format!("HTTP GET request to {endpoint} failed"))? {
294 Some(mut body) => {
295 let json =
296 body.read_json().with_context(|| format!("Failed to parse JSON response from {endpoint}"))?;
297 Ok(Some(json))
298 }
299 None => Ok(None),
300 }
301 }
302
303 fn http_get<N: Network>(base_url: &http::Uri, route: &str) -> Result<Option<ureq::Body>> {
305 let (endpoint, _api_version) = Self::build_endpoint::<N>(base_url, route)?;
306 debug!("Issuing GET request to \"{endpoint}\"");
307
308 let result = ureq::get(&endpoint).config().http_status_as_error(false).build().call().map_err(|err| err.into());
309
310 Self::handle_ureq_result(result).with_context(|| format!("HTTP GET request to {endpoint} failed"))
311 }
312
313 fn wait_for_transaction_confirmation<N: Network>(
315 endpoint: &Uri,
316 transaction_id: &N::TransactionID,
317 timeout_seconds: u64,
318 api_version: ApiVersion,
319 ) -> Result<()> {
320 let start_time = Instant::now();
321 let timeout_duration = Duration::from_secs(timeout_seconds);
322 let poll_interval = Duration::from_secs(1); while start_time.elapsed() < timeout_duration {
325 let result = Self::http_get::<N>(endpoint, &format!("transaction/{transaction_id}"));
327
328 match api_version {
329 ApiVersion::V1 => match result {
330 Ok(Some(_)) => return Ok(()),
331 Ok(None) => {
332 }
334 Err(err) => {
335 eprintln!("Got error when fetching transaction ({err}). Will retry...");
337 }
338 },
339 ApiVersion::V2 => {
340 match result.with_context(|| "Failed to check transaction status")? {
342 Some(_) => return Ok(()),
343 None => {
344 }
346 }
347 }
348 }
349
350 thread::sleep(poll_interval);
351 }
352
353 bail!("❌ Transaction {transaction_id} was not confirmed within {timeout_seconds} seconds");
355 }
356
357 fn get_latest_edition<N: Network>(endpoint: &Uri, program_id: &ProgramID<N>) -> Result<u16> {
359 match Self::http_get_json::<N, _>(endpoint, &format!("program/{program_id}/latest_edition"))? {
360 Some(edition) => Ok(edition),
361 None => bail!("Got unexpected 404 response"),
362 }
363 }
364
365 fn get_public_balance<N: Network>(endpoint: &Uri, address: &Address<N>) -> Result<Option<u64>> {
367 let account_mapping = Identifier::<N>::from_str("account")?;
369 let credits = ProgramID::<N>::from_str("credits.aleo")?;
370
371 let result: Option<Value<N>> =
375 Self::http_get_json::<N, _>(endpoint, &format!("program/{credits}/mapping/{account_mapping}/{address}"))?
376 .ok_or_else(|| anyhow!("Got unexpected 404 error when fetching public balance"))?;
377
378 match result {
380 Some(Value::Plaintext(Plaintext::Literal(Literal::<N>::U64(amount), _))) => Ok(Some(*amount)),
381 Some(..) => bail!("Failed to deserialize balance for {address}"),
382 None => Ok(None),
383 }
384 }
385
386 #[allow(clippy::too_many_arguments)]
393 fn handle_transaction<N: Network>(
394 endpoint: &Uri,
395 broadcast: &Option<Option<Uri>>,
396 dry_run: bool,
397 store: &Option<String>,
398 store_format: StoreFormat,
399 wait: bool,
400 timeout: u64,
401 transaction: Transaction<N>,
402 operation: String,
403 ) -> Result<String> {
404 let transaction_id = transaction.id();
406
407 ensure!(!transaction.is_fee(), "The transaction is a fee transaction and cannot be broadcast");
409
410 if let Some(path) = store {
412 match PathBuf::from_str(path) {
413 Ok(file_path) => {
414 match store_format {
415 StoreFormat::Bytes => {
416 let transaction_bytes = transaction.to_bytes_le()?;
417 std::fs::write(&file_path, transaction_bytes)?;
418 }
419 StoreFormat::String => {
420 let transaction_string = transaction.to_string();
421 std::fs::write(&file_path, transaction_string)?;
422 }
423 }
424
425 println!(
426 "Transaction {transaction_id} was stored to {} as {:?}",
427 file_path.display(),
428 store_format
429 );
430 }
431 Err(err) => {
432 println!("The transaction was unable to be stored due to: {err}");
433 }
434 }
435 };
436
437 if let Some(broadcast_value) = broadcast {
439 let (broadcast_endpoint, api_version) = if let Some(url) = broadcast_value {
440 debug!("Using custom endpoint for broadcasting: {url}");
441 Self::parse_custom_endpoint::<N>(url)
442 } else {
443 Self::build_endpoint::<N>(endpoint, "transaction/broadcast")?
444 };
445
446 let result: Result<String> = match Self::http_post_json(&broadcast_endpoint, &transaction) {
447 Ok(Some(s)) => Ok(s),
448 Ok(None) => Err(anyhow!("Got unexpected 404 error")),
449 Err(err) => Err(err),
450 };
451
452 match result {
453 Ok(response_string) => {
454 ensure!(
455 response_string == transaction_id.to_string(),
456 "The response does not match the transaction id. ({response_string} != {transaction_id})"
457 );
458
459 match transaction {
460 Transaction::Deploy(..) => {
461 println!(
462 "⌛ Deployment {transaction_id} ('{}') has been broadcast to {}.",
463 operation.bold(),
464 broadcast_endpoint
465 )
466 }
467 Transaction::Execute(..) => {
468 println!(
469 "⌛ Execution {transaction_id} ('{}') has been broadcast to {}.",
470 operation.bold(),
471 broadcast_endpoint
472 )
473 }
474 _ => unreachable!(),
475 }
476
477 if wait {
479 println!("⏳ Waiting for transaction confirmation (timeout: {timeout}s)...");
480 Self::wait_for_transaction_confirmation::<N>(endpoint, &transaction_id, timeout, api_version)?;
481
482 match transaction {
483 Transaction::Deploy(..) => {
484 println!("✅ Deployment {transaction_id} ('{}') confirmed!", operation.bold())
485 }
486 Transaction::Execute(..) => {
487 println!("✅ Execution {transaction_id} ('{}') confirmed!", operation.bold())
488 }
489 Transaction::Fee(..) => unreachable!(),
490 }
491 }
492 }
493 Err(error) => match transaction {
494 Transaction::Deploy(..) => {
495 return Err(error.context(anyhow!(
496 "Failed to deploy '{op}' to {broadcast_endpoint}",
497 op = operation.bold()
498 )));
499 }
500 Transaction::Execute(..) => {
501 return Err(error.context(anyhow!(
502 "Failed to broadcast execution '{op}' to {broadcast_endpoint}",
503 op = operation.bold()
504 )));
505 }
506 Transaction::Fee(..) => unreachable!(),
507 },
508 };
509
510 Ok(transaction_id.to_string())
512 } else if dry_run {
513 Ok(transaction.to_string())
515 } else {
516 Ok("".to_string())
517 }
518 }
519}
520
521#[cfg(test)]
522mod tests {
523 use super::*;
524
525 use snarkvm::ledger::test_helpers::CurrentNetwork;
526
527 #[test]
531 fn test_build_endpoint_default_v1() {
532 let base_uri_str = "http://localhost:3030";
533 let base_uri = Uri::try_from(base_uri_str).unwrap();
534 let (endpoint, api_version) =
535 Developer::build_endpoint::<CurrentNetwork>(&base_uri, "transaction/broadcast").unwrap();
536
537 assert_eq!(endpoint, format!("{base_uri_str}/{}/transaction/broadcast", CurrentNetwork::SHORT_NAME));
538 assert_eq!(api_version, ApiVersion::V1);
539 }
540
541 #[test]
543 fn test_build_endpoint_v1() {
544 let base_uri_str = "http://localhost:3030/v1";
545 let base_uri = Uri::try_from(base_uri_str).unwrap();
546 let (endpoint, api_version) =
547 Developer::build_endpoint::<CurrentNetwork>(&base_uri, "transaction/broadcast").unwrap();
548
549 assert_eq!(endpoint, format!("{base_uri_str}/{}/transaction/broadcast", CurrentNetwork::SHORT_NAME));
550 assert_eq!(api_version, ApiVersion::V1);
551 }
552
553 #[test]
555 fn test_build_endpoint_v2() {
556 let base_uri_str = "http://localhost:3030/v2";
557 let base_uri = Uri::try_from(base_uri_str).unwrap();
558 let (endpoint, api_version) =
559 Developer::build_endpoint::<CurrentNetwork>(&base_uri, "transaction/broadcast").unwrap();
560
561 assert_eq!(endpoint, format!("{base_uri_str}/{}/transaction/broadcast", CurrentNetwork::SHORT_NAME));
562 assert_eq!(api_version, ApiVersion::V2);
563 }
564
565 #[test]
566 fn test_custom_endpoint_v1() {
567 let endpoint_str = "http://localhost:3030/v1/mainnet/transaction/broadcast";
568 let endpoint = Uri::try_from(endpoint_str).unwrap();
569
570 let (parsed, api_version) = Developer::parse_custom_endpoint::<CurrentNetwork>(&endpoint);
571
572 assert_eq!(parsed, endpoint_str);
573 assert_eq!(api_version, ApiVersion::V1);
574 }
575
576 #[test]
577 fn test_custom_endpoint_v2() {
578 let endpoint_str = "http://localhost:3030/v2/mainnet/transaction/broadcast";
579 let endpoint = Uri::try_from(endpoint_str).unwrap();
580
581 let (parsed, api_version) = Developer::parse_custom_endpoint::<CurrentNetwork>(&endpoint);
582
583 assert_eq!(parsed, endpoint_str);
584 assert_eq!(api_version, ApiVersion::V2);
585 }
586}