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 = "1.1.0"
cosmwasm-std = "2.0.4"
schemars = "0.8.16"
serde = "1"
cosmwasm-schema = "2.0.4"
cw-storage-plus = "2.0.0"
[dev-dependencies]
anyhow = "1.0"
cw-multi-test = "2.1.0"
sylvia = { version = "1.1.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.rs
#![allow(unused)] fn main() { // [...] 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 cosmwasm_std::{Response, StdResult}; use sylvia::contract; use sylvia::types::InstantiateCtx; pub struct NonGenericContract; #[contract] #[sv::messages(crate::associated 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.
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 serde::Deserialize; 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 for<'msg_de> InstantiateParam: CustomMsg + Deserialize<'msg_de> + 'msg_de, 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.rs
#![allow(unused)] fn main() { // [...] use crate::generic_contract::GenericContract; 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()) } } }
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 as Associated)] impl<DataType, InstantiateParam> GenericContract<DataType, InstantiateParam> where for<'msg_de> InstantiateParam: CustomMsg + Deserialize<'msg_de> + 'msg_de, 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.rs
#![allow(unused)] fn main() { // [...] use crate::forward_contract::ForwardContract; 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 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 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; use super::GenericContract; #[test] fn instantiate_contract() { let app = App::default(); let code_id: CodeId<GenericContract<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 contract type as generic parameter.
Perfect! We have learned how to create generic contracts and interfaces. Good luck applying this knowledge to your projects!