SDK

Now that you’ve got your module set up you’re ready for our hot sauce. While you can create any regular smart contract in a module, it’s where our software shines. Instead, 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!

How it works

The abstract-sdk crate is a toolbox for developers to create composable smart contract APIs. It allows you to use composed functionality with a few keystrokes through its combination of supertraits and blanket implementations. Supertraits are Rust traits that have one or multiple trait bounds while a blanket implementation is a Rust implementation that is automatically implemented for every object that meets the 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:

APIs

Abstract API objects are Rust structs that expose some smart contract functionality. Such an API can only be retrieved if a contract (or feature-object) implements the required features/api traits. Access to an API is automatically provided if the trait constraints for the API are met by the contract.

Most of the APIs either return a CosmosMsg or an AccountAction. The CosmosMsg is a message that should be added as-is to the Response to perform some action.

CosmosMsg Example

This example sends coins from the local contract (module) to the account that the application is installed on which does not require the account itself to execute the action.

            // Get bank API struct from the app
            let bank: Bank<'_, MockModule> = app.bank(deps.as_ref());
            // Create coins to deposit
            let coins: Vec<Coin> = coins(100u128, "asset");
            // Construct messages for deposit (transfer from this contract to the account)
            let deposit_msgs: Vec<CosmosMsg> = bank.deposit(coins.clone()).unwrap();
            // Add to response
            let response: Response = Response::new().add_messages(deposit_msgs);

Alternatively AccountAction structs can also be returned by an API. An AccountAction is supposed to be forwarded to the Abstract Account to let the account perform action. AccountActions can be executed with the Executor API. The returned CosmosMsg should be added to the action’s Response.

AccountAction Example

This example sends coins from the account to another address which requires the account itself to execute the action.

            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);

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.

The code behind this example is available here.

// Trait to retrieve the Splitter object
// Depends on the ability to transfer funds
pub trait SplitterInterface: TransferInterface {
    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 {}

#[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),
            })
    }
}

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

Available API Objects

The following API objects are available in the Abstract SDK:

Other projects have also started building APIs. Here are some examples:

Features

Features are the lowest-level traits that are contained within the SDK and they don’t have any 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() {
use crate::{ans_resolve::Resolve, cw_helpers::wasm_smart_query, AbstractSdkResult};
use abstract_core::{
    ans_host::{AssetPairingFilter, AssetPairingMapEntry, PoolAddressListResponse, QueryMsg},
    objects::{ans_host::AnsHost, DexAssetPairing},
};
use cosmwasm_std::Deps;

/// 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 Abstract Name Service, and thus has a way to resolve ANS entries. By composing these features it is possible to write advanced APIs that are automatically implemented on objects that support its required features.

Now instead of letting you implement these traits yourself, we’ve already gone ahead and implemented them for the App and Adapter structs. Here’s the implementation for the App:

#![allow(unused)]
fn main() {
impl<
        Error: ContractError,
        CustomInitMsg,
        CustomExecMsg,
        CustomQueryMsg,
        CustomMigrateMsg,
        ReceiveMsg,
        SudoMsg,
    > AbstractNameService
    for AppContract<
        Error,
        CustomInitMsg,
        CustomExecMsg,
        CustomQueryMsg,
        CustomMigrateMsg,
        ReceiveMsg,
        SudoMsg,
    >
{
    fn ans_host(&self, deps: Deps) -> AbstractSdkResult<AnsHost> {
        // Retrieve the ANS host address from the base state.
        Ok(self.base_state.load(deps.storage)?.ans_host)
    }
}
}

So when you’re building your application the module struct already has the features and data required to do the basic abstract operations. With this in place we can start creating more advanced functionality.

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::*;