abstract_sdk/apis/
bank.rs

1//! # Bank
2//! The Bank object handles asset transfers to and from the Account.
3
4use abstract_std::objects::{ans_host::AnsHostError, AnsAsset, AssetEntry};
5use cosmwasm_std::{to_json_binary, Addr, Coin, CosmosMsg, Deps, Env};
6use cw_asset::Asset;
7use serde::Serialize;
8
9use super::AbstractApi;
10use crate::{
11    ans_resolve::Resolve,
12    cw_helpers::ApiQuery,
13    features::{AbstractNameService, AccountExecutor, AccountIdentification, ModuleIdentification},
14    AbstractSdkError, AbstractSdkResult, AccountAction,
15};
16
17/// Query and Transfer assets from and to the Abstract Account.
18pub trait TransferInterface:
19    AbstractNameService + AccountIdentification + ModuleIdentification
20{
21    /**
22        API for transferring funds to and from the account.
23
24        # Example
25        ```
26        use abstract_sdk::prelude::*;
27        # use cosmwasm_std::testing::mock_dependencies;
28        # use abstract_sdk::mock_module::MockModule;
29        # use abstract_testing::prelude::*;
30        # let deps = mock_dependencies();
31        # let env = mock_env_validated(deps.api);
32        # let account = admin_account(deps.api);
33        # let module = MockModule::new(deps.api, account);
34
35        let bank: Bank<MockModule>  = module.bank(deps.as_ref(), );
36        ```
37    */
38    fn bank<'a>(&'a self, deps: Deps<'a>) -> Bank<'a, Self> {
39        Bank { base: self, deps }
40    }
41}
42
43impl<T> TransferInterface for T where
44    T: AbstractNameService + AccountIdentification + ModuleIdentification
45{
46}
47
48impl<T: TransferInterface> AbstractApi<T> for Bank<'_, T> {
49    const API_ID: &'static str = "Bank";
50
51    fn base(&self) -> &T {
52        self.base
53    }
54    fn deps(&self) -> Deps {
55        self.deps
56    }
57}
58
59/**
60    API for transferring funds to and from the account.
61
62    # Example
63    ```
64    use abstract_sdk::prelude::*;
65    # use cosmwasm_std::testing::mock_dependencies;
66    # use abstract_sdk::mock_module::MockModule;
67    # use abstract_testing::prelude::*;
68    # let deps = mock_dependencies();
69    # let env = mock_env_validated(deps.api);
70    # let account = admin_account(deps.api);
71    # let module = MockModule::new(deps.api, account);
72
73    let bank: Bank<MockModule>  = module.bank(deps.as_ref(), );
74    ```
75*/
76#[derive(Clone)]
77pub struct Bank<'a, T: TransferInterface> {
78    base: &'a T,
79    deps: Deps<'a>,
80}
81
82impl<T: TransferInterface> Bank<'_, T> {
83    /// Get the balances of the provided assets.
84    pub fn balances(&self, assets: &[AssetEntry]) -> AbstractSdkResult<Vec<Asset>> {
85        assets
86            .iter()
87            .map(|asset| self.balance(asset))
88            .collect::<AbstractSdkResult<Vec<Asset>>>()
89    }
90    /// Get the balance of the provided asset.
91    pub fn balance(&self, asset: &AssetEntry) -> AbstractSdkResult<Asset> {
92        let resolved_info = asset
93            .resolve(&self.deps.querier, &self.base.ans_host(self.deps)?)
94            .map_err(|error| self.wrap_query_error(error))?;
95        let balance = resolved_info.query_balance(
96            &self.deps.querier,
97            self.base.account(self.deps)?.into_addr(),
98        )?;
99        Ok(Asset::new(resolved_info, balance))
100    }
101
102    /// Move funds from the contract into the Account.
103    pub fn deposit<R: Transferable>(&self, funds: Vec<R>) -> AbstractSdkResult<Vec<CosmosMsg>> {
104        let recipient = self.base.account(self.deps)?.into_addr();
105        let transferable_funds = funds
106            .into_iter()
107            .map(|asset| asset.transferable_asset(self.base, self.deps))
108            .collect::<AbstractSdkResult<Vec<Asset>>>()?;
109        transferable_funds
110            .iter()
111            .map(|asset| asset.transfer_msg(recipient.clone()))
112            .collect::<Result<Vec<_>, _>>()
113            .map_err(Into::into)
114    }
115}
116
117impl<T: TransferInterface + AccountExecutor> Bank<'_, T> {
118    /// Transfer the provided funds from the Account to the recipient.
119    /// ```
120    /// # use cosmwasm_std::{Addr, Response, Deps, DepsMut, MessageInfo, Env};
121    /// # use abstract_std::registry::Account;
122    /// # use abstract_std::objects::AnsAsset;
123    /// # use abstract_std::objects::ans_host::AnsHost;
124    /// # use abstract_sdk::{
125    /// #    features::{AccountIdentification, AbstractNameService, ModuleIdentification},
126    /// #    TransferInterface, AbstractSdkResult, Execution,
127    /// # };
128    /// # struct MockModule;
129    /// # impl AccountIdentification for MockModule {
130    /// #    fn account(&self, _deps: Deps) -> AbstractSdkResult<Account> {
131    /// #       unimplemented!("Not needed for this example")
132    /// #   }
133    /// # }
134    /// #
135    /// # impl ModuleIdentification for MockModule {
136    /// #   fn module_id(&self) -> &'static str {
137    /// #      "mock_module"
138    /// #  }
139    /// # }
140    /// #
141    /// # impl AbstractNameService for MockModule {
142    /// #   fn ans_host(&self, _deps: Deps) -> AbstractSdkResult<AnsHost> {
143    /// #     unimplemented!("Not needed for this example")
144    /// #  }
145    /// # }
146    /// fn transfer_asset_to_sender(app: MockModule, deps: DepsMut, info: MessageInfo, requested_asset: AnsAsset) -> AbstractSdkResult<Response> {
147    ///     let bank = app.bank(deps.as_ref());
148    ///     let executor = app.executor(deps.as_ref());    
149    ///     let transfer_action = bank.transfer(vec![requested_asset.clone()], &info.sender)?;
150    ///
151    ///     let transfer_msg = executor.execute(vec![transfer_action])?;
152    ///
153    ///     Ok(Response::new()
154    ///         .add_message(transfer_msg)
155    ///         .add_attribute("recipient", info.sender)
156    ///         .add_attribute("asset_sent", requested_asset.to_string()))
157    /// }
158    /// ```
159    pub fn transfer<R: Transferable>(
160        &self,
161        funds: Vec<R>,
162        recipient: &Addr,
163    ) -> AbstractSdkResult<AccountAction> {
164        let transferable_funds = funds
165            .into_iter()
166            .map(|asset| asset.transferable_asset(self.base, self.deps))
167            .collect::<AbstractSdkResult<Vec<Asset>>>()?;
168        let msgs = transferable_funds
169            .iter()
170            .map(|asset| asset.transfer_msg(recipient.clone()))
171            .collect::<Result<Vec<_>, _>>()?;
172
173        Ok(AccountAction::from_vec(msgs))
174    }
175
176    /// Withdraw funds from the Account to this contract.
177    pub fn withdraw<R: Transferable>(
178        &self,
179        env: &Env,
180        funds: Vec<R>,
181    ) -> AbstractSdkResult<AccountAction> {
182        let recipient = &env.contract.address;
183        self.transfer(funds, recipient)
184    }
185
186    /// Move cw20 assets from the Account to a recipient with the possibility using the cw20 send/receive hook
187    ///
188    /// Note:  **Native coins are NOT and will NEVER be supported by this method**.
189    ///
190    /// In order to send funds with your message, you need to construct the message yourself
191    pub fn send<R: Transferable, M: Serialize>(
192        &self,
193        funds: R,
194        recipient: &Addr,
195        message: &M,
196    ) -> AbstractSdkResult<AccountAction> {
197        let transferable_funds = funds.transferable_asset(self.base, self.deps)?;
198
199        let msgs = transferable_funds.send_msg(recipient, to_json_binary(message)?)?;
200
201        Ok(AccountAction::from_vec(vec![msgs]))
202    }
203}
204
205/// Turn an object that represents an asset into the blockchain representation of an asset, i.e. [`Asset`].
206pub trait Transferable {
207    /// Turn an object that represents an asset into the blockchain representation of an asset, i.e. [`Asset`].
208    fn transferable_asset<T: AbstractNameService + ModuleIdentification>(
209        self,
210        base: &T,
211        deps: Deps,
212    ) -> AbstractSdkResult<Asset>;
213}
214
215// Helper to wrap errors
216fn transferable_api_error(
217    base: &impl ModuleIdentification,
218    error: AnsHostError,
219) -> AbstractSdkError {
220    AbstractSdkError::ApiQuery {
221        api: "Transferable".to_owned(),
222        module_id: base.module_id().to_owned(),
223        error: Box::new(error.into()),
224    }
225}
226
227impl Transferable for &AnsAsset {
228    fn transferable_asset<T: AbstractNameService + ModuleIdentification>(
229        self,
230        base: &T,
231        deps: Deps,
232    ) -> AbstractSdkResult<Asset> {
233        self.resolve(&deps.querier, &base.ans_host(deps)?)
234            .map_err(|error| transferable_api_error(base, error))
235    }
236}
237
238impl Transferable for AnsAsset {
239    fn transferable_asset<T: AbstractNameService + ModuleIdentification>(
240        self,
241        base: &T,
242        deps: Deps,
243    ) -> AbstractSdkResult<Asset> {
244        self.resolve(&deps.querier, &base.ans_host(deps)?)
245            .map_err(|error| transferable_api_error(base, error))
246    }
247}
248
249impl Transferable for Asset {
250    fn transferable_asset<T: AbstractNameService>(
251        self,
252        _base: &T,
253        _deps: Deps,
254    ) -> AbstractSdkResult<Asset> {
255        Ok(self)
256    }
257}
258
259impl Transferable for Coin {
260    fn transferable_asset<T: AbstractNameService>(
261        self,
262        _base: &T,
263        _deps: Deps,
264    ) -> AbstractSdkResult<Asset> {
265        Ok(Asset::from(self))
266    }
267}
268
269#[cfg(test)]
270mod test {
271    #![allow(clippy::needless_borrows_for_generic_args)]
272    use abstract_testing::mock_env_validated;
273    use abstract_testing::prelude::*;
274    use cosmwasm_std::*;
275
276    use super::*;
277    use crate::apis::traits::test::abstract_api_test;
278    use crate::mock_module::*;
279
280    mod balance {
281        use super::*;
282
283        #[coverage_helper::test]
284        fn balance() {
285            let (mut deps, account, app) = mock_module_setup();
286
287            // API Query Error
288            {
289                let bank: Bank<'_, MockModule> = app.bank(deps.as_ref());
290                let res = bank
291                    .balances(&[AssetEntry::new("asset_entry")])
292                    .unwrap_err();
293                let AbstractSdkError::ApiQuery {
294                    api,
295                    module_id,
296                    error: _,
297                } = res
298                else {
299                    panic!("expected api error");
300                };
301                assert_eq!(api, "Bank");
302                assert_eq!(module_id, app.module_id());
303            }
304
305            let abstr = abstract_testing::prelude::AbstractMockAddrs::new(deps.api);
306            // update querier and balances
307            deps.querier = abstract_testing::abstract_mock_querier_builder(deps.api)
308                .with_contract_map_entry(
309                    &abstr.ans_host,
310                    abstract_std::ans_host::state::ASSET_ADDRESSES,
311                    (
312                        &AssetEntry::new("asset_entry"),
313                        cw_asset::AssetInfo::native("asset"),
314                    ),
315                )
316                .build();
317            let recipient: Addr = account.into_addr();
318            let coins: Vec<Coin> = coins(100u128, "asset");
319            deps.querier.bank.update_balance(recipient, coins.clone());
320
321            let bank: Bank<'_, MockModule> = app.bank(deps.as_ref());
322            let res = bank.balances(&[AssetEntry::new("asset_entry")]).unwrap();
323            assert_eq!(res, vec![Asset::native("asset", 100u128)]);
324        }
325    }
326
327    mod transfer_coins {
328        use abstract_std::account::ExecuteMsg;
329
330        use super::*;
331        use crate::{Execution, Executor, ExecutorMsg};
332
333        #[coverage_helper::test]
334        fn transfer_asset_to_sender() {
335            let (deps, account, app) = mock_module_setup();
336
337            // ANCHOR: transfer
338            let recipient: Addr = Addr::unchecked("recipient");
339            let bank: Bank<'_, MockModule> = app.bank(deps.as_ref());
340            let coins: Vec<Coin> = coins(100u128, "asset");
341            let bank_transfer: AccountAction = bank.transfer(coins.clone(), &recipient).unwrap();
342
343            let executor: Executor<'_, MockModule> = app.executor(deps.as_ref());
344            let account_message: ExecutorMsg = executor.execute(vec![bank_transfer]).unwrap();
345            let response: Response = Response::new().add_message(account_message);
346            // ANCHOR_END: transfer
347
348            let expected_msg = CosmosMsg::Bank(BankMsg::Send {
349                to_address: recipient.to_string(),
350                amount: coins,
351            });
352
353            assert_eq!(
354                response.messages[0].msg,
355                wasm_execute(
356                    account.addr(),
357                    &ExecuteMsg::<Empty>::Execute {
358                        msgs: vec![expected_msg],
359                    },
360                    vec![],
361                )
362                .unwrap()
363                .into(),
364            );
365        }
366    }
367
368    // transfer must be tested via integration test
369
370    mod deposit {
371
372        use super::*;
373        use crate::apis::respond::AbstractResponse;
374
375        #[coverage_helper::test]
376        fn deposit() {
377            let (deps, account, app) = mock_module_setup();
378
379            // ANCHOR: deposit
380            // Get bank API struct from the app
381            let bank: Bank<'_, MockModule> = app.bank(deps.as_ref());
382            // Define coins to send
383            let coins: Vec<Coin> = coins(100u128, "denom");
384            // Construct messages for deposit (transfer from this contract to the account)
385            let deposit_msgs: Vec<CosmosMsg> = bank.deposit(coins.clone()).unwrap();
386            // Create response and add deposit msgs
387            let response: Response = app.response("deposit").add_messages(deposit_msgs);
388            // ANCHOR_END: deposit
389
390            let bank_msg: CosmosMsg = CosmosMsg::Bank(BankMsg::Send {
391                to_address: account.addr().to_string(),
392                amount: coins,
393            });
394
395            assert_eq!(response.messages[0].msg, bank_msg);
396        }
397    }
398
399    mod withdraw_coins {
400        use super::*;
401
402        #[coverage_helper::test]
403        fn withdraw_coins() {
404            let (deps, _, app) = mock_module_setup();
405
406            let expected_amount = 100u128;
407            let env = mock_env_validated(deps.api);
408
409            let bank = app.bank(deps.as_ref());
410            let coins = coins(expected_amount, "asset");
411            let actual_res = bank.withdraw(&env, coins.clone());
412
413            let expected_msg: CosmosMsg = CosmosMsg::Bank(BankMsg::Send {
414                to_address: env.contract.address.to_string(),
415                amount: coins,
416            });
417
418            assert_eq!(actual_res.unwrap().messages()[0], expected_msg);
419        }
420    }
421
422    mod send_coins {
423        use super::*;
424
425        use cw20::Cw20ExecuteMsg;
426        use cw_asset::AssetError;
427
428        #[coverage_helper::test]
429        fn send_cw20() {
430            let (deps, _, app) = mock_module_setup();
431
432            let expected_amount = 100u128;
433            let expected_recipient = deps.api.addr_make("recipient");
434
435            let bank = app.bank(deps.as_ref());
436            let hook_msg = Empty {};
437            let asset = deps.api.addr_make("asset");
438            let coin = Asset::cw20(asset.clone(), expected_amount);
439            let actual_res = bank.send(coin, &expected_recipient, &hook_msg);
440
441            let expected_msg: CosmosMsg = CosmosMsg::Wasm(WasmMsg::Execute {
442                contract_addr: asset.to_string(),
443                msg: to_json_binary(&Cw20ExecuteMsg::Send {
444                    contract: expected_recipient.to_string(),
445                    amount: expected_amount.into(),
446                    msg: to_json_binary(&hook_msg).unwrap(),
447                })
448                .unwrap(),
449                funds: vec![],
450            });
451
452            assert_eq!(actual_res.unwrap().messages()[0], expected_msg);
453        }
454
455        #[coverage_helper::test]
456        fn send_coins() {
457            let (deps, _, app) = mock_module_setup();
458
459            let expected_amount = 100u128;
460            let expected_recipient = deps.api.addr_make("recipient");
461
462            let bank = app.bank(deps.as_ref());
463            let coin = coin(expected_amount, "asset");
464            let hook_msg = Empty {};
465            let actual_res = bank.send(coin, &expected_recipient, &hook_msg);
466
467            assert_eq!(
468                actual_res,
469                Err(AbstractSdkError::Asset(
470                    AssetError::UnavailableMethodForNative {
471                        method: "send".into(),
472                    }
473                )),
474            );
475        }
476    }
477
478    #[coverage_helper::test]
479    fn abstract_api() {
480        let (deps, _, app) = mock_module_setup();
481        let bank = app.bank(deps.as_ref());
482
483        abstract_api_test(bank);
484    }
485}