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;
#[derive(Debug, Parser)]
pub struct Deploy {
program_id: ProgramID<CurrentNetwork>,
#[clap(long)]
estimate_fee: bool,
#[clap(long)]
private_fee: bool,
#[clap(short, long)]
directory: Option<std::path::PathBuf>,
#[clap(short, long)]
endpoint: Option<String>,
#[clap(short, long)]
fee: Option<f64>,
#[clap(short, long)]
record: Option<Record<CurrentNetwork, Plaintext<CurrentNetwork>>>,
#[clap(short='k', long, conflicts_with_all = &["ciphertext", "password"])]
private_key: Option<PrivateKey<CurrentNetwork>>,
#[clap(short, long, conflicts_with = "private_key", requires = "password")]
ciphertext: Option<Ciphertext<CurrentNetwork>>,
#[clap(short, long, conflicts_with = "private_key", requires = "ciphertext")]
password: Option<String>,
}
impl Deploy {
pub fn parse(self) -> Result<String> {
if self.estimate_fee {
println!(
"Disclaimer: Fee estimation is experimental and may not represent a correct estimate on any current or future network"
);
}
ensure!(
!(self.private_key.is_none() && self.ciphertext.is_none()),
"Private key or private key ciphertext required to deploy a program"
);
let program_string = self.program_id.to_string();
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
};
let api_client = self
.endpoint
.map_or_else(
|| {
println!("Using default peer: https://api.explorer.aleo.org/v1/testnet3");
Ok(AleoAPIClient::<CurrentNetwork>::testnet3())
},
|peer| AleoAPIClient::<CurrentNetwork>::new(&peer, "testnet3"),
)
.map_err(|e| anyhow!("{:?}", e))?;
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());
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:?}");
let mut program_manager = ProgramManager::<CurrentNetwork>::new(
self.private_key,
self.ciphertext.clone(),
Some(api_client.clone()),
Some(program_directory),
false,
)?;
if self.estimate_fee {
let program = program_manager.find_program_on_disk(&self.program_id)?;
let (total, (storage, namespace)) = program_manager.estimate_deployment_fee::<Aleo>(&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());
}
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
};
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());
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() {
let recipient_private_key = PrivateKey::<CurrentNetwork>::new(&mut TestRng::default()).unwrap();
let ciphertext = Some(Encryptor::encrypt_private_key_with_secret(&recipient_private_key, "password").unwrap());
let deploy_missing_key_material = Deploy::try_parse_from(["aleo", "hello.aleo", "-f", "0.5"]);
assert!(deploy_missing_key_material.unwrap().parse().is_err());
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);
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);
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);
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());
}
}