Module Builder
Abstract provides multiple module bases, as detailed in our section on modules.
These bases (App
and Adapter
) implement some basic functionality and store some abstract-related state that will enable you to easily interact with our infrastructure through our SDK (which we’ll introduce later).
For now just know that we provide you with a builder pattern that allows you to easily add custom logic to these module bases. In the rest of this section we’ll outline how you can use this builder pattern to add custom functionality to your contract.
Overview
The builder pattern employed in building an Abstract module is a slight variation of the actual “builder” design pattern. Instead of creating a new builder at runtime, our module builder lets you set custom attributes on your module at compile time, meaning you end up with a const
value can be heavily optimized by the compiler. This system ensures that the overhead of using Abstract has little effect on both the code’s runtime and WASM binary size.
The code-snippets in this example can be found in the app template.
In this tutorial we will be working on an App
module.
App Type
Your custom AppType
will be a type alias for a specific type that fills in the base AppContract
type provided by the abstract-app
crate. By constructing this type you’re defining which messages you expect to receive at the custom endpoints of your contract.
Here’s what this looks like in the template:
// src/contract.rs
pub type App = AppContract<AppError, AppInstantiateMsg, AppExecuteMsg, AppQueryMsg, AppMigrateMsg>;
The type above contains all the mandatory types (Error
, Instantiate
, Execute
, Query
). An optional MigrateMsg
type is also added to allow you to customize the migration logic of your contract.
This new App
type alias will be used in a few more places throughout the contract, so it’s a good idea to define it at the top of the file.
Module ID
The Module identifier (Module ID) is a string that will identify your application. We covered it in detail in the section on modules, here.
You define your ID as a &'static str
like so:
pub const APP_ID: &str = "my-namespace:app";
Module Version
This is the version of your module. The version will be stored on-chain. When installing a module that depends on your module, our infrastructure will assert its version requirements. Ensuring that the contracts that depend on each other are version compatible. We’ll cover dependencies in more detail in the dependencies section.
pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
By default you should use the version of your package as your app version. That is what the env!
macro is doing in the example above. Alternatively you can provide any 3-digit version number as a valid version.
Build The App
Now that you have defined your type and all your attributes you can begin using the builder. To initiate this, first create the builder for the App:
// src/contract.rs
const APP: App = App::new(APP_ID, APP_VERSION, None)
The builder constructor takes three variables:
module_id
: The module ID is a string that we defined above.contract_version
: The contract version.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. This is explained in more detail in the metadata section.
Amazing! You now have a very basic Abstract module. You can now add your custom logic to your module by adding handlers to the module.
Below we’ve defined a complete App
module with a few custom handlers set:
const APP: App = App::new(APP_ID, APP_VERSION, None)
.with_instantiate(handlers::instantiate_handler)
.with_execute(handlers::execute_handler)
.with_query(handlers::query_handler)
.with_migrate(handlers::migrate_handler)
.with_replies(&[(INSTANTIATE_REPLY_ID, replies::instantiate_reply)]);
Handlers
The handler functions are defined in the
src/handlers
dir.
The app can then be customized by adding handler functions for your endpoints. These functions are executed whenever a specific endpoint is called on the module.
Writing a handler function
These handlers are where you will write your custom logic for your App. For example, below we’ve defined a custom execute
handler that handles all the different AppExecuteMsg
variants of our module.
A special feature of these 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.
// src/handlers/execute.rs
pub fn execute_handler(
deps: DepsMut,
_env: Env,
info: MessageInfo,
module: App,
msg: AppExecuteMsg,
) -> AppResult {
match msg {
AppExecuteMsg::Increment {} => increment(deps, module),
AppExecuteMsg::Reset { count } => reset(deps, info, count, module),
AppExecuteMsg::UpdateConfig {} => update_config(deps, info, module),
}
}
The code above should look very familiar. It’s only a slight variation of the code you would write in a regular CosmWasm contract. The only difference is that you have access to the module: App
attribute, which is the instance of your module.
You can find more application code to read in our 💥 Awesome Abstract repository 💥.
Summary
The Abstract SDK allows you to easily make new custom smart contracts through a simple builder pattern and straight forward type system usage.
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!
Appendix
This appendix contains all the available handlers, what type of handler Fn
they expect and the format of the messages that are exposed on the contract endpoints.
An overview of the available handlers:
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_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.with_module_ibc
: Called when a Module wants to call another module over IBC.
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> {
/// 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),
/// ModuleIbc endpoint to receive messages from modules on other chains
/// In order to trust this, the apps and adapters verify this comes from the ibc-host contract.
/// They should also trust the sending chain
ModuleIbc(ModuleIbcMsg),
}
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.
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, Module, Callback, IbcResult) -> 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> {
/// 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),
/// ModuleIbc endpoint to receive messages from modules on other chains
/// In order to trust this, the apps and adapters verify this comes from the ibc-host contract.
/// They should also trust the sending chain
ModuleIbc(ModuleIbcMsg),
}
Module Ibc
The module ibc handler is a mutable entry point of the contract. It is similar to the execute
handler but is
specifically geared towards handling module-to-module IBC calls. On this endpoint, the sender is a module on a remote chain. Module developers should test the client_chain
AND source_module
variables against their local storage. Without it, any module could execute the logic inside this functio
Function Signature
/// Function signature for an Module to Module IBC handler.
pub type ModuleIbcHandlerFn<Module, Error> =
fn(DepsMut, Env, Module, ModuleIbcInfo, Binary) -> Result<Response, Error>;
Message
Called when the App’s ExecuteMsg::ModuleIbc
variant is called on the execute entry point. The receiving type is not
customizable. It contains :
client_chain
the remote chain identificationsource_module
the sending module on the remote chainmsg
the message sent by the module. This is usually deserialized by the module’s developer to trigger actions.
/// Wrapper around all possible messages that can be sent to the module.
#[cosmwasm_schema::cw_serde]
pub enum ExecuteMsg<BaseMsg, CustomExecMsg> {
/// 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),
/// ModuleIbc endpoint to receive messages from modules on other chains
/// In order to trust this, the apps and adapters verify this comes from the ibc-host contract.
/// They should also trust the sending chain
ModuleIbc(ModuleIbcMsg),
}
#[cw_serde]
pub struct ModuleIbcInfo {
/// Remote chain identification
pub chain: TruncatedChainId,
/// Information about the module that called ibc action on this module
pub module: ModuleInfo,
}
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.