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, using the SDK is where our software shines. 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!
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 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:
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.
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 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 the 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);
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::ApiQuery, AbstractSdkResult}; /// 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.
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),
})
}
}
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))