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.
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:
contract_id
: The contract ID is a string that will be saved to acw2
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.contract_version
: The contract version should be the version of the crate, it is also stored in thecw2
item and is checked when performing migrations and on-chain dependency resolution.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}} }
You can find more application code to read in our 💥 Awesome Abstract repository 💥.
The available handlers are:
with_execute
: Called when the App’sExecuteMsg
is called on the instantiate entry point.with_instantiate
: Called when the App’sInstantiateMsg
is called on the instantiate entry point.with_query
: Called when the App’sQueryMsg::Module
is called on the query entry point.with_migrate
: Called when the App’sMigrateMsg
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’sSudoMsg
is called on the sudo entry point.with_receive
: Called when the App’sExecuteMsg::Receive
variant is called on the execute entry point.with_ibc_callbacks
: Called when the App’sExecuteMsg::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!