Generics

When implementing a contract, users might want to define some generic data it should store. One might even want the message to have some generic parameters or return type. Sylvia supports generics in the contracts and interfaces.

Prepare project

To improve readability and focus solely on the feature support in this chapter, paste this dependencies to your project Cargo.toml. It contains every dependency we will use in this chapter.

[package]
name = "generic"
version = "0.1.0"
edition = "2021"

[dependencies]
sylvia = "0.9.0"
cosmwasm-std = "1.5"
schemars = "0.8"
serde = "1"
cosmwasm-schema = "1.5"
cw-storage-plus = "1.1.0"

[dev-dependencies]
anyhow = "1.0"
cw-multi-test = "0.16"
sylvia = { version = "0.9.0", features = ["mt"] }

Generics in interface

Since 0.10.0 we no longer support generics in interfaces. Sylvia interfaces can be implemented only a single time per contract as otherwise the messages would overlap. Idiomatic approach in Rust is to use associated types to handle such cases.

src/associated.rs


#![allow(unused)]
fn main() {
use cosmwasm_std::{Response, StdError, StdResult};
use sylvia::interface;
use sylvia::types::{CustomMsg, ExecCtx, QueryCtx};

#[interface]
pub trait Associated {
    type Error: From<StdError>;
    type ExecParam: CustomMsg;
    type QueryParam: CustomMsg;

    #[sv::msg(exec)]
    fn generic_exec(&self, ctx: ExecCtx, param: Self::ExecParam) -> StdResult<Response>;

    #[sv::msg(query)]
    fn generic_query(&self, ctx: QueryCtx, param: Self::QueryParam) -> StdResult<String>;
}
}

Underhood sylvia will parse the associated types and generate generic messages as we cannot use associated types in enums.

Implement interface on the contract

Implementing an interface with associated types is the same as in case of implementing a regular interface. We first need to define the type we will assign to the associated type.

src/messages.rs


#![allow(unused)]
fn main() {
use cosmwasm_schema::cw_serde;
use cosmwasm_std::CustomMsg;

#[cw_serde]
pub struct MyMsg;

impl CustomMsg for MyMsg {}
}

We also need a contract on which we will implement the interface.

src/contract.rs


#![allow(unused)]
fn main() {
use cosmwasm_std::{Response, StdResult};
use sylvia::contract;
use sylvia::types::InstantiateCtx;

pub struct NonGenericContract;

#[contract]
impl NonGenericContract {
    pub const fn new() -> Self {
        Self {}
    }

    #[sv::msg(instantiate)]
    pub fn instantiate(&self, _ctx: InstantiateCtx) -> StdResult<Response> {
        Ok(Response::new())
    }
}
}

We are set and ready to implement the interface.

src/associated_impl.rs


#![allow(unused)]
fn main() {
use cosmwasm_std::{Response, StdError, StdResult};
use sylvia::types::{ExecCtx, QueryCtx};

use crate::associated::Associated;
use crate::contract::NonGenericContract;
use crate::messages::MyMsg;

impl Associated for NonGenericContract {
    type Error = StdError;
    type ExecParam = MyMsg;
    type QueryParam = MyMsg;

    fn generic_exec(&self, _ctx: ExecCtx, _param: Self::ExecParam) -> StdResult<Response> {
        Ok(Response::new())
    }

    fn generic_query(&self, _ctx: QueryCtx, _param: Self::QueryParam) -> StdResult<String> {
        Ok(String::default())
    }
}
}

Update impl contract

Now that we have implemented the interface on our contract, we can inform main sylvia::contract call about it.

src/contract.rs


#![allow(unused)]
fn main() {
use crate::messages::MyMsg;
use cosmwasm_std::{Response, StdResult};
use sylvia::contract;
use sylvia::types::InstantiateCtx;

pub struct NonGenericContract;

#[contract]
#[sv::messages(crate::associated<MyMsg, MyMsg> as Associated)]
impl NonGenericContract {
    pub const fn new() -> Self {
        Self {}
    }

    #[sv::msg(instantiate)]
    pub fn instantiate(&self, _ctx: InstantiateCtx) -> StdResult<Response> {
        Ok(Response::new())
    }
}
}

