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, anyhow, bail};
40use clap::{Parser, builder::NonEmptyStringValueParser};
41use colored::Colorize;
42use std::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)]
80 endpoint: Uri,
81 #[clap(long, default_value_t = 0)]
83 priority_fee: u64,
84 #[clap(short, long)]
86 record: Option<String>,
87 #[clap(short, long, group = "mode", verbatim_doc_comment)]
91 broadcast: Option<Option<Uri>>,
92 #[clap(short, long, group = "mode")]
94 dry_run: bool,
95 #[clap(long, group = "mode")]
97 store: Option<String>,
98 #[clap(long, value_enum, default_value_t = StoreFormat::Bytes, requires="store")]
101 store_format: StoreFormat,
102 #[clap(long, requires = "broadcast")]
104 wait: bool,
105 #[clap(long, default_value_t = 60, requires = "wait")]
107 timeout: u64,
108 #[clap(long, hide = true)]
110 skip_funds_check: bool,
111}
112
113impl Drop for Execute {
114 fn drop(&mut self) {
116 if let Some(mut pk) = self.private_key.take() {
117 pk.zeroize()
118 }
119 }
120}
121
122impl Execute {
123 pub fn parse<N: Network>(self) -> Result<String> {
125 let endpoint = prepare_endpoint(self.endpoint.clone())?;
126
127 let query = Query::<N, BlockMemory<N>>::from(endpoint.clone());
129
130 let is_static_query = matches!(query, Query::STATIC(_));
132
133 let private_key = parse_private_key(self.private_key.clone(), self.private_key_file.clone(), self.dev_key)?;
135
136 let program_id = ProgramID::from_str(&self.program_id).with_context(|| "Failed to parse program ID")?;
138
139 let function = Identifier::from_str(&self.function).with_context(|| "Failed to parse function ID")?;
141
142 let inputs = self.inputs.iter().map(|input| Value::from_str(input)).collect::<Result<Vec<Value<N>>>>()?;
144
145 let locator = Locator::<N>::from_str(&format!("{program_id}/{function}"))?;
146 println!("📦 Creating execution transaction for '{}'...\n", &locator.to_string().bold());
147
148 let transaction = {
150 let rng = &mut rand::thread_rng();
152
153 let store = ConsensusStore::<N, ConsensusMemory<N>>::open(StorageMode::Production)?;
155
156 let vm = VM::from(store)?;
158
159 if !is_static_query && program_id != ProgramID::from_str("credits.aleo")? {
160 let height = query.current_block_height().with_context(|| "Failed to retrieve current block height")?;
161 let version = N::CONSENSUS_VERSION(height)?;
162 debug!("At block height {height} and consensus {version:?}");
163
164 load_program(&query, &mut vm.process().write(), &program_id, &endpoint)?;
166 }
167
168 let fee_record = match &self.record {
170 Some(record_string) => Some(
171 Developer::parse_record(&private_key, record_string).with_context(|| "Failed to parse record")?,
172 ),
173 None => None,
174 };
175
176 vm.execute(
178 &private_key,
179 (program_id, function),
180 inputs.iter(),
181 fee_record,
182 self.priority_fee,
183 Some(&query),
184 rng,
185 )
186 .with_context(|| "VM failed to execute transaction locally")?
187 };
188
189 if self.record.is_none() && !is_static_query && !self.skip_funds_check {
191 let address = Address::try_from(&private_key)?;
193 let public_balance = Developer::get_public_balance::<N>(&endpoint, &address)
194 .with_context(|| "Failed to check for sufficient funds to send transaction")?
195 .ok_or_else(|| {
196 anyhow!(
197 "No public balance found for sending account `{}`. It may not exist.",
198 address.to_string().bold()
199 )
200 })?;
201
202 let storage_cost = transaction
204 .execution()
205 .with_context(|| "Failed to get execution cost of transaction")?
206 .size_in_bytes()?;
207
208 let base_fee = storage_cost.saturating_add(self.priority_fee);
212
213 if public_balance < base_fee {
215 bail!(
216 "The public balance of {} is insufficient to pay the base fee for `{}`",
217 public_balance,
218 locator.to_string().bold()
219 );
220 }
221 }
222
223 println!("✅ Created execution transaction for '{}'", locator.to_string().bold());
224
225 Developer::handle_transaction(
227 &endpoint,
228 &self.broadcast,
229 self.dry_run,
230 &self.store,
231 self.store_format,
232 self.wait,
233 self.timeout,
234 transaction,
235 locator.to_string(),
236 )
237 }
238}
239
240fn load_program<N: Network>(
242 query: &Query<N, BlockMemory<N>>,
243 process: &mut Process<N>,
244 program_id: &ProgramID<N>,
245 endpoint: &Uri,
246) -> Result<()> {
247 let program = query.get_program(program_id).with_context(|| "Failed to fetch program")?;
249 let edition = Developer::get_latest_edition(endpoint, program_id)
251 .with_context(|| format!("Failed to get latest edition for program {program_id}"))?;
252
253 if process.contains_program(program.id()) {
255 return Ok(());
256 }
257
258 for import_program_id in program.imports().keys() {
260 if !process.contains_program(import_program_id) {
262 load_program(query, process, import_program_id, endpoint)
264 .with_context(|| format!("Failed to load imported program {import_program_id}"))?;
265 }
266 }
267
268 if !process.contains_program(program.id()) {
270 debug!("Adding program {program_id} with edition {edition}");
271 process
272 .add_programs_with_editions(&[(program, edition)])
273 .with_context(|| format!("Failed to add program {program_id}"))?;
274 }
275
276 Ok(())
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282 use crate::commands::{CLI, Command, DeveloperCommand};
283
284 #[test]
285 fn clap_snarkos_execute() -> Result<()> {
286 let arg_vec = &[
287 "snarkos",
288 "developer",
289 "execute",
290 "--private-key",
291 "PRIVATE_KEY",
292 "--endpoint=ENDPOINT",
293 "--priority-fee",
294 "77",
295 "--record",
296 "RECORD",
297 "--dry-run",
298 "hello.aleo",
299 "hello",
300 "1u32",
301 "2u32",
302 ];
303 let cli = CLI::try_parse_from(arg_vec)?;
304
305 let Command::Developer(developer) = cli.command else {
306 bail!("Unexpected result of clap parsing!");
307 };
308 let DeveloperCommand::Execute(execute) = developer.command else {
309 bail!("Unexpected result of clap parsing!");
310 };
311
312 assert_eq!(developer.network, 0);
313 assert_eq!(execute.private_key, Some("PRIVATE_KEY".to_string()));
314 assert_eq!(execute.endpoint, "ENDPOINT");
315 assert_eq!(execute.priority_fee, 77);
316 assert_eq!(execute.record, Some("RECORD".into()));
317 assert_eq!(execute.program_id, "hello.aleo".to_string());
318 assert_eq!(execute.function, "hello".to_string());
319 assert_eq!(execute.inputs, vec!["1u32".to_string(), "2u32".to_string()]);
320
321 Ok(())
322 }
323
324 #[test]
325 fn clap_snarkos_execute_pk_file() -> Result<()> {
326 let arg_vec = &[
327 "snarkos",
328 "developer",
329 "execute",
330 "--private-key-file",
331 "PRIVATE_KEY_FILE",
332 "--endpoint=ENDPOINT",
333 "--record",
334 "RECORD",
335 "--dry-run",
336 "hello.aleo",
337 "hello",
338 "1u32",
339 "2u32",
340 ];
341 let cli = CLI::try_parse_from(arg_vec)?;
342
343 let Command::Developer(developer) = cli.command else {
344 bail!("Unexpected result of clap parsing!");
345 };
346 let DeveloperCommand::Execute(execute) = developer.command else {
347 bail!("Unexpected result of clap parsing!");
348 };
349
350 assert_eq!(developer.network, 0);
351 assert_eq!(execute.private_key_file, Some("PRIVATE_KEY_FILE".to_string()));
352 assert_eq!(execute.endpoint, "ENDPOINT");
353 assert_eq!(execute.priority_fee, 0); assert_eq!(execute.record, Some("RECORD".into()));
355 assert_eq!(execute.program_id, "hello.aleo".to_string());
356 assert_eq!(execute.function, "hello".to_string());
357 assert_eq!(execute.inputs, vec!["1u32".to_string(), "2u32".to_string()]);
358
359 Ok(())
360 }
361
362 #[test]
363 fn clap_snarkos_execute_two_private_keys() {
364 let arg_vec = &[
365 "snarkos",
366 "developer",
367 "execute",
368 "--private-key",
369 "PRIVATE_KEY",
370 "--private-key-file",
371 "PRIVATE_KEY_FILE",
372 "--endpoint=ENDPOINT",
373 "--priority-fee",
374 "77",
375 "--record",
376 "RECORD",
377 "--dry-run",
378 "hello.aleo",
379 "hello",
380 "1u32",
381 "2u32",
382 ];
383
384 let err = CLI::try_parse_from(arg_vec).unwrap_err();
385 assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
386 }
387
388 #[test]
389 fn clap_snarkos_execute_no_private_keys() {
390 let arg_vec = &[
391 "snarkos",
392 "developer",
393 "execute",
394 "--endpoint=ENDPOINT",
395 "--priority-fee",
396 "77",
397 "--record",
398 "RECORD",
399 "--dry-run",
400 "hello.aleo",
401 "hello",
402 "1u32",
403 "2u32",
404 ];
405
406 let err = CLI::try_parse_from(arg_vec).unwrap_err();
407 assert_eq!(err.kind(), clap::error::ErrorKind::MissingRequiredArgument);
408 }
409}