diff --git a/.circleci/config.yml b/.circleci/config.yml index 1e434e445..300e4b8a8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -21,6 +21,7 @@ workflows: - package_cw3 - package_cw4 - package_cw20 + - package_cw22 - lint - wasm-build - coverage @@ -419,6 +420,31 @@ jobs: - target key: cargocache-v2-cw20:1.64.0-{{ checksum "~/project/Cargo.lock" }} + package_cw22: + docker: + - image: rust:1.64.0 + working_directory: ~/project/packages/cw22 + steps: + - checkout: + path: ~/project + - run: + name: Version information + command: rustc --version; cargo --version; rustup --version; rustup target list --installed + - restore_cache: + keys: + - cargocache-v2-cw22:1.64.0-{{ checksum "~/project/Cargo.lock" }} + - run: + name: Build library for native target + command: cargo build --locked + - run: + name: Run unit tests + command: cargo test --locked + - save_cache: + paths: + - /usr/local/cargo/registry + - target + key: cargocache-v2-cw22:1.64.0-{{ checksum "~/project/Cargo.lock" }} + package_cw1155: docker: - image: rust:1.64.0 diff --git a/Cargo.lock b/Cargo.lock index d1cc452a8..4264598be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -375,6 +375,18 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cw22" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus", + "schemars", + "semver", + "serde", +] + [[package]] name = "cw3" version = "1.0.1" diff --git a/packages/cw22/.cargo/config b/packages/cw22/.cargo/config new file mode 100644 index 000000000..e82e5693f --- /dev/null +++ b/packages/cw22/.cargo/config @@ -0,0 +1,3 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +wasm-debug = "build --target wasm32-unknown-unknown" diff --git a/packages/cw22/Cargo.toml b/packages/cw22/Cargo.toml new file mode 100644 index 000000000..80aed4e8d --- /dev/null +++ b/packages/cw22/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "cw22" +version = "0.1.0" +authors = ["ThienLK1@aura-nw"] +edition = "2021" +description = "Definition and types for the CosmWasm-22 interface" +license = "Apache-2.0" +repository = "https://github.com/CosmWasm/cw-plus" +homepage = "https://cosmwasm.com" + +[dependencies] +cosmwasm-schema = "1.1.0" +cosmwasm-std = { version = "1.1.0", default-features = false } +cw-storage-plus = "1.0.1" +schemars = "0.8.1" +serde = { version = "1.0.103", default-features = false, features = ["derive"] } +semver = "1.0.14" diff --git a/packages/cw22/README.md b/packages/cw22/README.md new file mode 100644 index 000000000..6356ce69b --- /dev/null +++ b/packages/cw22/README.md @@ -0,0 +1,53 @@ +# CW22 Spec: Contract Info + +The standard used to declare which interface contract implements. This standard is inspired by the EIP-165 from +Ethereum. + +For more information on this specification, please check out the +[README](https://github.com/CosmWasm/cw-plus/blob/main/packages/cw22/README.md). + +### Data structures + +**Required** + +All CW22-compliant contracts must store the following data: + +* key: `contract_supported_interface` +* data: Json-serialized `ContractSupportedInterface` + +```rust +pub struct ContractSupportedInterface<'a> { + /// supported_interface is the name of an interface that the contract supports. + /// This is inspired by the EIP-165 from Ethereum. + /// Interface names should follow a common standard such as : in Rust crate registry. + /// e.g. "crates.io:cw2" + /// NOTE: this is just a hint for the caller to adapt on how to interact with this contract. + /// There is no guarantee that the contract actually implements the interface. + pub supported_interface: Cow<'a, str>, + /// Version on release tags of the interface package following [SemVer](https://semver.org/) guideline. + /// e.g. "0.16.0" + pub version: Cow<'a, str>, +} +``` + +Below is an example used in cw20 contract, where we declare to implement cw20 interface with version 0.16.0 at +instantiate: + +```rust +use cw22::{set_contract_supported_interface, ContractSupportedInterface}; + +pub fn instantiate( + mut deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + ///... + let supported_interface = ContractSupportedInterface { + supported_interface: "crates.io:cw20".into(), + version: "0.16.0".into(), + }; + set_contract_supported_interface(deps.storage, &[supported_interface])?; + ///... +} +``` diff --git a/packages/cw22/src/lib.rs b/packages/cw22/src/lib.rs new file mode 100644 index 000000000..f1fd477c1 --- /dev/null +++ b/packages/cw22/src/lib.rs @@ -0,0 +1,171 @@ +//! CW22 defines a way for a contract to declare which interfaces do the contract implement +//! This standard is inspired by the EIP-165 from Ethereum. Originally it was proposed to +//! be merged into CW2: Contract Info, then it is splitted to a separated cargo to keep CW2 +//! being backward compatible. + +//! Each supported interface contains a string value pointing to the corresponding cargo package +//! and a specific release of the package. There is also a function to check whether the contract +//! support a specific version of an interface or not. + +//! The version string for each interface follows Semantic Versioning standard. More info is in: +//! https://docs.rs/semver/latest/semver/ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{StdError, StdResult, Storage}; +use cw_storage_plus::Map; +use semver::{Version, VersionReq}; +use std::borrow::Cow; + +pub const SUPPORTED_INTERFACES: Map<&str, String> = Map::new("supported_interfaces"); + +#[cw_serde] +pub struct ContractSupportedInterface<'a> { + /// supported_interface is the name of an interface that the contract support. + /// This is inspired by the EIP-165 from Ethereum. + /// Interface names should follow a common standard such as : in Rust crate registry. + /// e.g. "crates.io:cw2" + /// NOTE: this is just a hint for the caller to adapt on how to interact with this contract. + /// There is no guarantee that the contract actually implement these interfaces. + pub supported_interface: Cow<'a, str>, + /// semantic version on release tags of the interface package following SemVer guideline. + /// e.g. "0.16.0" + pub version: Cow<'a, str>, +} + +/// set_contract_supported_interface should be used in instantiate to store the original version +/// of supported interfaces. It should also be used after every migration. +pub fn set_contract_supported_interface( + store: &mut dyn Storage, + supported_interfaces: &[ContractSupportedInterface], +) -> StdResult<()> { + for item in supported_interfaces { + let ver = Version::parse(&item.version); + match ver { + Ok(_) => { + SUPPORTED_INTERFACES.save( + store, + &item.supported_interface, + &item.version.to_string(), + )?; + } + Err(_) => { + return Err(StdError::generic_err("Version's format is invalid")); + } + } + } + Ok(()) +} + +/// query_supported_interface_version show the version of an interface supported by the contract +pub fn query_supported_interface_version( + store: &dyn Storage, + interface: &str, +) -> StdResult> { + let version = SUPPORTED_INTERFACES.may_load(store, interface)?; + Ok(version) +} + +pub fn minimum_version(version: &str, required: &str) -> bool { + if let Ok(ver) = Version::parse(version) { + if let Ok(req) = VersionReq::parse(format!(">={}", required).as_str()) { + return req.matches(&ver); + } + } + false +} + +/// query_supported_interface show if contract supports an interface with version following SemVer query +/// query example">=1.2.3, <1.8.0" +pub fn require_version(version: &str, request: &str) -> bool { + if let Ok(ver) = Version::parse(version) { + if let Ok(req) = VersionReq::parse(request) { + return req.matches(&ver); + } + } + false +} + +#[cfg(test)] +mod tests { + use super::*; + use cosmwasm_std::testing::MockStorage; + + #[test] + fn get_and_set_work() { + let mut store = MockStorage::new(); + + let interface2 = "crates.io:cw2"; + let interface22 = "crates.io:cw22"; + let interface721 = "crates.io:cw721"; + let contract_interface2 = ContractSupportedInterface { + supported_interface: Cow::Borrowed(interface2), + version: Cow::from("0.16.0"), + }; + let contract_interface22 = ContractSupportedInterface { + supported_interface: Cow::Borrowed(interface22), + version: "0.1.0".into(), + }; + let contract_interface721 = ContractSupportedInterface { + supported_interface: Cow::Borrowed(interface22), + version: Cow::from("v0.1.0"), + }; + + // set supported_interface error + let supported_interface = &[contract_interface721]; + + let rs_error = + set_contract_supported_interface(&mut store, supported_interface).unwrap_err(); + let expected = StdError::generic_err("Version's format is invalid"); + assert_eq!(expected, rs_error); + + // set supported_interface + let supported_interface = &[contract_interface2, contract_interface22]; + + set_contract_supported_interface(&mut store, supported_interface).unwrap(); + // get version of not supported interface + let loaded = query_supported_interface_version(&store, interface721).unwrap(); + assert_eq!(None, loaded); + + // get version of supported interface + let loaded = query_supported_interface_version(&store, interface2).unwrap(); + let expected = String::from("0.16.0"); + assert_eq!(Some(expected), loaded); + } + + #[test] + fn test_require_version() { + let version_req = ">=0.1.0"; + let result = require_version("0.16.0", version_req); + assert!(result); + + let version_req = ">=0.16.0"; + let result = require_version("0.1.0", version_req); + assert!(!result); + + let version_req = ">=1.2.3, <1.8.0"; + let result = require_version("0.16.0", version_req); + assert!(!result); + + let version_req = ">=0.2.3"; + let result = require_version("v0.16.0", version_req); + assert!(!result); + + let version_req = "!=0.2.3"; + let result = require_version("0.16.0", version_req); + assert!(!result); + } + + #[test] + fn test_minimum_version() { + let result = minimum_version("0.16.0", "0.2.3"); + assert!(result); + + let result = minimum_version("0.2.0", "0.2.3"); + assert!(!result); + + let result = minimum_version("v0.16.0", "0.2.3"); + assert!(!result); + + let result = minimum_version("0.16.0", "v0.2.3"); + assert!(!result); + } +}