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. AccountAction
s 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:
Cron Cats
- More coming soon…
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::*;