#cosmwasm #execute-query #fadroma #component #generate #boilerplate #macro

fadroma-proc-derive

用于生成 CosmWasm 合同样板代码并启用代码组合的 DSL

2 个不稳定版本

0.7.0 2023 年 2 月 7 日
0.6.0 2023 年 1 月 31 日

#1#fadroma

AGPL-3.0

55KB
1K SLoC

Fadroma 合同推导

用于可组合、无样板代码的 CosmWasm 智能合同的进程宏。

简介

合同推导宏负责 CosmWasm 智能合同中所有必要的样板代码,并提供更高效的开发体验。目标是生成您无论如何都会编写的重复代码,并且不再生成其他任何代码,同时提供尽可能多的灵活性。任何对宏的误用都将导致 编译错误,而不是隐藏或意外的运行时行为。

入门

推导宏可以用于直接合同实现,使用 #[contract(entry)],或者用于实现 接口 的合同,使用 #[contract_impl(entry, path="some_interface")]。如果有任何跨合同通信,应使用接口实现,这允许包含其他合同的消息,而不会产生任何循环依赖,因为这些消息是通过接口导出的,因此可以在共享 crate 中声明,然后由单个合同 crate 包含。这是编写基于 CosmWasm 的合同的极其常见的模式。

属性

推导合约宏支持以下属性

属性 描述
init 合约的 init 方法。每个合约只有一个。如果未使用 entry(在组件中),则可以省略。
execute 合约的 execute 方法。每个执行方法一个。
query 合约的 query 方法。每个查询方法一个。
execute_guard 标记此属性的函数将在执行任何执行方法之前被调用。每个合约只有一个(可选)。
component 用于包含组件。
entry 表示应为此合约生成WASM入口点。
path 指定到类型或命名空间的路由。
skip 用于不包括组件的执行/查询。
custom_impl 用于提供组件的定制实现,而不是使用自动生成的默认特质实现。

用法

由于它们的用法始终是事实,我们决定隐式包含来自 cosmwasm_stdEnvExtern 类型作为相关方法的参数,这样就不需要每次都指定。以下表格描述了哪个属性包含哪些参数

属性 参数名 参数类型
init deps DepsMut
init env Env
init info MessageInfo
execute deps DepsMut
execute env Env
execute info MessageInfo
query deps Deps
query env Env
execute_guard deps DepsMut
execute_guard env &Env
execute_guard info &MessageInfo

使用 executequery 注释的方法名称用作它们各自定义中的枚举消息变体(转换为大写字母开头)。

基本合约

// contract.rs
#[contract(entry)]
pub trait Contract {
    #[init]
    fn new(config: Config) -> StdResult<Response> {
        Ok(Response::default())
    }

    #[execute]
    fn set_config(config: Config) -> StdResult<Response> {
        Ok(Response::default())
    }

    #[query]
    fn get_config() -> StdResult<Config> {
        Ok(Config)
    }
}

#[derive(Serialize, Deserialize, JsonSchema, Debug)]
pub struct Config;

为了更好地了解宏实际做什么,上面的代码等同于以下内容(不包括WASM模板代码)

pub trait Contract {
    fn new(
        &self,
        config: Config,
        mut deps: cosmwasm_std::DepsMut,
        env: cosmwasm_std::Env,
        info: cosmwasm_std::MessageInfo,
    ) -> StdResult<Response> {
        Ok(Response::default())
    }

    fn set_config(
        &self,
        config: Config,
        mut deps: cosmwasm_std::DepsMut,
        env: cosmwasm_std::Env,
        info: cosmwasm_std::MessageInfo,
    ) -> StdResult<Response> {
        Ok(Response::default())
    }

    fn get_config(&self, deps: cosmwasm_std::Deps, env: cosmwasm_std::Env) -> StdResult<Config> {
        Ok(Config)
    }
}

#[derive(Clone, Copy)]
pub struct DefaultImpl;

impl Contract for DefaultImpl {}

#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema, Debug)]
pub struct InstantiateMsg {
    pub config: Config,
}

#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
    SetConfig { config: Config },
}

#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
    GetConfig {},
}

pub fn instantiate(
    deps: cosmwasm_std::DepsMut,
    env: cosmwasm_std::Env,
    info: cosmwasm_std::MessageInfo,
    msg: InstantiateMsg,
    contract: impl Contract,
) -> cosmwasm_std::StdResult<cosmwasm_std::Response> {
    contract.new(msg.config, deps, env, info)
}

pub fn execute(
    mut deps: cosmwasm_std::DepsMut,
    env: cosmwasm_std::Env,
    info: cosmwasm_std::MessageInfo,
    msg: ExecuteMsg,
    contract: impl Contract,
) -> cosmwasm_std::StdResult<cosmwasm_std::Response> {
    match msg {
        ExecuteMsg::SetConfig { config } => contract.set_config(config, deps, env, info),
    }
}

pub fn query(
    deps: cosmwasm_std::Deps,
    env: cosmwasm_std::Env,
    msg: QueryMsg,
    contract: impl Contract,
) -> cosmwasm_std::StdResult<cosmwasm_std::Binary> {
    match msg {
        QueryMsg::GetConfig {} => {
            let result = contract.get_config(deps, env)?;
            cosmwasm_std::to_binary(&result)
        }
    }
}

