abstract_sdk/apis/
ibc.rs

1//! # Ibc Client
2//! The IbcClient object provides helper function for ibc-related queries or actions.
3//!
4
5use abstract_std::{
6    account::ExecuteMsg,
7    account::ModuleInstallConfig,
8    base,
9    ibc::{Callback, ModuleQuery},
10    ibc_client::{self, ExecuteMsg as IbcClientMsg, InstalledModuleIdentification},
11    ibc_host::HostAction,
12    objects::{module::ModuleInfo, TruncatedChainId},
13    ABSTRACT_VERSION, IBC_CLIENT,
14};
15use cosmwasm_std::{
16    to_json_binary, wasm_execute, Addr, Coin, CosmosMsg, Deps, Empty, QueryRequest,
17};
18use serde::Serialize;
19
20use super::AbstractApi;
21use crate::{
22    features::{AccountExecutor, AccountIdentification, ModuleIdentification},
23    AbstractSdkResult, ModuleInterface, ModuleRegistryInterface,
24};
25
26/// Interact with other chains over IBC.
27pub trait IbcInterface:
28    AccountIdentification + ModuleRegistryInterface + ModuleIdentification + ModuleInterface
29{
30    /**
31        API for interacting with the Abstract IBC client.
32
33        # Example
34        ```
35        use abstract_sdk::prelude::*;
36        # use cosmwasm_std::testing::mock_dependencies;
37        # use abstract_sdk::mock_module::MockModule;
38        # use abstract_testing::prelude::*;
39        # let deps = mock_dependencies();
40        # let account = admin_account(deps.api);
41        # let module = MockModule::new(deps.api, account);
42
43        let ibc_client: IbcClient<MockModule>  = module.ibc_client(deps.as_ref());
44        ```
45    */
46    fn ibc_client<'a>(&'a self, deps: Deps<'a>) -> IbcClient<'a, Self> {
47        IbcClient { base: self, deps }
48    }
49}
50
51impl<T> IbcInterface for T where
52    T: AccountIdentification + ModuleRegistryInterface + ModuleIdentification + ModuleInterface
53{
54}
55
56impl<T: IbcInterface> AbstractApi<T> for IbcClient<'_, T> {
57    const API_ID: &'static str = "IbcClient";
58
59    fn base(&self) -> &T {
60        self.base
61    }
62    fn deps(&self) -> Deps {
63        self.deps
64    }
65}
66
67#[derive(Clone)]
68/**
69    API for interacting with the Abstract IBC client.
70
71    # Example
72    ```
73    use abstract_sdk::prelude::*;
74    # use cosmwasm_std::testing::mock_dependencies;
75    # use abstract_sdk::mock_module::MockModule;
76    # use abstract_testing::prelude::*;
77    # let deps = mock_dependencies();
78    # let account = admin_account(deps.api);
79    # let module = MockModule::new(deps.api, account);
80
81    let ibc_client: IbcClient<MockModule>  = module.ibc_client(deps.as_ref());
82    ```
83*/
84pub struct IbcClient<'a, T: IbcInterface> {
85    base: &'a T,
86    deps: Deps<'a>,
87}
88
89impl<T: IbcInterface> IbcClient<'_, T> {
90    /// Get address of this module
91    pub fn module_address(&self) -> AbstractSdkResult<Addr> {
92        let modules = self.base.modules(self.deps);
93        modules.assert_module_dependency(IBC_CLIENT)?;
94        self.base
95            .module_registry(self.deps)?
96            .query_module(ModuleInfo::from_id(IBC_CLIENT, ABSTRACT_VERSION.into())?)?
97            .reference
98            .unwrap_native()
99            .map_err(Into::into)
100    }
101
102    /// Send module action from this module to the target module
103    pub fn module_ibc_action<M: Serialize>(
104        &self,
105        host_chain: TruncatedChainId,
106        target_module: ModuleInfo,
107        exec_msg: &M,
108        callback: Option<Callback>,
109    ) -> AbstractSdkResult<CosmosMsg> {
110        let ibc_client_addr = self.module_address()?;
111        let msg = wasm_execute(
112            ibc_client_addr,
113            &ibc_client::ExecuteMsg::ModuleIbcAction {
114                host_chain,
115                target_module,
116                msg: to_json_binary(exec_msg)?,
117                callback,
118            },
119            vec![],
120        )?;
121        Ok(msg.into())
122    }
123
124    /// Send module query from this module to the target module
125    /// Use [`abstract_std::ibc::IbcResponseMsg::module_query_response`] to parse response
126    pub fn module_ibc_query<B: Serialize, M: Serialize>(
127        &self,
128        host_chain: TruncatedChainId,
129        target_module: InstalledModuleIdentification,
130        query_msg: &base::QueryMsg<B, M>,
131        callback: Callback,
132    ) -> AbstractSdkResult<CosmosMsg> {
133        let ibc_client_addr = self.module_address()?;
134        let msg = wasm_execute(
135            ibc_client_addr,
136            &ibc_client::ExecuteMsg::IbcQuery {
137                host_chain,
138                queries: vec![QueryRequest::Custom(ModuleQuery {
139                    target_module,
140                    msg: to_json_binary(query_msg)?,
141                })],
142                callback,
143            },
144            vec![],
145        )?;
146        Ok(msg.into())
147    }
148
149    /// Send query from this module to the host chain
150    pub fn ibc_query(
151        &self,
152        host_chain: TruncatedChainId,
153        query: impl Into<QueryRequest<ModuleQuery>>,
154        callback: Callback,
155    ) -> AbstractSdkResult<CosmosMsg> {
156        let ibc_client_addr = self.module_address()?;
157        let msg = wasm_execute(
158            ibc_client_addr,
159            &ibc_client::ExecuteMsg::IbcQuery {
160                host_chain,
161                queries: vec![query.into()],
162                callback,
163            },
164            vec![],
165        )?;
166        Ok(msg.into())
167    }
168
169    /// Send queries from this module to the host chain
170    pub fn ibc_queries(
171        &self,
172        host_chain: TruncatedChainId,
173        queries: Vec<QueryRequest<ModuleQuery>>,
174        callback: Callback,
175    ) -> AbstractSdkResult<CosmosMsg> {
176        let ibc_client_addr = self.module_address()?;
177        let msg = wasm_execute(
178            ibc_client_addr,
179            &ibc_client::ExecuteMsg::IbcQuery {
180                host_chain,
181                queries,
182                callback,
183            },
184            vec![],
185        )?;
186        Ok(msg.into())
187    }
188
189    /// Address of the remote account
190    /// Note: only Accounts that are remote to *this* chain are queryable
191    pub fn remote_account_addr(
192        &self,
193        host_chain: &TruncatedChainId,
194    ) -> AbstractSdkResult<Option<String>> {
195        let ibc_client_addr = self.module_address()?;
196        let account_id = self.base.account_id(self.deps)?;
197
198        let (trace, sequence) = account_id.decompose();
199        ibc_client::state::ACCOUNTS
200            .query(
201                &self.deps.querier,
202                ibc_client_addr,
203                (&trace, sequence, host_chain),
204            )
205            .map_err(Into::into)
206    }
207}
208
209impl<T: IbcInterface + AccountExecutor> IbcClient<'_, T> {
210    /// Execute on ibc client
211    pub fn execute(
212        &self,
213        msg: &abstract_std::ibc_client::ExecuteMsg,
214        funds: Vec<Coin>,
215    ) -> AbstractSdkResult<CosmosMsg> {
216        let wasm_msg = wasm_execute(
217            self.base.account(self.deps)?.into_addr().into_string(),
218            &ExecuteMsg::ExecuteOnModule::<Empty> {
219                module_id: IBC_CLIENT.to_owned(),
220                exec_msg: to_json_binary(&msg)?,
221                funds,
222            },
223            vec![],
224        )?;
225        Ok(wasm_msg.into())
226    }
227    /// A simple helper to create and register a remote account
228    pub fn create_remote_account(
229        &self,
230        // The chain on which you want to create an account
231        host_chain: TruncatedChainId,
232    ) -> AbstractSdkResult<CosmosMsg> {
233        self.execute(
234            &abstract_std::ibc_client::ExecuteMsg::Register {
235                host_chain,
236                namespace: None,
237                install_modules: vec![],
238            },
239            vec![],
240        )
241    }
242
243    /// Call a [`HostAction`] on the host of the provided `host_chain`.
244    pub fn host_action(
245        &self,
246        host_chain: TruncatedChainId,
247        action: HostAction,
248    ) -> AbstractSdkResult<CosmosMsg> {
249        self.execute(&IbcClientMsg::RemoteAction { host_chain, action }, vec![])
250    }
251
252    /// IbcClient the provided coins from the Account to its account on the `receiving_chain`.
253    pub fn ics20_transfer(
254        &self,
255        host_chain: TruncatedChainId,
256        funds: Vec<Coin>,
257        memo: Option<String>,
258        receiver: Option<String>,
259    ) -> AbstractSdkResult<CosmosMsg> {
260        self.execute(
261            &IbcClientMsg::SendFunds {
262                host_chain,
263                memo,
264                receiver,
265            },
266            funds,
267        )
268    }
269
270    /// A simple helper to install an app on an account
271    pub fn install_remote_app<M: Serialize>(
272        &self,
273        // The chain on which you want to install an app
274        host_chain: TruncatedChainId,
275        module: ModuleInfo,
276        init_msg: &M,
277    ) -> AbstractSdkResult<CosmosMsg> {
278        self.host_action(
279            host_chain,
280            HostAction::Dispatch {
281                account_msgs: vec![abstract_std::account::ExecuteMsg::InstallModules {
282                    modules: vec![ModuleInstallConfig::new(
283                        module,
284                        Some(to_json_binary(&init_msg)?),
285                    )],
286                }],
287            },
288        )
289    }
290
291    /// A simple helper install a remote api Module providing only the chain name
292    pub fn install_remote_api<M: Serialize>(
293        &self,
294        // The chain on which you want to install an api
295        host_chain: TruncatedChainId,
296        module: ModuleInfo,
297    ) -> AbstractSdkResult<CosmosMsg> {
298        self.host_action(
299            host_chain,
300            HostAction::Dispatch {
301                account_msgs: vec![abstract_std::account::ExecuteMsg::InstallModules {
302                    modules: vec![ModuleInstallConfig::new(module, None)],
303                }],
304            },
305        )
306    }
307
308    /// A simple helper to execute on a module
309    /// Executes the message as the Account of the remote account
310    /// I.e. can be used to execute admin actions on remote modules.
311    pub fn execute_on_module<M: Serialize>(
312        &self,
313        host_chain: TruncatedChainId,
314        module_id: String,
315        exec_msg: &M,
316    ) -> AbstractSdkResult<CosmosMsg> {
317        self.host_action(
318            host_chain,
319            HostAction::Dispatch {
320                account_msgs: vec![abstract_std::account::ExecuteMsg::ExecuteOnModule {
321                    module_id,
322                    exec_msg: to_json_binary(exec_msg)?,
323                    funds: vec![],
324                }],
325            },
326        )
327    }
328
329    /// Address of the remote account
330    /// Note: only works if account is local
331    pub fn remote_account(
332        &self,
333        host_chain: &TruncatedChainId,
334    ) -> AbstractSdkResult<Option<String>> {
335        let account_id = self.base.account_id(self.deps)?;
336        let ibc_client_addr = self.module_address()?;
337
338        let (trace, sequence) = account_id.decompose();
339        ibc_client::state::ACCOUNTS
340            .query(
341                &self.deps.querier,
342                ibc_client_addr,
343                (&trace, sequence, host_chain),
344            )
345            .map_err(Into::into)
346    }
347}
348
349#[cfg(test)]
350mod test {
351    #![allow(clippy::needless_borrows_for_generic_args)]
352    use abstract_testing::prelude::*;
353    use cosmwasm_std::*;
354
355    use super::*;
356    use crate::{apis::traits::test::abstract_api_test, mock_module::*};
357    const TEST_HOST_CHAIN: &str = "hostchain";
358
359    /// Tests that a host_action can be built with no callback
360    #[coverage_helper::test]
361    fn test_host_action_no_callback() {
362        let (deps, _, stub) = mock_module_setup();
363
364        let client = stub.ibc_client(deps.as_ref());
365        let msg = client.host_action(
366            TEST_HOST_CHAIN.parse().unwrap(),
367            HostAction::Dispatch {
368                account_msgs: vec![abstract_std::account::ExecuteMsg::UpdateStatus {
369                    is_suspended: None,
370                }],
371            },
372        );
373        assert!(msg.is_ok());
374
375        let base = test_account(deps.api);
376        let expected = CosmosMsg::Wasm(WasmMsg::Execute {
377            contract_addr: base.addr().to_string(),
378            msg: to_json_binary(&ExecuteMsg::ExecuteOnModule::<cosmwasm_std::Empty> {
379                module_id: IBC_CLIENT.to_owned(),
380                exec_msg: to_json_binary(&IbcClientMsg::RemoteAction {
381                    host_chain: TEST_HOST_CHAIN.parse().unwrap(),
382                    action: HostAction::Dispatch {
383                        account_msgs: vec![abstract_std::account::ExecuteMsg::UpdateStatus {
384                            is_suspended: None,
385                        }],
386                    },
387                })
388                .unwrap(),
389                funds: vec![],
390            })
391            .unwrap(),
392            funds: vec![],
393        });
394        assert_eq!(msg, Ok(expected));
395    }
396
397    /// Tests that the ics_20 transfer can be built and that the funds are passed into the sendFunds message not the execute message
398    #[coverage_helper::test]
399    fn test_ics20_transfer() {
400        let (deps, _, stub) = mock_module_setup();
401
402        let client = stub.ibc_client(deps.as_ref());
403
404        let expected_funds = coins(100, "denom");
405
406        let msg = client.ics20_transfer(
407            TEST_HOST_CHAIN.parse().unwrap(),
408            expected_funds.clone(),
409            None,
410            None,
411        );
412        assert!(msg.is_ok());
413
414        let base = test_account(deps.api);
415        let expected = CosmosMsg::Wasm(WasmMsg::Execute {
416            contract_addr: base.addr().to_string(),
417            msg: to_json_binary(&ExecuteMsg::ExecuteOnModule::<cosmwasm_std::Empty> {
418                module_id: IBC_CLIENT.to_owned(),
419                exec_msg: to_json_binary(&IbcClientMsg::SendFunds {
420                    host_chain: TEST_HOST_CHAIN.parse().unwrap(),
421                    memo: None,
422                    receiver: None,
423                })
424                .unwrap(),
425                funds: expected_funds,
426            })
427            .unwrap(),
428            // ensure empty
429            funds: vec![],
430        });
431        assert_eq!(msg, Ok(expected));
432    }
433
434    #[coverage_helper::test]
435    fn abstract_api() {
436        let (deps, _, app) = mock_module_setup();
437        let client = app.ibc_client(deps.as_ref());
438
439        abstract_api_test(client);
440    }
441}