From 09864b61179cffacc7b01ea94d71a719e4430cca Mon Sep 17 00:00:00 2001 From: Sebastian Miasojed Date: Wed, 4 Oct 2023 12:24:56 +0200 Subject: [PATCH] Add contract's ink! compatibility check (#1334) * Initial ink version compatibility check * Fixed issues * fmt applied * unit test fixed for check ink compatibility * Added json file with version requirements * Formatting fixed * Added empty line to the end of json file * Replaced unwrap with expect * Changed ink versionreq to start from stable version * Add total contract deposit * Revert "Add total contract deposit" This reverts commit a58ae705287f34cd5fa17a68e009091f7039c39a. * Remove custom matches implementation for VersionReq * Code refactored * Code fmt * Renamed structures * Code refactored --- CHANGELOG.md | 1 + crates/build/src/crate_metadata.rs | 2 +- crates/build/src/lib.rs | 9 +- crates/extrinsics/src/lib.rs | 7 + crates/metadata/compatibility_list.json | 10 ++ crates/metadata/src/compatibility.rs | 164 ++++++++++++++++++++++++ crates/metadata/src/lib.rs | 12 ++ 7 files changed, 203 insertions(+), 2 deletions(-) create mode 100644 crates/metadata/compatibility_list.json create mode 100644 crates/metadata/src/compatibility.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 14c868d15..886d561ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `--binary` flag for `info` command - [#1311](https://github.com/paritytech/cargo-contract/pull/1311/) - Add `--all` flag for `info` command - [#1319](https://github.com/paritytech/cargo-contract/pull/1319) - Fix for a Url to String conversion in `info` command - [#1330](https://github.com/paritytech/cargo-contract/pull/1330) +- Add warning message when using incompatible contract's ink! version [#1334](https://github.com/paritytech/cargo-contract/pull/1334) ## [4.0.0-alpha] diff --git a/crates/build/src/crate_metadata.rs b/crates/build/src/crate_metadata.rs index 99113a1bf..55bfe2618 100644 --- a/crates/build/src/crate_metadata.rs +++ b/crates/build/src/crate_metadata.rs @@ -117,7 +117,7 @@ impl CrateMetadata { .packages .iter() .find_map(|package| { - if package.name == "ink" { + if package.name == "ink" || package.name == "ink_lang" { Some( Version::parse(&package.version.to_string()) .expect("Invalid ink crate version string"), diff --git a/crates/build/src/lib.rs b/crates/build/src/lib.rs index da0c4f109..823d727ef 100644 --- a/crates/build/src/lib.rs +++ b/crates/build/src/lib.rs @@ -17,7 +17,10 @@ #![doc = include_str!("../README.md")] #![deny(unused_crate_dependencies)] -use contract_metadata::ContractMetadata; +use contract_metadata::{ + compatibility::check_contract_ink_compatibility, + ContractMetadata, +}; use which as _; mod args; @@ -855,6 +858,10 @@ pub fn execute(args: ExecuteArgs) -> Result { assert_debug_mode_supported(&crate_metadata.ink_version)?; } + if let Err(e) = check_contract_ink_compatibility(&crate_metadata.ink_version) { + eprintln!("{} {}", "warning:".yellow().bold(), e.to_string().bold()); + } + let clean_metadata = || { fs::remove_file(crate_metadata.metadata_path()).ok(); fs::remove_file(crate_metadata.contract_bundle_path()).ok(); diff --git a/crates/extrinsics/src/lib.rs b/crates/extrinsics/src/lib.rs index ca91641a8..8c8043748 100644 --- a/crates/extrinsics/src/lib.rs +++ b/crates/extrinsics/src/lib.rs @@ -28,6 +28,7 @@ mod upload; #[cfg(feature = "integration-tests")] mod integration_tests; +use colored::Colorize; use subxt::utils::AccountId32; use anyhow::{ @@ -195,6 +196,12 @@ impl ContractArtifacts { ) } }; + + if let Some(contract_metadata) = metadata.as_ref() { + if let Err(e) = contract_metadata.check_ink_compatibility() { + eprintln!("{} {}", "warning:".yellow().bold(), e.to_string().bold()); + } + } Ok(Self { artifacts_path: path.into(), metadata_path, diff --git a/crates/metadata/compatibility_list.json b/crates/metadata/compatibility_list.json new file mode 100644 index 000000000..58aa7fed4 --- /dev/null +++ b/crates/metadata/compatibility_list.json @@ -0,0 +1,10 @@ +{ + "cargo-contract": { + "1.5.0": { + "ink": [">=3.0.0,<4.0.0", ">=3.0.0-alpha, <4.0.0-alpha.3"] + }, + "4.0.0-alpha": { + "ink": ["4.0.0-alpha.3", "4.0.0", "5.0.0-alpha"] + } + } +} diff --git a/crates/metadata/src/compatibility.rs b/crates/metadata/src/compatibility.rs new file mode 100644 index 000000000..a55162c33 --- /dev/null +++ b/crates/metadata/src/compatibility.rs @@ -0,0 +1,164 @@ +// Copyright 2018-2023 Parity Technologies (UK) Ltd. +// This file is part of cargo-contract. +// +// cargo-contract is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// cargo-contract is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with cargo-contract. If not, see . + +use std::collections::HashMap; + +use anyhow::{ + anyhow, + bail, + Result, +}; +use semver::{ + Version, + VersionReq, +}; + +/// Version of the currently executing `cargo-contract` binary. +const VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[derive(Debug, serde::Deserialize)] +struct Compatibility { + #[serde(rename = "cargo-contract")] + cargo_contract_compatibility: HashMap, +} + +#[derive(Debug, serde::Deserialize)] +struct Requirements { + #[serde(rename = "ink")] + ink_requirements: Vec, +} + +/// Checks whether the contract's ink! version is compatible with the cargo-contract +/// binary. +pub fn check_contract_ink_compatibility(ink_version: &Version) -> Result<()> { + let compatibility_list = include_str!("../compatibility_list.json"); + let compatibility: Compatibility = serde_json::from_str(compatibility_list)?; + let cargo_contract_version = + semver::Version::parse(VERSION).expect("Parsing version failed"); + let ink_req = &compatibility + .cargo_contract_compatibility + .get(&cargo_contract_version) + .ok_or(anyhow!( + "Missing compatibility configuration for cargo-contract: {}", + cargo_contract_version + ))? + .ink_requirements; + + // Ink! requirements can not be empty + if ink_req.is_empty() { + bail!( + "Missing ink! requirements for cargo-contract: {}", + cargo_contract_version + ); + } + + // Check if the ink! version matches any of the requirement + if !ink_req.iter().any(|req| req.matches(ink_version)) { + // Get required ink! versions + let ink_required_versions = ink_req + .iter() + .map(|req| format!("'{}'", req)) + .collect::>() + .join(", "); + + let ink_update_message = format!( + "update the contract ink! to a version of {}", + ink_required_versions + ); + let contract_not_compatible_message = "The cargo-contract is not compatible \ + with the contract's ink! version. Please"; + + // Find best cargo-contract version + let best_cargo_contract_version = compatibility + .cargo_contract_compatibility + .iter() + .filter_map(|(ver, reqs)| { + if reqs + .ink_requirements + .iter() + .any(|req| req.matches(ink_version)) + { + return Some(ver) + } + None + }) + .max_by(|&a, &b| { + match (!a.pre.is_empty(), !b.pre.is_empty()) { + (false, true) => std::cmp::Ordering::Greater, + (true, false) => std::cmp::Ordering::Less, + (_, _) => a.cmp(b), + } + }) + .ok_or(anyhow!( + "{} {}", + contract_not_compatible_message, + ink_update_message + ))?; + + bail!( + "{} update the cargo-contract to version \ + '{}' or {}", + contract_not_compatible_message, + best_cargo_contract_version, + ink_update_message + ); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn ink_check_failes_when_incompatible_version() { + let ink_version = Version::new(3, 2, 0); + let res = check_contract_ink_compatibility(&ink_version) + .expect_err("Ink version check should fail"); + + assert_eq!( + res.to_string(), + "The cargo-contract is not compatible with the contract's ink! version. \ + Please update the cargo-contract to version '1.5.0' or update \ + the contract ink! to a version of '^4.0.0-alpha.3', '^4.0.0', '^5.0.0-alpha'" + ); + + let ink_version = + Version::parse("4.0.0-alpha.1").expect("Parsing version must work"); + let res = check_contract_ink_compatibility(&ink_version) + .expect_err("Ink version check should fail"); + + assert_eq!( + res.to_string(), + "The cargo-contract is not compatible with the contract's ink! version. \ + Please update the cargo-contract to version '1.5.0' or update \ + the contract ink! to a version of '^4.0.0-alpha.3', '^4.0.0', '^5.0.0-alpha'" + ); + } + + #[test] + fn ink_check_succeeds_when_compatible_version() { + let ink_version = Version::new(4, 2, 3); + let res = check_contract_ink_compatibility(&ink_version); + assert!(res.is_ok()); + + let ink_version = + Version::parse("4.0.0-alpha.4").expect("Parsing version must work"); + let res = check_contract_ink_compatibility(&ink_version); + assert!(res.is_ok()); + } +} diff --git a/crates/metadata/src/lib.rs b/crates/metadata/src/lib.rs index 5550a5919..dc95c7046 100644 --- a/crates/metadata/src/lib.rs +++ b/crates/metadata/src/lib.rs @@ -67,6 +67,7 @@ #![deny(unused_crate_dependencies)] mod byte_str; +pub mod compatibility; use anyhow::{ Context, @@ -148,6 +149,17 @@ impl ContractMetadata { path.display() )) } + + /// Checks whether the contract's ink! version is compatible with the cargo-contract + /// binary + pub fn check_ink_compatibility(&self) -> Result<()> { + if let Language::Ink = self.source.language.language { + compatibility::check_contract_ink_compatibility( + &self.source.language.version, + )?; + } + Ok(()) + } } /// Representation of the Wasm code hash.