diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index 040c944a..f29627d9 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -22,6 +22,7 @@ concurrency: env: CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 jobs: test-crates: diff --git a/Cargo.lock b/Cargo.lock index 7ce2073a..eb5d529e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4270,6 +4270,18 @@ dependencies = [ "tracing", ] +[[package]] +name = "jsonrpc" +version = "0.1.0" +source = "git+https://github.com/mystenlabs/sui?rev=3b96ab72dd5db2fb800837d6067bf45839178b62#3b96ab72dd5db2fb800837d6067bf45839178b62" +dependencies = [ + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tracing", +] + [[package]] name = "jsonrpsee" version = "0.24.10" @@ -5298,7 +5310,7 @@ dependencies = [ "indexmap 2.12.1", "indoc", "itertools 0.10.5", - "jsonrpc", + "jsonrpc 0.1.0 (git+https://github.com/MystenLabs/sui.git?rev=211e22a5b0e08f8840f6a3e74120e1b4b04d5adb)", "move-command-line-common", "move-compiler", "move-core-types", @@ -5569,7 +5581,7 @@ checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" [[package]] name = "mvr" -version = "0.0.14" +version = "0.1.0" dependencies = [ "anyhow", "bin-version 1.57.0", @@ -5577,6 +5589,7 @@ dependencies = [ "expect-test", "futures", "insta", + "jsonrpc 0.1.0 (git+https://github.com/mystenlabs/sui?rev=3b96ab72dd5db2fb800837d6067bf45839178b62)", "mvr-types", "regex", "reqwest", @@ -9283,7 +9296,7 @@ dependencies = [ "bip32", "colored", "fastcrypto 0.1.9 (git+https://github.com/MystenLabs/fastcrypto?rev=4db0e90c732bbf7420ca20de808b698883148d9c)", - "jsonrpc", + "jsonrpc 0.1.0 (git+https://github.com/MystenLabs/sui.git?rev=211e22a5b0e08f8840f6a3e74120e1b4b04d5adb)", "mockall", "rand 0.8.5", "regex", diff --git a/Cargo.toml b/Cargo.toml index d4501c93..717629bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ sui-indexer-alt-framework = { git = "https://github.com/MystenLabs/sui.git", rev sui-rpc-api = { git = "https://github.com/MystenLabs/sui.git", rev = "211e22a5b0e08f8840f6a3e74120e1b4b04d5adb" } sui-storage = { git = "https://github.com/MystenLabs/sui.git", rev = "211e22a5b0e08f8840f6a3e74120e1b4b04d5adb" } sui-field-count = { git = "https://github.com/MystenLabs/sui.git", rev = "211e22a5b0e08f8840f6a3e74120e1b4b04d5adb"} +jsonrpc = { git = "https://github.com/mystenlabs/sui.git", rev = "3b96ab72dd5db2fb800837d6067bf45839178b62" } # New Rust SDK sui-sdk-types = { git = "https://github.com/MystenLabs/sui-rust-sdk", package = "sui-sdk-types", rev = "339c2272fd5b8fb4e1fa6662cfa9acdbb0d05704", features = ["hash", "serde"] } diff --git a/crates/ci-tests/tests/snapshots/use_case_tester__cmd-mvr_--help.snap b/crates/ci-tests/tests/snapshots/use_case_tester__cmd-mvr_--help.snap index 913b7952..00967656 100644 --- a/crates/ci-tests/tests/snapshots/use_case_tester__cmd-mvr_--help.snap +++ b/crates/ci-tests/tests/snapshots/use_case_tester__cmd-mvr_--help.snap @@ -13,7 +13,7 @@ Commands: help Print this message or the help of the given subcommand(s) Options: - --resolve-move-dependencies - --json Output the result in JSON format - -h, --help Print help - -V, --version Print version + --resolve-deps + --json Output the result in JSON format + -h, --help Print help + -V, --version Print version diff --git a/crates/ci-tests/tests/snapshots/use_case_tester__std-output-regular-mainnet-add-with-dot-sui.snap b/crates/ci-tests/tests/snapshots/use_case_tester__std-output-regular-mainnet-add-with-dot-sui.snap index f8fb6d5b..207f9069 100644 --- a/crates/ci-tests/tests/snapshots/use_case_tester__std-output-regular-mainnet-add-with-dot-sui.snap +++ b/crates/ci-tests/tests/snapshots/use_case_tester__std-output-regular-mainnet-add-with-dot-sui.snap @@ -7,3 +7,7 @@ Active environment switched to [mainnet] Successfully added dependency pkg.sui/qwer/1 to your Move.toml You can use this dependency in your modules by calling: use mvr_a::; + +Output from mvr: + │ [mvr] detected supported SUI CLI version + │ [mvr] resolving: "@pkg/qwer/1" on network: mainnet diff --git a/crates/ci-tests/tests/snapshots/use_case_tester__std-output-unsupported-network-without-env.snap b/crates/ci-tests/tests/snapshots/use_case_tester__std-output-unsupported-network-without-env.snap index bf3a8ef1..e162bc3d 100644 --- a/crates/ci-tests/tests/snapshots/use_case_tester__std-output-unsupported-network-without-env.snap +++ b/crates/ci-tests/tests/snapshots/use_case_tester__std-output-unsupported-network-without-env.snap @@ -3,5 +3,6 @@ source: crates/ci-tests/tests/use_case_tester.rs expression: std_output --- Active environment switched to [devnet] +[mvr] detected supported SUI CLI version Error: The requested network (or chain identifier) is not supported. Only `mainnet` and `testnet` are supported. If you are using this locally, you can set the `MVR_FALLBACK_NETWORK` environment variable to `mainnet` or `testnet`. diff --git a/crates/mvr-api/.gitignore b/crates/mvr-api/.gitignore index 1c79342b..a305497b 100644 --- a/crates/mvr-api/.gitignore +++ b/crates/mvr-api/.gitignore @@ -1,2 +1,3 @@ .env .env.local +Move.lock diff --git a/crates/mvr-cli/Cargo.toml b/crates/mvr-cli/Cargo.toml index b958f72d..85579f42 100644 --- a/crates/mvr-cli/Cargo.toml +++ b/crates/mvr-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mvr" -version = "0.0.14" +version = "0.1.0" edition = "2021" description = "MVR is a command line tool to interact with the Move Registry for the Sui network." license = "Apache-2.0" @@ -14,15 +14,15 @@ path = "src/main.rs" name = "mvr" path = "src/lib.rs" -[[test]] -name = "unit_tests" -path = "tests/unit_tests.rs" - [dependencies] bin-version = { git = "https://github.com/mystenlabs/sui", package = "bin-version", rev = "3b96ab72dd5db2fb800837d6067bf45839178b62" } +jsonrpc.workspace = true clap = { workspace = true, features = ["derive"] } -reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } +reqwest = { version = "0.12", default-features = false, features = [ + "json", + "rustls-tls", +] } tempfile.workspace = true tokio = { workspace = true, features = ["full"] } toml_edit = "0.22" @@ -37,7 +37,7 @@ thiserror.workspace = true mvr-types = { path = "../mvr-types" } # Rust SDK -sui-sdk-types.workspace = true +sui-sdk-types = { workspace = true, features = ["serde"] } [dev-dependencies] expect-test = "1.5.0" diff --git a/crates/mvr-cli/src/commands.rs b/crates/mvr-cli/src/commands.rs index c4e04c0d..2b6f67fe 100644 --- a/crates/mvr-cli/src/commands.rs +++ b/crates/mvr-cli/src/commands.rs @@ -75,8 +75,7 @@ impl Display for CommandOutput { let description = pkg .metadata .get("description") - .map(|s| s.as_str()) - .flatten() + .and_then(|s| s.as_str()) .map(|s| s.to_string()) .unwrap_or("--".italic().to_string()); @@ -110,7 +109,6 @@ impl Display for CommandOutput { "\n{}", "There are multiple pages of results. Use the cursor to paginate through the results." .italic() - .to_string() )?; writeln!( f, @@ -118,7 +116,6 @@ impl Display for CommandOutput { format!("mvr search --cursor {}", next_cursor) .italic() .blue() - .to_string() )?; } diff --git a/crates/mvr-cli/src/constants.rs b/crates/mvr-cli/src/constants.rs index 37fb62ea..39560c22 100644 --- a/crates/mvr-cli/src/constants.rs +++ b/crates/mvr-cli/src/constants.rs @@ -1,6 +1,6 @@ use std::fmt::{self, Display, Formatter}; -pub const MINIMUM_BUILD_SUI_VERSION: (u32, u32) = (1, 35); +pub const MINIMUM_BUILD_SUI_VERSION: (u32, u32) = (1, 63); pub enum EnvVariables { SuiBinaryPath, diff --git a/crates/mvr-cli/src/lib.rs b/crates/mvr-cli/src/lib.rs index e752fd95..8db04d7e 100644 --- a/crates/mvr-cli/src/lib.rs +++ b/crates/mvr-cli/src/lib.rs @@ -4,652 +4,25 @@ pub mod errors; pub mod types; pub mod utils; -use crate::types::MoveTomlPublishedID; +use crate::constants::MINIMUM_BUILD_SUI_VERSION; +use crate::types::api_data::{query_multiple_dependencies, query_package, search_names}; +use crate::utils::sui_binary::{cache_package, check_sui_version}; use commands::CommandOutput; -use errors::CliError; -use mvr_types::name::VersionedName; -use types::api_types::PackageRequest; -use types::api_types::SafeGitInfo; use types::Network; -use utils::api_data::resolve_name; -use utils::api_data::search_names; -use utils::api_data::{query_multiple_dependencies, query_package}; -use utils::git::shallow_clone_repo; -use sui_sdk_types::Address; use types::MoveRegistryDependencies; -use utils::manifest::{MoveToml, ADDRESSES_KEY, DEPENDENCIES_KEY, PUBLISHED_AT_KEY}; +use utils::manifest::MoveToml; use utils::sui_binary::get_active_network; -use std::collections::HashMap; use std::env; -use std::fs::{self}; -use std::io::{self, Write}; -use std::path::PathBuf; -use std::str::FromStr; -use anyhow::{anyhow, bail, Context, Result}; -use tempfile::TempDir; -use toml_edit::{ - value, Array, ArrayOfTables, DocumentMut, Formatted, InlineTable, Item, Table, Value, -}; +use anyhow::{Context, Result}; use yansi::Paint; -const LOCK_MOVE_KEY: &str = "move"; -const LOCK_PACKAGE_KEY: &str = "package"; -const LOCK_PACKAGE_NAME_KEY: &str = "name"; -const LOCK_PACKAGE_ID_KEY: &str = "id"; -const LOCK_PACKAGE_VERSION_KEY: &str = "version"; - const TESTNET_CHAIN_ID: &str = "4c78adac"; const MAINNET_CHAIN_ID: &str = "35834a8a"; -/// Resolves move registry packages. This function is invoked by `sui move build` and given the -/// a key associated with the external resolution in a Move.toml file. For example, this -/// TOML would trigger sui move build to call this binary and hit this function with value `mvr`: -/// -/// MyDep = { r.mvr = "@mvr/demo" } -/// -/// The high-level logic of this function is as follows: -/// 1) Fetch on-chain data for `packages`: the GitHub repository, branch, and subpath -/// 2) Fetches the package dependency graph from the package source (e.g., from GitHub). -/// 3) Constructs the dependency graph represented in the `Move.lock` format and emits it -/// for each package, allow the `sui move build` command to resolve packages from GitHub. -pub async fn resolve_move_dependencies(key: &str) -> Result<()> { - let content = fs::read_to_string("Move.toml")?; - - // Get the active network from the CLI. - let network = get_active_network()?; - - // we won't be writing in this phase. - let move_toml = MoveToml::new_from_content(&content, None)?; - - let dependency = move_toml.get_dependencies_by_name(key)?; - - eprintln!( - "{} {}: {:?} {} {}", - "RESOLVING".blue(), - key.blue().bold(), - dependency.packages.blue().bold(), - "ON".blue(), - network.blue().bold(), - ); - - // If the network is set in the Move.toml, warn that it is deprecated and no longer used. - if move_toml.get_network().is_some() { - eprintln!("Warning: The `[r.mvr]` network's section in Move.toml is no longer used. Adding/building packages depends on the active network from Sui CLI."); - } - - let resolved_packages = query_multiple_dependencies(dependency, &network).await?; - - let temp_dir = TempDir::new().context("Failed to create temporary directory")?; - let mut fetched_files: HashMap = HashMap::new(); - - for (name_with_version, package_info) in &resolved_packages { - let (key, value) = fetch_package_files(name_with_version, package_info, &temp_dir).await?; - fetched_files.insert(key, value); - } - - check_address_consistency(&resolved_packages, &network, &fetched_files).await?; - - let lock_files = build_lock_files(&resolved_packages, &fetched_files).await?; - - // Output each lock file content separated by null characters - for (i, content) in lock_files.iter().enumerate() { - io::stdout().write_all(content.as_bytes())?; - - // Add null character separator, except after the last element - if i < lock_files.len() - 1 { - io::stdout().write_all(&[0])?; - } - } - - Ok(()) -} - -/// Returns the mvr name and associated (Move.toml, Move.lock) files -/// downloaded from the specified on-chain info. -async fn fetch_package_files( - name_with_version: &str, - request_data: &PackageRequest, - temp_dir: &TempDir, -) -> Result<(String, (PathBuf, PathBuf))> { - let name = VersionedName::from_str(name_with_version)?; - - let (move_toml_path, move_lock_path) = - fetch_move_files(&name, &request_data.get_git_info()?.try_into()?, temp_dir).await?; - - Ok(( - name_with_version.to_string(), - (move_toml_path, move_lock_path), - )) -} - -pub async fn check_address_consistency( - resolved_packages: &HashMap, - network: &Network, - fetched_files: &HashMap, -) -> Result<()> { - for (name_with_version, request_data) in resolved_packages { - check_single_package_consistency(name_with_version, request_data, network, fetched_files) - .await?; - } - Ok(()) -} - -async fn check_single_package_consistency( - name_with_version: &str, - request_data: &PackageRequest, - network: &Network, - fetched_files: &HashMap, -) -> Result<()> { - let versioned_name = VersionedName::from_str(name_with_version)?; - - eprintln!( - "{} {} {} {}", - "USING ON-CHAIN VERSION".blue(), - request_data.version.blue().bold(), - "OF".blue(), - name_with_version.blue().bold(), - ); - - let original_address_on_chain = if request_data.version > 1 { - let mut versioned_one_name = versioned_name.clone(); - versioned_one_name.version = Some(1); - - resolve_name(&versioned_one_name, network) - .await - .map_err(|_| { - anyhow::anyhow!("Failed to retrieve original package address at version 1".red()) - })? - } else { - Address::from_str(&request_data.package_address)? - }; - - let git_info: SafeGitInfo = request_data.get_git_info()?.try_into()?; - - let (move_toml_path, move_lock_path) = - fetched_files.get(name_with_version).ok_or_else(|| { - anyhow!( - "{} {}", - "Failed to find fetched files `Move.toml` and \ - `Move.lock when checking address consistency for" - .red(), - name_with_version.red().bold() - ) - })?; - let move_toml_content = fs::read_to_string(move_toml_path)?; - let move_lock_content = fs::read_to_string(move_lock_path)?; - - // The `published-at` address or package in the [addresses] section _may_ correspond to - // the original ID in the Move.toml (we will check). - let MoveTomlPublishedID { - addresses_id, - published_at_id, - .. - } = published_ids(&move_toml_content, &original_address_on_chain).await; - - let target_chain_id = get_chain_id(network)?; - - let address = addresses_id - .map(|id_str| { - Address::from_str(&id_str).map_err(|e| { - anyhow!( - "{} {}", - "Failed to parse address in [addresses] section of Move.toml:".red(), - e.red() - ) - }) - }) - .transpose()?; - let published_at = published_at_id - .map(|id_str| { - Address::from_str(&id_str).map_err(|e| { - anyhow!( - "{} {}", - "Failed to parse published-at address of Move.toml:".red(), - e.red() - ) - }) - }) - .transpose()?; - - // The original-published-id may exist in the Move.lock - let original_published_id_in_lock = original_published_id(&move_lock_content, &target_chain_id) - .map(|id_str| { - Address::from_str(&id_str).map_err(|e| { - anyhow!( - "{} {}", - "Failed to parse original-published-id in Move.lock:".red(), - e.red() - ) - }) - }) - .transpose()?; - - let (original_source_id, provenance): (Address, String) = match ( - original_published_id_in_lock, - published_at, - address, - ) { - (Some(id), _, _) => (id, "Move.lock".into()), // precedence is given to resolving an ID from the lock - (None, Some(published_at_id), None) => { - // We couldn't find a published ID from [addresses] (it doesn't exist, or does not have a name - // to reliably identify it by). - // Our best guess is that the published-id refers to the original package (it may not, but - // if it doesn't, there is nowhere else to look in this case). - ( - published_at_id, - "published-at address in the Move.toml".into(), - ) - } - (None, Some(published_at_id), Some(address_id)) - if address_id == Address::ZERO || published_at_id == address_id => - { - // The [addresses] section has a package name set to "0x0" or the same as the published_at_id. - // Our best guess is that the published-id refers to the original package (it may not, but - // if it doesn't, there is nowhere else to look in this case). - ( - published_at_id, - "published-at address in the Move.toml".into(), - ) - } - - (None, _, Some(address_id)) => { - // A published-at ID may or may not exist. In either case, it differs from the - // address ID. The address ID that may refer to the original package (e.g., if the - // package was upgraded). - // Our best guess is that the id in the [addresses] section refers to the original ID. - // It may be "0x0" or the original ID. - ( - address_id, - "address in the [addresses] section of the Move.toml".into(), - ) - } - _ => { - bail!( - "Unable to find the original published address in package {}'s \ - repository {} branch {} subdirectory {}. For predictable detection, see documentation \ - on Automated Address Management for recording published addresses in the `Move.lock` file.", - versioned_name.name, - git_info.repository_url, - git_info.tag, - git_info.path - ) - } - }; - - // Main consistency check: The on-chain package address should correspond to the original ID in the source package. - if original_address_on_chain != original_source_id { - bail!( - "Mismatch: The original package address for {} on {network} is {original_address_on_chain}, \ - but the {provenance} in {}'s repository was found to be {original_source_id}.\n\ - Check the configuration of the package's repository {} in branch {} in subdirectory {}", - versioned_name.name, - versioned_name.name, - git_info.repository_url, - git_info.tag, - git_info.path - ) - } - - Ok(()) -} - -/// Returns as information from the Move.toml that resovles the original published address of a -/// package, and likely internal package name based on addresses in the [addresses] section. The -/// internal package name may be assigned the "0x0" address (if automated address management is -/// used). Otherwise, it may be assigned the value of the known original_address_on_chain, which is -/// used to reverse-lookup a candidate package name in the addresses section of the Move.toml. -pub async fn published_ids( - move_toml_content: &str, - original_address_on_chain: &Address, -) -> MoveTomlPublishedID { - let doc = match move_toml_content.parse::() { - Ok(d) => d, - Err(_) => return MoveTomlPublishedID::default(), - }; - - let package_table = match doc.get("package").and_then(|p| p.as_table()) { - Some(t) => t, - None => return MoveTomlPublishedID::default(), - }; - - // Get published-at - let published_at_id = package_table - .get(PUBLISHED_AT_KEY) - .and_then(|value| value.as_str()) - .map(String::from); - - // Get the addresses table - let addresses = match doc.get(ADDRESSES_KEY).and_then(|a| a.as_table()) { - Some(a) => a, - None => { - return MoveTomlPublishedID { - published_at_id, - ..Default::default() - } - } - }; - - // Find a potential original published ID in the addresses section. - // In general we can't identify which entry in `[addresses]` correspond - // to this package solely from the `Move.toml` unless we compile and parse - // it out of a compiled module (we are not going to do that). - // - // We can determine if such an original published ID exists with high - // likelihood by: - // (1) Identifying a package set to the original_address_on_chain - // (if not using automated address management); OR - // (2) Identifying a package set to `0x0`, which likely refers to the package itself - // (when using automated address management); OR - // (3) Identifying an entry where the key corresponds to the lowercase string - // of the package name. This is a heuristic based on the convention generally - // followed in Move.toml, but not guaranteed. In practice we expect either of - // (1) or (2) to succeed for a published package. - // - // If the Move.toml contents thwart all these attempts at identifying the original - // package ID, the package is better off adopting Automated Address Management for - // predictable detection, and the caller can raise an error. - - // (1) First, check if any address corresponds to the original_address_on_chain - let package_with_original_address = addresses - .iter() - .find(|(_, v)| v.as_str() == Some(&original_address_on_chain.to_string())) - .map(|(k, _)| k.to_string()); - if let Some(name) = package_with_original_address { - // (1) A package name exists that corresponds to the original address - return MoveTomlPublishedID { - addresses_id: Some(original_address_on_chain.to_string()), - published_at_id, - internal_pkg_name: Some(name), - }; - }; - - // (2) & (3) Next, check if any address is set to "0x0" - let package_with_zero_address = addresses - .iter() - .find(|(_, v)| v.as_str() == Some("0x0")) - .map(|(k, _)| k.to_string()); - match package_with_zero_address { - // (2) We found a package set to 0x0 - Some(_) => MoveTomlPublishedID { - addresses_id: Some("0x0".into()), - published_at_id, - internal_pkg_name: package_with_zero_address, - }, - // (3) We'll return a package ID that corresponds to the lowercase package name - // set in the Move.toml (if any). - None => { - let package_name = package_table - .get(LOCK_PACKAGE_NAME_KEY) - .and_then(|v| v.as_str()) - .map(|s| s.to_lowercase()); - let addresses_id = package_name.clone().and_then(|name| { - addresses - .get(&name) - .and_then(|v| v.as_str()) - .map(String::from) - }); - MoveTomlPublishedID { - addresses_id, - published_at_id, - internal_pkg_name: package_name, - } - } - } -} - -fn original_published_id(move_toml_content: &str, target_chain_id: &str) -> Option { - let doc = move_toml_content.parse::().ok()?; - let table = doc - .get("env")? - .as_table()? - .iter() - .filter_map(|(_, value)| value.as_table()) - .find(|table| { - table - .get("chain-id") - .and_then(|v| v.as_str()) - .map_or(false, |id| id == target_chain_id) - }); - let original_published_id = table.and_then(|table| { - table - .get("original-published-id") - .and_then(|v| v.as_str()) - .map(String::from) - }); - original_published_id -} - -/// Constructs the dependency graphs for packages, represented in the `Move.lock` format. -/// For a given package `foo`, it fetches `foo`'s `Move.lock` in a source repository. -/// This `Move.lock` contains the transitive dependency graph of `foo`, but not `foo` itself. -/// Since we want to communicate `foo` (and the URL where it can be found) to `sui move build`, -/// we create a dependency graph in the `Move.lock` derived from `foo`'s original lock file -/// that contains `foo`. See `insert_root_dependency` for how this works. -pub async fn build_lock_files( - resolved_packages: &HashMap, - fetched_files: &HashMap, -) -> Result> { - let mut lock_files: Vec = vec![]; - - for (name_with_version, package_info) in resolved_packages { - let git_info: SafeGitInfo = package_info.get_git_info()?.try_into()?; - - let (move_toml_path, move_lock_path) = - fetched_files.get(name_with_version).ok_or_else(|| { - anyhow!("{} {}", - "Failed to find fetched files `Move.toml` and `Move.lock` when building package graph for".red(), - name_with_version.red().bold() - ) - })?; - let move_toml_content = fs::read_to_string(move_toml_path)?; - let move_lock_content = fs::read_to_string(move_lock_path)?; - let root_name_from_source = parse_source_package_name(&move_toml_content)?; - let lock_with_root = - insert_root_dependency(&move_lock_content, &root_name_from_source, &git_info)?; - - lock_files.push(lock_with_root); - } - - Ok(lock_files) -} - -async fn fetch_move_files( - name: &VersionedName, - git_info: &SafeGitInfo, - temp_dir: &TempDir, -) -> Result<(PathBuf, PathBuf)> { - let files_to_fetch = ["Move.toml", "Move.lock"]; - let repo_dir = shallow_clone_repo(name, git_info, temp_dir)?; - - let file_paths = files_to_fetch.map(|file_name| repo_dir.join(&git_info.path).join(file_name)); - - if !file_paths[0].exists() { - bail!(CliError::MissingTomlFile(name.to_string())); - } - - if !file_paths[1].exists() { - bail!(CliError::MissingLockFile(name.to_string())); - } - - Ok((file_paths[0].clone(), file_paths[1].clone())) -} - -/// For a given package `foo` and its original `Move.lock` (containing transitive dependencies), -/// we create a new graph represented in `Move.lock` such that: -/// 1) `foo` becomes the root dependency -/// 2) `foo` is added as a dependency in `packages` -/// 3) `foo`'s dependencies is set to the original `Move.lock`'s root dependencies. -fn insert_root_dependency( - toml_content: &str, - root_name: &str, - git_info: &SafeGitInfo, -) -> Result { - let mut doc = toml_content - .parse::() - .context("Failed to parse TOML content")?; - - let move_section = doc[LOCK_MOVE_KEY].as_table_mut().ok_or_else(|| { - anyhow!( - "{}{}{}", - "Expected [".red(), - LOCK_MOVE_KEY.red(), - "] table to construct graph in lock file".red() - ) - })?; - - let package_version = move_section - .get(LOCK_PACKAGE_VERSION_KEY) - .unwrap_or(&value(1)) - .as_integer() - .ok_or_else(|| anyhow!("Invalid version in lock file".red()))?; - - // Save the top-level `dependencies`, which will become the dependencies of the new root package. - let original_deps = move_section.get("dependencies").cloned(); - // Do the same for `dev-dependencies` - let original_dev_deps = move_section.get("dev-dependencies").cloned(); - - // Make the top-level `dependencies` point to the new root package. - let new_dep = { - let mut table = InlineTable::new(); - table.insert( - LOCK_PACKAGE_NAME_KEY, - Value::String(Formatted::new(root_name.to_string())), - ); - table.insert( - LOCK_PACKAGE_ID_KEY, - Value::String(Formatted::new(root_name.to_string())), - ); - Value::InlineTable(table) - }; - - let mut new_deps = Array::new(); - new_deps.push(new_dep); - move_section["dependencies"] = value(new_deps); - // Reset the `dev-dependencies` as they will get re-rooted, as long as - // there are some deps there. - if original_dev_deps.is_some() { - move_section["dev-dependencies"] = value(Array::new()); - } - - // Create a new root package entry, set its dependencies to the original top-level dependencies, and persist. - let mut new_package = Table::new(); - new_package.insert(LOCK_PACKAGE_ID_KEY, value(root_name)); - - let mut source = Table::new(); - source.insert("git", value(&git_info.repository_url.clone())); - source.insert("rev", value(&git_info.tag.clone())); - source.insert("subdir", value(&git_info.path.clone())); - new_package.insert("source", value(source.into_inline_table())); - - if let Some(deps) = original_deps { - new_package.insert("dependencies", deps); - } - - if let Some(deps) = original_dev_deps { - new_package.insert("dev-dependencies", deps); - } - - let packages = move_section - .entry("package") - .or_insert(Item::ArrayOfTables(ArrayOfTables::new())) - .as_array_of_tables_mut() - .ok_or_else(|| anyhow!("Failed to get or create package array in lock file".red()))?; - - for package in packages.iter_mut() { - if let Some(source) = convert_local_dep_to_git(package, &git_info)? { - package.insert("source", value(source)); - } - } - - packages.push(new_package); - - // If the lockfile is version 2, migrate to version 3. - // Migration to version 3 requires an `id` field in the lockfile. - if package_version < 3 { - migrate_to_version_three(packages)?; - move_section.insert(LOCK_PACKAGE_VERSION_KEY, value(3)); - } - - Ok(doc.to_string()) -} - -/// For `local` dependencies of a given package, we want to convert them into `git` dependencies, -/// where the `git` dependency's `repository_url` is the parent package's `repository_url`, -/// the `tag` is the parent package's `rev`, and the `path` is the parent package's `path` joined -/// with the `local` dependency's `path`. -/// -/// Before: -/// -/// id = "dep" -/// source = { local = "../token" } -/// After: -/// -/// id = "dep" -/// source = { git = "https://github.com/mvr-test/parent-package.git", rev = "v1.0.0", subdir = "packages/parent-package-dir/token" } -/// -fn convert_local_dep_to_git( - dependency: &Table, - git_info: &SafeGitInfo, -) -> Result> { - dependency - .get("source") - .and_then(|items| items.as_table_like()) - .and_then(|items| items.get("local")) - .map(|local| { - let mut new_source = Table::new(); - new_source.insert("git", value(&git_info.repository_url.clone())); - new_source.insert("rev", value(&git_info.tag.clone())); - - let local_str = local.as_str().ok_or_else(|| { - anyhow!("Failed to get local dependency path. Found empty path on transitive dependency: {}", local) - })?; - - let path = PathBuf::from(git_info.path.clone()) - .join(local_str) - .to_string_lossy() - .to_string(); - - new_source.insert("subdir", value(&path)); - - Ok(new_source.into_inline_table()) - }) - .transpose() -} - -fn parse_source_package_name(toml_content: &str) -> Result { - let doc = toml_content - .parse::() - .context("Failed to parse TOML content in lock file".red())?; - - let package_table = doc[LOCK_PACKAGE_KEY].as_table().ok_or_else(|| { - anyhow!( - "{}{}{}", - "Failed to find [".red(), - LOCK_PACKAGE_KEY.red(), - "] table in lock file".red() - ) - })?; - - let name = package_table[LOCK_PACKAGE_NAME_KEY] - .as_str() - .ok_or_else(|| { - anyhow!( - "{}{}{}{}{}", - "Failed to find '".red(), - LOCK_PACKAGE_NAME_KEY.red(), - "' in [".red(), - LOCK_PACKAGE_KEY.red(), - "] table in lock file".red() - ) - })?; - - Ok(name.to_string()) -} - async fn update_mvr_packages( mut move_toml: MoveToml, package_name: &str, @@ -661,32 +34,13 @@ async fn update_mvr_packages( let resolved_packages = query_multiple_dependencies(dependencies.clone(), network).await?; - // Infer package name to insert. - let temp_dir = TempDir::new().context("Failed to create temporary directory")?; - - let request_data = resolved_packages.get(package_name).unwrap(); // Invariant: must have resolved + let package_request = resolved_packages + .get(package_name) + .expect("[mvr invariant] Package must exist in resolved packages."); - let (_, (dep_move_toml_path, _)) = - fetch_package_files(package_name, request_data, &temp_dir).await?; + let cached_package = cache_package(package_request.clone(), network)?; - let dep_move_toml_content = fs::read_to_string(dep_move_toml_path)?; - - let placeholder_address = Address::from_str(&request_data.package_address)?; - - let MoveTomlPublishedID { - internal_pkg_name: Some(name), - .. - } = published_ids(&dep_move_toml_content, &placeholder_address).await - else { - bail!( - "Unable to discover a local name to give this package in your Move.toml, \ - please give this dependency an appropriate name and add it under \ - [dependencies] as follows: \ - YourName = {{ r.mvr = \"{package_name}\" }}" - .red() - ); - }; - move_toml.add_dependency(&name, &package_name)?; + move_toml.add_dependency(&cached_package.name, package_name)?; move_toml.save_to_file()?; @@ -696,13 +50,14 @@ async fn update_mvr_packages( "\nSuccessfully added dependency {} to your Move.toml\n", package_name.green() ), - &format!("use {}::;\n", name).green() + &format!("use {}::;\n", cached_package.name).green() ); Ok(output_msg) } pub async fn subcommand_add_dependency(package_name: &str) -> Result { + check_sui_version(MINIMUM_BUILD_SUI_VERSION)?; let move_toml = MoveToml::new( env::current_dir() .context("Failed to get current directory")? @@ -736,42 +91,6 @@ pub async fn subcommand_search_names( Ok(CommandOutput::Search(search_results)) } -/// Migrates the lockfile to version 3 from older versions, if necessary. -fn migrate_to_version_three(packages: &mut ArrayOfTables) -> Result<()> { - for package in packages.iter_mut() { - // if ID is missing from the core dependency, add it. - if !package.contains_key(LOCK_PACKAGE_ID_KEY) { - let name = package - .get(LOCK_PACKAGE_NAME_KEY) - .ok_or_else(|| anyhow!("Failed to get name for dependency"))?; - package.insert(LOCK_PACKAGE_ID_KEY, name.clone()); - } - - // Skip if there are no dependencies or if dependencies are not an array. - let dependencies = match package - .get_mut(DEPENDENCIES_KEY) - .and_then(|v| v.as_array_mut()) - { - Some(deps) => deps, - None => continue, - }; - - for dep in dependencies.iter_mut() { - if let Some(val) = dep.as_inline_table_mut() { - // Get name, skipping if not found - let name = val - .get(LOCK_PACKAGE_NAME_KEY) - .ok_or_else(|| anyhow!("Failed to get name"))?; - - if !val.contains_key(LOCK_PACKAGE_ID_KEY) { - val.insert(LOCK_PACKAGE_ID_KEY, name.clone()); - } - } - } - } - Ok(()) -} - fn get_chain_id(network: &Network) -> Result { match network { Network::Testnet => Ok(TESTNET_CHAIN_ID.to_string()), diff --git a/crates/mvr-cli/src/main.rs b/crates/mvr-cli/src/main.rs index 86b6a925..a96fb7fd 100644 --- a/crates/mvr-cli/src/main.rs +++ b/crates/mvr-cli/src/main.rs @@ -1,15 +1,19 @@ +use mvr::types::resolver_alt::new_package_resolver; + +use std::env; + use anyhow::Result; use clap::Parser; use mvr::utils::sui_binary::check_sui_version; -use mvr::{commands::Command, constants::MINIMUM_BUILD_SUI_VERSION, resolve_move_dependencies}; +use mvr::{commands::Command, constants::MINIMUM_BUILD_SUI_VERSION}; bin_version::bin_version!(); #[derive(Parser)] #[command(author, version = VERSION, propagate_version = true, about)] struct Cli { - #[arg(long)] - resolve_move_dependencies: Option, + #[arg(long, global = true)] + resolve_deps: bool, #[command(subcommand)] command: Option, @@ -23,11 +27,14 @@ struct Cli { async fn main() -> Result<()> { let cli = Cli::parse(); - if let Some(ref value) = cli.resolve_move_dependencies { + // If we are in the new package resolver, we wanna special handle it and return early. + if cli.resolve_deps { check_sui_version(MINIMUM_BUILD_SUI_VERSION)?; - // Resolver function that `sui move build` expects to call. - resolve_move_dependencies(&value).await?; - } else if let Some(command) = cli.command { + new_package_resolver().await?; + return Ok(()); + } + + if let Some(command) = cli.command { let output = command.execute().await?; if cli.json { println!("{}", serde_json::to_string_pretty(&output)?); @@ -35,13 +42,10 @@ async fn main() -> Result<()> { println!("{}", output); } } else { - let cli = Cli::parse_from(&["mvr", "--help"]); - match cli.command { - Some(x) => { - let c = x.execute().await?; - println!("{:?}", c.to_string()); - } - None => {} + let cli = Cli::parse_from(["mvr", "--help"]); + if let Some(x) = cli.command { + let c = x.execute().await?; + println!("{:?}", c.to_string()); } } diff --git a/crates/mvr-cli/src/utils/api_data.rs b/crates/mvr-cli/src/types/api_data.rs similarity index 99% rename from crates/mvr-cli/src/utils/api_data.rs rename to crates/mvr-cli/src/types/api_data.rs index 08414e5b..b32ab393 100644 --- a/crates/mvr-cli/src/utils/api_data.rs +++ b/crates/mvr-cli/src/types/api_data.rs @@ -25,7 +25,7 @@ pub async fn query_package(name: &str, network: &Network) -> Result<(String, Pac let response = reqwest::get(format!( "{}/v1/names/{}", get_api_url(network)?, - versioned_name.to_string() + versioned_name )) .await .map_err(|e| CliError::Querying(e.to_string()))?; diff --git a/crates/mvr-cli/src/types/mod.rs b/crates/mvr-cli/src/types/mod.rs index d7ea9f25..7080034c 100644 --- a/crates/mvr-cli/src/types/mod.rs +++ b/crates/mvr-cli/src/types/mod.rs @@ -1,4 +1,6 @@ +pub mod api_data; pub mod api_types; +pub mod resolver_alt; use std::fmt; use std::str::FromStr; @@ -10,34 +12,12 @@ use crate::errors::CliError; use crate::MAINNET_CHAIN_ID; use crate::TESTNET_CHAIN_ID; -#[derive(Serialize, Default, Debug)] -pub struct MoveTomlPublishedID { - pub addresses_id: Option, - pub published_at_id: Option, - pub internal_pkg_name: Option, -} - #[derive(Debug, Deserialize, Serialize, Clone)] pub struct MoveRegistryDependencies { pub packages: Vec, } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub(crate) struct Env { - alias: String, - rpc: String, - ws: Option, - /// Basic HTTP access authentication in the format of username:password, if needed. - basic_auth: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub(crate) struct SuiConfig { - active_env: String, - envs: Vec, -} - -#[derive(Debug, Serialize, Deserialize, Clone, Copy)] +#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, PartialOrd, Eq, Ord)] pub enum Network { Mainnet, Testnet, diff --git a/crates/mvr-cli/src/types/resolver_alt.rs b/crates/mvr-cli/src/types/resolver_alt.rs new file mode 100644 index 00000000..51643d03 --- /dev/null +++ b/crates/mvr-cli/src/types/resolver_alt.rs @@ -0,0 +1,186 @@ +use std::{ + collections::{BTreeMap, BTreeSet}, + env, + io::stdin, + str::FromStr, +}; + +use anyhow::Result; +use jsonrpc::types::{BatchRequest, JsonRpcResult, RemoteError, RequestID, Response, TwoPointZero}; +use mvr_types::name::VersionedName; +use serde::Deserialize; +use sui_sdk_types::Address; +use yansi::Paint; + +use crate::{ + types::{api_data::query_multiple_dependencies, MoveRegistryDependencies, Network}, + utils::sui_binary::cache_package, +}; + +#[derive(Deserialize, Debug)] +struct ResolveRequest { + #[serde(default)] + // We expect a "chain-id" populated here, or we'll resolve on all known chain ids (mainnet / testnet) + env: Option, + + // we expect the "data" to be a plain string, being a MVR Name. + data: String, +} + +/// The new (package-alt) resolver which uses `sui`'s `cache-package` to bulk-resolve & cache packages. +pub async fn new_package_resolver() -> Result<()> { + let input = parse_input(); + + if input.is_empty() { + let response: Response = format_result( + 0, + JsonRpcResult::Err { + error: RemoteError { + code: 404, + message: "You requested a batch resolution, but no names were provided" + .to_string(), + data: None, + }, + }, + ); + println!( + "{}", + serde_json::to_string_pretty(&response).unwrap_or_default() + ); + return Ok(()); + } + + // Peek into the first value and extract the network we'll use for the resolution. + let network = get_normalized_network( + &input + .first_key_value() + .unwrap() + .1 + .env + .clone() + .unwrap_or_default(), + )?; + + let mut names = BTreeSet::new(); + + for (_, request) in &input { + let name = VersionedName::from_str(&request.data)?; + names.insert(name.to_string()); + } + + let package_responses = query_multiple_dependencies( + MoveRegistryDependencies { + packages: names.iter().map(|n| n.to_string()).collect(), + }, + &network, + ) + .await?; + + // Cache all packages and do an address/version check! + for (name, response) in &package_responses { + eprintln!( + "{}: {:?} {} {}", + "[mvr] resolving".blue(), + name.blue().bold(), + "on network: ".blue(), + network.blue().bold(), + ); + + let cached_package = cache_package(response.clone(), &network)?; + let address = Address::from_str(&response.package_address.clone())?; + + // Detect address missmatches to protect users. + if address != cached_package.original_id && address != cached_package.published_at { + let err_message = format!("Package {} on network {} has an address missmatch. Neither of its addresses match the expected address", name, network); + let response: Response = format_result( + 0, + JsonRpcResult::Err { + error: RemoteError { + code: 404, + message: err_message, + data: None, + }, + }, + ); + println!( + "{}", + serde_json::to_string_pretty(&response).unwrap_or_default() + ); + return Ok(()); + } + } + + let responses: Vec> = input + .into_iter() + .map(|(id, request)| { + let Some(response) = package_responses.get(&request.data) else { + return format_result(id, JsonRpcResult::Err { + error: RemoteError { code: 404, message: format!("No name entries found for {}", request.data), data: None } + }); + }; + + let Some(git_info) = &response.git_info else { + return format_result(id, JsonRpcResult::Err { + error: RemoteError { code: 404, message: format!("Package with name {} does not have git info for env {}", request.data, network), data: None } + }); + }; + + format_result(id, JsonRpcResult::Ok { + result: serde_json::json!({ "git": git_info.repository_url, "rev": git_info.tag, "subdir": git_info.path }) + }) + }) + .collect(); + + let json = serde_json::to_string(&responses).unwrap_or_default(); + + println!("{json}"); + Ok(()) +} + +/// Read a [Request] from [stdin] +fn parse_input() -> BTreeMap { + let mut line = String::new(); + stdin().read_line(&mut line).expect("stdin can be read"); + + let batch: BatchRequest = serde_json::from_str(&line) + .expect("External resolver must be passed a JSON RPC batch request"); + + batch + .into_iter() + .map(|req| { + assert!(req.method == "resolve"); + (req.id, req.params) + }) + .collect() +} + +/// Returns the "normalized" network: +/// 1. If the chain-id of the env is known, then we return that. +/// 2. If the chain-id is not known, we try to get the `flag`-based setup. +/// 3. We error with the "original" error. +fn get_normalized_network(env: &str) -> Result { + let normalized_network = Network::try_from_chain_identifier(&env); + + if let Ok(normalized_network) = normalized_network { + return Ok(normalized_network); + } + + let fallback_network = env::var("MVR_FALLBACK_NETWORK") + .ok() + .map(|s| Network::from_str(&s)) + .transpose(); + + if let Ok(Some(fallback_network)) = fallback_network { + return Ok(fallback_network); + } + + Ok(normalized_network?) +} + +fn format_result(id: u64, result: JsonRpcResult) -> Response { + Response { + jsonrpc: TwoPointZero, + id, + result, + } +} diff --git a/crates/mvr-cli/src/utils/git.rs b/crates/mvr-cli/src/utils/git.rs deleted file mode 100644 index 6da997ce..00000000 --- a/crates/mvr-cli/src/utils/git.rs +++ /dev/null @@ -1,193 +0,0 @@ -use std::{path::PathBuf, process::Command}; - -use anyhow::{anyhow, bail, Context, Result}; -use mvr_types::name::VersionedName; -use tempfile::TempDir; -use yansi::Paint; - -use crate::types::api_types::SafeGitInfo; - -macro_rules! clone_error { - ($phase:expr, $name:expr, $git:expr, $stderr:expr) => { - bail!( - "{}: `{}`, {} {} {} {}", - $phase.red(), - $name.red().bold(), - "Git error:".red(), - $stderr.red().bold(), - "Repository:".red(), - $git.repository_url.red().bold(), - ); - }; -} - -/// Shallow clones a git repository, into a temp directory. -/// This clone is shallow, because it does not download the entire history, -/// but only the latest commit (for mainnet), as well as the requested tag / branch. -pub(crate) fn shallow_clone_repo( - package_name: &VersionedName, - git_info: &SafeGitInfo, - temp_dir: &TempDir, -) -> Result { - let name = package_name.to_string(); - if Command::new("git").arg("--version").output().is_err() { - return Err(anyhow!( - "Git is not available in the system PATH. Please install git and try again.".red() - )); - } - let repo_dir = temp_dir.path().join(&name); - - // We clone the repo, but only with 1 level depth to avoid downloading the entire history. - let output = Command::new("git") - .arg("clone") - .arg("--filter=blob:none") - .arg("--no-checkout") - .arg(&git_info.repository_url) - .arg(&repo_dir) - .output() - .context("Failed to execute git clone command")?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - clone_error!("Failed to clone repository", &name, &git_info, &stderr); - } - - let output = Command::new("git") - .arg("-C") - .arg(&repo_dir) - .arg("sparse-checkout") - .arg("init") - .arg("--cone") - .output() - .context("Failed to execute git sparse-checkout init command")?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - clone_error!( - "Failed to initialize sparse checkout", - &name, - &git_info, - &stderr - ); - } - - let set_to_path = Command::new("git") - .arg("-C") - .arg(&repo_dir) - .arg("sparse-checkout") - .arg("set") - .arg(&git_info.path) - .output() - .context("Failed to execute git sparse-checkout set command")?; - - if !set_to_path.status.success() { - let stderr = String::from_utf8_lossy(&set_to_path.stderr); - clone_error!("Failed to set sparse checkout", &name, &git_info, &stderr); - } - - let checkout_cmd = Command::new("git") - .arg("-C") - .arg(&repo_dir) - .arg("checkout") - .arg(&git_info.tag) - .output() - .context("Failed to execute git checkout command")?; - - if !checkout_cmd.status.success() { - let stderr = String::from_utf8_lossy(&checkout_cmd.stderr); - clone_error!("Failed to checkout tag", &name, &git_info, &stderr); - } - - Ok(repo_dir) -} - -#[cfg(test)] -mod tests { - use std::str::FromStr; - - use super::*; - - fn get_git_sha(dir: &PathBuf) -> String { - let command = Command::new("git") - .arg("-C") - .arg(dir) - .arg("rev-parse") - .arg("HEAD") - .output() - .unwrap(); - - assert!(command.status.success()); - String::from_utf8_lossy(&command.stdout) - .trim() - .to_ascii_lowercase() - } - - fn get_git_info(tag: &str) -> SafeGitInfo { - SafeGitInfo { - repository_url: "https://github.com/MystenLabs/mvr.git".to_string(), - path: "crates/mvr-cli".to_string(), - tag: tag.to_string(), - } - } - - fn get_main_repo_git_info() -> SafeGitInfo { - SafeGitInfo { - repository_url: "https://github.com/MystenLabs/sui.git".to_string(), - path: "crates/sui-framework/packages/sui-framework".to_string(), - tag: "framework/mainnet".to_string(), - } - } - - #[test] - fn test_shallow_clone_repo_by_branch_name() { - let temp_dir = TempDir::new().unwrap(); - let git_info = get_git_info("release"); - - let versioned = VersionedName::from_str("@mvr/demo").unwrap(); - - let repo_dir = shallow_clone_repo(&versioned, &git_info, &temp_dir).unwrap(); - assert!(repo_dir.exists()); - } - - #[test] - fn test_shallow_clone_repo_by_tag_name() { - let temp_dir = TempDir::new().unwrap(); - let git_info = get_git_info("v0.0.1"); - - let versioned = VersionedName::from_str("@mvr/demo").unwrap(); - - let repo_dir = shallow_clone_repo(&versioned, &git_info, &temp_dir).unwrap(); - - let sha = get_git_sha(&repo_dir); - assert_eq!(sha, "188f032f3fd39485d38a8d07966164d895a64b13"); - } - - #[test] - fn test_shallow_clone_repo_by_commit_sha() { - let temp_dir = TempDir::new().unwrap(); - let git_info = get_git_info("188f032f3fd39485d38a8d07966164d895a64b13"); - - let versioned = VersionedName::from_str("@mvr/demo").unwrap(); - let repo_dir = shallow_clone_repo(&versioned, &git_info, &temp_dir).unwrap(); - - let sha = get_git_sha(&repo_dir); - assert_eq!(sha, "188f032f3fd39485d38a8d07966164d895a64b13"); - } - - #[test] - fn shallow_clone_sui_framework() { - let temp_dir: TempDir = TempDir::new().unwrap(); - let git_info = get_main_repo_git_info(); - - let versioned = VersionedName::from_str("@sui/framework").unwrap(); - let repo_dir = shallow_clone_repo(&versioned, &git_info, &temp_dir).unwrap(); - - assert!(repo_dir.exists()); - - let pkg_path = repo_dir.join(git_info.path); - - assert!(pkg_path.exists()); - assert!(pkg_path.join("Move.toml").exists()); - assert!(pkg_path.join("Move.lock").exists()); - } -} diff --git a/crates/mvr-cli/src/utils/manifest.rs b/crates/mvr-cli/src/utils/manifest.rs index 59f8473e..25270dc0 100644 --- a/crates/mvr-cli/src/utils/manifest.rs +++ b/crates/mvr-cli/src/utils/manifest.rs @@ -50,7 +50,7 @@ impl MoveToml { ); new_dep_table.insert(RESOLVER_PREFIX_KEY, Value::InlineTable(r_table)); - dependencies.insert(&name, Item::Value(Value::InlineTable(new_dep_table))); + dependencies.insert(name, Item::Value(Value::InlineTable(new_dep_table))); Ok(()) } @@ -61,8 +61,7 @@ impl MoveToml { .get(RESOLVER_PREFIX_KEY) .and_then(|v| v.get(MVR_RESOLVER_KEY)) .and_then(|v| v.get(NETWORK_KEY)) - .map(|v| v.as_str()) - .flatten() + .and_then(|v| v.as_str()) .map(|s| s.to_string()) } diff --git a/crates/mvr-cli/src/utils/mod.rs b/crates/mvr-cli/src/utils/mod.rs index fe926d7d..cf85fe56 100644 --- a/crates/mvr-cli/src/utils/mod.rs +++ b/crates/mvr-cli/src/utils/mod.rs @@ -1,4 +1,2 @@ -pub mod api_data; -pub mod git; pub mod manifest; pub mod sui_binary; diff --git a/crates/mvr-cli/src/utils/sui_binary.rs b/crates/mvr-cli/src/utils/sui_binary.rs index b94a5a0c..3b29c86b 100644 --- a/crates/mvr-cli/src/utils/sui_binary.rs +++ b/crates/mvr-cli/src/utils/sui_binary.rs @@ -3,18 +3,35 @@ use anyhow::bail; use anyhow::Error; use anyhow::Result; use regex::Regex; +use serde::Deserialize; +use serde::Serialize; use std::env; use std::process::Command; use std::process::Output; use std::str::FromStr; +use sui_sdk_types::Address; use yansi::Paint; -use crate::constants::{EnvVariables, MINIMUM_BUILD_SUI_VERSION}; +use crate::constants::EnvVariables; use crate::errors::CliError; +use crate::get_chain_id; +use crate::types::api_types::PackageRequest; +use crate::types::api_types::SafeGitInfo; use crate::types::Network; const VERSION_REGEX: &str = r"(\d+)\.(\d+)\.(\d+)"; +#[derive(Debug, Serialize, Deserialize, Clone)] +// The result of `cache-package` command +pub struct SuiCachePackageResponse { + pub name: String, + #[serde(rename = "published-at")] + pub published_at: Address, + #[serde(rename = "original-id")] + pub original_id: Address, + pub chain_id: String, +} + /// Check the sui binary's version and print it to the console. /// This can be used pub fn check_sui_version(expected_version: (u32, u32)) -> Result<(), Error> { @@ -56,13 +73,7 @@ pub fn check_sui_version(expected_version: (u32, u32)) -> Result<(), Error> { ), ); - eprintln!( - "{} {}{}{}", - "DETECTED sui VERSION".blue(), - major.blue().bold(), - ".".blue().bold(), - minor.blue().bold() - ); + eprintln!("{}", "[mvr] detected supported SUI CLI version".blue()); } else { eprintln!("Could not find version components in the output."); } @@ -76,15 +87,6 @@ pub fn check_sui_version(expected_version: (u32, u32)) -> Result<(), Error> { Ok(()) } -/// Calls `{sui} move build`. Currently needed when: -/// 1. Adding a new dependency (mvr add) -/// 2. Setting the network (mvr set-network) -pub fn force_build() -> Result<(), Error> { - check_sui_version(MINIMUM_BUILD_SUI_VERSION)?; - sui_command(["move", "build"].to_vec())?; - Ok(()) -} - /// Gets the active network by calling `sui client chain-identifier` from the Sui CLI, or falls back /// to the `MVR_FALLBACK_NETWORK` environment variable for other networks. This is useful for local testing. /// @@ -101,7 +103,7 @@ pub fn get_active_network() -> Result { let cli_network = Network::try_from_chain_identifier(&chain_id); let Ok(cli_network) = cli_network else { - if !fallback_network.is_ok() { + if fallback_network.is_err() { bail!(cli_network.unwrap_err()); } @@ -115,6 +117,38 @@ pub fn get_active_network() -> Result { Ok(cli_network) } +pub fn cache_package( + dependency: PackageRequest, + network: &Network, +) -> Result { + let git_info: SafeGitInfo = dependency.get_git_info()?.try_into()?; + + // Make the dependency like this: { git = "...", rev = "...", subdir = "..." } + let dependency_str = format!( + "{{ git = \"{}\", rev = \"{}\", subdir = \"{}\" }}", + git_info.repository_url, git_info.tag, git_info.path + ); + let network_name = network.to_string(); + let chain_id = get_chain_id(&network)?; + + let cli_output = sui_command( + [ + "move", + "cache-package", + &network_name, + chain_id.as_str(), + dependency_str.as_str(), + ] + .to_vec(), + )?; + + let response_str = String::from_utf8_lossy(&cli_output.stdout).to_string(); + let response: SuiCachePackageResponse = serde_json::from_str(&response_str) + .map_err(|e| CliError::UnexpectedParsing(e.to_string()))?; + + Ok(response) +} + fn sui_command(args: Vec<&str>) -> Result { let (bin, env) = get_sui_binary(); Command::new(bin) diff --git a/crates/mvr-cli/tests/snapshots/unit_tests__local_dep_to_git_dep.snap b/crates/mvr-cli/tests/snapshots/unit_tests__local_dep_to_git_dep.snap deleted file mode 100644 index 9654070a..00000000 --- a/crates/mvr-cli/tests/snapshots/unit_tests__local_dep_to_git_dep.snap +++ /dev/null @@ -1,29 +0,0 @@ ---- -source: crates/mvr-cli/tests/unit_tests.rs -expression: "result[0]" ---- -# @generated by Move, please check-in and do not edit manually. -[move] -version = 3 -dependencies = [{ name = "deepbook", id = "deepbook" }] - -[[move.package]] -id = "Sui" -source = { git = "https://github.com/MystenLabs/sui.git", rev = "framework/mainnet", subdir = "crates/sui-framework/packages/sui-framework" } - - -[[move.package]] -id = "token" -source = { git = "https://github.com/example/demo.git", rev = "v1.0.0", subdir = "packages/demo/../token" } - -dependencies = [ - { id = "Sui", name = "Sui" }, -] - -[[move.package]] -id = "deepbook" -source = { git = "https://github.com/example/demo.git", rev = "v1.0.0", subdir = "packages/demo" } -dependencies = [ - { id = "Sui", name = "Sui" }, - { id = "token", name = "token" }, -] diff --git a/crates/mvr-cli/tests/test_data/test_local_dep_to_git/Move.lock b/crates/mvr-cli/tests/test_data/test_local_dep_to_git/Move.lock deleted file mode 100644 index ecbeb005..00000000 --- a/crates/mvr-cli/tests/test_data/test_local_dep_to_git/Move.lock +++ /dev/null @@ -1,20 +0,0 @@ -# @generated by Move, please check-in and do not edit manually. -[move] -version = 3 -dependencies = [ - { id = "Sui", name = "Sui" }, - { id = "token", name = "token" }, -] - -[[move.package]] -id = "Sui" -source = { git = "https://github.com/MystenLabs/sui.git", rev = "framework/mainnet", subdir = "crates/sui-framework/packages/sui-framework" } - - -[[move.package]] -id = "token" -source = { local = "../token" } - -dependencies = [ - { id = "Sui", name = "Sui" }, -] diff --git a/crates/mvr-cli/tests/test_data/test_local_dep_to_git/Move.toml b/crates/mvr-cli/tests/test_data/test_local_dep_to_git/Move.toml deleted file mode 100644 index 3a019e6d..00000000 --- a/crates/mvr-cli/tests/test_data/test_local_dep_to_git/Move.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "deepbook" -edition = "2024.beta" -version = "0.0.1" - -[dependencies] -Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/mainnet" } -token = { local = "../token" } - -[addresses] -deepbook = "0x0" diff --git a/crates/mvr-cli/tests/unit_tests.rs b/crates/mvr-cli/tests/unit_tests.rs deleted file mode 100644 index fbd5c7a2..00000000 --- a/crates/mvr-cli/tests/unit_tests.rs +++ /dev/null @@ -1,416 +0,0 @@ -use anyhow::Result; -use expect_test::expect; -use insta::assert_snapshot; -use mvr::types::api_types::{GitInfo, PackageRequest}; -use mvr::types::Network; -use mvr::{build_lock_files, check_address_consistency, published_ids}; -use std::collections::HashMap; -use std::fs; -use std::path::PathBuf; -use std::str::FromStr; -use sui_sdk_types::Address; -use tempfile::tempdir; - -fn create_resolved_packages() -> HashMap { - let mut resolved_packages = HashMap::new(); - - resolved_packages.insert( - "@mvr-test/first-app/1".to_string(), - PackageRequest { - name: "@mvr-test/first-app/1".to_string(), - metadata: serde_json::Value::Null, - package_info: None, - git_info: Some(GitInfo { - repository_url: Some("https://github.com/example/demo.git".to_string()), - tag: Some("v1.0.0".to_string()), - path: Some("packages/demo".to_string()), - }), - version: 1, - package_address: "0x1234567890abcdef".to_string(), - }, - ); - resolved_packages -} - -#[tokio::test] -async fn test_build_lock_files() -> Result<()> { - let temp_dir = tempdir()?; - let move_toml_content = r#" -[package] -name = "demo" -edition = "2024.beta" - -[dependencies] -Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/mainnet" } - -[addresses] -demo = "0x0" -"#; - - let move_lock_content = r#"# @generated by Move, please check-in and do not edit manually. - -[move] -version = 3 -manifest_digest = "0" -deps_digest = "0" -dependencies = [ - { id = "Sui", name = "Sui" }, -] - -[[move.package]] -id = "MoveStdlib" -source = { git = "https://github.com/MystenLabs/sui.git", rev = "framework/mainnet", subdir = "crates/sui-framework/packages/move-stdlib" } - -[[move.package]] -id = "Sui" -source = { git = "https://github.com/MystenLabs/sui.git", rev = "framework/mainnet", subdir = "crates/sui-framework/packages/sui-framework" } - -dependencies = [ - { id = "MoveStdlib", name = "MoveStdlib" }, -] -"#; - - let move_toml_path = temp_dir.path().join("Move.toml"); - let move_lock_path = temp_dir.path().join("Move.lock"); - fs::write(&move_toml_path, move_toml_content)?; - fs::write(&move_lock_path, move_lock_content)?; - - let resolved_packages = create_resolved_packages(); - let mut fetched_files = HashMap::new(); - fetched_files.insert( - "@mvr-test/first-app/1".to_string(), - (move_toml_path, move_lock_path), - ); - let result = build_lock_files(&resolved_packages, &fetched_files).await?; - - // Note: roots "demo" in `move.dependencies` and creates a [move.package.source] entry for the demo package. - expect![[r##" - # @generated by Move, please check-in and do not edit manually. - - [move] - version = 3 - manifest_digest = "0" - deps_digest = "0" - dependencies = [{ name = "demo", id = "demo" }] - - [[move.package]] - id = "MoveStdlib" - source = { git = "https://github.com/MystenLabs/sui.git", rev = "framework/mainnet", subdir = "crates/sui-framework/packages/move-stdlib" } - - [[move.package]] - id = "Sui" - source = { git = "https://github.com/MystenLabs/sui.git", rev = "framework/mainnet", subdir = "crates/sui-framework/packages/sui-framework" } - - dependencies = [ - { id = "MoveStdlib", name = "MoveStdlib" }, - ] - - [[move.package]] - id = "demo" - source = { git = "https://github.com/example/demo.git", rev = "v1.0.0", subdir = "packages/demo" } - dependencies = [ - { id = "Sui", name = "Sui" }, - ] - "##]] - .assert_eq(&result[0]); - Ok(()) -} - -#[tokio::test] -async fn test_check_address_consistency() -> Result<()> { - let temp_dir = tempdir()?; - /////////////// - // Move.toml // - /////////////// - let move_toml_content = r#" -[package] -name = "demo" -version = "0.0.1" - -[addresses] -demo = "0x0" -"#; - - /////////////// - // Move.lock // - /////////////// - let move_lock_content = r#" -[move] -version = 3 -dependencies = [ - { name = "Sui", addr = "0x2" }, -] - -[env] - -[env.testnet] -chain-id = "4c78adac" -original-published-id = "0x1234567890abcdef" -latest-published-id = "0x1234567890abcdef" -published-version = "1" -"#; - - let move_toml_path = temp_dir.path().join("Move.toml"); - let move_lock_path = temp_dir.path().join("Move.lock"); - fs::write(&move_toml_path, move_toml_content)?; - fs::write(&move_lock_path, move_lock_content)?; - - let mut fetched_files = HashMap::new(); - fetched_files.insert( - "@mvr-test/first-app/1".to_string(), - (move_toml_path.clone(), move_lock_path.clone()), - ); - //////////////////////////////////////////////////////////////////////////////////////////// - // Expect success: Move.lock matches expected resolved address. // - //////////////////////////////////////////////////////////////////////////////////////////// - let resolved_packages = create_resolved_packages(); - let network = Network::Testnet; - let result = check_address_consistency(&resolved_packages, &network, &fetched_files).await; - expect![[r#" - Ok( - (), - ) - "#]] - .assert_debug_eq(&result); - - //////////////////////////////////////////////////////////////////////////////////////////// - // Expect failure: Move.lock does not match resolved address. // - //////////////////////////////////////////////////////////////////////////////////////////// - - /////////////// - // Move.lock // - /////////////// - let move_lock_content = r#" -[move] -version = 3 -dependencies = [ - { name = "Sui", addr = "0x2" }, -] - -[env] - -[env.testnet] -chain-id = "4c78adac" -original-published-id = "0xbad" -latest-published-id = "0xbad" -published-version = "1" -"#; - - let move_lock_path = temp_dir.path().join("Move.lock"); - fs::write(&move_lock_path, move_lock_content)?; - fetched_files.insert( - "@mvr-test/first-app/1".to_string(), - (move_toml_path.clone(), move_lock_path), - ); - let result = check_address_consistency(&resolved_packages, &network, &fetched_files).await; - expect![[r#" - Err( - "Mismatch: The original package address for @mvr-test/first-app on testnet is 0x0000000000000000000000000000000000000000000000001234567890abcdef, but the Move.lock in @mvr-test/first-app's repository was found to be 0x0000000000000000000000000000000000000000000000000000000000000bad.\nCheck the configuration of the package's repository https://github.com/example/demo.git in branch v1.0.0 in subdirectory packages/demo", - ) - "#]] - .assert_debug_eq(&result); - - //////////////////////////////////////////////////////////////////////////////////////////// - // Expect failure: no published address (empty lock and no address in Move.toml) // - //////////////////////////////////////////////////////////////////////////////////////////// - - /////////////// - // Move.lock // - /////////////// - let move_lock_content = r#" -[move] -version = 3 -dependencies = [ - { name = "Sui", addr = "0x2" }, -] -"#; - - let move_lock_path = temp_dir.path().join("Move.lock"); - fs::write(&move_lock_path, move_lock_content)?; - fetched_files.insert( - "@mvr-test/first-app/1".to_string(), - (move_toml_path.clone(), move_lock_path.clone()), - ); - let result = check_address_consistency(&resolved_packages, &network, &fetched_files).await; - expect![[r#" - Err( - "Mismatch: The original package address for @mvr-test/first-app on testnet is 0x0000000000000000000000000000000000000000000000001234567890abcdef, but the address in the [addresses] section of the Move.toml in @mvr-test/first-app's repository was found to be 0x0000000000000000000000000000000000000000000000000000000000000000.\nCheck the configuration of the package's repository https://github.com/example/demo.git in branch v1.0.0 in subdirectory packages/demo", - ) - "#]] - .assert_debug_eq(&result); - - //////////////////////////////////////////////////////////////////////////////////////////// - // Expect success: address resolved from published-at (0x0 in [addresses] // - //////////////////////////////////////////////////////////////////////////////////////////// - - /////////////// - // Move.toml // - /////////////// - let move_toml_content = r#" -[package] -name = "demo" -published-at = "0x1234567890abcdef" -version = "0.0.1" - -[addresses] -demo = "0x0" -"#; - - let move_toml_path = temp_dir.path().join("Move.toml"); - fs::write(&move_toml_path, move_toml_content)?; - fetched_files.insert( - "@mvr-test/first-app/1".to_string(), - (move_toml_path, move_lock_path.clone()), - ); - let result = check_address_consistency(&resolved_packages, &network, &fetched_files).await; - expect![[r#" - Ok( - (), - ) - "#]] - .assert_debug_eq(&result); - - //////////////////////////////////////////////////////////////////////////////////////////// - // Expect success: address resolved from [addresses] (published-at is upgraded pkg addr) // - //////////////////////////////////////////////////////////////////////////////////////////// - - /////////////// - // Move.toml // - /////////////// - let move_toml_content = r#" -[package] -name = "demo" -published-at = "0xabcdef" -version = "0.0.1" - -[addresses] -demo = "0x1234567890abcdef" -"#; - - let move_toml_path = temp_dir.path().join("Move.toml"); - fs::write(&move_toml_path, move_toml_content)?; - fetched_files.insert( - "@mvr-test/first-app/1".to_string(), - (move_toml_path, move_lock_path), - ); - let result = check_address_consistency(&resolved_packages, &network, &fetched_files).await; - expect![[r#" - Ok( - (), - ) - "#]] - .assert_debug_eq(&result); - Ok(()) -} - -#[tokio::test] -async fn test_get_published_ids() -> Result<()> { - // Case (1): Move.toml with published-at and addresses. - let move_toml_content = r#" -[package] -name = "demo" -published-at = "0xabcdef" - -[addresses] -demo = "0x1234567890abcdef" -"#; - let original_address_on_chain = Address::from_str("0x1234567890abcdef")?; - let result = published_ids(move_toml_content, &original_address_on_chain).await; - expect![[r#" - MoveTomlPublishedID { - addresses_id: Some( - "0x1234567890abcdef", - ), - published_at_id: Some( - "0xabcdef", - ), - internal_pkg_name: Some( - "demo", - ), - } - "#]] - .assert_debug_eq(&result); - - // Case (2): Move.toml with only published-at. - let move_toml_content = r#" -[package] -name = "demo" -published-at = "0x1234567890abcdef" -"#; - let result = published_ids(move_toml_content, &original_address_on_chain).await; - expect![[r#" - MoveTomlPublishedID { - addresses_id: None, - published_at_id: Some( - "0x1234567890abcdef", - ), - internal_pkg_name: None, - } - "#]] - .assert_debug_eq(&result); - - // Case (3): Move.toml with only addresses and 0x0 (e.g., automated address management in use). - let move_toml_content = r#" -[package] -name = "demo" - -[addresses] -demo = "0x0" -"#; - let result = published_ids(move_toml_content, &original_address_on_chain).await; - expect![[r#" - MoveTomlPublishedID { - addresses_id: Some( - "0x0", - ), - published_at_id: None, - internal_pkg_name: Some( - "demo", - ), - } - "#]] - .assert_debug_eq(&result); - - // Case (4): Move.toml with only addresses and not 0x0. - let move_toml_content = r#" -[package] -name = "demo" - -[addresses] -demo = "0x1234567890abcdef" -"#; - let result = published_ids(move_toml_content, &original_address_on_chain).await; - expect![[r#" - MoveTomlPublishedID { - addresses_id: Some( - "0x1234567890abcdef", - ), - published_at_id: None, - internal_pkg_name: Some( - "demo", - ), - } - "#]] - .assert_debug_eq(&result); - - Ok(()) -} - -#[tokio::test] -async fn test_local_dep_to_git_dep() -> Result<()> { - let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - path.extend(["tests", "test_data", "test_local_dep_to_git"]); - - let resolved_packages = create_resolved_packages(); - let mut fetched_files = HashMap::new(); - - fetched_files.insert( - "@mvr-test/first-app/1".to_string(), - (path.join("Move.toml"), path.join("Move.lock")), - ); - - let result = build_lock_files(&resolved_packages, &fetched_files).await?; - - assert_snapshot!(result[0]); - Ok(()) -} diff --git a/crates/mvr-types/src/name.rs b/crates/mvr-types/src/name.rs index 4303aec3..c5b56160 100644 --- a/crates/mvr-types/src/name.rs +++ b/crates/mvr-types/src/name.rs @@ -45,7 +45,7 @@ pub(crate) static VERSIONED_NAME_UNBOUND_REG: Lazy = pub(crate) static VERSIONED_NAME_REG: Lazy = Lazy::new(|| Regex::new(VERSIONED_NAME_REGEX).unwrap()); -#[derive(Debug, Serialize, Deserialize, Hash, Clone, Eq, PartialEq)] +#[derive(Debug, Serialize, Deserialize, Hash, Clone, Eq, PartialEq, Ord, PartialOrd)] pub struct VersionedName { /// A version name defaults at None, which means we need the latest version. pub version: Option, @@ -55,7 +55,7 @@ pub struct VersionedName { /// Attention: The format of this struct should not change unless the on-chain format changes, /// as we define it to deserialize on-chain data. -#[derive(Debug, Serialize, Deserialize, Hash, Clone, Eq, PartialEq)] +#[derive(Debug, Serialize, Deserialize, Hash, Clone, Eq, PartialEq, Ord, PartialOrd)] pub struct Name { pub org: Domain, pub app: Vec, diff --git a/crates/mvr-types/src/name_service.rs b/crates/mvr-types/src/name_service.rs index b3b0dd49..b708db2f 100644 --- a/crates/mvr-types/src/name_service.rs +++ b/crates/mvr-types/src/name_service.rs @@ -19,7 +19,7 @@ pub enum DomainFormat { Dot, } -#[derive(Debug, Serialize, Deserialize, Clone, Eq, Hash, PartialEq)] +#[derive(Debug, Serialize, Deserialize, Clone, Eq, Hash, PartialEq, Ord, PartialOrd)] pub struct Domain { pub labels: Vec, }