As in case of regular interface, we have to add the messages attribute to the contract. However because the interface has associated types, we have to pass the types to the messages. We do that by adding them in the <> brackets after the path to the interface module.

In this case we passed concrete types, but it is also possible to pass generic types defined on the contract. More on that in the next paragraph.

Generic contract

We have covered how we can allow the users to define a types of an interface during an interface implementation.

User might want to use a contract inside it's own contract. In such cases sylvia supports the generics on the contracts.

Let us define a new module in which we will define a generic contract.

src/generic_contract.rs


#![allow(unused)]
fn main() {
use cosmwasm_std::{Response, StdResult};
use cw_storage_plus::Item;
use std::marker::PhantomData;
use sylvia::contract;
use sylvia::types::{CustomMsg, InstantiateCtx};

pub struct GenericContract<DataType, InstantiateParam> {
    _data: Item<DataType>,
    _phantom: PhantomData<InstantiateParam>,
}

#[contract]
impl<DataType, InstantiateParam> GenericContract<DataType, InstantiateParam>
where
    InstantiateParam: CustomMsg + 'static,
    for<'data> DataType: 'data,
{
    pub const fn new() -> Self {
        Self {
            _data: Item::new("data"),
            _phantom: PhantomData,
        }
    }

    #[sv::msg(instantiate)]
    pub fn instantiate(
        &self,
        _ctx: InstantiateCtx,
        _param: InstantiateParam,
    ) -> StdResult<Response> {
        Ok(Response::new())
    }
}
}

This example showcases two usages of generics in contract:

  • Generic field
  • Generic message parameter

Sylvia works in both cases, and you can expand your generics to every message type, return type, or custom_queries as shown in the case of the interface. For the readability of this example, we will keep just these two generic types.

InstantiateParam is passed as a field to the InstantiateMsg. This enforces some bounds to this type, which we pass in the where clause. Just like that, we created a generic contract.

Implement interface on generic contract

Now that we have the generic contract, let's implement an interface from previous paragraphs on it.

src/associated_impl.rs


#![allow(unused)]
fn main() {
use cosmwasm_std::{Response, StdError, StdResult};
use sylvia::contract;
use sylvia::types::{ExecCtx, QueryCtx};

use crate::associated::Associated;
use crate::generic_contract::GenericContract;
use crate::messages::MyMsg;

impl<DataType, InstantiateParam> Associated for GenericContract<DataType, InstantiateParam> {
    type Error = StdError;
    type ExecParam = MyMsg;
    type QueryParam = MyMsg;

    fn generic_exec(&self, _ctx: ExecCtx, _param: Self::ExecParam) -> StdResult<Response> {
        Ok(Response::new())
    }

    fn generic_query(&self, _ctx: QueryCtx, _param: Self::QueryParam) -> StdResult<String> {
        Ok(String::default())
    }
}
}

Only thing missing is to add the messages attribute to the contract implementation. It is the same as in case of the non generic contract so we will skip it.

Implementing a generic interface on the generic contract is very similar to implementing it on a regular one. The only change is to pass the generics into the contract.

src/generic_contract.rs


#![allow(unused)]
fn main() {
#[contract]
#[sv::messages(crate::associated<MyMsg, MyMsg> as Associated)]
impl<DataType, InstantiateParam> GenericContract<DataType, InstantiateParam>
where
    InstantiateParam: CustomMsg + 'static,
    for<'data> DataType: 'data,
{
    ..
}
}

Forwarding generics

User might want to link the generics from the contract with associated types in implemented interface. To do so we have to simply assign the generics to appropriate associated types.

Let's one more time define a new contract.

src/forward_contract.rs


#![allow(unused)]
fn main() {
use cosmwasm_std::{Response, StdResult};
use std::marker::PhantomData;
use sylvia::contract;
use sylvia::types::{CustomMsg, InstantiateCtx};

pub struct ForwardContract<ExecParam, QueryParam> {
    _phantom: PhantomData<(ExecParam, QueryParam)>,
}

#[contract]
impl<ExecParam, QueryParam> ForwardContract<ExecParam, QueryParam>
where
    ExecParam: CustomMsg + 'static,
    QueryParam: CustomMsg + 'static,
{
    pub const fn new() -> Self {
        Self {
            _phantom: PhantomData,
        }
    }

    #[sv::msg(instantiate)]
    pub fn instantiate(&self, _ctx: InstantiateCtx) -> StdResult<Response> {
        Ok(Response::new())
    }
}
}

