Module Builder

Abstract provides multiple module bases, as detailed in our section on modules. These base implementation provide you with the minimal state and configuration required to start building your modular application. After setting up your module base from our template you’ll probably want to customize it. Our module builder pattern allows you to do just that. It also gives you a great overview on all the entry points to you module, and those that others have built.

Overview

The builder pattern employed in building an Abstract module is a slight variation of the actual design pattern. Instead, the module builder lets you set custom entry point handlers at compile time, meaning you end up with a const value that is heavily optimized by the compiler. This ensures that the overhead of using Abstract has a negligible effect on both the code’s runtime and WASM binary size.

Info

The code-snippets in this example can be found in the counter app example.

In this tutorial we will be working with an App Module.

App Type

To set up your App module, start by integrating your custom messages. These messages are inserted in the top-level entry point message types, which will be discussed in more detail later. Here’s an example:

pub type CounterApp = AppContract<
    CounterError,
    CounterInitMsg,
    CounterExecMsg,
    CounterQueryMsg,
    CounterMigrateMsg,
    CounterReceiveMsg,
    CounterSudoMsg,
>;

All of these messages can be customized and will be used to type-check the rest of your implementation.

Build The App

Now that you have defined your type, you can begin using the builder. To initiate this, first create the base version of the app:

pub const COUNTER_APP: CounterApp = CounterApp::new(COUNTER_ID, APP_VERSION, None)

The constructor takes three variables:

  1. contract_id: The contract ID is a string that will be saved to a cw2 storage item. It’s an important security measure as this ensures that the contract can not be migrated to a different contract with a different function and also acts as an informational tag for off-chain processes.
  2. contract_version: The contract version should be the version of the crate, it is also stored in the cw2 item and is checked when performing migrations and on-chain dependency resolution.
  3. metadata: An optional URL that can be used to retrieve data off-chain. Can be used with the Abstract Metadata Standard to automatically generate interactive front-end components for the module.

All these fields are used in a custom ModuleData store as well, along with the module’s dependencies, which we will come back to later. Here’s the definition of the ModuleData field:

pub const MODULE: Item<ModuleData> = Item::new("module_data");

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ModuleData {
    /// The name of the module, which should be composed of
    /// the publisher's namespace and module id. eg. `cw-plus:cw20-base`
    pub module: String,
    /// Semantic version of the module's crate on release.
    /// Is used for migration assertions
    pub version: String,
    /// List of modules that this module depends on
    /// along with its version requirements.
    pub dependencies: Vec<Dependency>,
    /// URL to data that follows the Abstract metadata standard for
    /// resolving off-chain module information.
    pub metadata: Option<String>,
}

Handlers

The app can then be customized by adding whatever handler functions you need. These functions are executed whenever a specific endpoint is called on the module. A special feature about the functions is that we insert the instance of your module into the function’s attributes. This enables you to access the module struct in your code. You will learn why this is such a powerful feature in the next section on the Abstract SDK.

Here’s an example of a module with some handlers set:

pub const COUNTER_APP: CounterApp = CounterApp::new(COUNTER_ID, APP_VERSION, None)
    .with_instantiate(handlers::instantiate)
    .with_execute(handlers::execute)
    .with_query(handlers::query)
    .with_sudo(handlers::sudo)
    .with_receive(handlers::receive)
    .with_replies(&[(1u64, handlers::reply)])
    .with_migrate(handlers::migrate);

These handlers are functions that allow you to customize the smart contract’s behavior. For example, here’s a custom execute handler that updates the contract’s config state.

