croncat_integration_utils/
task_creation.rs

1use crate::error::CronCatContractError;
2use crate::types::{CronCatTaskSubmessageParams, SubMessageReplyType};
3use crate::{REPLY_CRONCAT_TASK_CREATION, TASKS_NAME};
4use cosmwasm_std::CosmosMsg::Wasm;
5use cosmwasm_std::WasmMsg::{self, Execute};
6use cosmwasm_std::{to_binary, Addr, BankMsg, CosmosMsg, MessageInfo, QuerierWrapper, SubMsg};
7use croncat_sdk_factory::msg::ContractMetadataResponse;
8use croncat_sdk_factory::msg::FactoryQueryMsg::LatestContract;
9use croncat_sdk_manager::types::Config as ManagerConfig;
10use croncat_sdk_tasks::msg::TasksExecuteMsg::CreateTask;
11use croncat_sdk_tasks::types::{
12    AmountForOneTask, Boundary, BoundaryHeight, BoundaryTime, Config as TasksConfig, Interval,
13    Task, TaskRequest,
14};
15use cw20::{Cw20CoinVerified, Cw20ExecuteMsg};
16
17/// Given the CronCat factory address, returns the proper contract address if it exists.
18/// See [TASKS_NAME](crate::TASKS_NAME), [MANAGER_NAME](crate::MANAGER_NAME), and [AGENTS_NAME](crate::AGENTS_NAME)
19pub fn get_latest_croncat_contract(
20    querier: &QuerierWrapper,
21    croncat_factory_address: Addr,
22    croncat_contract_name: String,
23) -> Result<Addr, CronCatContractError> {
24    let query_factory_msg = LatestContract {
25        contract_name: croncat_contract_name.clone(),
26    };
27    let latest_contract_res: ContractMetadataResponse =
28        querier.query_wasm_smart(&croncat_factory_address, &query_factory_msg)?;
29
30    // Check validity of result
31    let contract_metadata =
32        latest_contract_res
33            .metadata
34            .ok_or(CronCatContractError::NoSuchContractOnFactory {
35                contract_name: croncat_contract_name,
36                factory_addr: croncat_factory_address,
37                version: "latest".to_owned(),
38            })?;
39
40    Ok(contract_metadata.contract_addr)
41}
42
43/// Given the CronCat factory address and a version, returns the proper contract address if it exists.
44/// Use [get_latest_croncat_contract] to get latest version
45/// See [TASKS_NAME](crate::TASKS_NAME), [MANAGER_NAME](crate::MANAGER_NAME), and [AGENTS_NAME](crate::AGENTS_NAME)
46pub fn get_croncat_contract(
47    querier: &QuerierWrapper,
48    croncat_factory_address: Addr,
49    croncat_contract_name: String,
50    croncat_version: String,
51) -> Result<Addr, CronCatContractError> {
52    // Parse string to a version array, for example from "1.0" to [1,0]
53    let croncat_version_parsed = croncat_version
54        .split('.')
55        .map(|ver| {
56            ver.parse()
57                .map_err(|_| CronCatContractError::InvalidVersionString {})
58        })
59        .collect::<Result<Vec<u8>, _>>()?;
60
61    // Raw query the factory
62    let contract_addr = croncat_factory::state::CONTRACT_ADDRS
63        .query(
64            querier,
65            croncat_factory_address.clone(),
66            (&croncat_contract_name, &croncat_version_parsed),
67        )?
68        .ok_or(CronCatContractError::NoSuchContractOnFactory {
69            contract_name: croncat_contract_name,
70            factory_addr: croncat_factory_address,
71            version: croncat_version,
72        })?;
73
74    Ok(contract_addr)
75}
76
77/// Returns a SubMsg
78/// This can be conveniently used when returning a Response
79/// where you might handle what happened in the reply entry point.
80/// `Ok(Response::new().add_submessage(returned_val))`
81pub fn create_croncat_task_submessage(
82    querier: &QuerierWrapper,
83    info: MessageInfo,
84    croncat_factory_address: Addr,
85    task: TaskRequest,
86    reply_type: Option<CronCatTaskSubmessageParams>,
87) -> Result<SubMsg, CronCatContractError> {
88    croncat_basic_validation(&info)?;
89    let wasm_exec_msg =
90        create_croncat_task_cosmos_msg(querier, info, croncat_factory_address, task)?;
91
92    // If no reply_type is provided, will use "always"
93    let (reply_id, sub_reply_type) = match reply_type {
94        None => (REPLY_CRONCAT_TASK_CREATION, SubMessageReplyType::Always),
95        Some(params) => (
96            params.reply_id.unwrap_or(REPLY_CRONCAT_TASK_CREATION),
97            params.reply_type.unwrap_or(SubMessageReplyType::Always),
98        ),
99    };
100
101    let sub_message = match sub_reply_type {
102        SubMessageReplyType::Always => SubMsg::reply_always(wasm_exec_msg, reply_id),
103        SubMessageReplyType::OnError => SubMsg::reply_on_error(wasm_exec_msg, reply_id),
104        SubMessageReplyType::OnSuccess => SubMsg::reply_on_success(wasm_exec_msg, reply_id),
105    };
106
107    Ok(sub_message)
108}
109
110/// Returns a CosmosMsg
111/// This can be conveniently used when returning a Response
112/// `Ok(Response::new().add_message(returned_val))`
113pub fn create_croncat_task_message(
114    querier: &QuerierWrapper,
115    info: MessageInfo,
116    croncat_factory_address: Addr,
117    task: TaskRequest,
118) -> Result<CosmosMsg, CronCatContractError> {
119    croncat_basic_validation(&info)?;
120    let wasm_exec_msg =
121        create_croncat_task_cosmos_msg(querier, info, croncat_factory_address, task)?;
122
123    Ok(wasm_exec_msg)
124}
125
126/// This returns a CosmosMsg Execute object
127/// It's a helper in this crate, but is exposed
128/// for external usage as well.
129pub fn create_croncat_task_cosmos_msg(
130    querier: &QuerierWrapper,
131    info: MessageInfo,
132    croncat_factory_address: Addr,
133    task: TaskRequest,
134) -> Result<CosmosMsg, CronCatContractError> {
135    let tasks_addr =
136        get_latest_croncat_contract(querier, croncat_factory_address, TASKS_NAME.to_string())?;
137
138    Ok(Wasm(Execute {
139        contract_addr: String::from(tasks_addr),
140        msg: to_binary(&CreateTask {
141            task: Box::new(task),
142        })?,
143        funds: info.funds,
144    }))
145}
146
147pub fn croncat_basic_validation(info: &MessageInfo) -> Result<(), CronCatContractError> {
148    // To create a CronCat task you will need to provide funds. All funds sent
149    // to this method will be used for task creation
150    // Because we cannot detect detailed error information from replies
151    // (See CosmWasm Discord: https://discord.com/channels/737637324434833438/737643344712171600/1040920787512725574)
152    // We'll add a check here to ensure they've attached funds
153    if info.funds.is_empty() {
154        return Err(CronCatContractError::TaskCreationNoFunds {});
155    }
156
157    Ok(())
158}
159
160/// This will simulate created task structure,
161/// assuming no errors happened during creation
162pub fn simulate_task(
163    task_request: TaskRequest,
164    tasks_config: TasksConfig,
165    manager_config: ManagerConfig,
166    owner_addr: Addr,
167) -> Task {
168    let amount_for_one_task =
169        simulate_amount_for_one_task(&tasks_config, &manager_config, &task_request);
170    let boundary = boundary_mock_validate(task_request.boundary, &task_request.interval);
171
172    Task {
173        owner_addr,
174        interval: task_request.interval,
175        boundary,
176        stop_on_fail: task_request.stop_on_fail,
177        amount_for_one_task,
178        actions: task_request.actions,
179        // NOTE: See process_queries in manager contract for details on limitations of malformed queries
180        queries: task_request.queries.unwrap_or_default(),
181        transforms: task_request.transforms.unwrap_or_default(),
182        version: tasks_config.version,
183    }
184}
185
186pub fn simulate_amount_for_one_task(
187    tasks_config: &TasksConfig,
188    manager_config: &ManagerConfig,
189    task_request: &TaskRequest,
190) -> AmountForOneTask {
191    let mut amount_for_one_task = AmountForOneTask {
192        cw20: None,
193        coin: [None, None],
194        gas: tasks_config.gas_base_fee,
195        agent_fee: manager_config.agent_fee,
196        treasury_fee: manager_config.treasury_fee,
197        gas_price: manager_config.gas_price.clone(),
198    };
199    for action in task_request.actions.iter() {
200        amount_for_one_task.add_gas(action.gas_limit.unwrap_or(tasks_config.gas_action_fee));
201
202        match &action.msg {
203            CosmosMsg::Wasm(WasmMsg::Execute {
204                contract_addr,
205                funds,
206                msg,
207            }) => {
208                for coin in funds {
209                    if coin.amount.is_zero() || !amount_for_one_task.add_coin(coin.clone()).unwrap()
210                    {
211                        // Ignore errors, we assuming we have valid task here
212                    }
213                }
214                if let Ok(cw20_msg) = cosmwasm_std::from_binary(msg) {
215                    match cw20_msg {
216                        Cw20ExecuteMsg::Send { amount, .. } if !amount.is_zero() => {
217                            amount_for_one_task
218                                .add_cw20(Cw20CoinVerified {
219                                    address: Addr::unchecked(contract_addr),
220                                    amount,
221                                })
222                                .unwrap();
223                        }
224                        Cw20ExecuteMsg::Transfer { amount, .. } if !amount.is_zero() => {
225                            amount_for_one_task
226                                .add_cw20(Cw20CoinVerified {
227                                    address: Addr::unchecked(contract_addr),
228                                    amount,
229                                })
230                                .unwrap();
231                        }
232                        _ => {}
233                    }
234                }
235            }
236            CosmosMsg::Bank(BankMsg::Send {
237                to_address: _,
238                amount,
239            }) => {
240                for coin in amount {
241                    amount_for_one_task.add_coin(coin.clone()).unwrap();
242                }
243            }
244            _ => {}
245        }
246    }
247
248    if let Some(queries) = &task_request.queries {
249        amount_for_one_task.add_gas(queries.len() as u64 * tasks_config.gas_query_fee)
250    }
251    amount_for_one_task
252}
253
254fn boundary_mock_validate(boundary: Option<Boundary>, interval: &Interval) -> Boundary {
255    match (interval, boundary) {
256        (
257            Interval::Cron(_) | Interval::Block(_) | Interval::Once,
258            Some(Boundary::Time(boundary_time)),
259        ) => Boundary::Time(boundary_time),
260        (_, Some(Boundary::Height(boundary_height))) => Boundary::Height(boundary_height),
261        (Interval::Cron(_), None) => Boundary::Time(BoundaryTime {
262            start: None,
263            end: None,
264        }),
265        (_, None) => Boundary::Height(BoundaryHeight {
266            start: None,
267            end: None,
268        }),
269        _ => unimplemented!(),
270    }
271}