The implementation of the interface should look like this:

src/associated_impl.rs


#![allow(unused)]
fn main() {
use cosmwasm_std::{Response, StdError, StdResult};
use sylvia::contract;
use sylvia::types::{CustomMsg, ExecCtx, QueryCtx};

use crate::associated::Associated;
use crate::generic_contract::GenericContract;

impl<ExecParam, QueryParam> Associated for ForwardContract<ExecParam, QueryParam>
where
    ExecParam: CustomMsg,
    QueryParam: CustomMsg,
{
    type Error = StdError;
    type ExecParam = ExecParam;
    type QueryParam = QueryParam;

    fn generic_exec(&self, _ctx: ExecCtx, _param: Self::ExecParam) -> StdResult<Response> {
        Ok(Response::new())
    }

    fn generic_query(&self, _ctx: QueryCtx, _param: Self::QueryParam) -> StdResult<String> {
        Ok(String::default())
    }
}
}

And as always, we have to add the messages attribute to the contract implementation.

src/forward_contract.rs


#![allow(unused)]
fn main() {
#[contract]
#[sv::messages(crate::associated<ExecParam, QueryParam> as Associated)]
impl<ExecParam, QueryParam> ForwardContract<ExecParam, QueryParam>
where
    ExecParam: CustomMsg + 'static,
    QueryParam: CustomMsg + 'static,
{
    ..
}
}

As you can see sylvia is very flexible with how the user might want to use it.

Generate entry points

Without the entry points, our contract is just a library defining some message types. Let's make it a proper contract. We have to pass solid types to the entry_points macro, as the contract cannot have a generic types in the entry points. To achieve this, we pass the concrete types to the entry_points macro call.


#![allow(unused)]
fn main() {
use crate::messages::MyMsg;
use cosmwasm_std::{Response, StdResult};
use cw_storage_plus::Item;
use std::marker::PhantomData;
use sylvia::types::{CustomMsg, InstantiateCtx};
use sylvia::{contract, entry_points};

pub struct GenericContract<DataType, InstantiateParam> {
    _data: Item<DataType>,
    _phantom: PhantomData<InstantiateParam>,
}

#[entry_points(generics<String, MyMsg>)]
#[contract]
#[sv::messages(crate::associated<MyMsg, MyMsg> as Associated)]
impl<DataType, InstantiateParam> GenericContract<DataType, InstantiateParam>
where
    InstantiateParam: CustomMsg + 'static,
    for<'data> DataType: 'data,
{
    pub const fn new() -> Self {
        Self {
            _data: Item::new("data"),
            _phantom: PhantomData,
        }
    }

    #[sv::msg(instantiate)]
    pub fn instantiate(
        &self,
        _ctx: InstantiateCtx,
        _param: InstantiateParam,
    ) -> StdResult<Response> {
        Ok(Response::new())
    }
}
}

Notice the generics attribute in the entry_points macro. It's goal is to provide us a way to declare the types we want to use in the entry points. Our contract is ready to use. We can test it in the last step of this chapter.

Test generic contract

Similar to defining a contract, we have to create a App with the `cw_multi

src/contract.rs


#![allow(unused)]
fn main() {
...

#[cfg(test)]
mod tests {
    use sylvia::cw_multi_test::IntoAddr;
    use sylvia::multitest::App;

    use crate::messages::MyMsg;

    use super::sv::mt::CodeId;

    #[test]
    fn instantiate_contract() {
        let app = App::default();
        let code_id: CodeId<String, MyMsg, _> = CodeId::store_code(&app);

        let owner = "owner".into_addr();

        let _ = code_id
            .instantiate(MyMsg {})
            .with_label("GenericContract")
            .with_admin(owner.as_str())
            .call(&owner)
            .unwrap();
    }
}
}

While creating the CodeId we have to pass the types we want to use in the contract. It seems strange that CodeId is generic over three types while we defined only two, but CodeId is also generic over MtApp (cw-multi-test::App). The compiler will always deduce this type from the app passed to it, so don't worry and pass a placeholder there.

Perfect! We have learned how to create generic contracts and interfaces. Good luck applying this knowledge to your projects!