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::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}
107
108impl Drop for Execute {
109 fn drop(&mut self) {
111 if let Some(mut pk) = self.private_key.take() {
112 pk.zeroize()
113 }
114 }
115}
116
117impl Execute {
118 pub fn parse<N: Network>(self) -> Result<String> {
120 let endpoint = prepare_endpoint(self.endpoint.clone())?;
121
122 let query = Query::<N, BlockMemory<N>>::from(endpoint.clone());
124
125 let is_static_query = matches!(query, Query::STATIC(_));
127
128 let private_key = parse_private_key(self.private_key.clone(), self.private_key_file.clone(), self.dev_key)?;
130
131 let program_id = ProgramID::from_str(&self.program_id).with_context(|| "Failed to parse program ID")?;
133
134 let function = Identifier::from_str(&self.function).with_context(|| "Failed to parse function ID")?;
136
137 let inputs = self.inputs.iter().map(|input| Value::from_str(input)).collect::<Result<Vec<Value<N>>>>()?;
139
140 let locator = Locator::<N>::from_str(&format!("{program_id}/{function}"))?;
141 println!("📦 Creating execution transaction for '{}'...\n", &locator.to_string().bold());
142
143 let transaction = {
145 let rng = &mut rand::thread_rng();
147
148 let store = ConsensusStore::<N, ConsensusMemory<N>>::open(StorageMode::Production)?;
150
151 let vm = VM::from(store)?;
153
154 if !is_static_query && program_id != ProgramID::from_str("credits.aleo")? {
155 let height = query.current_block_height()?;
156 let version = N::CONSENSUS_VERSION(height)?;
157 debug!("At block height {height} and consensus {version:?}");
158 let edition = Developer::get_latest_edition(&endpoint, &program_id)
159 .with_context(|| format!("Failed to get latest edition for program {program_id}"))?;
160
161 load_program(&query, &mut vm.process().write(), &program_id, edition)?;
163 }
164
165 let fee_record = match &self.record {
167 Some(record_string) => Some(
168 Developer::parse_record(&private_key, record_string).with_context(|| "Failed to parse record")?,
169 ),
170 None => None,
171 };
172
173 vm.execute(
175 &private_key,
176 (program_id, function),
177 inputs.iter(),
178 fee_record,
179 self.priority_fee,
180 Some(&query),
181 rng,
182 )
183 .with_context(|| "VM failed to execute transaction locally")?
184 };
185
186 if self.record.is_none() && !is_static_query {
188 let address = Address::try_from(&private_key)?;
190 let public_balance = Developer::get_public_balance::<N>(&endpoint, &address)
191 .with_context(|| "Failed to check for sufficient funds to send transaction")?;
192
193 let storage_cost = transaction
195 .execution()
196 .with_context(|| "Failed to get execution cost of transaction")?
197 .size_in_bytes()?;
198
199 let base_fee = storage_cost.saturating_add(self.priority_fee);
203
204 if public_balance < base_fee {
206 bail!(
207 "The public balance of {} is insufficient to pay the base fee for `{}`",
208 public_balance,
209 locator.to_string().bold()
210 );
211 }
212 }
213
214 println!("✅ Created execution transaction for '{}'", locator.to_string().bold());
215
216 Developer::handle_transaction(
218 &endpoint,
219 &self.broadcast,
220 self.dry_run,
221 &self.store,
222 self.store_format,
223 self.wait,
224 self.timeout,
225 transaction,
226 locator.to_string(),
227 )
228 }
229}
230
231fn load_program<N: Network>(
233 query: &Query<N, BlockMemory<N>>,
234 process: &mut Process<N>,
235 program_id: &ProgramID<N>,
236 edition: u16,
237) -> Result<()> {
238 let program = query.get_program(program_id).with_context(|| "Failed to fetch program")?;
240
241 if process.contains_program(program.id()) {
243 return Ok(());
244 }
245
246 for import_program_id in program.imports().keys() {
248 if !process.contains_program(import_program_id) {
250 load_program(query, process, import_program_id, edition)
252 .with_context(|| format!("Failed to load imported program {import_program_id}"))?;
253 }
254 }
255
256 if !process.contains_program(program.id()) {
258 process
259 .add_programs_with_editions(&[(program, edition)])
260 .with_context(|| format!("Failed to add program {program_id}"))?;
261 }
262
263 Ok(())
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269 use crate::commands::{CLI, Command, DeveloperCommand};
270
271 #[test]
272 fn clap_snarkos_execute() -> Result<()> {
273 let arg_vec = &[
274 "snarkos",
275 "developer",
276 "execute",
277 "--private-key",
278 "PRIVATE_KEY",
279 "--endpoint=ENDPOINT",
280 "--priority-fee",
281 "77",
282 "--record",
283 "RECORD",
284 "--dry-run",
285 "hello.aleo",
286 "hello",
287 "1u32",
288 "2u32",
289 ];
290 let cli = CLI::try_parse_from(arg_vec)?;
291
292 let Command::Developer(developer) = cli.command else {
293 bail!("Unexpected result of clap parsing!");
294 };
295 let DeveloperCommand::Execute(execute) = developer.command else {
296 bail!("Unexpected result of clap parsing!");
297 };
298
299 assert_eq!(developer.network, 0);
300 assert_eq!(execute.private_key, Some("PRIVATE_KEY".to_string()));
301 assert_eq!(execute.endpoint, "ENDPOINT");
302 assert_eq!(execute.priority_fee, 77);
303 assert_eq!(execute.record, Some("RECORD".into()));
304 assert_eq!(execute.program_id, "hello.aleo".to_string());
305 assert_eq!(execute.function, "hello".to_string());
306 assert_eq!(execute.inputs, vec!["1u32".to_string(), "2u32".to_string()]);
307
308 Ok(())
309 }
310
311 #[test]
312 fn clap_snarkos_execute_pk_file() -> Result<()> {
313 let arg_vec = &[
314 "snarkos",
315 "developer",
316 "execute",
317 "--private-key-file",
318 "PRIVATE_KEY_FILE",
319 "--endpoint=ENDPOINT",
320 "--record",
321 "RECORD",
322 "--dry-run",
323 "hello.aleo",
324 "hello",
325 "1u32",
326 "2u32",
327 ];
328 let cli = CLI::try_parse_from(arg_vec)?;
329
330 let Command::Developer(developer) = cli.command else {
331 bail!("Unexpected result of clap parsing!");
332 };
333 let DeveloperCommand::Execute(execute) = developer.command else {
334 bail!("Unexpected result of clap parsing!");
335 };
336
337 assert_eq!(developer.network, 0);
338 assert_eq!(execute.private_key_file, Some("PRIVATE_KEY_FILE".to_string()));
339 assert_eq!(execute.endpoint, "ENDPOINT");
340 assert_eq!(execute.priority_fee, 0); assert_eq!(execute.record, Some("RECORD".into()));
342 assert_eq!(execute.program_id, "hello.aleo".to_string());
343 assert_eq!(execute.function, "hello".to_string());
344 assert_eq!(execute.inputs, vec!["1u32".to_string(), "2u32".to_string()]);
345
346 Ok(())
347 }
348
349 #[test]
350 fn clap_snarkos_execute_two_private_keys() {
351 let arg_vec = &[
352 "snarkos",
353 "developer",
354 "execute",
355 "--private-key",
356 "PRIVATE_KEY",
357 "--private-key-file",
358 "PRIVATE_KEY_FILE",
359 "--endpoint=ENDPOINT",
360 "--priority-fee",
361 "77",
362 "--record",
363 "RECORD",
364 "--dry-run",
365 "hello.aleo",
366 "hello",
367 "1u32",
368 "2u32",
369 ];
370
371 let err = CLI::try_parse_from(arg_vec).unwrap_err();
372 assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
373 }
374
375 #[test]
376 fn clap_snarkos_execute_no_private_keys() {
377 let arg_vec = &[
378 "snarkos",
379 "developer",
380 "execute",
381 "--endpoint=ENDPOINT",
382 "--priority-fee",
383 "77",
384 "--record",
385 "RECORD",
386 "--dry-run",
387 "hello.aleo",
388 "hello",
389 "1u32",
390 "2u32",
391 ];
392
393 let err = CLI::try_parse_from(arg_vec).unwrap_err();
394 assert_eq!(err.kind(), clap::error::ErrorKind::MissingRequiredArgument);
395 }
396}