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}