use std::fmt::{Debug, Display};
use abstract_interface::{
Abstract, AbstractAccount, AbstractInterfaceError, AccountDetails, DependencyCreation,
InstallConfig, MFactoryQueryFns, ManagerExecFns, ManagerQueryFns, RegisteredModule, VCQueryFns,
};
use abstract_std::{
ibc_client,
manager::{
state::AccountInfo, InfoResponse, ManagerModuleInfo, ModuleAddressesResponse,
ModuleInfosResponse, ModuleInstallConfig,
},
objects::{
gov_type::GovernanceDetails,
module::{ModuleInfo, ModuleVersion},
namespace::Namespace,
nested_admin::MAX_ADMIN_RECURSION,
validation::verifiers,
AccountId, AssetEntry,
},
version_control::NamespaceResponse,
IBC_CLIENT, PROXY,
};
use cosmwasm_std::{to_json_binary, Attribute, Coins, CosmosMsg, Uint128};
use cw_orch::{contract::Contract, environment::MutCwEnv, prelude::*};
use crate::{
client::AbstractClientResult,
infrastructure::{Environment, Infrastructure},
AbstractClientError, Application,
};
pub struct AccountBuilder<'a, Chain: CwEnv> {
pub(crate) abstr: &'a Abstract<Chain>,
name: Option<String>,
description: Option<String>,
link: Option<String>,
namespace: Option<Namespace>,
base_asset: Option<AssetEntry>,
ownership: Option<GovernanceDetails<String>>,
owner_account: Option<&'a Account<Chain>>,
install_modules: Vec<ModuleInstallConfig>,
funds: AccountCreationFunds,
fetch_if_namespace_claimed: bool,
install_on_sub_account: bool,
expected_local_account_id: Option<u32>,
}
enum AccountCreationFunds {
#[allow(clippy::type_complexity)]
Auto(Box<dyn Fn(&[Coin]) -> bool>),
Coins(Coins),
}
impl<'a, Chain: CwEnv> AccountBuilder<'a, Chain> {
pub(crate) fn new(abstr: &'a Abstract<Chain>) -> Self {
Self {
abstr,
name: None,
description: None,
link: None,
namespace: None,
base_asset: None,
ownership: None,
owner_account: None,
install_modules: vec![],
funds: AccountCreationFunds::Coins(Coins::default()),
fetch_if_namespace_claimed: true,
install_on_sub_account: true,
expected_local_account_id: None,
}
}
pub fn name(&mut self, name: impl Into<String>) -> &mut Self {
self.name = Some(name.into());
self
}
pub fn description(&mut self, description: impl Into<String>) -> &mut Self {
self.description = Some(description.into());
self
}
pub fn link(&mut self, link: impl Into<String>) -> &mut Self {
self.link = Some(link.into());
self
}
pub fn namespace(&mut self, namespace: Namespace) -> &mut Self {
self.namespace = Some(namespace);
self
}
pub fn base_asset(&mut self, base_asset: AssetEntry) -> &mut Self {
self.base_asset = Some(base_asset);
self
}
pub fn fetch_if_namespace_claimed(&mut self, value: bool) -> &mut Self {
self.fetch_if_namespace_claimed = value;
self
}
pub fn install_on_sub_account(&mut self, value: bool) -> &mut Self {
self.install_on_sub_account = value;
self
}
pub fn sub_account(&mut self, owner_account: &'a Account<Chain>) -> &mut Self {
self.owner_account = Some(owner_account);
self
}
pub fn ownership(&mut self, ownership: GovernanceDetails<String>) -> &mut Self {
self.ownership = Some(ownership);
self
}
pub fn install_adapter<M: InstallConfig<InitMsg = Empty>>(
&mut self,
) -> AbstractClientResult<&mut Self> {
self.install_modules.push(M::install_config(&Empty {})?);
Ok(self)
}
pub fn install_app<M: InstallConfig>(
&mut self,
configuration: &M::InitMsg,
) -> AbstractClientResult<&mut Self> {
self.install_modules.push(M::install_config(configuration)?);
Ok(self)
}
pub fn install_app_with_dependencies<M: DependencyCreation + InstallConfig>(
&mut self,
module_configuration: &M::InitMsg,
dependencies_config: M::DependenciesConfig,
) -> AbstractClientResult<&mut Self> {
let deps_install_config = M::dependency_install_configs(dependencies_config)?;
self.install_modules.extend(deps_install_config);
self.install_modules
.push(M::install_config(module_configuration)?);
Ok(self)
}
pub fn auto_fund_assert<F: Fn(&[Coin]) -> bool + 'static>(&mut self, f: F) -> &mut Self {
self.funds = AccountCreationFunds::Auto(Box::new(f));
self
}
pub fn auto_fund(&mut self) -> &mut Self {
self.funds = AccountCreationFunds::Auto(Box::new(|_| true));
self
}
pub fn funds(&mut self, funds: &[Coin]) -> AbstractClientResult<&mut Self> {
let coins = match &mut self.funds {
AccountCreationFunds::Auto(_) => return Err(AbstractClientError::FundsWithAutoFund {}),
AccountCreationFunds::Coins(coins) => coins,
};
for coin in funds {
coins
.add(coin.clone())
.map_err(AbstractInterfaceError::from)?;
}
Ok(self)
}
pub fn expected_account_id(&mut self, local_account_id: u32) -> &mut Self {
self.expected_local_account_id = Some(local_account_id);
self
}
pub fn build(&self) -> AbstractClientResult<Account<Chain>> {
if self.fetch_if_namespace_claimed {
if let Some(ref namespace) = self.namespace {
let account_from_namespace_result: Option<Account<Chain>> =
Account::maybe_from_namespace(
self.abstr,
namespace.clone(),
self.install_on_sub_account,
)?;
if let Some(account_from_namespace) = account_from_namespace_result {
return Ok(account_from_namespace);
}
}
}
let chain = self.abstr.version_control.get_chain();
let sender = chain.sender().to_string();
let name = self
.name
.clone()
.unwrap_or_else(|| String::from("Default Abstract Account"));
let ownership = self
.ownership
.clone()
.unwrap_or(GovernanceDetails::Monarchy { monarch: sender });
verifiers::validate_name(&name)?;
verifiers::validate_description(self.description.as_deref())?;
verifiers::validate_link(self.link.as_deref())?;
let install_modules = self.install_modules.clone();
let funds = match &self.funds {
AccountCreationFunds::Auto(auto_funds_assert) => {
let modules = install_modules.iter().map(|m| m.module.clone()).collect();
let simulate_response = self
.abstr
.module_factory
.simulate_install_modules(modules)?;
let mut funds = Coins::try_from(simulate_response.total_required_funds).unwrap();
if self.namespace.is_some() {
let vc_config = self.abstr.version_control.config()?;
if let Some(namespace_fee) = vc_config.namespace_registration_fee {
funds
.add(namespace_fee)
.map_err(AbstractInterfaceError::from)?;
}
};
let funds = funds.into_vec();
if !auto_funds_assert(&funds) {
return Err(AbstractClientError::AutoFundsAssertFailed(funds));
}
funds
}
AccountCreationFunds::Coins(coins) => coins.to_vec(),
};
let account_details = AccountDetails {
name,
description: self.description.clone(),
link: self.link.clone(),
namespace: self.namespace.as_ref().map(ToString::to_string),
base_asset: self.base_asset.clone(),
install_modules,
account_id: self.expected_local_account_id,
};
let abstract_account = if let Some(owner_account) = self.owner_account {
owner_account
.abstr_account
.create_sub_account(account_details, Some(&funds))?
} else {
self.abstr.account_factory.create_new_account(
account_details,
ownership,
Some(&funds),
)?
};
Ok(Account::new(abstract_account, self.install_on_sub_account))
}
}
#[derive(Clone)]
pub struct Account<Chain: CwEnv> {
pub(crate) abstr_account: AbstractAccount<Chain>,
install_on_sub_account: bool,
}
impl<Chain: CwEnv> AsRef<AbstractAccount<Chain>> for Account<Chain> {
fn as_ref(&self) -> &AbstractAccount<Chain> {
&self.abstr_account
}
}
struct ParsedAccountCreationResponse {
sub_account_id: u32,
module_address: String,
}
impl<Chain: CwEnv> Account<Chain> {
pub(crate) fn new(
abstract_account: AbstractAccount<Chain>,
install_on_sub_account: bool,
) -> Self {
Self {
abstr_account: abstract_account,
install_on_sub_account,
}
}
pub(crate) fn maybe_from_namespace(
abstr: &Abstract<Chain>,
namespace: Namespace,
install_on_sub_account: bool,
) -> AbstractClientResult<Option<Self>> {
let namespace_response: NamespaceResponse = abstr.version_control.namespace(namespace)?;
let NamespaceResponse::Claimed(info) = namespace_response else {
return Ok(None);
};
let abstract_account: AbstractAccount<Chain> = AbstractAccount::new(abstr, info.account_id);
Ok(Some(Self::new(abstract_account, install_on_sub_account)))
}
pub fn id(&self) -> AbstractClientResult<AccountId> {
self.abstr_account.id().map_err(Into::into)
}
pub fn install_on_sub_account(&self) -> bool {
self.install_on_sub_account
}
pub fn query_balance(&self, denom: impl Into<String>) -> AbstractClientResult<Uint128> {
let coins = self
.environment()
.bank_querier()
.balance(self.proxy()?, Some(denom.into()))
.map_err(Into::into)?;
Ok(coins[0].amount)
}
pub fn query_balances(&self) -> AbstractClientResult<Vec<Coin>> {
self.environment()
.bank_querier()
.balance(self.proxy()?, None)
.map_err(Into::into)
.map_err(Into::into)
}
pub fn info(&self) -> AbstractClientResult<AccountInfo<Addr>> {
let info_response: InfoResponse = self.abstr_account.manager.info()?;
Ok(info_response.info)
}
pub fn install_app<M: InstallConfig + From<Contract<Chain>>>(
&self,
configuration: &M::InitMsg,
funds: &[Coin],
) -> AbstractClientResult<Application<Chain, M>> {
let modules = vec![M::install_config(configuration)?];
match self.install_on_sub_account {
true => self.install_module_sub_internal(modules, funds),
false => self.install_module_current_internal(modules, funds),
}
}
pub fn install_adapter<M: InstallConfig<InitMsg = Empty> + From<Contract<Chain>>>(
&self,
funds: &[Coin],
) -> AbstractClientResult<Application<Chain, M>> {
let modules = vec![M::install_config(&Empty {})?];
match self.install_on_sub_account {
true => self.install_module_sub_internal(modules, funds),
false => self.install_module_current_internal(modules, funds),
}
}
pub fn install_app_with_dependencies<
M: DependencyCreation + InstallConfig + From<Contract<Chain>>,
>(
&self,
module_configuration: &M::InitMsg,
dependencies_config: M::DependenciesConfig,
funds: &[Coin],
) -> AbstractClientResult<Application<Chain, M>> {
let mut install_configs: Vec<ModuleInstallConfig> =
M::dependency_install_configs(dependencies_config)?;
install_configs.push(M::install_config(module_configuration)?);
match self.install_on_sub_account {
true => self.install_module_sub_internal(install_configs, funds),
false => self.install_module_current_internal(install_configs, funds),
}
}
pub fn upgrade(&self, version: ModuleVersion) -> AbstractClientResult<()> {
self.abstr_account.manager.upgrade(vec![
(
ModuleInfo::from_id(abstract_std::registry::MANAGER, version.clone())?,
Some(
to_json_binary(&abstract_std::manager::MigrateMsg {})
.map_err(Into::<CwOrchError>::into)?,
),
),
(
ModuleInfo::from_id(abstract_std::registry::PROXY, version)?,
Some(
to_json_binary(&abstract_std::proxy::MigrateMsg {})
.map_err(Into::<CwOrchError>::into)?,
),
),
])?;
Ok(())
}
pub fn ownership(&self) -> AbstractClientResult<cw_ownable::Ownership<String>> {
self.abstr_account.manager.ownership().map_err(Into::into)
}
pub fn owner(&self) -> AbstractClientResult<Addr> {
let mut governance = self.abstr_account.manager.info()?.info.governance_details;
let environment = self.environment();
for _ in 0..MAX_ADMIN_RECURSION {
match &governance {
GovernanceDetails::SubAccount { manager, .. } => {
governance = environment
.query::<_, InfoResponse>(
&abstract_std::manager::QueryMsg::Info {},
manager,
)
.map_err(|err| err.into())?
.info
.governance_details;
}
_ => break,
}
}
governance
.owner_address()
.ok_or(AbstractClientError::RenouncedAccount {})
}
pub fn execute(
&self,
execute_msgs: impl IntoIterator<Item = impl Into<CosmosMsg>>,
funds: &[Coin],
) -> AbstractClientResult<<Chain as TxHandler>::Response> {
let msgs = execute_msgs.into_iter().map(Into::into).collect();
self.abstr_account
.manager
.execute(
&abstract_std::manager::ExecuteMsg::ExecOnModule {
module_id: PROXY.to_owned(),
exec_msg: to_json_binary(&abstract_std::proxy::ExecuteMsg::ModuleAction {
msgs,
})
.map_err(AbstractInterfaceError::from)?,
},
Some(funds),
)
.map_err(Into::into)
}
pub fn set_ibc_status(&self, enabled: bool) -> AbstractClientResult<()> {
self.abstr_account.manager.update_settings(Some(enabled))?;
Ok(())
}
pub fn create_ibc_account(
&self,
host_chain: impl Into<String>,
base_asset: Option<AssetEntry>,
namespace: Option<String>,
mut install_modules: Vec<ModuleInstallConfig>,
) -> AbstractClientResult<<Chain as TxHandler>::Response> {
if !install_modules.iter().any(|m| m.module.id() == IBC_CLIENT) {
install_modules.push(ModuleInstallConfig::new(
ModuleInfo::from_id_latest(IBC_CLIENT)?,
None,
));
}
self.abstr_account
.manager
.execute(
&abstract_std::manager::ExecuteMsg::ExecOnModule {
module_id: PROXY.to_owned(),
exec_msg: to_json_binary(&abstract_std::proxy::ExecuteMsg::IbcAction {
msg: ibc_client::ExecuteMsg::Register {
host_chain: host_chain.into(),
base_asset,
namespace,
install_modules,
},
})
.map_err(AbstractInterfaceError::from)?,
},
None,
)
.map_err(Into::into)
}
pub fn module_infos(&self) -> AbstractClientResult<ModuleInfosResponse> {
let mut module_infos: Vec<ManagerModuleInfo> = vec![];
loop {
let last_module_id: Option<String> = module_infos
.last()
.map(|module_info| module_info.id.clone());
let res: ModuleInfosResponse = self
.abstr_account
.manager
.module_infos(None, last_module_id)?;
if res.module_infos.is_empty() {
break;
}
module_infos.extend(res.module_infos);
}
Ok(ModuleInfosResponse { module_infos })
}
pub fn module_addresses(
&self,
ids: Vec<String>,
) -> AbstractClientResult<ModuleAddressesResponse> {
self.abstr_account
.manager
.module_addresses(ids)
.map_err(Into::into)
}
pub fn sub_accounts(&self) -> AbstractClientResult<Vec<Account<Chain>>> {
let mut sub_accounts = vec![];
let mut start_after = None;
let abstr_deployment = Abstract::load_from(self.environment())?;
loop {
let sub_account_ids = self
.abstr_account
.manager
.sub_account_ids(None, start_after)?
.sub_accounts;
start_after = sub_account_ids.last().cloned();
if sub_account_ids.is_empty() {
break;
}
sub_accounts.extend(sub_account_ids.into_iter().map(|id| {
Account::new(
AbstractAccount::new(&abstr_deployment, AccountId::local(id)),
false,
)
}));
}
Ok(sub_accounts)
}
pub fn proxy(&self) -> AbstractClientResult<Addr> {
self.abstr_account.proxy.address().map_err(Into::into)
}
pub fn manager(&self) -> AbstractClientResult<Addr> {
self.abstr_account.manager.address().map_err(Into::into)
}
pub fn application<M: RegisteredModule + From<Contract<Chain>>>(
&self,
) -> AbstractClientResult<Application<Chain, M>> {
let module = self.module()?;
let account = self.clone();
Application::new(account, module)
}
fn install_module_current_internal<M: RegisteredModule + From<Contract<Chain>>>(
&self,
modules: Vec<ModuleInstallConfig>,
funds: &[Coin],
) -> AbstractClientResult<Application<Chain, M>> {
let install_module_response = self
.abstr_account
.manager
.install_modules(modules, Some(funds))?;
let module_addr = Self::parse_modules_installing_response(install_module_response);
let contract = Contract::new(M::module_id().to_owned(), self.environment());
contract.set_address(&module_addr);
let adapter: M = contract.into();
Application::new(
Account::new(self.abstr_account.clone(), self.install_on_sub_account),
adapter,
)
}
fn install_module_sub_internal<M: RegisteredModule + From<Contract<Chain>>>(
&self,
modules: Vec<ModuleInstallConfig>,
funds: &[Coin],
) -> AbstractClientResult<Application<Chain, M>> {
let sub_account_response = self.abstr_account.manager.create_sub_account(
modules,
"Sub Account".to_owned(),
None,
None,
None,
None,
None,
funds,
)?;
let parsed_account_creation_response =
Self::parse_account_creation_response(sub_account_response);
let sub_account: AbstractAccount<Chain> = AbstractAccount::new(
&self.infrastructure()?,
AccountId::local(parsed_account_creation_response.sub_account_id),
);
let contract = Contract::new(M::module_id().to_owned(), self.environment());
contract.set_address(&Addr::unchecked(
parsed_account_creation_response.module_address,
));
let app: M = contract.into();
Application::new(Account::new(sub_account, false), app)
}
fn parse_account_creation_response(
response: <Chain as TxHandler>::Response,
) -> ParsedAccountCreationResponse {
let wasm_abstract_attributes: Vec<Attribute> = response
.events()
.into_iter()
.filter(|e| e.ty == "wasm-abstract")
.flat_map(|e| e.attributes)
.collect();
let sub_account_id: Option<u32> = wasm_abstract_attributes
.iter()
.find(|a| a.key == "sub_account_added")
.map(|a| a.value.parse().unwrap());
let module_addresses: Option<String> = wasm_abstract_attributes
.iter()
.find(|a| a.key == "new_modules")
.map(|a| a.value.parse().unwrap());
let module_address: String = module_addresses
.unwrap()
.split(',')
.last()
.unwrap()
.to_string();
ParsedAccountCreationResponse {
sub_account_id: sub_account_id.unwrap(),
module_address,
}
}
fn parse_modules_installing_response(response: <Chain as TxHandler>::Response) -> Addr {
let wasm_abstract_attributes: Vec<Attribute> = response
.events()
.into_iter()
.filter(|e| e.ty == "wasm-abstract")
.flat_map(|e| e.attributes)
.collect();
let module_addresses: String = wasm_abstract_attributes
.iter()
.find(|a| a.key == "new_modules")
.map(|a| a.value.parse().unwrap())
.unwrap();
let module_address = module_addresses.split(',').last().unwrap();
Addr::unchecked(module_address)
}
pub(crate) fn module<T: RegisteredModule + From<Contract<Chain>>>(
&self,
) -> AbstractClientResult<T> {
let module_id = T::module_id();
let maybe_module_addr = self.module_addresses(vec![module_id.to_string()])?.modules;
if !maybe_module_addr.is_empty() {
let contract = Contract::new(module_id.to_owned(), self.environment());
contract.set_address(&maybe_module_addr[0].1);
let module: T = contract.into();
Ok(module)
} else {
Err(AbstractClientError::ModuleNotInstalled {})
}
}
}
impl<Chain: MutCwEnv> Account<Chain> {
pub fn set_balance(&self, amount: &[Coin]) -> AbstractClientResult<()> {
self.environment()
.set_balance(&self.proxy()?, amount.to_vec())
.map_err(Into::into)
.map_err(Into::into)
}
pub fn add_balance(&self, amount: &[Coin]) -> AbstractClientResult<()> {
self.environment()
.add_balance(&self.proxy()?, amount.to_vec())
.map_err(Into::into)
.map_err(Into::into)
}
}
impl<Chain: CwEnv> Display for Account<Chain> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.abstr_account)
}
}
impl<Chain: CwEnv> Debug for Account<Chain> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.abstr_account)
}
}