// Copyright (C) 2019-2023 Aleo Systems Inc. // This file is part of the Aleo SDK library. // The Aleo SDK library is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // The Aleo SDK library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with the Aleo SDK library. If not, see . use crate::{Aleo, CurrentNetwork}; use aleo_rust::{AleoAPIClient, Encryptor, ProgramManager, RecordFinder}; use snarkvm::prelude::{Ciphertext, Plaintext, PrivateKey, ProgramID, Record}; use anyhow::{anyhow, ensure, Result}; use clap::Parser; use colored::Colorize; /// Deploys an Aleo program #[derive(Debug, Parser)] pub struct Deploy { /// The program identifier program_id: ProgramID, /// Estimate the deployment fee in credits. If set, this will estimate the fee for deploying the /// program but will NOT deploy the program #[clap(long)] estimate_fee: bool, #[clap(long)] /// Use private fee private_fee: bool, /// Directory containing the program files #[clap(short, long)] directory: Option, /// Aleo Network peer to broadcast the deployment to #[clap(short, long)] endpoint: Option, /// Deployment fee in credits #[clap(short, long)] fee: Option, /// The record to spend the fee from #[clap(short, long)] record: Option>>, /// Private key used to generate the deployment #[clap(short='k', long, conflicts_with_all = &["ciphertext", "password"])] private_key: Option>, /// Private key ciphertext used to generate the deployment (requires password to decrypt) #[clap(short, long, conflicts_with = "private_key", requires = "password")] ciphertext: Option>, /// Password to decrypt the private key #[clap(short, long, conflicts_with = "private_key", requires = "ciphertext")] password: Option, } impl Deploy { pub fn parse(self) -> Result { if self.estimate_fee { println!( "Disclaimer: Fee estimation is experimental and may not represent a correct estimate on any current or future network" ); } // Check for config errors ensure!( !(self.private_key.is_none() && self.ciphertext.is_none()), "Private key or private key ciphertext required to deploy a program" ); // Get strings for the program for logging let program_string = self.program_id.to_string(); // Ensure a fee is specified if deploying a program let fee_microcredits = if !self.estimate_fee { ensure!(self.fee.is_some(), "Fee must be specified when deploying a program"); let fee = self.fee.unwrap(); ensure!(fee > 0.0, "Deployment fee must be greater than 0"); println!( "{}", format!("Attempting to deploy program '{}' with a fee of {} credits", &program_string, fee) .bright_blue() ); (fee * 1000000.0) as u64 } else { 0u64 }; // Setup the API client to use configured peer or default to https://api.explorer.aleo.org/v1/testnet3 let api_client = self .endpoint .map_or_else( || { println!("Using default peer: https://api.explorer.aleo.org/v1/testnet3"); Ok(AleoAPIClient::::testnet3()) }, |peer| AleoAPIClient::::new(&peer, "testnet3"), ) .map_err(|e| anyhow!("{:?}", e))?; // Verify program is not already deployed println!("Verifying {} is not already deployed on the aleo network..", program_string.bright_blue()); ensure!(api_client.get_program(self.program_id).is_err(), "Program is already deployed"); println!("{} was not found on the Aleo Network, continuing deployment..", program_string.bright_blue()); // Assume the local directory is the program directory if none is specified let program_directory = self .directory .map_or_else(std::env::current_dir, Ok) .map_err(|_| anyhow!("No program directory specified and attempting to use local path failed"))?; println!("Using program directory: {program_directory:?}"); // Create a program manager to deploy the program let mut program_manager = ProgramManager::::new( self.private_key, self.ciphertext.clone(), Some(api_client.clone()), Some(program_directory), false, )?; // Estimate the fee if specified if self.estimate_fee { let program = program_manager.find_program_on_disk(&self.program_id)?; let (total, (storage, namespace)) = program_manager.estimate_deployment_fee::(&program)?; let (total, storage, namespace) = ((total as f64) / 1_000_000.0, (storage as f64) / 1_000_000.0, (namespace as f64) / 1_000_000.0); let program_id = program.id(); println!( "\n{} {} {} {} {} {} {} {} {}", "Program".bright_green(), format!("{program_id}").bright_blue(), "has a storage fee of".bright_green(), format!("{storage}").bright_blue(), "credits and a namespace fee of".bright_green(), format!("{namespace}").bright_blue(), "credits for a total deployment fee of".bright_green(), format!("{total}").bright_blue(), "credits".bright_green() ); return Ok("".to_string()); } // Find a fee record to pay the fee if necessary let fee_record = if self.record.is_none() { println!("Searching for a record to spend the deployment fee from, this may take a while.."); let private_key = if let Some(private_key) = self.private_key { private_key } else { let ciphertext = self.ciphertext.as_ref().unwrap(); Encryptor::decrypt_private_key_with_secret(ciphertext, self.password.as_ref().unwrap())? }; let record_finder = RecordFinder::new(api_client); if self.private_fee { Some(record_finder.find_one_record(&private_key, fee_microcredits, None)?) } else { None } } else { self.record }; // Deploy the program println!("Attempting to deploy program: {}", program_string.bright_blue()); let result = program_manager.deploy_program(self.program_id, fee_microcredits, fee_record, self.password.as_deref()); // Inform the user of the result of the program deployment if result.is_err() { println!("Deployment of program {} failed with error:", program_string.red().bold()); } else { println!("Deployment of program {} successful!", program_string.green().bold()); println!("Transaction ID:"); } result } } #[cfg(test)] mod tests { use super::*; use snarkvm::prelude::TestRng; #[test] fn test_deployment_config_errors() { // Generate key material let recipient_private_key = PrivateKey::::new(&mut TestRng::default()).unwrap(); let ciphertext = Some(Encryptor::encrypt_private_key_with_secret(&recipient_private_key, "password").unwrap()); // Assert deploy fails without a private key or private key ciphertext let deploy_missing_key_material = Deploy::try_parse_from(["aleo", "hello.aleo", "-f", "0.5"]); assert!(deploy_missing_key_material.unwrap().parse().is_err()); // Assert deploy fails if both a private key and ciphertext are provided let deploy_conflicting_inputs = Deploy::try_parse_from([ "aleo", "hello.aleo", "-f", "0.5", "-k", &recipient_private_key.to_string(), "--ciphertext", &ciphertext.as_ref().unwrap().to_string(), "--password", "password", ]); assert_eq!(deploy_conflicting_inputs.unwrap_err().kind(), clap::error::ErrorKind::ArgumentConflict); // Assert deploy fails if a ciphertext is provided without a password let ciphertext = Some(Encryptor::encrypt_private_key_with_secret(&recipient_private_key, "password").unwrap()); let deploy_no_password = Deploy::try_parse_from([ "aleo", "hello.aleo", "-f", "0.5", "--ciphertext", &ciphertext.as_ref().unwrap().to_string(), ]); assert_eq!(deploy_no_password.unwrap_err().kind(), clap::error::ErrorKind::MissingRequiredArgument); // Assert deploy fails if only a password is provided let deploy_password_only = Deploy::try_parse_from(["aleo", "hello.aleo", "-f", "0.5", "--password", "password"]); assert_eq!(deploy_password_only.unwrap_err().kind(), clap::error::ErrorKind::MissingRequiredArgument); // Assert deploy fails if invalid peer is specified let deploy_bad_peer = Deploy::try_parse_from([ "aleo", "hello.aleo", "-f", "0.5", "-k", &recipient_private_key.to_string(), "-e", "localhost:3033", ]); assert!(deploy_bad_peer.unwrap().parse().is_err()); } }