#[cfg(target_arch = "wasm32")]
mod wasm {
    use super::cosmwasm_std::{
        do_execute, do_instantiate, do_query, to_binary, Deps, DepsMut, Env, MessageInfo,
        QueryResponse, Response, StdResult,
    };

    fn entry_init(
        deps: DepsMut,
        env: Env,
        info: MessageInfo,
        msg: super::InstantiateMsg,
    ) -> StdResult<Response> {
        super::instantiate(deps, env, info, msg, super::DefaultImpl)
    }

    pub fn entry_execute(
        deps: DepsMut,
        env: Env,
        info: MessageInfo,
        msg: super::ExecuteMsg,
    ) -> StdResult<Response> {
        super::execute(deps, env, info, msg, super::DefaultImpl)
    }

    fn entry_query(deps: Deps, env: Env, msg: super::QueryMsg) -> StdResult<QueryResponse> {
        let result = super::query(deps, env, msg, super::DefaultImpl)?;
        to_binary(&result)
    }

    #[no_mangle]
    extern "C" fn instantiate(env_ptr: u32, info_ptr: u32, msg_ptr: u32) -> u32 {
        do_instantiate(&entry_init, env_ptr, info_ptr, msg_ptr)
    }

    #[no_mangle]
    extern "C" fn execute(env_ptr: u32, info_ptr: u32, msg_ptr: u32) -> u32 {
        do_execute(&entry_execute, env_ptr, info_ptr, msg_ptr)
    }
    
    #[no_mangle]
    extern "C" fn query(env_ptr: u32, msg_ptr: u32) -> u32 {
        do_query(&entry_query, env_ptr, msg_ptr)
    }
}

具有接口

// shared/interfaces/contract.rs
#[interface]
pub trait Contract {
    #[init]
    pub fn new(config: Config) -> StdResult<Response>;

    #[execute]
    pub fn set_config(config: Config) -> StdResult<Response>;

    #[query]
    pub fn get_config() -> StdResult<Config>;
}

#[derive(Serialize, Deserialize, JsonSchema, Debug)]
pub struct Config;

// contracts/contract.rs
#[contract_impl(entry, path="shared::interfaces::contract")]
pub trait Contract {
    #[init]
    fn new(config: Config) -> StdResult<Response> {
        Ok(Response::default())
    }

    #[execute]
    fn set_config(config: Config) -> StdResult<Response> {
        Ok(Response::default())
    }

    #[query]
    fn get_config() -> StdResult<Config> {
        Ok(Config)
    }
}

// some other contract
// contracts/other_contract.rs
use shared::interfaces::contract::{ExecuteMsg, QueryMsg};
-- snip --

此代码将使用接口模块导出的消息生成必要的入口点和调度函数。接口定义仅生成 InstantiateMsgExecuteMsgQueryMsg 类型。此外,其方法不能有默认实现。请注意,接口定义和实现合约不能不同步,因为两者之间的任何差异都将导致编译错误。

执行保护

执行保护函数是在宏生成的 execute 函数内部匹配 ExecuteMsg 枚举之前调用的特殊函数。它必须没有参数,并带有 execute_guard 属性的注释。每个合约定义只能存在一个这样的函数。在需要在我们执行传入消息之前断言某些状态并在必要时失败的情况下很有用。例如,应与 Fadroma 杀死开关组件一起使用。在执行保护中,我们检查合约是否暂停或迁移,如果是这样,则返回一个 Err(())

组件

组件是简单地使用 contract 宏在别处声明的合约。我们可以通过在我们的当前合约中使用 component 属性来重用其功能。

单个合约可以使用一个或多个组件,如下所示

#[contract(
    component(path = "fadroma::admin"),
    component(path = "fadroma::killswitch")
)]

或当实现接口时

#[contract_impl(
    path = "shared::interfaces::contract",
    component(path = "fadroma::admin"),
    component(path = "fadroma::killswitch")
)]

宏将包括它们的执行和查询消息枚举作为当前消息枚举的元组变体。变体的名称是从组件的 path 参数的最后一段派生的。例如,上面的代码将生成以下执行消息

pub enum ExecuteMsg {
    Admin(fadroma::admin::ExecuteMsg),
    Killswitch(fadroma::killswitch::ExecuteMsg)
    // .. other variants
}

skip

某些组件可能不实现任何查询或执行方法(如Fadroma认证组件)。在这种情况下,可以通过指定skip属性来在导入合约消息时跳过这些方法。

#[contract(
    component(path = "fadroma::auth", skip(query)),
)]

有效的令牌是queryexecute。两者可以同时使用。

custom_impl

有时我们可能想要使用一个组件,但更改其一个(或多个)方法的实现。在这种情况下,我们只需要在新的空结构体上实现组件特质(就像我们通常在Rust中做的那样),并在组件定义中使用custom_impl属性指定其名称。默认情况下,宏将使用为每个contractcontract_impl生成的DefaultImpl结构体。如果我们想使用自定义实现,它看起来是这样的

#[contract(
    component(path = "fadroma::admin", custom_impl = "MyCustomAdminImplStruct"),
)]

依赖项

~1.5MB
~35K SLoC