1use super::{DEFAULT_ENDPOINT, Developer};
17use crate::{
18 commands::StoreFormat,
19 helpers::args::{parse_private_key, prepare_endpoint},
20};
21
22use snarkvm::{
23 console::network::Network,
24 ledger::{query::QueryTrait, store::helpers::memory::BlockMemory},
25 prelude::{
26 Address,
27 Identifier,
28 Locator,
29 Process,
30 ProgramID,
31 VM,
32 Value,
33 query::Query,
34 store::{ConsensusStore, helpers::memory::ConsensusMemory},
35 },
36};
37
38use aleo_std::StorageMode;
39use anyhow::{Context, Result, bail};
40use clap::{Parser, builder::NonEmptyStringValueParser};
41use colored::Colorize;
42use std::{path::PathBuf, str::FromStr};
43use tracing::debug;
44use ureq::http::Uri;
45use zeroize::Zeroize;
46
47#[derive(Debug, Parser)]
49#[command(
50 group(clap::ArgGroup::new("mode").required(true).multiple(false)),
51 group(clap::ArgGroup::new("key").required(true).multiple(false))
52)]
53pub struct Execute {
54 #[clap(value_parser=NonEmptyStringValueParser::default())]
56 program_id: String,
57 #[clap(value_parser=NonEmptyStringValueParser::default())]
59 function: String,
60 inputs: Vec<String>,
62 #[clap(short = 'p', long, group = "key", value_parser=NonEmptyStringValueParser::default())]
64 private_key: Option<String>,
65 #[clap(long, group = "key", value_parser=NonEmptyStringValueParser::default())]
67 private_key_file: Option<String>,
68 #[clap(long, group = "key")]
70 dev_key: Option<u16>,
71 #[clap(short, long, alias="query", default_value=DEFAULT_ENDPOINT, verbatim_doc_comment)]
78 endpoint: Uri,
79 #[clap(long, default_value_t = 0)]
81 priority_fee: u64,
82 #[clap(short, long)]
84 record: Option<String>,
85 #[clap(short, long, group = "mode", verbatim_doc_comment)]
89 broadcast: Option<Option<Uri>>,
90 #[clap(short, long, group = "mode")]
92 dry_run: bool,
93 #[clap(long, group = "mode")]
95 store: Option<String>,
96 #[clap(long, value_enum, default_value_t = StoreFormat::Bytes, requires="store")]
99 store_format: StoreFormat,
100 #[clap(long, requires = "broadcast")]
102 wait: bool,
103 #[clap(long, default_value_t = 60, requires = "wait")]
105 timeout: u64,
106 #[clap(long = "storage_path")]
108 storage_path: Option<PathBuf>,
109}
110
111impl Drop for Execute {
112 fn drop(&mut self) {
114 if let Some(mut pk) = self.private_key.take() {
115 pk.zeroize()
116 }
117 }
118}
119
120impl Execute {
121 pub fn parse<N: Network>(self) -> Result<String> {
123 let endpoint = prepare_endpoint(self.endpoint.clone())?;
124
125 let query = Query::<N, BlockMemory<N>>::from(endpoint.clone());
127
128 let is_static_query = matches!(query, Query::STATIC(_));
130
131 let private_key = parse_private_key(self.private_key.clone(), self.private_key_file.clone(), self.dev_key)?;
133
134 let program_id = ProgramID::from_str(&self.program_id).with_context(|| "Failed to parse program ID")?;
136
137 let function = Identifier::from_str(&self.function).with_context(|| "Failed to parse function ID")?;
139
140 let inputs = self.inputs.iter().map(|input| Value::from_str(input)).collect::<Result<Vec<Value<N>>>>()?;
142
143 let locator = Locator::<N>::from_str(&format!("{program_id}/{function}"))?;
144 println!("📦 Creating execution transaction for '{}'...\n", &locator.to_string().bold());
145
146 let transaction = {
148 let rng = &mut rand::thread_rng();
150
151 let storage_mode = match &self.storage_path {
153 Some(path) => StorageMode::Custom(path.clone()),
154 None => StorageMode::Production,
155 };
156 let store = ConsensusStore::<N, ConsensusMemory<N>>::open(storage_mode)?;
157
158 let vm = VM::from(store)?;
160
161 if !is_static_query && program_id != ProgramID::from_str("credits.aleo")? {
162 let height = query.current_block_height()?;
163 let version = N::CONSENSUS_VERSION(height)?;
164 debug!("At block height {height} and consensus {version:?}");
165 let edition = Developer::get_latest_edition(&endpoint, &program_id)
166 .with_context(|| format!("Failed to get latest edition for program {program_id}"))?;
167
168 load_program(&query, &mut vm.process().write(), &program_id, edition)?;
170 }
171
172 let fee_record = match &self.record {
174 Some(record_string) => Some(
175 Developer::parse_record(&private_key, record_string).with_context(|| "Failed to parse record")?,
176 ),
177 None => None,
178 };
179
180 vm.execute(
182 &private_key,
183 (program_id, function),
184 inputs.iter(),
185 fee_record,
186 self.priority_fee,
187 Some(&query),
188 rng,
189 )
190 .with_context(|| "VM failed to execute transaction locally")?
191 };
192
193 if self.record.is_none() && !is_static_query {
195 let address = Address::try_from(&private_key)?;
197 let public_balance = Developer::get_public_balance::<N>(&endpoint, &address)
198 .with_context(|| "Failed to check for sufficient funds to send transaction")?;
199
200 let storage_cost = transaction
202 .execution()
203 .with_context(|| "Failed to get execution cost of transaction")?
204 .size_in_bytes()?;
205
206 let base_fee = storage_cost.saturating_add(self.priority_fee);
210
211 if public_balance < base_fee {
213 bail!(
214 "The public balance of {} is insufficient to pay the base fee for `{}`",
215 public_balance,
216 locator.to_string().bold()
217 );
218 }
219 }
220
221 println!("✅ Created execution transaction for '{}'", locator.to_string().bold());
222
223 Developer::handle_transaction(
225 &endpoint,
226 &self.broadcast,
227 self.dry_run,
228 &self.store,
229 self.store_format,
230 self.wait,
231 self.timeout,
232 transaction,
233 locator.to_string(),
234 )
235 }
236}
237
238fn load_program<N: Network>(
240 query: &Query<N, BlockMemory<N>>,
241 process: &mut Process<N>,
242 program_id: &ProgramID<N>,
243 edition: u16,
244) -> Result<()> {
245 let program = query.get_program(program_id).with_context(|| "Failed to fetch program")?;
247
248 if process.contains_program(program.id()) {
250 return Ok(());
251 }
252
253 for import_program_id in program.imports().keys() {
255 if !process.contains_program(import_program_id) {
257 load_program(query, process, import_program_id, edition)
259 .with_context(|| format!("Failed to load imported program {import_program_id}"))?;
260 }
261 }
262
263 if !process.contains_program(program.id()) {
265 process
266 .add_programs_with_editions(&[(program, edition)])
267 .with_context(|| format!("Failed to add program {program_id}"))?;
268 }
269
270 Ok(())
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276 use crate::commands::{CLI, Command, DeveloperCommand};
277
278 #[test]
279 fn clap_snarkos_execute() -> Result<()> {
280 let arg_vec = &[
281 "snarkos",
282 "developer",
283 "execute",
284 "--private-key",
285 "PRIVATE_KEY",
286 "--endpoint=ENDPOINT",
287 "--priority-fee",
288 "77",
289 "--record",
290 "RECORD",
291 "--dry-run",
292 "hello.aleo",
293 "hello",
294 "1u32",
295 "2u32",
296 ];
297 let cli = CLI::try_parse_from(arg_vec)?;
298
299 let Command::Developer(developer) = cli.command else {
300 bail!("Unexpected result of clap parsing!");
301 };
302 let DeveloperCommand::Execute(execute) = developer.command else {
303 bail!("Unexpected result of clap parsing!");
304 };
305
306 assert_eq!(developer.network, 0);
307 assert_eq!(execute.private_key, Some("PRIVATE_KEY".to_string()));
308 assert_eq!(execute.endpoint, "ENDPOINT");
309 assert_eq!(execute.priority_fee, 77);
310 assert_eq!(execute.record, Some("RECORD".into()));
311 assert_eq!(execute.program_id, "hello.aleo".to_string());
312 assert_eq!(execute.function, "hello".to_string());
313 assert_eq!(execute.inputs, vec!["1u32".to_string(), "2u32".to_string()]);
314
315 Ok(())
316 }
317
318 #[test]
319 fn clap_snarkos_execute_pk_file() -> Result<()> {
320 let arg_vec = &[
321 "snarkos",
322 "developer",
323 "execute",
324 "--private-key-file",
325 "PRIVATE_KEY_FILE",
326 "--endpoint=ENDPOINT",
327 "--record",
328 "RECORD",
329 "--dry-run",
330 "hello.aleo",
331 "hello",
332 "1u32",
333 "2u32",
334 ];
335 let cli = CLI::try_parse_from(arg_vec)?;
336
337 let Command::Developer(developer) = cli.command else {
338 bail!("Unexpected result of clap parsing!");
339 };
340 let DeveloperCommand::Execute(execute) = developer.command else {
341 bail!("Unexpected result of clap parsing!");
342 };
343
344 assert_eq!(developer.network, 0);
345 assert_eq!(execute.private_key_file, Some("PRIVATE_KEY_FILE".to_string()));
346 assert_eq!(execute.endpoint, "ENDPOINT");
347 assert_eq!(execute.priority_fee, 0); assert_eq!(execute.record, Some("RECORD".into()));
349 assert_eq!(execute.program_id, "hello.aleo".to_string());
350 assert_eq!(execute.function, "hello".to_string());
351 assert_eq!(execute.inputs, vec!["1u32".to_string(), "2u32".to_string()]);
352
353 Ok(())
354 }
355
356 #[test]
357 fn clap_snarkos_execute_two_private_keys() {
358 let arg_vec = &[
359 "snarkos",
360 "developer",
361 "execute",
362 "--private-key",
363 "PRIVATE_KEY",
364 "--private-key-file",
365 "PRIVATE_KEY_FILE",
366 "--endpoint=ENDPOINT",
367 "--priority-fee",
368 "77",
369 "--record",
370 "RECORD",
371 "--dry-run",
372 "hello.aleo",
373 "hello",
374 "1u32",
375 "2u32",
376 ];
377
378 let err = CLI::try_parse_from(arg_vec).unwrap_err();
379 assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
380 }
381
382 #[test]
383 fn clap_snarkos_execute_no_private_keys() {
384 let arg_vec = &[
385 "snarkos",
386 "developer",
387 "execute",
388 "--endpoint=ENDPOINT",
389 "--priority-fee",
390 "77",
391 "--record",
392 "RECORD",
393 "--dry-run",
394 "hello.aleo",
395 "hello",
396 "1u32",
397 "2u32",
398 ];
399
400 let err = CLI::try_parse_from(arg_vec).unwrap_err();
401 assert_eq!(err.kind(), clap::error::ErrorKind::MissingRequiredArgument);
402 }
403}