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"] }
Generic interface
To define a generic interface, we don't have to add any additional attributes. Simply add generics to the trait as you would in regular rust code.
src/generic.rs
#![allow(unused)] fn main() { use cosmwasm_std::{CustomMsg, Response, StdError, StdResult}; use serde::de::DeserializeOwned; use sylvia::interface; use sylvia::types::{ExecCtx, QueryCtx}; #[interface] pub trait Generic<ExecParam, QueryParam> where ExecParam: CustomMsg + DeserializeOwned, QueryParam: CustomMsg + DeserializeOwned, { type Error: From<StdError>; #[msg(exec)] fn generic_exec(&self, ctx: ExecCtx, param: ExecParam) -> StdResult<Response>; #[msg(query)] fn generic_query(&self, ctx: QueryCtx, param: QueryParam) -> StdResult<String>; } }
Simple and expected behavior.
What if we want to extend the custom
functionality with generics?
In such a case, we would have to add #[sv::custom(..)]
attribute to the trait with the name of our generics.
It is a standard approach when using CustomMsg
and CustomQuery
in sylvia
so there is nothing new here.
src/generic.rs
#![allow(unused)] fn main() { use cosmwasm_std::{CustomMsg, CustomQuery, Response, StdError, StdResult}; use serde::de::DeserializeOwned; use sylvia::interface; use sylvia::types::{ExecCtx, QueryCtx}; #[interface] #[sv::custom(msg = MsgCustom, query = QueryCustom)] pub trait Generic<ExecParam, QueryParam, QueryCustom, MsgCustom> where ExecParam: CustomMsg + DeserializeOwned, QueryParam: CustomMsg + DeserializeOwned, QueryCustom: CustomQuery, MsgCustom: CustomMsg + DeserializeOwned, { type Error: From<StdError>; #[msg(exec)] fn generic_exec( &self, ctx: ExecCtx<QueryCustom>, param: ExecParam, ) -> StdResult<Response<MsgCustom>>; #[msg(query)] fn generic_query( &self, ctx: QueryCtx<QueryCustom>, param: QueryParam, ) -> StdResult<String>; } }
Implement a generic interface on the contract
Defining generic interfaces is as simple as defining generic traits. Implementing one on the contract is also straightforward.
We will start by defining the types we will use in place of generics.
src/messages.rs
#![allow(unused)] fn main() { use cosmwasm_schema::cw_serde; use cosmwasm_std::{CustomMsg, CustomQuery}; #[cw_serde] pub struct MyMsg; impl CustomMsg for MyMsg {} #[cw_serde] pub struct MyQuery; impl CustomQuery for MyQuery {} }
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 fn new() -> Self { Self {} } #[msg(instantiate)] pub fn instantiate(&self, _ctx: InstantiateCtx) -> StdResult<Response> { Ok(Response::new()) } } }
We are set and ready to implement the interface.
src/generic_impl.rs
#![allow(unused)] fn main() { use cosmwasm_std::{Response, StdError, StdResult}; use sylvia::contract; use sylvia::types::{ExecCtx, QueryCtx}; use crate::contract::NonGenericContract; use crate::generic::Generic; use crate::messages::{MyMsg, MyQuery}; #[contract(module=crate::contract)] #[messages(crate::generic as Generic)] #[sv::custom(msg=MyMsg, query=MyQuery)] impl Generic<MyMsg, MyMsg, MyQuery, MyMsg> for NonGenericContract { type Error = StdError; #[msg(exec)] fn generic_exec(&self, _ctx: ExecCtx<MyQuery>, _param: MyMsg) -> StdResult<Response<MyMsg>> { Ok(Response::new()) } #[msg(query)] fn generic_query(&self, _ctx: QueryCtx<MyQuery>, _param: MyMsg) -> StdResult<String> { Ok(String::default()) } } }
To implement a generic interface, we have to specify the types. Here we use MyMsg
and MyQuery
we defined earlier. No additional attributes have to be passed as sylvia
will read them
from the interface type.
Because we defined the interface as custom
we have to pass these types into sv::custom(..)
attribute
same as in the interface definition.
Update impl contract
Now that we have implemented the generic interface on our contract, we can inform
main sylvia::contract
call about it.
src/contract.rs
#![allow(unused)] fn main() { use crate::messages::{MyMsg, MyQuery}; use cosmwasm_std::{Response, StdResult}; use sylvia::contract; use sylvia::types::InstantiateCtx; pub struct NonGenericContract; #[contract] #[messages(crate::generic<MyMsg, MyMsg, MyQuery, MyMsg> as Generic)] #[sv::custom(msg=MyMsg, query=MyQuery)] impl NonGenericContract { pub fn new() -> Self { Self {} } #[msg(instantiate)] pub fn instantiate(&self, _ctx: InstantiateCtx<MyQuery>) -> StdResult<Response<MyMsg>> { Ok(Response::new()) } } }
First of all, we added types to the #[messages(..)]
attribute.
We have to add them in the same order as in the interface definition.
We also had to add #[sv::custom(..)]
attribute to the contract definition and update
InstantiateCtx
and Response
types as our interface is custom
.
Generic contract
Using generic interfaces in sylvia
is simple. Let's see how it works with generic 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::{CustomMsg, Response, StdResult}; use serde::de::DeserializeOwned; use std::marker::PhantomData; use sylvia::contract; use sylvia::types::InstantiateCtx; pub struct GenericContract<DataType, InstantiateParam> { _data: Item<'static, DataType>, _phantom: PhantomData<InstantiateParam>, } #[contract] impl<DataType, InstantiateParam> GenericContract<DataType, InstantiateParam> where InstantiateParam: CustomMsg + DeserializeOwned + 'static, for<'data> DataType: 'data, { pub fn new(data: DataType) -> Self { Self { _data: Item::new("data"), _phantom: PhantomData, } } #[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 generic interface on generic contract
Now that we have the generic contract, let's implement a generic interface on it. Sorry in advance for the naming of the following file ^^.
src/generic_generic_impl.rs
#![allow(unused)] fn main() { use cosmwasm_std::{Response, StdError, StdResult}; use sylvia::contract; use sylvia::types::{ExecCtx, QueryCtx}; use crate::generic::Generic; use crate::generic_contract::GenericContract; use crate::messages::{MyMsg, MyQuery}; #[contract(module=crate::generic_contract)] #[messages(crate::generic as Generic)] #[sv::custom(msg=MyMsg, query=MyQuery)] impl<DataType, InstantiateParam> Generic<MyMsg, MyMsg, MyQuery, MyMsg> for GenericContract<DataType, InstantiateParam> { type Error = StdError; #[msg(exec)] fn generic_exec(&self, _ctx: ExecCtx<MyQuery>, _param: MyMsg) -> StdResult<Response<MyMsg>> { Ok(Response::new()) } #[msg(query)] fn generic_query(&self, _ctx: QueryCtx<MyQuery>, _param: MyMsg) -> StdResult<String> { Ok(String::default()) } } }
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.
Now we will have to change the GenericContract
. If the interface wasn't custom
we could just
add the messages
attribute to it, but because we cannot implement custom
interface on non custom
contract, as explained in the custom
chapter, we have to change the contract to be custom
.
#![allow(unused)] fn main() { use crate::messages::{MyMsg, MyQuery}; use cosmwasm_std::{CustomMsg, Response, StdResult}; use serde::de::DeserializeOwned; use std::marker::PhantomData; use sylvia::contract; use sylvia::types::InstantiateCtx; pub struct GenericContract<DataType, InstantiateParam> { _data: Item<'static, DataType>, _phantom: PhantomData<InstantiateParam>, } #[contract] #[sv::custom(msg=MyMsg, query=MyQuery)] impl<DataType, InstantiateParam> GenericContract<DataType, InstantiateParam> where InstantiateParam: CustomMsg + DeserializeOwned + 'static, for<'data> DataType: 'data, { pub fn new(data: DataType) -> Self { Self { _data: Item::new("data"), _phantom: PhantomData, } } #[msg(instantiate)] pub fn instantiate( &self, _ctx: InstantiateCtx<MyQuery>, _param: InstantiateParam, ) -> StdResult<Response<MyMsg>> { Ok(Response::new()) } } }
Now that the contract is set as custom
we can add the messages
attribute to it.
#![allow(unused)] fn main() { use crate::messages::{MyMsg, MyQuery}; use cosmwasm_std::{CustomMsg, Response, StdResult}; use serde::de::DeserializeOwned; use std::marker::PhantomData; use sylvia::contract; use sylvia::types::InstantiateCtx; pub struct GenericContract<DataType, InstantiateParam> { _data: Item<'static, DataType>, _phantom: PhantomData<InstantiateParam>, } #[contract] #[messages(crate::generic<MyMsg, MyMsg, MyQuery, MyMsg> as Generic)] #[sv::custom(msg=MyMsg, query=MyQuery)] impl<DataType, InstantiateParam> GenericContract<DataType, InstantiateParam> where InstantiateParam: CustomMsg + DeserializeOwned + 'static, for<'data> DataType: 'data, { pub fn new(data: DataType) -> Self { Self { _data: Item::new("data"), _phantom: PhantomData, } } #[msg(instantiate)] pub fn instantiate( &self, _ctx: InstantiateCtx<MyQuery>, _param: InstantiateParam, ) -> StdResult<Response<MyMsg>> { Ok(Response::new()) } } }
Cool. The contract is almost ready. We are missing just one thing.
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 solid types to the entry_points
macro call.
#![allow(unused)] fn main() { use crate::messages::{MyMsg, MyQuery}; use cosmwasm_std::{CustomMsg, Response, StdResult}; use cw_storage_plus::Item; use serde::de::DeserializeOwned; use std::marker::PhantomData; use sylvia::types::InstantiateCtx; use sylvia::{contract, entry_points}; pub struct GenericContract<DataType, InstantiateParam> { _data: Item<'static, DataType>, _phantom: PhantomData<InstantiateParam>, } #[entry_points(generics<String, MyMsg>)] #[contract] #[messages(crate::generic<MyMsg, MyMsg, MyQuery, MyMsg> as Generic)] #[sv::custom(msg=MyMsg, query=MyQuery)] impl<DataType, InstantiateParam> GenericContract<DataType, InstantiateParam> where InstantiateParam: CustomMsg + DeserializeOwned + 'static, for<'data> DataType: 'data, { pub fn new() -> Self { Self { _data: Item::new("data"), _phantom: PhantomData, } } #[msg(instantiate)] pub fn instantiate( &self, _ctx: InstantiateCtx<MyQuery>, _param: InstantiateParam, ) -> StdResult<Response<MyMsg>> { Ok(Response::new()) } } }
Our contract is ready to use. We can test it in the last step of this chapter.
Test generic contract
Sylvia
enforces the user to specify a solid type while implementing a generic interface on the contract.
Due to this, we test NonGenericContract
as a regular contract.
In case of the generic contract, we have to pass the types while constructing CodeId
.
src/contract.rs
#![allow(unused)] fn main() { use crate::messages::{MyMsg, MyQuery}; use cosmwasm_std::{CustomMsg, Response, StdResult}; use cw_storage_plus::Item; use serde::de::DeserializeOwned; use std::marker::PhantomData; use sylvia::types::InstantiateCtx; use sylvia::{contract, entry_points}; pub struct GenericContract<DataType, InstantiateParam> { _data: Item<'static, DataType>, _phantom: PhantomData<InstantiateParam>, } #[entry_points(generics<String, MyMsg>)] #[contract] #[messages(crate::generic<MyMsg, MyMsg, MyQuery, MyMsg> as Generic)] #[sv::custom(msg=MyMsg, query=MyQuery)] impl<DataType, InstantiateParam> GenericContract<DataType, InstantiateParam> where InstantiateParam: CustomMsg + DeserializeOwned + 'static, for<'data> DataType: 'data, { pub fn new() -> Self { Self { _data: Item::new("data"), _phantom: PhantomData, } } #[msg(instantiate)] pub fn instantiate( &self, _ctx: InstantiateCtx<MyQuery>, _param: InstantiateParam, ) -> StdResult<Response<MyMsg>> { Ok(Response::new()) } } #[cfg(test)] mod tests { use sylvia::multitest::App; use crate::messages::{MyMsg, MyQuery}; use super::sv::multitest_utils::CodeId; #[test] fn generic_contract() { let app = App::<cw_multi_test::BasicApp<MyMsg, MyQuery>>::custom(|_, _, _| {}); let code_id: CodeId<String, _, _> = CodeId::store_code(&app); let owner = "owner"; let _ = code_id .instantiate(MyMsg {}) .with_label("GenericContract") .with_admin(owner) .call(owner) .unwrap(); } } }
We have to create the App
by inserting cw_multi_test::BasicApp
to it as it was explained
in the custom
chapter.
Because custom_msg
is defined as one of the generic types we have to pass only one type
to the CodeId
, which is DataType
.
It seems strange that CodeId
is generic over three types while we defined only two,
but CodeId
is generic over 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!