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 and Adapter). 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)

source

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: "<proxy_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);

source

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.

Info

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:

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<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.

Info

Other structs that implement a feature without being module bases are called Feature Objects.

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 + ModuleIdentification {
    fn splitter<'a>(&'a self, deps: Deps<'a>) -> Splitter<Self> {
        Splitter { base: self, deps }
    }
}

// Implement for every object that can transfer funds
impl<T> SplitterInterface for T where T: TransferInterface + ModuleIdentification {}

impl<'a, T: SplitterInterface> AbstractApi<T> for Splitter<'a, T> {
    fn base(&self) -> &T {
        self.base
    }
    fn deps(&self) -> Deps {
        self.deps
    }
}

impl<'a, T: SplitterInterface> ApiIdentification for Splitter<'a, T> {
    fn api_id() -> String {
        "Splitter".to_owned()
    }
}

#[derive(Clone)]
pub struct Splitter<'a, T: SplitterInterface> {
    base: &'a T,
    deps: Deps<'a>,
}

impl<'a, T: SplitterInterface> Splitter<'a, 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::new(), |mut acc, v| match v {
                Ok(action) => {
                    // Merge two AccountAction objects
                    acc.merge(action);
                    Ok(acc)
                }
                Err(e) => Err(e),
            })
    }
}

source

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![
            Addr::unchecked("receiver1"),
            Addr::unchecked("receiver2"),
            Addr::unchecked("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))

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: