abstract_sdk/apis/
execution.rs

1//! # Executor
2//! The executor provides function for executing commands on the Account.
3//!
4
5use abstract_macros::with_abstract_event;
6use abstract_std::account::ExecuteMsg;
7use cosmwasm_std::{Binary, Coin, CosmosMsg, Deps, ReplyOn, Response, SubMsg};
8
9use super::AbstractApi;
10use crate::{
11    features::{AccountExecutor, ModuleIdentification},
12    AbstractSdkResult, AccountAction,
13};
14
15/// Execute an `AccountAction` on the Account.
16pub trait Execution: AccountExecutor + ModuleIdentification {
17    /**
18        API for executing [`AccountAction`]s on the Account.
19        Group your actions together in a single execute call if possible.
20
21        Executing [`CosmosMsg`] on the account is possible by creating an [`AccountAction`].
22
23        # Example
24        ```
25        use abstract_sdk::prelude::*;
26        # use cosmwasm_std::testing::mock_dependencies;
27        # use abstract_sdk::mock_module::MockModule;
28        # use abstract_testing::prelude::*;
29        # let deps = mock_dependencies();
30        # let account = admin_account(deps.api);
31        # let module = MockModule::new(deps.api, account);
32
33        let executor: Executor<MockModule>  = module.executor(deps.as_ref());
34        ```
35    */
36    fn executor<'a>(&'a self, deps: Deps<'a>) -> Executor<'a, Self> {
37        Executor { base: self, deps }
38    }
39}
40
41impl<T> Execution for T where T: AccountExecutor + ModuleIdentification {}
42
43impl<T: Execution> AbstractApi<T> for Executor<'_, T> {
44    const API_ID: &'static str = "Executor";
45
46    fn base(&self) -> &T {
47        self.base
48    }
49    fn deps(&self) -> Deps {
50        self.deps
51    }
52}
53
54/**
55    API for executing [`AccountAction`]s on the Account.
56    Group your actions together in a single execute call if possible.
57
58    Executing [`CosmosMsg`] on the account is possible by creating an [`AccountAction`].
59
60    # Example
61    ```
62    use abstract_sdk::prelude::*;
63    # use cosmwasm_std::testing::mock_dependencies;
64    # use abstract_sdk::mock_module::MockModule;
65    # use abstract_testing::prelude::*;
66    # let deps = mock_dependencies();
67    # let account = admin_account(deps.api);
68    # let module = MockModule::new(deps.api, account);
69
70    let executor: Executor<MockModule>  = module.executor(deps.as_ref());
71    ```
72*/
73#[derive(Clone)]
74pub struct Executor<'a, T: Execution> {
75    base: &'a T,
76    deps: Deps<'a>,
77}
78
79impl<T: Execution> Executor<'_, T> {
80    /// Execute a single message on the `ModuleActionWithData` endpoint.
81    fn execute_with_data(&self, msg: CosmosMsg) -> AbstractSdkResult<ExecutorMsg> {
82        let msg = self.base.execute_on_account(
83            self.deps,
84            &ExecuteMsg::ExecuteWithData { msg },
85            vec![],
86        )?;
87        Ok(ExecutorMsg(msg))
88    }
89
90    /// Execute the msgs on the Account.
91    /// These messages will be executed on the account contract and the sending module must be whitelisted.
92    pub fn execute(
93        &self,
94        actions: impl IntoIterator<Item = impl Into<AccountAction>>,
95    ) -> AbstractSdkResult<ExecutorMsg> {
96        self.execute_with_funds(actions, vec![])
97    }
98
99    /// Execute the msgs on the Account.
100    /// These messages will be executed on the account contract and the sending module must be whitelisted.
101    /// Funds attached from sending module to account
102    pub fn execute_with_funds(
103        &self,
104        actions: impl IntoIterator<Item = impl Into<AccountAction>>,
105        funds: Vec<Coin>,
106    ) -> AbstractSdkResult<ExecutorMsg> {
107        let msgs = actions
108            .into_iter()
109            .flat_map(|a| a.into().messages())
110            .collect();
111        let msg = self
112            .base
113            .execute_on_account(self.deps, &ExecuteMsg::Execute { msgs }, funds)?;
114        Ok(ExecutorMsg(msg))
115    }
116
117    /// Execute the msgs on the Account.
118    /// These messages will be executed on the account contract and the sending module must be whitelisted.
119    /// The execution will be executed in a submessage and the reply will be sent to the provided `reply_on`.
120    pub fn execute_with_reply(
121        &self,
122        actions: impl IntoIterator<Item = impl Into<AccountAction>>,
123        reply_on: ReplyOn,
124        id: u64,
125    ) -> AbstractSdkResult<SubMsg> {
126        let msg = self.execute(actions)?;
127        let sub_msg = SubMsg {
128            id,
129            msg: msg.into(),
130            gas_limit: None,
131            reply_on,
132            payload: Binary::default(),
133        };
134        Ok(sub_msg)
135    }
136
137    /// Execute a single msg on the Account.
138    /// This message will be executed on the account contract. Any data returned from the execution will be forwarded to the account's response through a reply.
139    /// The resulting data should be available in the reply of the specified ID.
140    pub fn execute_with_reply_and_data(
141        &self,
142        actions: CosmosMsg,
143        reply_on: ReplyOn,
144        id: u64,
145    ) -> AbstractSdkResult<SubMsg> {
146        let msg = self.execute_with_data(actions)?;
147        let sub_msg = SubMsg {
148            id,
149            msg: msg.into(),
150            gas_limit: None,
151            reply_on,
152            payload: Binary::default(),
153        };
154        Ok(sub_msg)
155    }
156
157    /// Execute the msgs on the Account.
158    /// These messages will be executed on the account contract and the sending module must be whitelisted.
159    /// Return a "standard" response for the executed messages. (with the provided action).
160    pub fn execute_with_response(
161        &self,
162        actions: impl IntoIterator<Item = impl Into<AccountAction>>,
163        action: &str,
164    ) -> AbstractSdkResult<Response> {
165        let msg = self.execute(actions)?;
166        let resp = Response::default();
167
168        Ok(with_abstract_event!(resp, self.base.module_id(), action).add_message(msg))
169    }
170}
171
172/// CosmosMsg from the executor methods
173#[must_use = "ExecutorMsg should be provided to Response::add_message"]
174#[cfg_attr(not(target_arch = "wasm32"), derive(Debug, PartialEq, Eq))]
175pub struct ExecutorMsg(CosmosMsg);
176
177impl From<ExecutorMsg> for CosmosMsg {
178    fn from(val: ExecutorMsg) -> Self {
179        val.0
180    }
181}
182
183#[cfg(test)]
184mod test {
185    #![allow(clippy::needless_borrows_for_generic_args)]
186    use abstract_std::account::ExecuteMsg;
187    use abstract_testing::prelude::*;
188    use cosmwasm_std::*;
189
190    use super::*;
191    use crate::{apis::traits::test::abstract_api_test, mock_module::*};
192
193    fn mock_bank_send(amount: Vec<Coin>) -> AccountAction {
194        AccountAction::from(CosmosMsg::Bank(BankMsg::Send {
195            to_address: "to_address".to_string(),
196            amount,
197        }))
198    }
199
200    fn flatten_actions(actions: Vec<AccountAction>) -> Vec<CosmosMsg> {
201        actions.into_iter().flat_map(|a| a.messages()).collect()
202    }
203
204    mod execute {
205        use super::*;
206
207        /// Tests that no error is thrown with empty messages provided
208        #[coverage_helper::test]
209        fn empty_actions() {
210            let (deps, account, stub) = mock_module_setup();
211            let executor = stub.executor(deps.as_ref());
212
213            let messages = vec![];
214
215            let actual_res = executor.execute(messages.clone());
216            assert!(actual_res.is_ok());
217
218            let expected = ExecutorMsg(CosmosMsg::Wasm(WasmMsg::Execute {
219                contract_addr: account.addr().to_string(),
220                msg: to_json_binary(&ExecuteMsg::<Empty>::Execute {
221                    msgs: flatten_actions(messages),
222                })
223                .unwrap(),
224                funds: vec![],
225            }));
226            assert_eq!(actual_res, Ok(expected));
227        }
228
229        #[coverage_helper::test]
230        fn with_actions() {
231            let (deps, account, stub) = mock_module_setup();
232            let executor = stub.executor(deps.as_ref());
233
234            // build a bank message
235            let messages = vec![mock_bank_send(coins(100, "juno"))];
236
237            let actual_res = executor.execute(messages.clone());
238            assert!(actual_res.is_ok());
239
240            let expected = ExecutorMsg(CosmosMsg::Wasm(WasmMsg::Execute {
241                contract_addr: account.addr().to_string(),
242                msg: to_json_binary(&ExecuteMsg::<Empty>::Execute {
243                    msgs: flatten_actions(messages),
244                })
245                .unwrap(),
246                // funds should be empty
247                funds: vec![],
248            }));
249            assert_eq!(actual_res, Ok(expected));
250        }
251    }
252
253    mod execute_with_reply {
254
255        use super::*;
256
257        /// Tests that no error is thrown with empty messages provided
258        #[coverage_helper::test]
259        fn empty_actions() {
260            let (deps, account, stub) = mock_module_setup();
261            let executor = stub.executor(deps.as_ref());
262
263            let empty_actions = vec![];
264            let expected_reply_on = ReplyOn::Success;
265            let expected_reply_id = 10952;
266
267            let actual_res = executor.execute_with_reply(
268                empty_actions.clone(),
269                expected_reply_on.clone(),
270                expected_reply_id,
271            );
272            assert!(actual_res.is_ok());
273
274            let expected = SubMsg {
275                id: expected_reply_id,
276                msg: CosmosMsg::Wasm(WasmMsg::Execute {
277                    contract_addr: account.addr().to_string(),
278                    msg: to_json_binary(&ExecuteMsg::<Empty>::Execute {
279                        msgs: flatten_actions(empty_actions),
280                    })
281                    .unwrap(),
282                    funds: vec![],
283                }),
284                gas_limit: None,
285                reply_on: expected_reply_on,
286                payload: Binary::default(),
287            };
288            assert_eq!(actual_res, Ok(expected));
289        }
290
291        #[coverage_helper::test]
292        fn with_actions() {
293            let (deps, account, stub) = mock_module_setup();
294            let executor = stub.executor(deps.as_ref());
295
296            // build a bank message
297            let action = vec![mock_bank_send(coins(1, "denom"))];
298            // reply on never
299            let expected_reply_on = ReplyOn::Never;
300            let expected_reply_id = 1;
301
302            let actual_res = executor.execute_with_reply(
303                action.clone(),
304                expected_reply_on.clone(),
305                expected_reply_id,
306            );
307            assert!(actual_res.is_ok());
308
309            let expected = SubMsg {
310                id: expected_reply_id,
311                msg: CosmosMsg::Wasm(WasmMsg::Execute {
312                    contract_addr: account.addr().to_string(),
313                    msg: to_json_binary(&ExecuteMsg::<Empty>::Execute {
314                        msgs: flatten_actions(action),
315                    })
316                    .unwrap(),
317                    // funds should be empty
318                    funds: vec![],
319                }),
320                gas_limit: None,
321                reply_on: expected_reply_on,
322                payload: Binary::default(),
323            };
324            assert_eq!(actual_res, Ok(expected));
325        }
326    }
327
328    mod execute_with_response {
329        use super::*;
330
331        /// Tests that no error is thrown with empty messages provided
332        #[coverage_helper::test]
333        fn empty_actions() {
334            let (deps, account, stub) = mock_module_setup();
335            let executor = stub.executor(deps.as_ref());
336
337            let empty_actions = vec![];
338            let expected_action = "THIS IS AN ACTION";
339
340            let actual_res = executor.execute_with_response(empty_actions.clone(), expected_action);
341
342            let expected_msg = CosmosMsg::Wasm(WasmMsg::Execute {
343                contract_addr: account.addr().to_string(),
344                msg: to_json_binary(&ExecuteMsg::<Empty>::Execute {
345                    msgs: flatten_actions(empty_actions),
346                })
347                .unwrap(),
348                funds: vec![],
349            });
350
351            let expected = Response::new()
352                .add_event(
353                    Event::new("abstract")
354                        .add_attribute("contract", stub.module_id())
355                        .add_attribute("action", expected_action),
356                )
357                .add_message(expected_msg);
358
359            assert_eq!(actual_res, Ok(expected));
360        }
361
362        #[coverage_helper::test]
363        fn with_actions() {
364            let (deps, account, stub) = mock_module_setup();
365
366            let executor = stub.executor(deps.as_ref());
367
368            // build a bank message
369            let action = vec![mock_bank_send(coins(1, "denom"))];
370            let expected_action = "provide liquidity";
371
372            let actual_res = executor.execute_with_response(action.clone(), expected_action);
373
374            let expected_msg = CosmosMsg::Wasm(WasmMsg::Execute {
375                contract_addr: account.addr().to_string(),
376                msg: to_json_binary(&ExecuteMsg::<Empty>::Execute {
377                    msgs: flatten_actions(action),
378                })
379                .unwrap(),
380                // funds should be empty
381                funds: vec![],
382            });
383            let expected = Response::new()
384                .add_event(
385                    Event::new("abstract")
386                        .add_attribute("contract", stub.module_id())
387                        .add_attribute("action", expected_action),
388                )
389                .add_message(expected_msg);
390            assert_eq!(actual_res, Ok(expected));
391        }
392    }
393
394    #[coverage_helper::test]
395    fn abstract_api() {
396        let (deps, _, app) = mock_module_setup();
397        let executor = app.executor(deps.as_ref());
398
399        abstract_api_test(executor);
400    }
401}