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

Cw22 add supported interface #844

wants to merge 24 commits into from

Conversation

eledra89
Copy link

@eledra89 eledra89 commented Nov 3, 2022

Implement #842

Copy link
Contributor

@uint uint left a comment

Choose a reason for hiding this comment

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

Looks useful. Nice!

I left some comments. In general if we're doing a separate spec, I'd prefer to make it independent from cw2 rather than a superset.

packages/cw2-supported-interface/README.md Outdated Show resolved Hide resolved
packages/cw2-supported-interface/src/lib.rs Outdated Show resolved Hide resolved
packages/cw2-supported-interface/Cargo.toml Outdated Show resolved Hide resolved
@peara
Copy link

peara commented Nov 11, 2022

Hi @eledra89 @uint @ThienLK1. Since this is a new package, I think we could add more functionalities.
For example, instead of a Vec stored in a Item, we could use Map which map an interface_name to a version:
const SUPPORTED_INTERFACES: Map<&str, &str> = Map::new("supported_interfaces");
With that, we can support smart query like check_supported_interface(interface, version) which would make using this much more convenient.

@eledra89
Copy link
Author

also cc @shanev per our discussion on Twitter. Can't cc Larry here though

@eledra89 eledra89 marked this pull request as draft November 12, 2022 02:46
@ThienLK1
Copy link

Hi @eledra89 @uint @ThienLK1. Since this is a new package, I think we could add more functionalities. For example, instead of a Vec stored in a Item, we could use Map which map an interface_name to a version: const SUPPORTED_INTERFACES: Map<&str, &str> = Map::new("supported_interfaces"); With that, we can support smart query like check_supported_interface(interface, version) which would make using this much more convenient.

@peara I just merge a new pull request, add more function to define and check version of supported interfaces, please take a look!

