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(|| "HTTP POST request failed")? {
278 Some(mut body) => {
279 let json = body.read_json().with_context(|| "Failed to parse JSON response")?;
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(|| "HTTP GET request failed")? {
294 Some(mut body) => {
295 let json = body.read_json().with_context(|| "Failed to parse JSON response")?;
296 Ok(Some(json))
297 }
298 None => Ok(None),
299 }
300 }
301
302 fn http_get<N: Network>(base_url: &http::Uri, route: &str) -> Result<Option<ureq::Body>> {
304 let (endpoint, _api_version) = Self::build_endpoint::<N>(base_url, route)?;
305 debug!("Issuing GET request to \"{endpoint}\"");
306
307 let result = ureq::get(&endpoint).config().http_status_as_error(false).build().call().map_err(|err| err.into());
308
309 Self::handle_ureq_result(result).with_context(|| "HTTP GET request failed")
310 }
311
312 fn wait_for_transaction_confirmation<N: Network>(
314 endpoint: &Uri,
315 transaction_id: &N::TransactionID,
316 timeout_seconds: u64,
317 api_version: ApiVersion,
318 ) -> Result<()> {
319 let start_time = Instant::now();
320 let timeout_duration = Duration::from_secs(timeout_seconds);
321 let poll_interval = Duration::from_secs(1); while start_time.elapsed() < timeout_duration {
324 let result = Self::http_get::<N>(endpoint, &format!("transaction/{transaction_id}"));
326
327 match api_version {
328 ApiVersion::V1 => match result {
329 Ok(Some(_)) => return Ok(()),
330 Ok(None) => {
331 }
333 Err(err) => {
334 eprintln!("Got error when fetching transaction ({err}). Will retry...");
336 }
337 },
338 ApiVersion::V2 => {
339 match result.with_context(|| "Failed to check transaction status")? {
341 Some(_) => return Ok(()),
342 None => {
343 }
345 }
346 }
347 }
348
349 thread::sleep(poll_interval);
350 }
351
352 bail!("❌ Transaction {transaction_id} was not confirmed within {timeout_seconds} seconds");
354 }
355
356 fn get_latest_edition<N: Network>(endpoint: &Uri, program_id: &ProgramID<N>) -> Result<u16> {
358 match Self::http_get_json::<N, _>(endpoint, &format!("program/{program_id}/latest_edition"))? {
359 Some(edition) => Ok(edition),
360 None => bail!("Got unexpected 404 response"),
361 }
362 }
363
364 fn get_public_balance<N: Network>(endpoint: &Uri, address: &Address<N>) -> Result<Option<u64>> {
366 let account_mapping = Identifier::<N>::from_str("account")?;
368 let credits = ProgramID::<N>::from_str("credits.aleo")?;
369
370 let result: Option<Value<N>> =
374 Self::http_get_json::<N, _>(endpoint, &format!("program/{credits}/mapping/{account_mapping}/{address}"))?
375 .ok_or_else(|| anyhow!("Got unexpected 404 error when fetching public balance"))?;
376
377 match result {
379 Some(Value::Plaintext(Plaintext::Literal(Literal::<N>::U64(amount), _))) => Ok(Some(*amount)),
380 Some(..) => bail!("Failed to deserialize balance for {address}"),
381 None => Ok(None),
382 }
383 }
384
385 #[allow(clippy::too_many_arguments)]
392 fn handle_transaction<N: Network>(
393 endpoint: &Uri,
394 broadcast: &Option<Option<Uri>>,
395 dry_run: bool,
396 store: &Option<String>,
397 store_format: StoreFormat,
398 wait: bool,
399 timeout: u64,
400 transaction: Transaction<N>,
401 operation: String,
402 ) -> Result<String> {
403 let transaction_id = transaction.id();
405
406 ensure!(!transaction.is_fee(), "The transaction is a fee transaction and cannot be broadcast");
408
409 if let Some(path) = store {
411 match PathBuf::from_str(path) {
412 Ok(file_path) => {
413 match store_format {
414 StoreFormat::Bytes => {
415 let transaction_bytes = transaction.to_bytes_le()?;
416 std::fs::write(&file_path, transaction_bytes)?;
417 }
418 StoreFormat::String => {
419 let transaction_string = transaction.to_string();
420 std::fs::write(&file_path, transaction_string)?;
421 }
422 }
423
424 println!(
425 "Transaction {transaction_id} was stored to {} as {:?}",
426 file_path.display(),
427 store_format
428 );
429 }
430 Err(err) => {
431 println!("The transaction was unable to be stored due to: {err}");
432 }
433 }
434 };
435
436 if let Some(broadcast_value) = broadcast {
438 let (broadcast_endpoint, api_version) = if let Some(url) = broadcast_value {
439 debug!("Using custom endpoint for broadcasting: {url}");
440 Self::parse_custom_endpoint::<N>(url)
441 } else {
442 Self::build_endpoint::<N>(endpoint, "transaction/broadcast")?
443 };
444
445 let result: Result<String> = match Self::http_post_json(&broadcast_endpoint, &transaction) {
446 Ok(Some(s)) => Ok(s),
447 Ok(None) => Err(anyhow!("Got unexpected 404 error")),
448 Err(err) => Err(err),
449 };
450
451 match result {
452 Ok(response_string) => {
453 ensure!(
454 response_string == transaction_id.to_string(),
455 "The response does not match the transaction id. ({response_string} != {transaction_id})"
456 );
457
458 match transaction {
459 Transaction::Deploy(..) => {
460 println!(
461 "⌛ Deployment {transaction_id} ('{}') has been broadcast to {}.",
462 operation.bold(),
463 broadcast_endpoint
464 )
465 }
466 Transaction::Execute(..) => {
467 println!(
468 "⌛ Execution {transaction_id} ('{}') has been broadcast to {}.",
469 operation.bold(),
470 broadcast_endpoint
471 )
472 }
473 _ => unreachable!(),
474 }
475
476 if wait {
478 println!("⏳ Waiting for transaction confirmation (timeout: {timeout}s)...");
479 Self::wait_for_transaction_confirmation::<N>(endpoint, &transaction_id, timeout, api_version)?;
480
481 match transaction {
482 Transaction::Deploy(..) => {
483 println!("✅ Deployment {transaction_id} ('{}') confirmed!", operation.bold())
484 }
485 Transaction::Execute(..) => {
486 println!("✅ Execution {transaction_id} ('{}') confirmed!", operation.bold())
487 }
488 Transaction::Fee(..) => unreachable!(),
489 }
490 }
491 }
492 Err(error) => match transaction {
493 Transaction::Deploy(..) => {
494 return Err(error.context(anyhow!(
495 "Failed to deploy '{op}' to {broadcast_endpoint}",
496 op = operation.bold()
497 )));
498 }
499 Transaction::Execute(..) => {
500 return Err(error.context(anyhow!(
501 "Failed to broadcast execution '{op}' to {broadcast_endpoint}",
502 op = operation.bold()
503 )));
504 }
505 Transaction::Fee(..) => unreachable!(),
506 },
507 };
508
509 Ok(transaction_id.to_string())
511 } else if dry_run {
512 Ok(transaction.to_string())
514 } else {
515 Ok("".to_string())
516 }
517 }
518}
519
520#[cfg(test)]
521mod tests {
522 use super::*;
523
524 use snarkvm::ledger::test_helpers::CurrentNetwork;
525
526 #[test]
530 fn test_build_endpoint_default_v1() {
531 let base_uri_str = "http://localhost:3030";
532 let base_uri = Uri::try_from(base_uri_str).unwrap();
533 let (endpoint, api_version) =
534 Developer::build_endpoint::<CurrentNetwork>(&base_uri, "transaction/broadcast").unwrap();
535
536 assert_eq!(endpoint, format!("{base_uri_str}/{}/transaction/broadcast", CurrentNetwork::SHORT_NAME));
537 assert_eq!(api_version, ApiVersion::V1);
538 }
539
540 #[test]
542 fn test_build_endpoint_v1() {
543 let base_uri_str = "http://localhost:3030/v1";
544 let base_uri = Uri::try_from(base_uri_str).unwrap();
545 let (endpoint, api_version) =
546 Developer::build_endpoint::<CurrentNetwork>(&base_uri, "transaction/broadcast").unwrap();
547
548 assert_eq!(endpoint, format!("{base_uri_str}/{}/transaction/broadcast", CurrentNetwork::SHORT_NAME));
549 assert_eq!(api_version, ApiVersion::V1);
550 }
551
552 #[test]
554 fn test_build_endpoint_v2() {
555 let base_uri_str = "http://localhost:3030/v2";
556 let base_uri = Uri::try_from(base_uri_str).unwrap();
557 let (endpoint, api_version) =
558 Developer::build_endpoint::<CurrentNetwork>(&base_uri, "transaction/broadcast").unwrap();
559
560 assert_eq!(endpoint, format!("{base_uri_str}/{}/transaction/broadcast", CurrentNetwork::SHORT_NAME));
561 assert_eq!(api_version, ApiVersion::V2);
562 }
563
564 #[test]
565 fn test_custom_endpoint_v1() {
566 let endpoint_str = "http://localhost:3030/v1/mainnet/transaction/broadcast";
567 let endpoint = Uri::try_from(endpoint_str).unwrap();
568
569 let (parsed, api_version) = Developer::parse_custom_endpoint::<CurrentNetwork>(&endpoint);
570
571 assert_eq!(parsed, endpoint_str);
572 assert_eq!(api_version, ApiVersion::V1);
573 }
574
575 #[test]
576 fn test_custom_endpoint_v2() {
577 let endpoint_str = "http://localhost:3030/v2/mainnet/transaction/broadcast";
578 let endpoint = Uri::try_from(endpoint_str).unwrap();
579
580 let (parsed, api_version) = Developer::parse_custom_endpoint::<CurrentNetwork>(&endpoint);
581
582 assert_eq!(parsed, endpoint_str);
583 assert_eq!(api_version, ApiVersion::V2);
584 }
585}