Cw22 add supported interface#844
Conversation
uint
left a comment
There was a problem hiding this comment.
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.
|
Hi @eledra89 @uint @ThienLK1. Since this is a new package, I think we could add more functionalities. |
|
also cc @shanev per our discussion on Twitter. Can't cc Larry here though |
@peara I just merge a new pull request, add more function to define and check version of supported interfaces, please take a look! |
| store: &mut dyn Storage, | ||
| mut supported_interfaces: Vec<ContractSupportedInterface>, | ||
| ) -> Result<(), StdError> { | ||
| while let Some(supported_interface) = supported_interfaces.pop() { |
There was a problem hiding this comment.
can we use for supported_interface in supported_interfaces {} here?
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
since this is a query, I think just return true or false would be enough.
There was a problem hiding this comment.
it returns more information to the querier. Example which specifies version interface is supported, or not?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
If you go for the Result type, please define a custom Cw22Error type in the spec.
There was a problem hiding this comment.
@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)
}
|
@uint @ethanfrey , we added cw22: supported interface with additional support for version checking. |
|
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:
I will go through briefly on the code, but this is my high-level input. |
ethanfrey
left a comment
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
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>, |
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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)?;
}There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
If you go for the Result type, please define a custom Cw22Error type in the spec.
|
oh wow this is super detail @ethanfrey, we are kind of new to Rust & CosmWasm so this is very helpful in the long term ! |
|
Would love to get this merged in @eledra89 if you get a chance to resolve the PR comments. |
|
@shanev thanks for reminding us. We have been pretty busy. We will fix it this week. |
|
@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) |
01d948c to
de1fb0b
Compare
Cw22 improvement
|
@ethanfrey I think I will just do this one myself for now. |
hashedone
left a comment
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
| supported_interface: String::from("crates.io:cw20"), | ||
| version: String::from("0.16.0"), |
There was a problem hiding this comment.
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".
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
| 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(), |
| /*! | ||
| 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/ | ||
| */ |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
This is where the comment about Cow should actually go.
|
@hashedone I have updated with |
hashedone
left a comment
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
| supported_interface: String::from("crates.io:cw20"), | ||
| version: String::from("0.16.0"), |
There was a problem hiding this comment.
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.
| supported_interface: String::from("crates.io:cw20"), | ||
| version: String::from("0.16.0"), |
There was a problem hiding this comment.
| 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(), |
@hashedone No problem, I actually forgot that, would have hate it myself. |
Implement #842