use super::{DEFAULT_ENDPOINT, Developer};
use crate::{
commands::StoreFormat,
helpers::args::{parse_private_key, prepare_endpoint},
};
use snarkvm::{
console::network::Network,
ledger::{query::QueryTrait, store::helpers::memory::BlockMemory},
prelude::{
Address,
Identifier,
Locator,
Process,
ProgramID,
VM,
Value,
query::Query,
store::{ConsensusStore, helpers::memory::ConsensusMemory},
},
};
use aleo_std::StorageMode;
use anyhow::{Context, Result, anyhow, bail};
use clap::{Parser, builder::NonEmptyStringValueParser};
use colored::Colorize;
use std::str::FromStr;
use tracing::debug;
use ureq::http::Uri;
use zeroize::Zeroize;
#[derive(Debug, Parser)]
#[command(
group(clap::ArgGroup::new("mode").required(true).multiple(false)),
group(clap::ArgGroup::new("key").required(true).multiple(false))
)]
pub struct Execute {
#[clap(value_parser=NonEmptyStringValueParser::default())]
program_id: String,
#[clap(value_parser=NonEmptyStringValueParser::default())]
function: String,
inputs: Vec<String>,
#[clap(short = 'p', long, group = "key", value_parser=NonEmptyStringValueParser::default())]
private_key: Option<String>,
#[clap(long, group = "key", value_parser=NonEmptyStringValueParser::default())]
private_key_file: Option<String>,
#[clap(long, group = "key")]
dev_key: Option<u16>,
#[clap(short, long, alias="query", default_value=DEFAULT_ENDPOINT, verbatim_doc_comment)]
endpoint: Uri,
#[clap(long, default_value_t = 0)]
priority_fee: u64,
#[clap(short, long)]
record: Option<String>,
#[clap(short, long, group = "mode", verbatim_doc_comment)]
broadcast: Option<Option<Uri>>,
#[clap(short, long, group = "mode")]
dry_run: bool,
#[clap(long, group = "mode")]
store: Option<String>,
#[clap(long, value_enum, default_value_t = StoreFormat::Bytes, requires="store")]
store_format: StoreFormat,
#[clap(long, requires = "broadcast")]
wait: bool,
#[clap(long, default_value_t = 60, requires = "wait")]
timeout: u64,
#[clap(long, hide = true)]
skip_funds_check: bool,
}
impl Drop for Execute {
fn drop(&mut self) {
if let Some(mut pk) = self.private_key.take() {
pk.zeroize()
}
}
}
impl Execute {
pub fn parse<N: Network>(self) -> Result<String> {
let endpoint = prepare_endpoint(self.endpoint.clone())?;
let query = Query::<N, BlockMemory<N>>::from(endpoint.clone());
let is_static_query = matches!(query, Query::STATIC(_));
let private_key = parse_private_key(self.private_key.clone(), self.private_key_file.clone(), self.dev_key)?;
let program_id = ProgramID::from_str(&self.program_id).with_context(|| "Failed to parse program ID")?;
let function = Identifier::from_str(&self.function).with_context(|| "Failed to parse function ID")?;
let inputs = self.inputs.iter().map(|input| Value::from_str(input)).collect::<Result<Vec<Value<N>>>>()?;
let locator = Locator::<N>::from_str(&format!("{program_id}/{function}"))?;
println!("📦 Creating execution transaction for '{}'...\n", &locator.to_string().bold());
let transaction = {
let rng = &mut rand::thread_rng();
let store = ConsensusStore::<N, ConsensusMemory<N>>::open(StorageMode::Production)?;
let vm = VM::from(store)?;
if !is_static_query && program_id != ProgramID::from_str("credits.aleo")? {
let height = query.current_block_height().with_context(|| "Failed to retrieve current block height")?;
let version = N::CONSENSUS_VERSION(height)?;
debug!("At block height {height} and consensus {version:?}");
load_program(&query, &mut vm.process().write(), &program_id, &endpoint)?;
}
let fee_record = match &self.record {
Some(record_string) => Some(
Developer::parse_record(&private_key, record_string).with_context(|| "Failed to parse record")?,
),
None => None,
};
vm.execute(
&private_key,
(program_id, function),
inputs.iter(),
fee_record,
self.priority_fee,
Some(&query),
rng,
)
.with_context(|| "VM failed to execute transaction locally")?
};
if self.record.is_none() && !is_static_query && !self.skip_funds_check {
let address = Address::try_from(&private_key)?;
let public_balance = Developer::get_public_balance::<N>(&endpoint, &address)
.with_context(|| "Failed to check for sufficient funds to send transaction")?
.ok_or_else(|| {
anyhow!(
"No public balance found for sending account `{}`. It may not exist.",
address.to_string().bold()
)
})?;
let storage_cost = transaction
.execution()
.with_context(|| "Failed to get execution cost of transaction")?
.size_in_bytes()?;
let base_fee = storage_cost.saturating_add(self.priority_fee);
if public_balance < base_fee {
bail!(
"The public balance of {} is insufficient to pay the base fee for `{}`",
public_balance,
locator.to_string().bold()
);
}
}
println!("✅ Created execution transaction for '{}'", locator.to_string().bold());
Developer::handle_transaction(
&endpoint,
&self.broadcast,
self.dry_run,
&self.store,
self.store_format,
self.wait,
self.timeout,
transaction,
locator.to_string(),
)
}
}
fn load_program<N: Network>(
query: &Query<N, BlockMemory<N>>,
process: &mut Process<N>,
program_id: &ProgramID<N>,
endpoint: &Uri,
) -> Result<()> {
let program = query.get_program(program_id).with_context(|| "Failed to fetch program")?;
let edition = Developer::get_latest_edition(endpoint, program_id)
.with_context(|| format!("Failed to get latest edition for program {program_id}"))?;
if process.contains_program(program.id()) {
return Ok(());
}
for import_program_id in program.imports().keys() {
if !process.contains_program(import_program_id) {
load_program(query, process, import_program_id, endpoint)
.with_context(|| format!("Failed to load imported program {import_program_id}"))?;
}
}
if !process.contains_program(program.id()) {
debug!("Adding program {program_id} with edition {edition}");
process
.add_programs_with_editions(&[(program, edition)])
.with_context(|| format!("Failed to add program {program_id}"))?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::commands::{CLI, Command, DeveloperCommand};
#[test]
fn clap_snarkos_execute() -> Result<()> {
let arg_vec = &[
"snarkos",
"developer",
"execute",
"--private-key",
"PRIVATE_KEY",
"--endpoint=ENDPOINT",
"--priority-fee",
"77",
"--record",
"RECORD",
"--dry-run",
"hello.aleo",
"hello",
"1u32",
"2u32",
];
let cli = CLI::try_parse_from(arg_vec)?;
let Command::Developer(developer) = cli.command else {
bail!("Unexpected result of clap parsing!");
};
let DeveloperCommand::Execute(execute) = developer.command else {
bail!("Unexpected result of clap parsing!");
};
assert_eq!(developer.network, 0);
assert_eq!(execute.private_key, Some("PRIVATE_KEY".to_string()));
assert_eq!(execute.endpoint, "ENDPOINT");
assert_eq!(execute.priority_fee, 77);
assert_eq!(execute.record, Some("RECORD".into()));
assert_eq!(execute.program_id, "hello.aleo".to_string());
assert_eq!(execute.function, "hello".to_string());
assert_eq!(execute.inputs, vec!["1u32".to_string(), "2u32".to_string()]);
Ok(())
}
#[test]
fn clap_snarkos_execute_pk_file() -> Result<()> {
let arg_vec = &[
"snarkos",
"developer",
"execute",
"--private-key-file",
"PRIVATE_KEY_FILE",
"--endpoint=ENDPOINT",
"--record",
"RECORD",
"--dry-run",
"hello.aleo",
"hello",
"1u32",
"2u32",
];
let cli = CLI::try_parse_from(arg_vec)?;
let Command::Developer(developer) = cli.command else {
bail!("Unexpected result of clap parsing!");
};
let DeveloperCommand::Execute(execute) = developer.command else {
bail!("Unexpected result of clap parsing!");
};
assert_eq!(developer.network, 0);
assert_eq!(execute.private_key_file, Some("PRIVATE_KEY_FILE".to_string()));
assert_eq!(execute.endpoint, "ENDPOINT");
assert_eq!(execute.priority_fee, 0); assert_eq!(execute.record, Some("RECORD".into()));
assert_eq!(execute.program_id, "hello.aleo".to_string());
assert_eq!(execute.function, "hello".to_string());
assert_eq!(execute.inputs, vec!["1u32".to_string(), "2u32".to_string()]);
Ok(())
}
#[test]
fn clap_snarkos_execute_two_private_keys() {
let arg_vec = &[
"snarkos",
"developer",
"execute",
"--private-key",
"PRIVATE_KEY",
"--private-key-file",
"PRIVATE_KEY_FILE",
"--endpoint=ENDPOINT",
"--priority-fee",
"77",
"--record",
"RECORD",
"--dry-run",
"hello.aleo",
"hello",
"1u32",
"2u32",
];
let err = CLI::try_parse_from(arg_vec).unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
}
#[test]
fn clap_snarkos_execute_no_private_keys() {
let arg_vec = &[
"snarkos",
"developer",
"execute",
"--endpoint=ENDPOINT",
"--priority-fee",
"77",
"--record",
"RECORD",
"--dry-run",
"hello.aleo",
"hello",
"1u32",
"2u32",
];
let err = CLI::try_parse_from(arg_vec).unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::MissingRequiredArgument);
}
}