aptos_testcontainer/aptos_container.rs
1use std::collections::{HashMap, HashSet};
2use std::future::Future;
3use std::path::{Path, PathBuf};
4use std::pin::Pin;
5use std::time::Duration;
6use std::{fs, path};
7
8use anyhow::{ensure, Error, Result};
9use base64::prelude::BASE64_STANDARD;
10use base64::Engine;
11use log::debug;
12use rand::distributions::Alphanumeric;
13use rand::Rng;
14use regex::Regex;
15use testcontainers::core::{ExecCommand, IntoContainerPort, WaitFor};
16use testcontainers::runners::AsyncRunner;
17use testcontainers::{ContainerAsync, GenericImage, ImageExt};
18use tokio::io::AsyncReadExt;
19use tokio::sync::mpsc::{Receiver, Sender};
20use tokio::sync::{mpsc, Mutex, RwLock};
21use tokio::time::Instant;
22use walkdir::{DirEntry, WalkDir};
23
24use crate::config::EnvConfig;
25use crate::errors::AptosContainerError::{CommandFailed, DockerExecFailed};
26
27const MOVE_TOML: &[u8] = include_bytes!("../contract-samples/sample1/Move.toml");
28
29/// `AptosContainer` is a struct that encapsulates the configuration and runtime details
30/// for managing an Aptos node and its associated resources within a Docker container.
31///
32/// # Fields
33///
34/// * `node_url` - URL for accessing the Aptos node from external systems.
35///
36/// * `inner_url` - Internal URL for accessing the Aptos node from within the container
37/// or local environment.
38///
39/// * `chain_id` - Chain ID for the network.
40///
41/// * `deploy_contract` - If set to `true`, contracts will be deployed upon initialization.
42///
43/// * `override_accounts` - Flag indicating whether to deploy contracts to the Aptos node.
44/// Optional list of account addresses to override default accounts.
45///
46/// * `container` - The Docker container instance running the Aptos node or shell.
47///
48/// * `contract_path` - Path to the directory where contract files are stored.
49///
50/// * `contracts` - A mutex-protected set of contracts.
51///
52/// * `accounts` - A read-write lock protecting a list of account addresses.
53///
54/// * `accounts_channel_rx` - A mutex-protected optional receiver for account-related
55/// communication channels.
56///
57/// * `accounts_channel_tx` - A read-write lock protecting an optional sender for account-related
58/// communication channels.
59pub struct AptosContainer {
60 node_url: String,
61 inner_url: String,
62 chain_id: u8,
63 deploy_contract: bool,
64 override_accounts: Option<Vec<String>>,
65 container: ContainerAsync<GenericImage>,
66 contract_path: String,
67 contracts: Mutex<HashSet<String>>,
68 accounts: RwLock<Vec<String>>,
69 accounts_channel_rx: Mutex<Option<Receiver<String>>>,
70 accounts_channel_tx: RwLock<Option<Sender<String>>>,
71}
72
73const APTOS_IMAGE: &str = "sotazklabs/aptos-tools";
74const APTOS_IMAGE_TAG: &str = "mainnet";
75const FILTER_PATTERN: &str = r"^(?:\.git|target\/|.idea|Cargo.lock|build\/|.aptos\/)";
76
77const ACCOUNTS_ENV: &str = "ACCOUNTS";
78const CONTENT_MAX_CHARS: usize = 120000; // 120 KB
79
80impl AptosContainer {
81 /// Initializes a `AptosContainer`.
82 ///
83 /// # Returns
84 /// A new `AptosContainer` instance.
85 ///
86 /// # Example
87 /// ```rust
88 /// use aptos_testcontainer::aptos_container::AptosContainer;
89 ///
90 /// #[tokio::main]
91 /// async fn main() {
92 /// let aptos_container = AptosContainer::init().await.unwrap();
93 /// }
94 /// ```
95 pub async fn init() -> Result<Self> {
96 // Load configuration from environment
97 let config = EnvConfig::new();
98 let enable_node = config.enable_node.unwrap_or(true);
99
100 // Set up the container's entrypoint, command, and wait condition based on whether the node is enabled.
101 let (entrypoint, cmd, wait_for) = if enable_node {
102 (
103 "aptos",
104 vec!["node", "run-localnet", "--performance", "--no-faucet"],
105 WaitFor::message_on_stderr("Setup is complete, you can now use the localnet!"),
106 )
107 } else {
108 ("/bin/sh", vec!["-c", "sleep infinity"], WaitFor::Nothing)
109 };
110
111 // Create and start a new Docker container with the specified image and settings.
112 let container = GenericImage::new(APTOS_IMAGE, APTOS_IMAGE_TAG)
113 .with_exposed_port(8080.tcp())
114 .with_wait_for(wait_for)
115 .with_entrypoint(entrypoint)
116 .with_cmd(cmd)
117 .with_startup_timeout(Duration::from_secs(10))
118 .start()
119 .await?;
120
121 // Configure URLs and other parameters based on whether the node is enabled.
122 let (node_url, inner_url, deploy_contract, override_accounts, chain_id) = if enable_node {
123 let node_url = format!(
124 "http://{}:{}",
125 container.get_host().await?,
126 container.get_host_port_ipv4(8080).await?
127 );
128 (
129 node_url.to_string(),
130 "http://localhost:8080".to_string(),
131 true,
132 None,
133 4,
134 )
135 } else {
136 let node_url = config.node_url.unwrap().first().unwrap().to_string();
137 (
138 node_url.clone(),
139 node_url,
140 config.deploy_contract.unwrap_or(true),
141 Some(config.accounts.unwrap()),
142 config.chain_id.unwrap(),
143 )
144 };
145
146 Ok(Self {
147 node_url,
148 inner_url,
149 deploy_contract,
150 chain_id,
151 container,
152 override_accounts,
153 contract_path: "/contract".to_string(),
154 contracts: Default::default(),
155 accounts: Default::default(),
156 accounts_channel_rx: Default::default(),
157 accounts_channel_tx: Default::default(),
158 })
159 }
160}
161
162impl AptosContainer {
163 /// Get `node_url` from `AptosContainer`
164 pub fn get_node_url(&self) -> String {
165 self.node_url.clone()
166 }
167 /// Get `chain_id` from `AptosContainer`
168 pub fn get_chain_id(&self) -> u8 {
169 self.chain_id
170 }
171
172 /// Retrieves the list of accounts, either from an overridden source or from an initialized state.
173 ///
174 /// # Returns
175 ///
176 /// * `Result<Vec<String>>` - A vector containing the accounts as `String` if successful.
177 ///
178 /// # Example
179 /// ```rust
180 /// use aptos_testcontainer::aptos_container::AptosContainer;
181 ///
182 /// #[tokio::main]
183 /// async fn main() {
184 /// let aptos_container = AptosContainer::init().await.unwrap();
185 /// let accounts = aptos_container.get_initiated_accounts().await.unwrap();
186 /// }
187 /// ```
188 pub async fn get_initiated_accounts(&self) -> Result<Vec<String>> {
189 match &self.override_accounts {
190 Some(accounts) => Ok(accounts.clone()),
191 None => {
192 self.lazy_init_accounts().await?;
193 Ok(self.accounts.read().await.clone())
194 }
195 }
196 }
197 /// Generates a random alphanumeric string of the specified length.
198 ///
199 /// # Arguments
200 ///
201 /// * `length` - The length of the random string to generate.
202 ///
203 /// # Returns
204 ///
205 /// * `String` - A string of random alphanumeric characters of the specified length.
206 ///
207 fn generate_random_string(length: usize) -> String {
208 // Initialize a random number generator.
209 let rng = rand::thread_rng();
210 // Create an iterator that samples random characters from the Alphanumeric set.
211 let random_string: String = rng
212 .sample_iter(&Alphanumeric)
213 .take(length)
214 .map(char::from)
215 .collect();
216 random_string
217 }
218
219 /// Executes a shell command inside the Docker container.
220 ///
221 /// # Arguments
222 ///
223 /// * `command` - A string representing the shell command to execute inside the container.
224 ///
225 /// # Returns
226 ///
227 /// * `Result<(String, String)>` - A tuple containing the `stdout` and `stderr` outputs from
228 /// the command execution.
229 ///
230 /// # Example
231 /// ```rust
232 /// use aptos_testcontainer::aptos_container::AptosContainer;
233 ///
234 /// #[tokio::main]
235 /// async fn main() {
236 /// let aptos_container = AptosContainer::init().await.unwrap();
237 /// let command = "bin/sh -c mkdir my_file".to_string();
238 /// let (stdout, stderr) = aptos_container.run_command(&command).await.unwrap();
239 /// println!("stdout: {:?}", stdout);
240 /// println!("stderr: {:?}", stderr)
241 /// }
242 /// ```
243 pub async fn run_command(&self, command: &str) -> Result<(String, String)> {
244 // Execute the command inside the container using `/bin/sh -c`.
245 let mut result = self
246 .container
247 .exec(ExecCommand::new(vec!["/bin/sh", "-c", command]))
248 .await?;
249
250 // Check the exit code of the command.
251 result
252 .exit_code()
253 .await?
254 .map(|code| Err(Error::new(DockerExecFailed(code))))
255 .unwrap_or(Ok(()))?;
256 // Initialize empty strings for capturing stdout and stderr.
257 let mut stdout = String::new();
258 let mut stderr = String::new();
259
260 // Read the command's stdout into the `stdout` string.
261 result.stdout().read_to_string(&mut stdout).await?;
262 // Read the command's stderr into the `stderr` string.
263 result.stderr().read_to_string(&mut stderr).await?;
264 Ok((stdout, stderr))
265 }
266
267 /// Recursively retrieves a list of files from directory.
268 ///
269 /// # Arguments
270 ///
271 /// * `local_dir` - A string slice representing the path to the local directory to search for files.
272 ///
273 /// # Returns
274 ///
275 /// * `Vec<DirEntry>` - A vector of `DirEntry` objects representing the files that match the
276 /// filtering criteria.
277 fn get_files(local_dir: &str) -> Vec<DirEntry> {
278 WalkDir::new(local_dir)
279 .into_iter()
280 .filter_map(|e| e.ok())
281 .filter_map(|entry| {
282 let source_path = entry.path();
283 // Ignore files located in build folders.
284 if source_path.to_str().unwrap().contains("/build/") {
285 return None;
286 }
287
288 // Only consider files, not directories.
289 if !source_path.is_file() {
290 return None;
291 }
292 // Determine the relative path from the source directory
293 let relative_path = source_path.strip_prefix(local_dir).unwrap();
294 // Compile the regex pattern and check if the relative path matches the pattern.
295 let re = Regex::new(FILTER_PATTERN).unwrap();
296 if re.is_match(relative_path.to_str().unwrap()) {
297 return None;
298 }
299
300 // Check file size, excluding files larger than 1 MB.
301 let metadata = fs::metadata(source_path).unwrap();
302 let file_size = metadata.len();
303 let file_size_mb = file_size as f64 / (1024.0 * 1024.0);
304 if file_size_mb > 1_f64 {
305 return None;
306 }
307 // Include the entry if it passes all filters.
308 Some(entry)
309 })
310 .collect()
311 }
312
313 /// Lazily initializes the accounts if it has been initialized yet.
314 /// This ensures that accounts are set up either from an external source or
315 /// from environment variables only once, and avoids redundant initialization.
316 async fn lazy_init_accounts(&self) -> Result<()> {
317 // If override accounts are provided, skip initialization and return early.
318 if self.override_accounts.is_some() {
319 return Ok(());
320 }
321
322 // Lock the accounts_channel_tx to check if it's already initialized.
323 let mut guard = self.accounts_channel_tx.write().await;
324
325 // If accounts_channel_tx is already initialized, return early.
326 if guard.is_some() {
327 return Ok(());
328 }
329
330 // Prepare to fetch the accounts from the environment variable.
331 let command = format!("echo ${}", ACCOUNTS_ENV);
332 // Run the command to retrieve the accounts and capture stdout and stderr.
333 let (stdout, stderr) = self.run_command(&command).await?;
334 // Ensure that the command returned valid output; otherwise, raise an error.
335 ensure!(
336 !stdout.is_empty(),
337 CommandFailed {
338 command,
339 stderr: format!("stdout: {} \n\n stderr: {}", stdout, stderr)
340 }
341 );
342
343 // Parse the stdout into a list of account strings.
344 let accounts = stdout
345 .trim()
346 .split(",")
347 .map(|s| s.to_string())
348 .collect::<Vec<String>>();
349 // Create a new mpsc channel with a buffer size equal to the number of accounts.
350 let (tx, rx) = mpsc::channel(accounts.len());
351 // Send each account into the channel.
352 for account in accounts.iter() {
353 tx.send(account.to_string()).await?
354 }
355 // Lock the accounts field and write the parsed accounts into it.
356 *self.accounts.write().await = accounts;
357 // Lock the accounts_channel_rx and assign the receiver.
358 *self.accounts_channel_rx.lock().await = Some(rx);
359 // Assign the sender to accounts_channel_tx to finalize the initialization.
360 *guard = Some(tx);
361 // Return success.
362 Ok(())
363 }
364
365 /// Copies contract files from a local directory into the container's filesystem.
366 ///
367 /// # Arguments
368 ///
369 /// * `local_dir` - A path that refers to the local directory containing the contract files
370 /// to be copied into the container.
371 ///
372 /// # Returns
373 ///
374 /// * `Result<PathBuf>` - Returns the path where the contracts are copied in the container,
375 /// or an error if the copying process fails.
376 async fn copy_contracts(&self, local_dir: impl AsRef<Path>) -> Result<PathBuf> {
377 // Generate a random destination path by appending a random string to contract_path.
378 let contract_path =
379 Path::new(&self.contract_path).join(AptosContainer::generate_random_string(6));
380 let contract_path_str = contract_path.to_str().unwrap();
381
382 // Clear the previous run by removing any existing files at the target path.
383 let command = format!("rm -rf {}", contract_path_str);
384 let (_, stderr) = self.run_command(&command).await?;
385 // Ensure there are no errors when executing the removal command.
386 ensure!(stderr.is_empty(), CommandFailed { command, stderr });
387
388 // Copy files into the container
389 let local_dir_str = local_dir.as_ref().to_str().unwrap();
390 // Iterate over each file in the local directory.
391 for entry in AptosContainer::get_files(local_dir_str) {
392 let source_path = entry.path();
393 let relative_path = source_path.strip_prefix(local_dir_str)?;
394 let dest_path = contract_path.join(relative_path);
395 let content = fs::read(source_path)?;
396 let encoded_content = BASE64_STANDARD.encode(&content);
397 for chunk in encoded_content
398 .chars()
399 .collect::<Vec<char>>()
400 .chunks(CONTENT_MAX_CHARS)
401 {
402 let command = format!(
403 "mkdir -p \"$(dirname '{}')\" && (echo '{}' | base64 --decode >> '{}')",
404 dest_path.to_str().unwrap(),
405 chunk.iter().collect::<String>(),
406 dest_path.to_str().unwrap()
407 );
408 let (_, stderr) = self.run_command(&command).await?;
409 ensure!(stderr.is_empty(), CommandFailed { command, stderr });
410 }
411 }
412 Ok(contract_path)
413 }
414
415 /// This async function handles account initialization and execution of a callback function with the provided or received accounts.
416 ///
417 /// # Parameters:
418 /// - `number_of_accounts`: The number of accounts required for the operation.
419 /// - `callback`: A closure that takes the accounts and returns a `Future` wrapped in a `Pin` and boxed as a dynamic trait `Future<Output = Result<()>>`.
420 ///
421 /// # Example
422 /// ```rust
423 /// use aptos_testcontainer::aptos_container::AptosContainer;
424 /// use aptos_testcontainer::utils::get_account_address;
425 /// use std::collections::HashMap;
426 ///
427 /// #[tokio::main]
428 /// async fn main() {
429 /// let aptos_containe = AptosContainer::init().await.unwrap();
430 /// let _ = aptos_containe.run(2, |accounts| {
431 /// Box::pin(async move {
432 /// let aptos_container = AptosContainer::init().await.unwrap();
433 /// let accounts = aptos_container.get_initiated_accounts().await.unwrap();
434 /// let module_account_private_key = accounts.first().unwrap();
435 /// let module_account_address = get_account_address(module_account_private_key);
436 /// let mut named_addresses = HashMap::new();
437 /// named_addresses.insert("verifier_addr".to_string(), module_account_address);
438 /// aptos_container
439 /// .upload_contract(
440 /// "./contract-samples/sample1",
441 /// module_account_private_key,
442 /// &named_addresses,
443 /// None,
444 /// false,
445 /// )
446 /// .await
447 /// .unwrap();
448 /// Ok(())
449 /// })
450 /// });
451 /// }
452 /// ```
453 pub async fn run(
454 &self,
455 number_of_accounts: usize,
456 callback: impl FnOnce(Vec<String>) -> Pin<Box<dyn Future<Output = Result<()>>>>,
457 ) -> Result<()> {
458 // Ensure that accounts are initialized, if not already done.
459 self.lazy_init_accounts().await?;
460
461 // Determine whether to use overridden accounts or to receive them via the channel.
462 let accounts = match &self.override_accounts {
463 // If override_accounts is Some, clone the provided accounts.
464 Some(accounts) => accounts.clone(),
465 // Otherwise, receive the accounts from the accounts_channel_rx.
466 None => {
467 // TODO: check received messages size
468 let mut result = vec![];
469 // Lock the accounts_channel_rx to ensure exclusive access and receive accounts.
470 self.accounts_channel_rx
471 .lock()
472 .await
473 .as_mut()
474 .unwrap()
475 .recv_many(&mut result, number_of_accounts)
476 .await;
477 result
478 }
479 };
480
481 // Invoke the provided callback with the received or overridden accounts.
482 let result = callback(accounts.clone()).await;
483
484 if self.override_accounts.is_none() {
485 let guard = self.accounts_channel_tx.read().await;
486 for account in accounts {
487 guard.as_ref().unwrap().send(account).await?;
488 }
489 }
490 result
491 }
492
493 /// Executes a script located within the specified directory.
494 ///
495 /// # Parameters
496 /// - `local_dir`: The directory path containing contract code.
497 /// - `private_key`: The private key of the account that will sign and execute the scripts.
498 /// - `named_addresses`: A mapping of named addresses used for the script compilation.
499 /// - `script_paths`: A vector of sub-directory paths within the `local_dir` where the scripts are located.
500 ///
501 /// # Example
502 /// ```rust
503 /// use std::collections::HashMap;
504 /// use aptos_testcontainer::aptos_container::AptosContainer;
505 /// use aptos_testcontainer::utils::get_account_address;
506 ///
507 /// #[tokio::main]
508 /// async fn main() {
509 /// let aptos_container = AptosContainer::init().await.unwrap();
510 /// let accounts = aptos_container.get_initiated_accounts().await.unwrap();
511 /// let module_account_private_key = accounts.first().unwrap();
512 /// let module_account_address = get_account_address(module_account_private_key);
513 ///
514 /// let local_dir = "./contract-samples/sample2";
515 ///
516 /// let mut named_addresses = HashMap::new();
517 /// named_addresses.insert("verifier_addr".to_string(), module_account_address.clone());
518 /// named_addresses.insert("lib_addr".to_string(), module_account_address);
519 /// aptos_container
520 /// .run_script(
521 /// local_dir,
522 /// module_account_private_key,
523 /// &named_addresses,
524 /// &vec!["verifier"],
525 /// )
526 /// .await
527 /// .unwrap();
528 /// }
529 /// ```
530 pub async fn run_script(
531 &self,
532 local_dir: impl AsRef<Path>,
533 private_key: &str,
534 named_addresses: &HashMap<String, String>,
535 script_paths: &Vec<&str>,
536 ) -> Result<()> {
537 // Start the timer for performance measurement
538 let now = Instant::now();
539
540 // Copy contract files to the container and get the path
541 let contract_path = self.copy_contracts(local_dir).await?;
542 debug!("copy_contracts takes: {:.2?}", now.elapsed());
543
544 // Convert contract path to a string
545 let contract_path_str = contract_path.to_str().unwrap();
546
547 // Build named addresses as CLI parameters
548 let named_address_params = named_addresses
549 .iter()
550 .map(|(k, v)| format!("{}={}", k, v))
551 .reduce(|acc, cur| format!("{},{}", acc, cur))
552 .map(|named_addresses| format!("--named-addresses {}", named_addresses))
553 .unwrap_or("".to_string());
554
555 // Compile and run each script in the provided paths
556 for script_path in script_paths {
557 // Compile script
558 let command = format!(
559 "cd {}/{} && aptos move compile-script --skip-fetch-latest-git-deps {}",
560 contract_path_str,
561 script_path,
562 named_address_params.as_str()
563 );
564 let (stdout, stderr) = self.run_command(&command).await?;
565 ensure!(
566 stdout.contains(r#""script_location":"#),
567 CommandFailed {
568 command,
569 stderr: format!("stdout: {} \n\n stderr: {}", stdout, stderr)
570 }
571 );
572
573 // Run script
574 let command = format!(
575 "cd {}/{} && aptos move run-script --compiled-script-path script.mv --private-key {} --url {} --assume-yes",
576 contract_path_str, script_path, private_key, self.inner_url
577 );
578 let (stdout, stderr) = self.run_command(&command).await?;
579 ensure!(
580 stdout.contains(r#""vm_status": "Executed successfully""#),
581 CommandFailed {
582 command,
583 stderr: format!("stdout: {} \n\n stderr: {}", &stdout, stderr)
584 }
585 );
586 }
587 Ok(())
588 }
589
590 /// Uploads smart contracts to the Aptos node, optionally overriding existing contracts and
591 /// handling sub-packages.
592 ///
593 /// # Arguments
594 ///
595 /// * `local_dir` - The local directory containing the contract files.
596 /// * `private_key` - The private key used for publishing the contract.
597 /// * `named_addresses` - A hash map of named addresses for the contracts.
598 /// * `sub_packages` - Optional list of sub-packages to handle separately. If `None`, the entire
599 /// contract directory is handled as a whole.
600 /// * `override_contract` - A boolean flag indicating whether to override existing contracts.
601 ///
602 /// # Example
603 /// ```rust
604 /// use std::collections::HashMap;
605 /// use aptos_testcontainer::aptos_container::AptosContainer;
606 /// use aptos_testcontainer::utils::get_account_address;
607 ///
608 /// #[tokio::main]
609 /// async fn main() {
610 /// let aptos_container = AptosContainer::init().await.unwrap();
611 /// let accounts = aptos_container.get_initiated_accounts().await.unwrap();
612 /// let module_account_private_key = accounts.first().unwrap();
613 /// let module_account_address = get_account_address(module_account_private_key);
614 /// let mut named_addresses = HashMap::new();
615 /// named_addresses.insert("verifier_addr".to_string(), module_account_address);
616 /// aptos_container
617 /// .upload_contract(
618 /// "./contract-samples/sample1",
619 /// module_account_private_key,
620 /// &named_addresses,
621 /// None,
622 /// false,
623 /// )
624 /// .await
625 /// .unwrap();
626 /// }
627 /// ```
628 pub async fn upload_contract(
629 &self,
630 local_dir: &str,
631 private_key: &str,
632 named_addresses: &HashMap<String, String>,
633 sub_packages: Option<Vec<&str>>,
634 override_contract: bool,
635 ) -> Result<()> {
636 // Skip the upload process if contracts should not be deployed.
637 if !self.deploy_contract {
638 return Ok(());
639 }
640
641 // Compute absolute path and contract key.
642 let absolute = path::absolute(local_dir)?;
643 let absolute_contract_path = absolute.to_str().unwrap();
644 let contract_key = format!("{}:{}", private_key, absolute_contract_path);
645
646 // Check if the contract has already been uploaded and whether overriding is allowed.
647 let mut inserted_contracts = self.contracts.lock().await;
648 if !override_contract && inserted_contracts.contains(&contract_key) {
649 return Ok(());
650 }
651 // Copy contracts to a new location and log the time taken.
652 let now = Instant::now();
653 let contract_path = self.copy_contracts(local_dir).await?;
654 debug!("copy_contracts takes: {:.2?}", now.elapsed());
655
656 let contract_path_str = contract_path.to_str().unwrap();
657
658 // Override `Move.toml` if no sub-packages are provided.
659 if sub_packages.is_none() {
660 // Override Move.toml
661 let dest_path = contract_path.join("Move.toml");
662 let encoded_content = BASE64_STANDARD.encode(MOVE_TOML);
663 let command = format!(
664 "mkdir -p \"$(dirname '{}')\" && (echo '{}' | base64 --decode > '{}')",
665 dest_path.to_str().unwrap(),
666 encoded_content,
667 dest_path.to_str().unwrap()
668 );
669 let (_, stderr) = self.run_command(&command).await?;
670 ensure!(stderr.is_empty(), CommandFailed { command, stderr });
671 }
672
673 // Run move publish
674 let named_address_params = named_addresses
675 .iter()
676 .map(|(k, v)| format!("{}={}", k, v))
677 .reduce(|acc, cur| format!("{},{}", acc, cur))
678 .map(|named_addresses| format!("--named-addresses {}", named_addresses))
679 .unwrap_or("".to_string());
680 match sub_packages {
681 None => {
682 let command = format!(
683 "cd {} && aptos move publish --skip-fetch-latest-git-deps --private-key {} --assume-yes {} --url {} --included-artifacts none",
684 contract_path_str, private_key, named_address_params, self.inner_url
685 );
686 let (stdout, stderr) = self.run_command(&command).await?;
687 ensure!(
688 stdout.contains(r#""vm_status": "Executed successfully""#),
689 CommandFailed {
690 command,
691 stderr: format!("stdout: {} \n\n stderr: {}", stdout, stderr)
692 }
693 );
694 }
695 Some(sub_packages) => {
696 for sub_package in sub_packages {
697 let command = format!(
698 "cd {}/{} && aptos move publish --skip-fetch-latest-git-deps --private-key {} --assume-yes {} --url {} --included-artifacts none",
699 contract_path_str, sub_package, private_key, named_address_params, self.inner_url
700 );
701 let (stdout, stderr) = self.run_command(&command).await?;
702 ensure!(
703 stdout.contains(r#""vm_status": "Executed successfully""#),
704 CommandFailed {
705 command,
706 stderr: format!("stdout: {} \n\n stderr: {}", stdout, stderr)
707 }
708 );
709 }
710 }
711 }
712
713 // Add the contract key to the set of inserted contracts.
714 inserted_contracts.insert(contract_key);
715 Ok(())
716 }
717}
718
719#[cfg(test)]
720#[cfg(feature = "testing")]
721mod tests {
722 use log::info;
723 use test_log::test;
724
725 use super::*;
726 use crate::test_utils::aptos_container_test_utils::{lazy_aptos_container, run};
727 use crate::utils::get_account_address;
728
729 #[test(tokio::test)]
730 async fn run_script_test() {
731 run(2, |accounts| {
732 Box::pin(async move {
733 let aptos_container = lazy_aptos_container().await?;
734 let module_account_private_key = accounts.first().unwrap();
735 let module_account_address = get_account_address(module_account_private_key);
736
737 let mut named_addresses = HashMap::new();
738 named_addresses.insert("verifier_addr".to_string(), module_account_address.clone());
739 named_addresses.insert("lib_addr".to_string(), module_account_address);
740 aptos_container
741 .run_script(
742 "./contract-samples/sample2",
743 module_account_private_key,
744 &named_addresses,
745 &vec!["verifier"],
746 )
747 .await
748 .unwrap();
749 let node_url = aptos_container.get_node_url();
750 info!("node_url = {:#?}", node_url);
751 Ok(())
752 })
753 })
754 .await
755 .unwrap();
756 }
757
758 #[test(tokio::test)]
759 async fn upload_contract_1_test() {
760 run(2, |accounts| {
761 Box::pin(async move {
762 let aptos_container = lazy_aptos_container().await?;
763 let module_account_private_key = accounts.first().unwrap();
764 let module_account_address = get_account_address(module_account_private_key);
765
766 let mut named_addresses = HashMap::new();
767 named_addresses.insert("verifier_addr".to_string(), module_account_address);
768 aptos_container
769 .upload_contract(
770 "./contract-samples/sample1",
771 module_account_private_key,
772 &named_addresses,
773 None,
774 false,
775 )
776 .await
777 .unwrap();
778 let node_url = aptos_container.get_node_url();
779 info!("node_url = {:#?}", node_url);
780 Ok(())
781 })
782 })
783 .await
784 .unwrap();
785 }
786
787 #[test(tokio::test)]
788 async fn upload_contract_1_test_duplicated() {
789 run(2, |accounts| {
790 Box::pin(async move {
791 let aptos_container = lazy_aptos_container().await?;
792 let module_account_private_key = accounts.first().unwrap();
793
794 let module_account_address = get_account_address(module_account_private_key);
795
796 let mut named_addresses = HashMap::new();
797 named_addresses.insert("verifier_addr".to_string(), module_account_address);
798 aptos_container
799 .upload_contract(
800 "./contract-samples/sample1",
801 module_account_private_key,
802 &named_addresses,
803 None,
804 false,
805 )
806 .await
807 .unwrap();
808 let node_url = aptos_container.get_node_url();
809 info!("node_url = {:#?}", node_url);
810 Ok(())
811 })
812 })
813 .await
814 .unwrap();
815 }
816
817 #[test(tokio::test)]
818 async fn upload_contract_2_test() {
819 run(2, |accounts| {
820 Box::pin(async move {
821 let aptos_container = lazy_aptos_container().await?;
822 let module_account_private_key = accounts.first().unwrap();
823 let module_account_address = get_account_address(module_account_private_key);
824 let mut named_addresses = HashMap::new();
825 named_addresses.insert("verifier_addr".to_string(), module_account_address.clone());
826 named_addresses.insert("lib_addr".to_string(), module_account_address);
827 aptos_container
828 .upload_contract(
829 "./contract-samples/sample2",
830 module_account_private_key,
831 &named_addresses,
832 Some(vec!["libs", "verifier"]),
833 false,
834 )
835 .await
836 .unwrap();
837 let node_url = aptos_container.get_node_url();
838 println!("node_url = {:#?}", node_url);
839 Ok(())
840 })
841 })
842 .await
843 .unwrap();
844 }
845}