Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cw22 add supported interface #844

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ workflows:
- package_cw3
- package_cw4
- package_cw20
- package_cw22
- lint
- wasm-build
- coverage
Expand Down Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions packages/cw22/.cargo/config
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[alias]
wasm = "build --release --target wasm32-unknown-unknown"
wasm-debug = "build --target wasm32-unknown-unknown"
17 changes: 17 additions & 0 deletions packages/cw22/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
54 changes: 54 additions & 0 deletions packages/cw22/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# 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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this is different than implementation, please update

/// supported_interface is an optional parameter returning a vector of string represents interfaces
/// that the contract support The string value is the interface crate names in Rust crate Registry.
/// This parameter is inspired by the EIP-165 from Ethereum.
/// Each string value should follow a common standard such as <Registry Domain>:<Crate Name>
/// 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: String,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:
This thing is mainly used with string literals to store it in DB. However, you always need to allocate the string for no real reason adding unnecessary cost. I understand you do it, so you can deserialize it keeping ownership.

You can avoid it with a Cow type. Just add a lifetime generic on top (ContractSupportedInterface<'a>) and then change all strings to Cow<'a, str>. Then instead of String::from("foo") you use Cow::borrowed("foo"), Cow::from("foo"), or even "foo".into(), and you avoid allocation unless you really need it (and you will not need it for serialization - serde handles (de)serialization through Cow transparently).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, even though it is not exactly the same as in implementation, I would leave it this way - not sure if you forgot to update or left it on purpose, but it is probably easier to think about this way and it is semantically equivalent.

/// semantic version on release tags of the interface package following SemVer guideline.
/// e.g "0.16.0"
pub version: String,
}
```

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<Response, ContractError> {
///...
let supported_interface = ContractSupportedInterface {
supported_interface: String::from("crates.io:cw20"),
version: String::from("0.16.0"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, I may be a little bit biased but seeing this and then sending the whole structure by reference line later my whole brain screams "cost".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, those should be updated - as supported_interface and version are Cow internally, this won't work. Both should be changed either to:

  supported_interface: From::from("crates.io:cw20"),
  version: From::from("0.16.0"),

or

  supported_interface: "crates.io:cw20".into(),
  version: "0.16.0".into(),

Both of those work for ContractSupportedInterface defined as String and as Cow. I personally prefer the .into() as it feels more natural to me, but I'll accept both.

Sorry for the annoying nitpicking here. We struggle with documentation quality, and we don't want to introduce any more dept in this area.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
supported_interface: String::from("crates.io:cw20"),
version: String::from("0.16.0"),
supported_interface: "crates.io:cw20".into(),
version: String::from"0.16.0".into(),

};
set_contract_supported_interface(deps.storage, &[supported_interface])?;
///...
}
```
169 changes: 169 additions & 0 deletions packages/cw22/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/*!
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/
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit/consistency: we use /// for doc comments everywhere and for mod-level comments, //! - please stick to this pattern. It is more common in general in rust ecosystems.

use cosmwasm_schema::cw_serde;
use cosmwasm_std::{StdError, StdResult, Storage};
use cw_storage_plus::Map;
use semver::{Version, VersionReq};

pub const SUPPORTED_INTERFACES: Map<&str, String> = Map::new("supported_interfaces");

#[cw_serde]
pub struct ContractSupportedInterface {
/// supported_interface is an optional parameter returning a vector of string represents interfaces
/// that the contract support The string value is the interface crate names in Rust crate Registry.
/// This parameter is inspired by the EIP-165 from Ethereum.
/// Each string value should follow a common standard such as <Registry Domain>:<Crate Name>
/// 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: String,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where the comment about Cow should actually go.

/// semantic version on release tags of the interface package following SemVer guideline.
/// e.g "0.16.0"
pub version: String,
}

/// 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)?;
}
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<Option<String>> {
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: String::from(interface2),
version: String::from("0.16.0"),
};
let contract_interface22 = ContractSupportedInterface {
supported_interface: String::from(interface22),
version: String::from("0.1.0"),
};
let contract_interface721 = ContractSupportedInterface {
supported_interface: String::from(interface22),
version: String::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);
}
}