#![allow(unused)]
fn main() {
{{# include../../../ packages /abstract - app / examples / counter.rs:execute}}
}

Info

You can find more application code to read in our 💥 Awesome Abstract repository 💥.

The available handlers are:

  • with_execute: Called when the App’s ExecuteMsg is called on the instantiate entry point.
  • with_instantiate: Called when the App’s InstantiateMsg is called on the instantiate entry point.
  • with_query: Called when the App’s QueryMsg::Module is called on the query entry point.
  • with_migrate: Called when the App’s MigrateMsg is called on the migrate entry point.
  • with_replies: Called when the App’s reply entry point is called. Matches the function’s associated reply-id.
  • with_sudo: Called when the App’s SudoMsg is called on the sudo entry point.
  • with_receive: Called when the App’s ExecuteMsg::Receive variant is called on the execute entry point.
  • with_ibc_callbacks: Called when the App’s ExecuteMsg::IbcCallback is called on the execute entry point. Matches the callback’s callback ID to its associated function.

In the case of adapters, the handlers are the same, except for with_migrate and with_sudo that are missing for reasons we explain in the adapter section.

For a full overview of the list of handlers available, please refer to the respective module type documentation:

Below, we examine each handler in greater detail. The base fields and variants mentioned in the messages below are defined by the base module type that you chose to use, an App in this case.

Instantiate

The instantiate entry point is a mutable entry point of the contract that can only be called on contract instantiation. Instantiation of a contract is essentially the association of a public address to a contract’s state.

Function Signature

Expected function signature for the custom instantiate handler:

/// Function signature for an instantiate handler.
pub type InstantiateHandlerFn<Module, CustomInitMsg, Error> =
    fn(DepsMut, Env, MessageInfo, Module, CustomInitMsg) -> Result<Response, Error>;

Message

In order to instantiate an Abstract Module, you need to provide an InstantiateMsg with the following structure:

#[cosmwasm_schema::cw_serde]
pub struct InstantiateMsg<BaseMsg, CustomInitMsg = Empty> {
    /// base instantiate information
    pub base: BaseMsg,
    /// custom instantiate msg
    pub module: CustomInitMsg,
}

When the module’s instantiate function is called the struct’s module field is passed to your custom instantiation handler for you to perform any custom logic.

Execute

The execute entry point is a mutable entry point of the contract. Logic in this function can update the contract’s state and trigger state changes in other contracts by calling them. It is where the majority of your contract’s logic will reside.

Function Signature

Expected function signature for the custom execute handler:

/// Function signature for an execute handler.
pub type ExecuteHandlerFn<Module, CustomExecMsg, Error> =
    fn(DepsMut, Env, MessageInfo, Module, CustomExecMsg) -> Result<Response, Error>;

Message

Called when the App’s ExecuteMsg::Module variant is called on the execute entry point.

/// Wrapper around all possible messages that can be sent to the module.
#[cosmwasm_schema::cw_serde]
pub enum ExecuteMsg<BaseMsg, CustomExecMsg, ReceiveMsg = Empty> {
    /// A configuration message, defined by the base.
    Base(BaseMsg),
    /// An app request defined by a base consumer.
    Module(CustomExecMsg),
    /// IbcReceive to process IBC callbacks
    /// In order to trust this, the apps and adapters verify this comes from the ibc-client contract.
    IbcCallback(IbcResponseMsg),
    /// Receive endpoint for CW20 / external service integrations
    Receive(ReceiveMsg),
}

The content of the Module variant is passed to your custom execute handler.

Query

The query entry point is the non-mutable entry point of the contract. Like its name implies it it used to retrieve data from the contract’s state. This state retrieval can have a computation component but it can not alter the contract’s or any other state.

Function Signature

Expected function signature for the custom query handler:

/// Function signature for a query handler.
pub type QueryHandlerFn<Module, CustomQueryMsg, Error> =
    fn(Deps, Env, &Module, CustomQueryMsg) -> Result<Binary, Error>;

Message

Called when the App’s QueryMsg::Module variant is called on the query entry point.

#[cosmwasm_schema::cw_serde]
#[derive(QueryResponses)]
#[query_responses(nested)]
pub enum QueryMsg<BaseMsg, CustomQueryMsg = Empty> {
    /// A query to the base.
    Base(BaseMsg),
    /// Custom query
    Module(CustomQueryMsg),
}

The content of the Module variant is passed to your custom query handler.

Migrate

The migrate entry point is a mutable entry point that is called after a code_id change is applied to the contract. A migration in CosmWasm essentially swaps out the code that’s executed at the contract’s address while keeping the state as-is. The implementation of this function is often used to change the format of the contract’s state by loading the data as the original format and overwriting it with a new format, in case it changed. All adapter base implementations already perform version assertions that make it impossible to migrate to a contract with a different ID or with a version that is lesser or equal to the old version.

Function Signature

Expected function signature for the custom migrate handler:

/// Function signature for a migrate handler.
pub type MigrateHandlerFn<Module, CustomMigrateMsg, Error> =
    fn(DepsMut, Env, Module, CustomMigrateMsg) -> Result<Response, Error>;

Message

Called when the App’s migrate entry point is called. Uses the struct’s module field to customize the migration. Only this field is passed to the handler function.

#[cosmwasm_schema::cw_serde]
pub struct MigrateMsg<BaseMsg = Empty, CustomMigrateMsg = Empty> {
    /// base migrate information
    pub base: BaseMsg,
    /// custom migrate msg
    pub module: CustomMigrateMsg,
}

Reply

The reply entry point is a mutable entry point that is optionally called after a previous mutable action. It is often used by factory contracts to retrieve the contract of a newly instantiated contract. It essentially provides the ability perform callbacks on actions. A reply can be requested using CosmWasm’s SubMsg type and requires a unique ReplyId which is a u64. The customizable handler takes an array of (ReplyId, ReplyFn) tuples and matches any incoming reply on the correct ReplyId for you.

Function Signature

Expected function signature for the custom reply handler:

/// Function signature for a reply handler.
pub type ReplyHandlerFn<Module, Error> = fn(DepsMut, Env, Module, Reply) -> Result<Response, Error>;

Message

There is no customizable message associated with this entry point.

Sudo

The sudo entry point is a mutable entry point that can only be called by the chain’s governance module. I.e. any calls made to this contract should have been required to have gone through the chain’s governance process. This can vary from chain to chain.

Function Signature

Expected function signature for the custom sudo handler:

/// Function signature for a sudo handler.
pub type SudoHandlerFn<Module, CustomSudoMsg, Error> =
    fn(DepsMut, Env, Module, CustomSudoMsg) -> Result<Response, Error>;

Message

There is no base message for this entry point. Your message will be the message that the endpoint accepts.

Receive

The receive handler is a mutable entry point of the contract. It is similar to the execute handler but is specifically geared towards handling messages that expect a Receive variant in the ExecuteMsg. Examples of this include but are not limited to:

  • Cw20 send messages
  • Nois Network random number feed

Function Signature

Expected function signature for the custom receive handler:

/// Function signature for a receive handler.
pub type ReceiveHandlerFn<Module, ReceiveMsg, Error> =
    fn(DepsMut, Env, MessageInfo, Module, ReceiveMsg) -> Result<Response, Error>;

Message

Called when the App’s ExecuteMsg::Receive variant is called on the execute entry point.

/// Wrapper around all possible messages that can be sent to the module.
#[cosmwasm_schema::cw_serde]
pub enum ExecuteMsg<BaseMsg, CustomExecMsg, ReceiveMsg = Empty> {
    /// A configuration message, defined by the base.
    Base(BaseMsg),
    /// An app request defined by a base consumer.
    Module(CustomExecMsg),
    /// IbcReceive to process IBC callbacks
    /// In order to trust this, the apps and adapters verify this comes from the ibc-client contract.
    IbcCallback(IbcResponseMsg),
    /// Receive endpoint for CW20 / external service integrations
    Receive(ReceiveMsg),
}

Ibc Callback

The ibc callback handler is a mutable entry point of the contract. It is similar to the execute handler but is specifically geared towards handling callbacks from IBC actions. Since interacting with IBC is an asynchronous process we aim to provide you with the means to easily work with IBC. Our SDK helps you send IBC messages while this handler helps you execute logic whenever the IBC action succeeds or fails. Our framework does this by optionally allowing you to add callback information to any IBC action. A callback requires a unique CallbackId which is a String. The callback handler takes an array of (CallbackId, IbcCallbackFn) tuples and matches any incoming callback on the correct CallbackId for you. Every call to this handler is verified by asserting that the caller is the framework’s IBC-Client contract.

Function Signature

/// Function signature for an IBC callback handler.
pub type IbcCallbackHandlerFn<Module, Error> = fn(
    DepsMut,
    Env,
    MessageInfo,
    Module,
    CallbackId,
    CallbackMessage,
    Callback,
) -> Result<Response, Error>;

Message

Called when the App’s ExecuteMsg::IbcCallback variant is called on the execute entry point. The receiving type is not customizable but contains the IBC action acknowledgment.

/// Wrapper around all possible messages that can be sent to the module.
#[cosmwasm_schema::cw_serde]
pub enum ExecuteMsg<BaseMsg, CustomExecMsg, ReceiveMsg = Empty> {
    /// A configuration message, defined by the base.
    Base(BaseMsg),
    /// An app request defined by a base consumer.
    Module(CustomExecMsg),
    /// IbcReceive to process IBC callbacks
    /// In order to trust this, the apps and adapters verify this comes from the ibc-client contract.
    IbcCallback(IbcResponseMsg),
    /// Receive endpoint for CW20 / external service integrations
    Receive(ReceiveMsg),
}

Dependencies

There is another method accessible on the module builder, which is the with_dependencies function. As it states it allows you to specify any smart contract dependencies that your module might require. This is a key requirement for building truly composable and secure applications. We’ll cover dependencies further the dependencies section.

Summary

The Abstract SDK allows you to easily construct modules by using our low-overhead smart contract builder. By employing this pattern you re-use the base contract’s code, allowing you to focus on the ideas that make your product unique.

In the next section we’ll cover how you can use the module object that we make available in the function handlers to write highly functional smart contract code.

Ever wanted to swap on any cosmos DEX with only one line of code? Look no further!