Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up

CosmWasm smart contract framework

License

NotificationsYou must be signed in to change notification settings

CosmWasm/sylvia

Repository files navigation

Sylvia is the old name meaning Spirit of The Wood.

Sylvia is the Roman goddess of the forest.

Sylvia is also a framework created to give you the abstraction-focused andscalable solution for building your CosmWasm Smart Contracts. Find your wayinto the forest of Cosmos ecosystem. We provide you with the toolset, so insteadof focusing on the raw structure of your contract, you can create it in properand idiomatic Rust and then just let cargo make sure that they are sound.

Learn more aboutsylvia inthe book

Sylvia contract template

The Sylvia template streamlines the development of CosmWasm smart contracts by providing a project scaffold that adheres to best practices and leverages the Sylvia framework's powerful features. It's designed to help developers focus more on their contract's business logic rather than boilerplate code.

Learn more here:Sylvia Template on GitHub

The approach

CosmWasm ecosystem core provides the base buildingblocks for smart contracts - thecosmwasm-std for basic CW bindings, thecw-storage-plus for easier state management,and thecw-multi-test for testing them.Sylvia framework is built on top of them, so for creating contracts, you don'thave to think about message structure, how their API is (de)serialized, or howto handle message dispatching. Instead, the API of your contract is a set oftraits you implement on your contract type. The framework generates things like entrypoint structures, functions dispatching the messages, or even helpers for multitest.It allows for better control of interfaces, including validating their completenessin compile time.

Code generation

Sylvia macros generate code in thesv module. This means that everycontract andinterface macro call must be made in a separate module to avoid collisions betweenthe generated modules.

Contract type

In Sylvia, we define our contracts as structures:

use cw_storage_plus::Item;use cosmwasm_schema::cw_serde;use sylvia::types::QueryCtx;use sylvia::cw_std::ensure;/// Our new contract type.///structMyContract<'a>{pubcounter:Item<'a,u64>,}/// Response type returned by the/// query method.///#[cw_serde]pubstructCounterResp{pubcounter:u64,}#[entry_points]#[contract]#[sv::error(ContractError)]implMyContract<'_>{pubfnnew() ->Self{Self{counter:Item::new("counter")}}#[sv::msg(instantiate)]pubfninstantiate(&self,ctx:InstantiateCtx,counter:u64) ->StdResult<Response>{self.counter.save(ctx.deps.storage,&counter)?;Ok(Response::new())}#[sv::msg(exec)]pubfnincrement(&self,ctx:ExecCtx) ->Result<Response,ContractError>{let counter =self.counter.load(ctx.deps.storage)?;ensure!(counter <10,ContractError::LimitReached);self.counter.save(ctx.deps.storage,&(counter +1))?;Ok(Response::new())}#[sv::msg(query)]pubfncounter(&self,ctx:QueryCtx) ->StdResult<CounterResp>{self.counter.load(ctx.deps.storage).map(|counter|CounterResp{ counter})}}

Sylvia will generate the following new structures:

pubmod sv{usesuper::*;structInstantiateMsg{counter:u64,}enumExecMsg{Increment{}}enumContractExecMsg{MyContract(ExecMsg)}enumQueryMsg{Counter{}}enumContractQueryMsg{MyContract(QueryMsg)}// [...]}pubmod entry_points{usesuper::*;#[sylvia::cw_std::entry_point]pubfninstantiate(deps: sylvia::cw_std::DepsMut,env: sylvia::cw_std::Env,info: sylvia::cw_std::MessageInfo,msg:InstantiateMsg,) ->Result<sylvia::cw_std::Response,StdError>{        msg.dispatch(&MyContract::new(),(deps, env, info)).map_err(Into::into)}// [...]}

entry_points macro generatesinstantiate,execute,query andsudo entry points.All those methods calldispatch on the msg received and run proper logic defined for the sentvariant of the message.

What is essential - the field in theInstantiateMsg (and other messages) gets the same name as thefunction argument.

TheExecMsg is the primary one you may use to send messages to the contract.TheContractExecMsg is only an additional abstraction layer that would matterlater when we define traits for our contract.Thanks to theentry_point macro it is already being used in the generated entry point and we don'thave to do it manually.

What you might notice - we can still useStdResult (soStdError) if we don'tneedContractError in a particular function. What is important is that the returnedresult type has to implementInto<ContractError>, whereContractError is a contracterror type - it will all be commonized in the generated dispatching function (soentry points have to returnContractError as its error variant).

Interfaces

One of the fundamental ideas of the Sylvia framework is the interface, allowing thegrouping of messages into their semantical groups. Let's define a Sylvia interface:

pubmod group{usesuper::*;use sylvia::interface;use sylvia::types::ExecCtx;use sylvia::cw_std::StdError;#[cw_serde]pubstructIsMemberResp{pubis_member:bool,}#[interface]pubtraitGroup{typeError:From<StdError>;#[sv::msg(exec)]fnadd_member(&self,ctx:ExecCtx,member:String) ->Result<Response,Self::Error>;#[sv::msg(query)]fnis_member(&self,ctx:QueryCtx,member:String) ->Result<IsMemberResp,Self::Error>;}}

Then we need to implement the trait on the contract type:

use sylvia::cw_std::{Empty,Addr};use cw_storage_plus::{Map,Item};pubstructMyContract<'a>{counter:Item<'a,u64>,// New field added - remember to initialize it in `new`members:Map<'a,&'aAddr,Empty>,}impl group::GroupforMyContract<'_>{typeError =ContractError;fnadd_member(&self,ctx:ExecCtx,member:String) ->Result<Response,ContractError>{let member = ctx.deps.api.addr_validate(&member)?;self.members.save(ctx.deps.storage,&member,&Empty{})?;Ok(Response::new())}fnis_member(&self,ctx:QueryCtx,member:String) ->Result<group::IsMemberResp,ContractError>{let is_member =self.members.has(ctx.deps.storage,&Addr::unchecked(&member));let resp = group::IsMemberResp{            is_member,};Ok(resp)}}#[contract]#[sv::messages(groupasGroup)]implMyContract<'_>{// Nothing changed here}

First, note that I defined the interface trait in its separate module with a namematching the trait name, but written "snake_case" instead of CamelCase. Here I have thegroup module for theGroup trait, but theCrossStaking trait should be placedin its owncross_staking module (note the underscore). This is a requirement rightnow - Sylvia generates all the messages and boilerplate in this module and will tryto access them through this module. If the interface's name is a camel-caseversion of the last module path's segment, theas InterfaceName can be omitted.F.e.#[sv::messages(cw1 as Cw1)] can be reduced to#[sv::messages(cw1)]

Then there is theError type embedded in the trait - it is also needed there,and the trait bound here has to be at leastFrom<StdError>, as Sylvia mightgenerate code returning theStdError in deserialization/dispatching implementation.The trait can be more strict - this is the minimum.

Finally, the implementation block has an additional#[sv::messages(module as Identifier)] attribute. Sylvia needs it to generate the dispatchingproperly - there is the limitation that every macro has access only to its localscope. In particular - we cannot see all traits implemented by a type and theirimplementation from the#[contract] crate.

To solve this issue, we put this#[sv::messages(...)] attribute pointing to Sylviawhat is the module name where the interface is defined, and giving a unique namefor this interface (it would be used in generated code to provide proper enum variant).

Macro attributes

structMyMsg;implCustomMsgforMyMsg{}structMyQuery;implCustomQueryforMyMsg{}#[entry_point]#[contract]#[sv::error(ContractError)]#[sv::messages(interfaceasInterface)]#[sv::messages(interfaceasInterfaceWithCustomType: custom(msg, query))]#[sv::custom(msg=MyMsg, query=MyQuery)]#[sv::msg_attr(exec,PartialOrd)]#[sv::override_entry_point(sudo=crate::entry_points::sudo(crate::SudoMsg))]implMyContract{// ...#[sv::msg(query)]#[sv::attr(serde(rename(serialize ="CustomQueryMsg")))]fnquery_msg(&self,_ctx:QueryCtx) ->StdResult<Response>{// ...}}
  • sv::error is used by bothcontract andentry_point macros. It is necessary in case a customerror is being used by your contract. If omitted generated code will useStdError.

  • sv::messages is the attribute for thecontract macro. Its purpose is to inform Sylviaabout interfaces implemented for the contract. If the implemented interface does not use adefaultEmpty message response for query and/or exec then the: custom(query),: custom(msg) or: custom(msg, query) should be indicated.

  • sv::override_entry_point - refer to theOverriding entry points section.

  • sv::custom allows to define CustomMsg and CustomQuery for the contract. By default generated codewill returnResponse<Empty> and will useDeps<Empty> andDepsMut<Empty>.

  • sv::msg_attr forwards any attribute to the message's type.

  • sv::attr forwards any attribute to the enum's variant.

Usage in external crates

What is important is the possibility of using generated code in the external code.First, let's start with generating the documentation of the crate:

cargo doc --document-private-items --open

This generates and opens documentation of the crate, including all generated structures.--document-private-item is optional, but it will generate documentation of non-publicmodules which is sometimes useful.

Going through the doc, you will see that all messages are generated in their structs/traitsmodules. To send messages to the contract, we can just use them:

use sylvia::cw_std::{WasmMsg, to_json_binary};fnsome_handler(my_contract_addr:String) ->StdResult<Response>{let msg = my_contract_crate::sv::ExecMsg::Increment{};let msg =WasmMsg::ExecMsg{contract_addr: my_contract_addr,msg:to_json_binary(&msg)?,funds:vec![],}let resp =Response::new().add_message(msg);Ok(resp)}

We can use messages from traits in a similar way:

let msg = my_contract_crate::group::QueryMsg::IsMember{member: addr,};let is_member: my_contract_crate::group::IsMemberResp =    deps.querier.query_wasm_smart(my_contract_addr,&msg)?;

It is important not to confuse the generatedContractExecMsg/ContractQueryMsgwithExecMsg/QueryMsg - the former is generated only for contract, not for interfaces,and is not meant to be used to send messages to the contract - their purpose is for propermessages dispatching only, and should not be used besides the entry points.

Query helpers

To make querying more user-friendlySylvia provides users withsylvia::types::BoundQuerier andsylvia::types::Remote helpers. The latter is meant to store the address of some remote contract.For each query method in the contract, Sylvia will add a method in a generatedsv::Querier trait.Thesv::Querier is then implemented forsylvia::types::BoundQuerier so the user can call the method.

Let's modify the query from the previous paragraph. Currently, it will look as follows:

let is_member =Remote::<OtherContractType>::new(remote_addr).querier(&ctx.deps.querier).is_member(addr)?;

Your contract might communicate with some other contract regularly.In such a case you might want to store it as a field in your Contract:

pubstructMyContract<'a>{counter:Item<'a,u64>,members:Map<'a,&'aAddr,Empty>,remote:Item<'a,Remote<'static,OtherContractType>>,}#[sv::msg(exec)]pubfnevaluate_member(&self,ctx:ExecCtx, ...) ->StdResult<Response>{let is_member =self.remote.load(ctx.deps.storage)?.querier(&ctx.deps.querier).is_member(addr)?;}

Executor message builder

Sylvia defines theExecutorBuildertype, which can be accessed throughRemote::executor.It's generic over the contract type and exposes execute methods from thecontract and every interface implemented on it through an auto-generatedExecutor traits.Execute messages of other contracts can be built withRemote as well bycallingexecutor method. It returns a message builder that implementsauto-generatedExecutor traits of all Sylvia contracts.Methods defined in theExecutor traits constructs an execute message,which variant corresponds to the method name.The message is then wrapped in theWasmMsg, and returned onceExecutorBuilder::build()method is called.

use sylvia::types::Remote;use other_contract::contract::OtherContract;use other_contract::contract::sv::Executor;let some_exec_msg:WasmMsg =Remote::<OtherContract>::new(remote_addr).executor().some_exec_method()?.build();

Using unsupported entry points

If there's a need for an entry point that is not implemented in Sylvia, you can implementit manually using the#[entry_point] macro. As an example, let's see how to implementreplies for messages:

use sylvia::cw_std::{DepsMut,Env,Reply,Response};#[contract]#[entry_point]#[sv::error(ContractError)]#[sv::messages(groupasGroup)]implMyContract<'_>{fnreply(&self,deps:DepsMut,env:Env,reply:Reply) ->Result<Response,ContractError>{todo!()}// [...]}#[entry_point]fnreply(deps:DepsMut,env:Env,reply:Reply) ->Result<Response,ContractError>{&MyContract::new().reply(deps, env, reply)}

It is important to create an entry function in the contract type - this way, itgains access to all the state accessors defined on the type.

Overriding entry points

There is a way to override an entry point or to add a custom-defined one.Let's consider the following code:

#[cw_serde]pubenumUserExecMsg{IncreaseByOne{},}pubfnincrease_by_one(ctx:ExecCtx) ->StdResult<Response>{crate::COUNTER.update(ctx.deps.storage, |count| ->Result<u32,StdError>{Ok(count +1)})?;Ok(Response::new())}#[cw_serde]pubenumCustomExecMsg{ContractExec(crate::ContractExecMsg),CustomExec(UserExecMsg),}implCustomExecMsg{pubfndispatch(self,ctx:(DepsMut,Env,MessageInfo)) ->StdResult<Response>{matchself{CustomExecMsg::ContractExec(msg) =>{                msg.dispatch(&crate::contract::Contract::new(), ctx)}CustomExecMsg::CustomExec(_) =>increase_by_one(ctx.into()),}}}#[entry_point]pubfnexecute(deps:DepsMut,env:Env,info:MessageInfo,msg:CustomExecMsg,) ->StdResult<Response>{    msg.dispatch((deps, env, info))}

It is possible to define a customexec message that will dispatch over one generatedby your contract and one defined by you. To use this custom entry point withcontract macroyou can add thesv::override_entry_point(...) attribute.

#[contract]#[sv::override_entry_point(exec=crate::entry_points::execute(crate::exec::CustomExecMsg))]#[sv::override_entry_point(sudo=crate::entry_points::sudo(crate::SudoMsg))]implContract{// ...}

It is possible to override all message types like that. Next to the entry point path, you willalso have to provide the type of your custom message. It is required to deserialize the messagein themultitest helpers.

Multitest

Sylvia also generates some helpers for testing contracts - it is hidden behind themt feature flag, which has to be enabled.

It is important to ensure nomt flag is set when the contract is built inwasmtarget because of some dependencies it uses, which are not buildable on Wasm. Therecommendation is to add an extrasylvia entry withmt enabled in thedev-dependencies, and also add themt feature on your contract, which enablesmt utilities in other contract tests. An exampleCargo.toml:

[package]name ="my-contract"version ="0.1.0"edition ="2021"[lib]crate-type = ["cdylib","rlib"][features]library = []mt = ["sylvia/mt"][dependencies]sylvia ="0.10.0"# [...][dev-dependencies]sylvia = {version ="0.10.0",features = ["mt"] }

And the example code:

#[cfg(test)]mod tests{usesuper::*;use sylvia::multitest::App;#[test]fncounter_test(){let app =App::default();let owner ="owner";let code_id = contract::CodeId::store_code(&app);let contract = code_id.instantiate(3).with_label("My contract").call(&owner).unwrap();let counter = contract.counter().unwrap();assert_eq!(counter, contract::CounterResp{ counter:3});        contract.increment().call(&owner).unwrap();let counter = contract.counter().unwrap();assert_eq!(counter, contract::CounterResp{ counter:4});}}

Note thecontract module I am using here - it is a slight changethat doesn't match the previous code - I assume here that all the contract codesits in thecontract module to make sure it is clear where the used type lies.So if I usecontract::something, it issomething in the module of the originalcontract (most probably Sylvia-generated).

First of all - we do not usecw-multi-test app directly. Instead, we use thesylviawrapper over it. It contains the original multi-test App internally, but it doesit in an internally mutable manner which makes it possible to avoid passing iteverywhere around. It adds some overhead, but it should not matter for testing code.

We are first using theCodeId type generated for every single Sylvia contractseparately. Its purpose is to abstract storing the contract in the blockchain. Itmakes sure to create the contract object and pass it to the multitest.

A contract'sCodeId type has one particularly interesting function - theinstantiate,which calls an instantiation function. It takes the same arguments as an instantiationfunction in the contract, except for the context that Sylvia's utilities would provide.

The function doesn't instantiate contract immediately - instead, it returns whatis calledInstantiationProxy. We decided that we don't want to force users to setall the metadata - admin, label, and funds to send with every instantiation call,as in the vast majority of cases, they are irrelevant. Instead, theInstantiationProxy provideswith_label,with_funds, andwith_amin functions,which set those meta fields in the builder pattern style.

When the instantiation is ready, we call thecall function, passing the messagesender - we could add anotherwith_sender function, but we decided that as thesender has to be passed every single time, we can save some keystrokes on that.

The thing is similar when it comes to execution messages. The biggest differenceis that we don't call it on theCodeId, but on instantiated contracts instead.We also have fewer fields to set on that - the proxy for execution provides onlythewith_funds function.

All the instantiation and execution functions return theResult<cw_multi_test::AppResponse, ContractError> type, whereContractErroris an error type of the contract.

Interface items in multitest

Trait declaring all the interface methods is directly implemented onthe contracts Proxy type.

use contract::mt::Group;#[test]fnmember_test(){let app =App::default();let owner ="owner";let member ="john";let code_id = contract::mt::CodeId::store_code(&app);let contract = code_id.instantiate(0).with_label("My contract").call(&owner);    contract.add_member(member.to_owned()).call(&owner);let resp = contract.is_member(member.to_owned())assert_eq!(resp, group::IsMemberResp{ is_member:true});}

Generics

Interface

Defining associated types on an interface is as simple as defining them on a regular trait.

#[interface]pubtraitGeneric{typeError:From<StdError>;typeExecParam:CustomMsg;typeQueryParam:CustomMsg;typeRetType:CustomMsg;#[sv::msg(exec)]fngeneric_exec(&self,ctx:ExecCtx,msgs:Vec<CosmosMsg<Self::ExecParam>>,) ->Result<Response,Self::Error>;#[sv::msg(query)]fngeneric_query(&self,ctx:QueryCtx,param:Self::QueryParam) ->Result<Self::RetType,Self::Error>;}

Generic contract

Generics in a contract might be either used as generic field types or as generic parameters of returntypes in the messages. When Sylvia generates the messages' enums, only generics used in respective methodswill be part of a given generated message type.

Example of usage:

pubstructGenericContract<InstantiateParam,ExecParam,FieldType,>{_field:Item<'static,FieldType>,_phantom: std::marker::PhantomData<(InstantiateParam,ExecParam,)>,}#[contract]impl<InstantiateParam,ExecParam,FieldType>GenericContract<InstantiateParam,ExecParam,FieldType>wherefor<'msg_de>InstantiateParam:CustomMsg +Deserialize<'msg_de> +'msg_de,ExecParam:CustomMsg +DeserializeOwned +'static,FieldType:'static,{pubconstfnnew() ->Self{Self{_field:Item::new("field"),_phantom: std::marker::PhantomData,}}#[sv::msg(instantiate)]pubfninstantiate(&self,_ctx:InstantiateCtx,_msg:InstantiateParam,) ->StdResult<Response>{Ok(Response::new())}#[sv::msg(exec)]pubfncontract_execute(&self,_ctx:ExecCtx,_msg:ExecParam,) ->StdResult<Response>{Ok(Response::new())}}

Generics in entry_points

Entry points have to be generated with concrete types. Using theentry_points macroon the generic contract we have to specify the types that have to be used.We do that withentry_points(generics<..>):

#[cfg_attr(not(feature ="library"), entry_points(generics<SvCustomMsg,SvCustomMsg,SvCustomMsg>))]#[contract]impl<InstantiateParam,ExecParam,FieldType>GenericContract<InstantiateParam,ExecParam,FieldType>wherefor<'msg_de>InstantiateParam:CustomMsg +Deserialize<'msg_de> +'msg_de,ExecParam:CustomMsg +DeserializeOwned +'static,FieldType:'static,{    ...}

The contract might define a generic type in place of a custom message and query.In such case we have to informentry_points macro usingcustom:

#[cfg_attr(not(feature ="library"), entry_points(generics<SvCustomMsg,SvCustomMsg,SvCustomMsg>, custom(msg=SvCustomMsg, query=SvCustomQuery))]#[contract]#[sv::custom(msg=MsgT, query=QueryT)]impl<InstantiateParam,ExecParam,FieldType,MsgT,QueryT>GenericContract<InstantiateParam,ExecParam,FieldType,MsgT,QueryT>wherefor<'msg_de>InstantiateParam:CustomMsg +Deserialize<'msg_de> +'msg_de,ExecParam:CustomMsg +DeserializeOwned +'static,FieldType:'static,{    ...}

Generating schema

Sylvia is designed to generate all the code thatcosmwasm-schema relies on - thismakes it very easy to generate schema for the contract. Just add abin/schema.rsmodule, which would be recognized as a binary, and add a simple main function there:

use cosmwasm_schema::write_api;use my_contract_crate::contract::{ContractExecMsg,ContractQueryMsg,InstantiateMsg};fnmain(){write_api!{        instantiate:InstantiateMsg,        execute:ContractExecMsg,        query:ContractQueryMsg,}}

Road map

Sylvia is in the adoption stage right now, but we are still working on more and morefeatures for you. Here is a rough roadmap for the coming months:

  • Replies - Sylvia still needs support for essential CosmWasm messages, which arereplies. We want to make them smart, so expressing the correlation between the sentmessage and the executed handler is more direct and not hidden in the reply dispatcher.
  • Migrations - Another important message we don't support, but the reason is similarto replies - we want them to be smart. We want to give you a nice way to provideupgrading Api for your contract, which would take care of its versioning.
  • IBC - we want to give you a nice IBC Api too! However, expect it to be awhile - we must first understand the best patterns here.
  • Better tooling support - The biggest Sylvia issue is that the code it generatesis not trivial, and not all the tooling handles it well. We are working on improvinguser experience in that regard.

Troubleshooting

For more descriptive error messages, consider using the nightly toolchain (add+nightlyargument for cargo)

  • Missing messages from an interface on your contract - You may be missing the#[sv::messages(interface as Interface)] attribute.

[8]ページ先頭

©2009-2025 Movatter.jp