Account SDK
Now that that you’re familiar with construction of your module you’re ready for our hot sauce. While you can traditional code in your module, using our SDK will give you a huge productivity boost. In short, we’ve created an account abstraction programming toolbox that allows you to easily control an Abstract Account’s interactions, as well as create your own APIs that can be used by other developers to interact with your unique application. Composability galore!
APIs
Abstract API objects are Rust structs that expose some smart contract functionality. Such an API object can only be constructed if a contract implements the traits that are required for that API. Access to an API is automatically provided if the trait constraints for the API are met by the contract.
We’ve created a set of APIs that can be used to interact with the Abstract Account and have implemented their trait requirements on the module base types that we provide (
App
andAdapter
). So for you, it’s just plug and play! 🎉
Most of the APIs either return a CosmosMsg
or an AccountAction
.
CosmosMsg
Example
The CosmosMsg
is a message that should be added as-is to the Response
to perform some action.
This example sends coins from the local contract (module) to the account that the application is installed on.
// Get bank API struct from the app
let bank: Bank<'_, MockModule> = app.bank(deps.as_ref());
// Define coins to send
let coins: Vec<Coin> = coins(100u128, "denom");
// Construct messages for deposit (transfer from this contract to the account)
let deposit_msgs: Vec<CosmosMsg> = bank.deposit(coins.clone()).unwrap();
// Create response and add deposit msgs
let response: Response = app.response("deposit").add_messages(deposit_msgs);
Ok(response)
Custom CosmosMsgs
can be added in the same way through the app.response("<action>")
function. The action
attribute of the function is a string that will be added to the response’s attributes and will be available in the transaction result under the wasm-abstract
event. This way you can easily figure out which actions were called in a tx!
The above example can equally be written as:
#![allow(unused)] fn main() { let coins: Vec<Coin> = coins(100u128, "denom"); // Create CosmosMsg let bank_msg: CosmosMsg = CosmosMsg::Bank(BankMsg::Send { to_address: "<account_address>".to_string(), amount: coins, }); // Add to Response let response: Response = app.response("deposit").add_message(deposit_msg); Ok(response) }
This gives you all the flexibility you are used to when working with CosmWasm!
AccountAction
Example
The other kind of struct that can be returned by an Abstract API is the AccountAction
. An AccountAction
is a single, or collection of CosmosMsgs
that should be executed on the App’s Abstract Account.
AccountActions
can be executed with the Executor
API. The returned CosmosMsg
should be added to the action’s Response
.
The following example sends coins from the account to another address. This action requires the account itself to execute the message and transfer the funds.
let recipient: Addr = Addr::unchecked("recipient");
let bank: Bank<'_, MockModule> = app.bank(deps.as_ref());
let coins: Vec<Coin> = coins(100u128, "asset");
let bank_transfer: AccountAction = bank.transfer(coins.clone(), &recipient).unwrap();
let executor: Executor<'_, MockModule> = app.executor(deps.as_ref());
let account_message: ExecutorMsg = executor.execute(vec![bank_transfer]).unwrap();
let response: Response = Response::new().add_message(account_message);
So through the Executor
API you can execute messages on behalf of the Account! Also notice that you can provide multiple actions to the executor to be executed in sequence.
How it works
As you’re aware, abstract-sdk
crate is a toolbox for developers to create composable smart contract APIs. It does this through a combination of supertraits and blanket implementations, two concepts that are native to the Rust language.
Supertraits are Rust traits that have one or multiple trait bounds while a blanket implementation is a Rust trait implementation that is automatically implemented for every object that meets that trait’s trait bounds. The Abstract SDK uses both to achieve its modular design.
For more information about traits, supertraits and blanket implementations, check out the Rust documentation:
Usage
Add abstract-sdk
to your Cargo.toml
by running:
cargo add abstract-sdk
Then import the prelude in your contract. This will ensure that you have access to all the traits which should help your IDE with auto-completion.
use abstract_sdk::prelude::*;
Creating your own API
The Bank
API allows developers to transfer assets from and to the Account. We now want to use this API to create a Splitter
API that splits the transfer of some amount of funds between a set of receivers.
// Trait to retrieve the Splitter object
// Depends on the ability to transfer funds
pub trait SplitterInterface: TransferInterface + AccountExecutor + ModuleIdentification {
fn splitter<'a>(&'a self, deps: Deps<'a>) -> Splitter<'a, Self> {
Splitter { base: self, deps }
}
}
// Implement for every object that can transfer funds
impl<T> SplitterInterface for T where T: TransferInterface + AccountExecutor + ModuleIdentification {}
impl<T: SplitterInterface> AbstractApi<T> for Splitter<'_, T> {
const API_ID: &'static str = "Splitter";
fn base(&self) -> &T {
self.base
}
fn deps(&self) -> Deps {
self.deps
}
}
#[derive(Clone)]
pub struct Splitter<'a, T: SplitterInterface> {
base: &'a T,
deps: Deps<'a>,
}
impl<T: SplitterInterface> Splitter<'_, T> {
/// Split an asset to multiple users
pub fn split(&self, asset: AnsAsset, receivers: &[Addr]) -> AbstractSdkResult<AccountAction> {
// split the asset between all receivers
let receives_each = AnsAsset {
amount: asset
.amount
.multiply_ratio(Uint128::one(), Uint128::from(receivers.len() as u128)),
..asset
};
// Retrieve the bank API
let bank = self.base.bank(self.deps);
receivers
.iter()
.map(|receiver| {
// Construct the transfer message
bank.transfer(vec![&receives_each], receiver)
})
.try_fold(AccountAction::default(), |mut acc, v| match v {
Ok(action) => {
// Merge two AccountAction objects
acc.merge(action);
Ok(acc)
}
Err(e) => Err(e),
})
}
}
These APIs can then be used by any contract that implements its required traits, in this case the TransferInterface
.
let asset = AnsAsset {
amount: Uint128::from(100u128),
name: "usd".into(),
};
let receivers = vec![receiver1, receiver2, receiver3];
let split_funds = module.splitter(deps.as_ref()).split(asset, &receivers)?;
assert_eq!(split_funds.messages().len(), 3);
let msg: ExecutorMsg = module.executor(deps.as_ref()).execute(vec![split_funds])?;
Ok(Response::new().add_message(msg))
Features
Features are the lowest-level traits that are contained within the SDK and they don’t have any (custom) trait bounds. They generally act as data accessor traits. I.e. if a struct implements a feature it means that it has some way to get the information required by that feature.
Here’s an example of such a feature:
#![allow(unused)] fn main() { /// Accessor to the Abstract Name Service. pub trait AbstractNameService: Sized { /// Get the ANS host address. fn ans_host(&self, deps: Deps) -> AbstractSdkResult<AnsHost>; /// Construct the name service client. fn name_service<'a>(&'a self, deps: Deps<'a>) -> AbstractNameServiceClient<'a, Self> { AbstractNameServiceClient { base: self, deps, host: self.ans_host(deps).unwrap(), } } } }
Any structure that implements this trait has access to the AnsHost
struct, which is a wrapper around an Addr
. Because that structure now has the address of that contract, it can resolve ANS entries.
Now instead of letting you implement these traits yourself, we’ve already gone ahead and implemented them for the App
and Adapter
structs.
So when you’re building your application, the module struct already has the features and data required to do abstract operations (😉). With this in place we can start creating more advanced functionality.
Appendix
Available API Objects
The following API objects are available in the Abstract SDK:
Other projects have also started building APIs. Here are some examples:
Cron Cats
- More coming soon…