Sylvia 0.9.0

Jan Woźniak
Confio
Published in
4 min readNov 20, 2023

We’re happy to announce that Sylvia 0.9.0 has been released.

This release features support for the generics, which I will briefly describe here.

Sv module

Before going into the details of how to use generics in Sylvia, I want to mention one breaking change that was delivered as part of the 0.9.0 release.

Sylvia populates the scope a lot with generated types and functions. From now on, we generate all the code in the “sv” module.

This is a breaking change and will require you to update all the imports while migrating to the newer version.

Generic

With the new module covered, let’s explore the feature that the release 0.9.0 introduces — generics.

Generics in interfaces

Let’s start by explaining how this feature affects the interface macro.

We will start with an example interface.

#[interface]
pub trait Generic<ExecParam, QueryParam, RetType>
where
for<'msg_de> ExecParam: CustomMsg + Deserialize<'msg_de>,
QueryParam: sylvia::types::CustomMsg,
RetType: CustomMsg + DeserializeOwned,
{
type Error: From<StdError>;

#[msg(exec)]
fn generic_exec(
&self,
ctx: ExecCtx,
msgs: Vec<CosmosMsg<ExecParam>>,
) -> Result<Response, Self::Error>;

#[msg(query)]
fn generic_query(&self, ctx: QueryCtx, param: QueryParam) -> Result<RetType, Self::Error>;
}

As you can see, nothing special happens here. We don’t have to define any additional attributes, and we simply define generics on a trait and define the bounds in the where clause.

Messages generated this way are generic only over types used in the respective methods. So, for our interface, Sylvia will generate ExecMsg generic over ExecParam, but not the QueryParam.

We can also define our customs as traits generics.

#[interface]
#[sv::custom(msg=RetType, query=CtxQuery)]
pub trait CustomAndGeneric<ExecParam, QueryParam, CtxQuery, RetType>
where
for<'msg_de> ExecParam: CustomMsg + Deserialize<'msg_de>,
QueryParam: sylvia::types::CustomMsg,
CtxQuery: sylvia::types::CustomQuery,
RetType: CustomMsg + DeserializeOwned,
{
type Error: From<StdError>;

#[msg(exec)]
fn custom_generic_execute(
&self,
ctx: ExecCtx<CtxQuery>,
msgs: Vec<CosmosMsg<ExecParam>>,
) -> Result<Response<RetType>, Self::Error>;

#[msg(query)]
fn custom_generic_query(
&self,
ctx: QueryCtx<CtxQuery>,
param: QueryParam,
) -> Result<RetType, Self::Error>;
}

Generics in contracts

Generics in interfaces require no additional knowledge except Rust, and as you will soon find out, the same goes for generic contracts.

Let’s first define the generic type of our contract.

pub struct GenericContract<
InstantiateParam,
ExecParam,
FieldType,
> {
_field: Item<'static, FieldType>,
_phantom: std::marker::PhantomData<(
InstantiateParam,
ExecParam,
)>,
}

We will use one type for the generic field and two others to be used in the messages.

Now, let’s create the implementation for our contract.

#[contract]
impl<InstantiateParam, ExecParam, FieldType>
GenericContract<
InstantiateParam,
ExecParam,
FieldType,
>
where
for<'msg_de> InstantiateParam: CustomMsg + Deserialize<'msg_de> + 'msg_de,
ExecParam: CustomMsg + DeserializeOwned + 'static,
FieldType: 'static,
{
pub const fn new() -> Self {
Self {
_field: Item::new("field"),
_phantom: std::marker::PhantomData,
}
}

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

#[msg(exec)]
pub fn contract_execute(
&self,
_ctx: ExecCtx,
_msg: ExecParam,
) -> StdResult<Response> {
Ok(Response::new())
}
}

Same as in the case of the generic interface, generics used in the exec methods are passed to the ExecMsg, and the same happens to every other type of generated message.

Interface implementation

We can now implement the interface on our contract. At this stage, we have to specify concrete types for interface generics. Unfortunately, forwarding generics from the contract here is not yet supported, and we aim to add this functionality in the next release.

#[contract(module = crate::contract)]
#[messages(generic as Generic)]
impl<InstantiateParam, ExecParam, FieldType>
Generic<SvCustomMsg, SvCustomMsg, sylvia::types::SvCustomMsg>
for crate::contract::GenericContract<
InstantiateParam,
ExecParam,
FieldType,
>
{
type Error = StdError;

#[msg(exec)]
fn generic_exec(
&self,
_ctx: ExecCtx,
_msgs: Vec<CosmosMsg<sylvia::types::SvCustomMsg>>,
) -> StdResult<Response> {
Ok(Response::new())
}

#[msg(query)]
fn generic_query(
&self,
_ctx: QueryCtx,
_msg: sylvia::types::SvCustomMsg,
) -> StdResult<SvCustomMsg> {
Ok(SvCustomMsg {})
}
}

Sylvia deduces types used in place of generics from the trait type, and we don’t have to bother with any additional attributes.

Now that the interface is implemented, the only thing left is to inform Sylvia that the contract implementation should use the interface in the generated code. We, of course, do this with the messages attribute. However, here, we will have to alter the attribute and provide a list of types used in place of generics, as otherwise, this would be out of scope for the single macro call. This is done via a parameter in the attribute: #[messsages(generic<..>)] .

#[contract]
#[messages(generic<SvCustomMsg, SvCustomMsg, sylvia::types::SvCustomMsg> as Generic)]
impl<InstantiateParam, ExecParam, FieldType>
GenericContract<
InstantiateParam,
ExecParam,
FieldType,
>
where
for<'msg_de> InstantiateParam: CustomMsg + Deserialize<'msg_de> + 'msg_de,
ExecParam: CustomMsg + DeserializeOwned + 'static,
FieldType: 'static,
{
pub const fn new() -> Self {
Self {
_field: Item::new("field"),
_phantom: std::marker::PhantomData,
}
}

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

#[msg(exec)]
pub fn contract_execute(
&self,
_ctx: ExecCtx,
_msg: ExecParam,
) -> StdResult<Response> {
Ok(Response::new())
}
}

Generic entry points

Messages in our contract are defined, and we are only missing the entry points for it to be fully functional.

Here, as in the case of interface implementation, we have to provide concrete types as they cannot be generic. We do this by extending the entry_points macro into #[entry_points(generics<..>)] .

#[cfg_attr(not(feature = "library"), entry_points(generics<SvCustomMsg, SvCustomMsg, String>))]
#[contract]
#[messages(generic<SvCustomMsg, SvCustomMsg, sylvia::types::SvCustomMsg> as Generic)]
impl<InstantiateParam, ExecParam, FieldType>
GenericContract<
InstantiateParam,
ExecParam,
FieldType,
>
where
for<'msg_de> InstantiateParam: CustomMsg + Deserialize<'msg_de> + 'msg_de,
ExecParam: CustomMsg + DeserializeOwned + 'static,
FieldType: 'static,
{
..
}

Done. Our generic contract is set up and ready to use.

As you explore the possibilities unlocked by Sylvia 0.9.0, remember to update your imports and embrace the power of generics in your Sylvia projects. Upgrade today and unlock a new realm of possibilities in smart contract development with Sylvia.

For a more comprehensive understanding and practical inside, we encourage you to explore the examples and delve into the details provided in Sylvia’s book.

--

--