packages/cw22/Cargo.toml Outdated Show resolved Hide resolved
packages/cw22/Cargo.toml Outdated Show resolved Hide resolved
store: &mut dyn Storage,
mut supported_interfaces: Vec<ContractSupportedInterface>,
) -> Result<(), StdError> {
while let Some(supported_interface) = supported_interfaces.pop() {
Copy link

Choose a reason for hiding this comment

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

can we use for supported_interface in supported_interfaces {} here?

Choose a reason for hiding this comment

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

yes, the code will be similar.

Copy link
Member

Choose a reason for hiding this comment

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

Argument change would force this for loop here, which is better anyway

.unwrap_or_default();
let supported_version = Version::parse(&supported_version_rs);
match supported_version {
Ok(ver) => Ok(VersionResponse {
Copy link

Choose a reason for hiding this comment

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

since this is a query, I think just return true or false would be enough.

Choose a reason for hiding this comment

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

it returns more information to the querier. Example which specifies version interface is supported, or not?

Copy link
Member

Choose a reason for hiding this comment

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

I think this function can be removed.

query_supported_interface_version already returns the version (or missing), be DRY.
Also, we will most likely be getting this though Map.query(...) as we remotely query a contract (don't compare with our own contracts version). Let's make the version comparison helper separate.

I would just add two functions that don't touch storage and show the semver usage, like:

// most comparisons just require a minimum - make this easy
minimum_version(version: &str, required: &str) -> bool {} - maybe make Result<(), Error>?

// allow more powerful query strings - ">=1.2.3,<2"
require_version(version: &str, request: &str) -> bool {} - or Result as above

Copy link
Member

Choose a reason for hiding this comment

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

If you go for the Result type, please define a custom Cw22Error type in the spec.

Copy link

Choose a reason for hiding this comment

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

@ethanfrey for the minimum_version, you mean a helper function like this?

pub fn minimum_version(version: &str, required: &str) -> bool {
  let req = VersionReq::parse(">={required}").unwrap();
  let version = Version::parse(version);
  req.matches(&version)
}

@eledra89 eledra89 marked this pull request as ready for review November 28, 2022 22:54
@eledra89
Copy link
Author

@uint @ethanfrey , we added cw22: supported interface with additional support for version checking.

@eledra89 eledra89 changed the title Cw2 add supported interface Cw22 add supported interface Nov 28, 2022
@ethanfrey
Copy link
Member

Nice. I took a quick look over the comments and agree with the direction.

Using a separate independent interface, as well as using a Map for easier checks.

I want to say that one nice part of cw2 is that it has a well-defined storage structure, so you can do "raw queries" against it if needed. Making the storage layout part of this spec also sounds like a good idea. Sure, we can do smart queries, but that depends on a higher-level API and is less efficient. A well defined storage layer allows efficient raw queries with no more code on the queried contract.

We also have helpers for this, a calling contract can do something like:

let version = SUPPORTED_INTERFACES.query(contract_addr, "crates.io:cw20")?;

I will go through briefly on the code, but this is my high-level input.

Copy link
Member

@ethanfrey ethanfrey left a comment

Choose a reason for hiding this comment

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

A number of comments on the code.

I like the direction, but please clean this up so it is up to standards to merge

* 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

use query::VersionResponse;
use semver::{Version, VersionReq};

pub const SUPPORTED_INTERFACES: Map<String, String> = Map::new("supported_interfaces");
Copy link
Member

Choose a reason for hiding this comment

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

Please use Map<&str, String>, it is more efficient and easier to call

/// of supported interfaces. It should also be used after every migration.
pub fn set_contract_supported_interface(
store: &mut dyn Storage,
mut supported_interfaces: Vec<ContractSupportedInterface>,
Copy link
Member

Choose a reason for hiding this comment

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

If we just use &str in the Map, this cheaply becomes: supported_interfaces: &[ContractSupportedInterface]

store: &mut dyn Storage,
mut supported_interfaces: Vec<ContractSupportedInterface>,
) -> Result<(), StdError> {
while let Some(supported_interface) = supported_interfaces.pop() {
Copy link
Member

Choose a reason for hiding this comment

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

Argument change would force this for loop here, which is better anyway

mut supported_interfaces: Vec<ContractSupportedInterface>,
) -> Result<(), StdError> {
while let Some(supported_interface) = supported_interfaces.pop() {
let id = supported_interface.supported_interface;
Copy link
Member

Choose a reason for hiding this comment

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

Why not just use a shorter local variable than "supported_interface" and inline this. The following should be clear enough (but change x to item or so if you prefer)

for x in supported_interfaces {
  SUPPORTED_INTERFACES.save(store, &x.supported_interface, &x.version)?;
}

Copy link
Member

Choose a reason for hiding this comment

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

Actually, since we assume all stored versions are semver later on in queries, enforce it here.
Parse the values as Version and error if they are invalid - much better to error on instantiate (fail fast in tests) than later when first querying in production.

pub fn query_supported_interface_version(
store: &dyn Storage,
interface: String,
) -> StdResult<ContractSupportedInterface> {
Copy link
Member

Choose a reason for hiding this comment

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

I would return StdResult<Option<String>> here. Unless there is a read error, it returns None for not registered, and the version if registered.

) -> StdResult<ContractSupportedInterface> {
let version = SUPPORTED_INTERFACES
.may_load(store, interface.clone())?
.unwrap_or_default();
Copy link
Member

Choose a reason for hiding this comment

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

We do want the difference between None and Some here (not just "")

.unwrap_or_default();
let supported_version = Version::parse(&supported_version_rs);
match supported_version {
Ok(ver) => Ok(VersionResponse {
Copy link
Member

Choose a reason for hiding this comment

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

I think this function can be removed.

query_supported_interface_version already returns the version (or missing), be DRY.
Also, we will most likely be getting this though Map.query(...) as we remotely query a contract (don't compare with our own contracts version). Let's make the version comparison helper separate.

I would just add two functions that don't touch storage and show the semver usage, like:

// most comparisons just require a minimum - make this easy
minimum_version(version: &str, required: &str) -> bool {} - maybe make Result<(), Error>?

// allow more powerful query strings - ">=1.2.3,<2"
require_version(version: &str, request: &str) -> bool {} - or Result as above

use cosmwasm_schema::cw_serde;

#[cw_serde]
pub struct VersionResponse {
Copy link
Member

Choose a reason for hiding this comment

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

Let's remove this. I don't define smart queries for this, they require more wiring.

Let's be like cw2 (and cw4) and focus on spec'ing the storage layout for efficient queries. The contract should be able to easily write its own storage. Other contracts will be querying it's storage (Map.query) and testing the resulting value.

.unwrap_or_default();
let supported_version = Version::parse(&supported_version_rs);
match supported_version {
Ok(ver) => Ok(VersionResponse {
Copy link
Member

Choose a reason for hiding this comment

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

If you go for the Result type, please define a custom Cw22Error type in the spec.

@eledra89
Copy link
Author

oh wow this is super detail @ethanfrey, we are kind of new to Rust & CosmWasm so this is very helpful in the long term !

@shanev
Copy link
Contributor

shanev commented Jun 4, 2023

Would love to get this merged in @eledra89 if you get a chance to resolve the PR comments.

@peara
Copy link

peara commented Jun 5, 2023

@shanev thanks for reminding us. We have been pretty busy. We will fix it this week.

@ethanfrey
Copy link
Member

ethanfrey commented Jun 5, 2023

@uint no longer works at Confio and I am too booked to review this well.

@hashedone maybe you could recommend someone else who could review this? (Once they push the update. No rush, but it would be good to figure out who can review this)

@peara peara force-pushed the main branch 2 times, most recently from 01d948c to de1fb0b Compare June 7, 2023 07:17
@CLAassistant
Copy link

CLAassistant commented Jun 7, 2023

CLA assistant check
All committers have signed the CLA.

@hashedone
Copy link
Contributor

@ethanfrey I think I will just do this one myself for now.

Copy link
Contributor

@hashedone hashedone left a comment

Choose a reason for hiding this comment

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

The idea is great, and the solution is pretty good. Still, I would strongly advise migrating from String to Cow - I know we do String in most places in cw, but we already tried to use Cow where possible in Sylvia. We should go for that in interfaces, particularly those which we want to be implemented by most of the contracts to reduce extra gas cost on those small things (I get it is only for instantiation/migration, but it is very much a free change).

@jawoznia please take a look at that - we probably will auto-support this in Sylvia in the future.

I give approval as the Cow thing is not super-critical, but if you can please align. If not, please at least create an issue out of my comment.

/// 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.

Comment on lines 48 to 49
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(),

Comment on lines 1 to 13
/*!
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.

/// 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.

@peara
Copy link

peara commented Jun 10, 2023

@hashedone I have updated with Cow.
I'd like to add some examples of QueryRaw like in cw4 but haven't been able to finished.
I also want to add this to cw20-base as well.
So maybe in a follow up PR, I will add this to cw20-base and add some QueryRaw examples as test there.

Copy link
Contributor

@hashedone hashedone left a comment

Choose a reason for hiding this comment

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

LGTM with minor docs comment.

/// 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.

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.

Comment on lines 48 to 49
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.

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.

Comment on lines 48 to 49
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.

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(),

@peara
Copy link

peara commented Jun 10, 2023

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

@hashedone No problem, I actually forgot that, would have hate it myself.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants