1use crate::error::map_contract_error;
2use crate::state::{ACCOUNTS, CONFIG, FINISHED_JOBS, PENDING_JOBS};
3use crate::{execute, query, state::STATE, ContractError};
4use warp_account_pkg::{GenericMsg, WithdrawAssetsMsg};
5use warp_controller_pkg::account::{Account, Fund, FundTransferMsgs, TransferFromMsg, TransferNftMsg};
6use warp_controller_pkg::job::{Job, JobStatus};
7use cosmwasm_schema::cw_serde;
8
9use warp_controller_pkg::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, State};
10use cosmwasm_std::{
11 entry_point, to_binary, Addr, Attribute, BalanceResponse, BankMsg, BankQuery, Binary, Coin,
12 CosmosMsg, Deps, DepsMut, Env, MessageInfo, QueryRequest, Reply, Response, StdError, StdResult,
13 SubMsgResult, Uint128, Uint64, WasmMsg,
14};
15use cw_storage_plus::Item;
16
17#[cfg_attr(not(feature = "library"), entry_point)]
18pub fn instantiate(
19 deps: DepsMut,
20 _env: Env,
21 info: MessageInfo,
22 msg: InstantiateMsg,
23) -> Result<Response, ContractError> {
24 let state = State {
25 current_job_id: Uint64::one(),
26 q: Uint64::zero(),
27 };
28
29 let config = Config {
30 owner: deps
31 .api
32 .addr_validate(&msg.owner.unwrap_or_else(|| info.sender.to_string()))?,
33 fee_denom: msg.fee_denom,
34 fee_collector: deps
35 .api
36 .addr_validate(&msg.fee_collector.unwrap_or_else(|| info.sender.to_string()))?,
37 warp_account_code_id: msg.warp_account_code_id,
38 minimum_reward: msg.minimum_reward,
39 creation_fee_percentage: msg.creation_fee,
40 cancellation_fee_percentage: msg.cancellation_fee,
41 resolver_address: deps.api.addr_validate(&msg.resolver_address)?,
42 t_max: msg.t_max,
43 t_min: msg.t_min,
44 a_max: msg.a_max,
45 a_min: msg.a_min,
46 q_max: msg.q_max,
47 };
48
49 if config.a_max < config.a_min {
50 return Err(ContractError::MaxFeeUnderMinFee {});
51 }
52
53 if config.t_max < config.t_min {
54 return Err(ContractError::MaxTimeUnderMinTime {});
55 }
56
57 if config.minimum_reward < config.a_min {
58 return Err(ContractError::RewardSmallerThanFee {});
59 }
60
61 if config.creation_fee_percentage.u64() > 100 {
62 return Err(ContractError::CreationFeeTooHigh {});
63 }
64
65 if config.cancellation_fee_percentage.u64() > 100 {
66 return Err(ContractError::CancellationFeeTooHigh {});
67 }
68
69 STATE.save(deps.storage, &state)?;
70 CONFIG.save(deps.storage, &config)?;
71
72 Ok(Response::new())
73}
74
75#[cfg_attr(not(feature = "library"), entry_point)]
76pub fn execute(
77 deps: DepsMut,
78 env: Env,
79 info: MessageInfo,
80 msg: ExecuteMsg,
81) -> Result<Response, ContractError> {
82 match msg {
83 ExecuteMsg::CreateJob(data) => execute::job::create_job(deps, env, info, data),
84 ExecuteMsg::DeleteJob(data) => execute::job::delete_job(deps, env, info, data),
85 ExecuteMsg::UpdateJob(data) => execute::job::update_job(deps, env, info, data),
86 ExecuteMsg::ExecuteJob(data) => execute::job::execute_job(deps, env, info, data),
87 ExecuteMsg::EvictJob(data) => execute::job::evict_job(deps, env, info, data),
88
89 ExecuteMsg::CreateAccount(data) => execute::account::create_account(deps, env, info, data),
90
91 ExecuteMsg::UpdateConfig(data) => execute::controller::update_config(deps, env, info, data),
92
93 ExecuteMsg::MigrateAccounts(data) => {
94 execute::controller::migrate_accounts(deps, env, info, data)
95 }
96 ExecuteMsg::MigratePendingJobs(data) => {
97 execute::controller::migrate_pending_jobs(deps, env, info, data)
98 }
99 ExecuteMsg::MigrateFinishedJobs(data) => {
100 execute::controller::migrate_finished_jobs(deps, env, info, data)
101 }
102 }
103}
104
105#[cfg_attr(not(feature = "library"), entry_point)]
106pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
107 match msg {
108 QueryMsg::QueryJob(data) => to_binary(&query::job::query_job(deps, env, data)?),
109 QueryMsg::QueryJobs(data) => to_binary(&query::job::query_jobs(deps, env, data)?),
110
111 QueryMsg::QueryAccount(data) => to_binary(&query::account::query_account(deps, env, data)?),
112 QueryMsg::QueryAccounts(data) => {
113 to_binary(&query::account::query_accounts(deps, env, data)?)
114 }
115
116 QueryMsg::QueryConfig(data) => {
117 to_binary(&query::controller::query_config(deps, env, data)?)
118 }
119 }
120}
121
122#[cfg_attr(not(feature = "library"), entry_point)]
123pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result<Response, ContractError> {
124 #[cw_serde]
126 pub struct V1State {
127 pub current_job_id: Uint64,
128 pub current_template_id: Uint64,
129 pub q: Uint64,
130 }
131
132 const V1STATE: Item<V1State> = Item::new("state");
133 let v1_state = V1STATE.load(deps.storage)?;
134
135 STATE.save(
136 deps.storage,
137 &State {
138 current_job_id: v1_state.current_job_id,
139 q: v1_state.q,
140 },
141 )?;
142
143 #[cw_serde]
145 pub struct V1Config {
146 pub owner: Addr,
147 pub fee_denom: String,
148 pub fee_collector: Addr,
149 pub warp_account_code_id: Uint64,
150 pub minimum_reward: Uint128,
151 pub creation_fee_percentage: Uint64,
152 pub cancellation_fee_percentage: Uint64,
153 pub t_max: Uint64,
155 pub t_min: Uint64,
157 pub a_max: Uint128,
159 pub a_min: Uint128,
161 pub q_max: Uint64,
163 }
164
165 const V1CONFIG: Item<V1Config> = Item::new("config");
166
167 let v1_config = V1CONFIG.load(deps.storage)?;
168
169 CONFIG.save(
170 deps.storage,
171 &Config {
172 owner: v1_config.owner,
173 fee_denom: v1_config.fee_denom,
174 fee_collector: v1_config.fee_collector,
175 warp_account_code_id: msg.warp_account_code_id,
176 minimum_reward: v1_config.minimum_reward,
177 creation_fee_percentage: v1_config.creation_fee_percentage,
178 cancellation_fee_percentage: v1_config.cancellation_fee_percentage,
179 resolver_address: deps.api.addr_validate(&msg.resolver_address)?,
180 t_max: v1_config.t_max,
181 t_min: v1_config.t_min,
182 a_max: v1_config.a_max,
183 a_min: v1_config.a_min,
184 q_max: v1_config.q_max,
185 },
186 )?;
187
188 Ok(Response::new())
189}
190
191#[cfg_attr(not(feature = "library"), entry_point)]
192pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result<Response, ContractError> {
193 match msg.id {
194 0 => {
196 let reply = msg.result.into_result().map_err(StdError::generic_err)?;
197
198 let event = reply
199 .events
200 .iter()
201 .find(|event| {
202 event
203 .attributes
204 .iter()
205 .any(|attr| attr.key == "action" && attr.value == "instantiate")
206 })
207 .ok_or_else(|| StdError::generic_err("cannot find `instantiate` event"))?;
208
209 let owner = event
210 .attributes
211 .iter()
212 .cloned()
213 .find(|attr| attr.key == "owner")
214 .ok_or_else(|| StdError::generic_err("cannot find `owner` attribute"))?
215 .value;
216
217 let address = event
218 .attributes
219 .iter()
220 .cloned()
221 .find(|attr| attr.key == "contract_addr")
222 .ok_or_else(|| StdError::generic_err("cannot find `contract_addr` attribute"))?
223 .value;
224
225 let funds: Vec<Coin> = serde_json_wasm::from_str(
226 &event
227 .attributes
228 .iter()
229 .cloned()
230 .find(|attr| attr.key == "funds")
231 .ok_or_else(|| StdError::generic_err("cannot find `funds` attribute"))?
232 .value,
233 )?;
234
235 let cw_funds: Option<Vec<Fund>> = serde_json_wasm::from_str(
236 &event
237 .attributes
238 .iter()
239 .cloned()
240 .find(|attr| attr.key == "cw_funds")
241 .ok_or_else(|| StdError::generic_err("cannot find `cw_funds` attribute"))?
242 .value,
243 )?;
244
245 let cw_funds_vec = match cw_funds {
246 None => {
247 vec![]
248 }
249 Some(funds) => funds,
250 };
251
252 let mut msgs_vec: Vec<CosmosMsg> = vec![];
253
254 for cw_fund in &cw_funds_vec {
255 msgs_vec.push(CosmosMsg::Wasm(match cw_fund {
256 Fund::Cw20(cw20_fund) => WasmMsg::Execute {
257 contract_addr: deps
258 .api
259 .addr_validate(&cw20_fund.contract_addr)?
260 .to_string(),
261 msg: to_binary(&FundTransferMsgs::TransferFrom(TransferFromMsg {
262 owner: owner.clone(),
263 recipient: address.clone(),
264 amount: cw20_fund.amount,
265 }))?,
266 funds: vec![],
267 },
268 Fund::Cw721(cw721_fund) => WasmMsg::Execute {
269 contract_addr: deps
270 .api
271 .addr_validate(&cw721_fund.contract_addr)?
272 .to_string(),
273 msg: to_binary(&FundTransferMsgs::TransferNft(TransferNftMsg {
274 recipient: address.clone(),
275 token_id: cw721_fund.token_id.clone(),
276 }))?,
277 funds: vec![],
278 },
279 }))
280 }
281
282 if ACCOUNTS().has(deps.storage, deps.api.addr_validate(&owner)?) {
283 return Err(ContractError::AccountAlreadyExists {});
284 }
285
286 ACCOUNTS().save(
287 deps.storage,
288 deps.api.addr_validate(&owner)?,
289 &Account {
290 owner: deps.api.addr_validate(&owner.clone())?,
291 account: deps.api.addr_validate(&address)?,
292 },
293 )?;
294 Ok(Response::new()
295 .add_attribute("action", "save_account")
296 .add_attribute("owner", owner)
297 .add_attribute("account_address", address)
298 .add_attribute("funds", serde_json_wasm::to_string(&funds)?)
299 .add_attribute("cw_funds", serde_json_wasm::to_string(&cw_funds_vec)?)
300 .add_messages(msgs_vec))
301 }
302 _ => {
304 let mut state = STATE.load(deps.storage)?;
305
306 let new_status = match msg.result {
307 SubMsgResult::Ok(_) => JobStatus::Executed,
308 SubMsgResult::Err(_) => JobStatus::Failed,
309 };
310
311 let job = PENDING_JOBS().load(deps.storage, msg.id)?;
312 PENDING_JOBS().remove(deps.storage, msg.id)?;
313
314 let finished_job = FINISHED_JOBS().update(deps.storage, msg.id, |j| match j {
315 None => Ok(Job {
316 id: job.id,
317 owner: job.owner,
318 last_update_time: job.last_update_time,
319 name: job.name,
320 description: job.description,
321 labels: job.labels,
322 status: new_status,
323 condition: job.condition,
324 terminate_condition: job.terminate_condition,
325 msgs: job.msgs,
326 vars: job.vars,
327 recurring: job.recurring,
328 requeue_on_evict: job.requeue_on_evict,
329 reward: job.reward,
330 assets_to_withdraw: job.assets_to_withdraw,
331 }),
332 Some(_) => Err(ContractError::JobAlreadyFinished {}),
333 })?;
334
335 let res_attrs = match msg.result {
336 SubMsgResult::Err(e) => vec![Attribute::new(
337 "transaction_error",
338 format!("{}. {}", &e, map_contract_error(&e)),
339 )],
340 _ => vec![],
341 };
342
343 let mut msgs = vec![];
344 let mut new_job_attrs = vec![];
345
346 let account = ACCOUNTS().load(deps.storage, finished_job.owner.clone())?;
347 let config = CONFIG.load(deps.storage)?;
348
349 let fee = finished_job.reward * Uint128::from(config.creation_fee_percentage)
351 / Uint128::new(100);
352
353 let account_amount = deps
354 .querier
355 .query::<BalanceResponse>(&QueryRequest::Bank(BankQuery::Balance {
356 address: account.account.to_string(),
357 denom: config.fee_denom.clone(),
358 }))?
359 .amount
360 .amount;
361
362 if finished_job.recurring {
363 if account_amount < fee + finished_job.reward {
364 new_job_attrs.push(Attribute::new("action", "recur_job"));
365 new_job_attrs.push(Attribute::new("creation_status", "failed_insufficient_fee"))
366 } else if !(finished_job.status == JobStatus::Executed
367 || finished_job.status == JobStatus::Failed)
368 {
369 new_job_attrs.push(Attribute::new("action", "recur_job"));
370 new_job_attrs.push(Attribute::new(
371 "creation_status",
372 "failed_invalid_job_status",
373 ));
374 } else {
375 let new_vars: String = deps.querier.query_wasm_smart(
376 config.resolver_address.clone(),
377 &warp_resolver_pkg::QueryMsg::QueryApplyVarFn(warp_resolver_pkg::QueryApplyVarFnMsg {
378 vars: finished_job.vars,
379 status: finished_job.status.clone(),
380 }),
381 )?;
382
383 let should_terminate_job: bool;
384 match finished_job.terminate_condition.clone() {
385 Some(terminate_condition) => {
386 let resolution: StdResult<bool> = deps.querier.query_wasm_smart(
387 config.resolver_address,
388 &warp_resolver_pkg::QueryMsg::QueryResolveCondition(
389 warp_resolver_pkg::QueryResolveConditionMsg {
390 condition: terminate_condition,
391 vars: new_vars.clone(),
392 },
393 ),
394 );
395 if let Err(e) = resolution {
396 should_terminate_job = true;
397 new_job_attrs.push(Attribute::new("action", "recur_job"));
398 new_job_attrs.push(Attribute::new(
399 "job_terminate_condition_status",
400 "invalid",
401 ));
402 new_job_attrs.push(Attribute::new(
403 "creation_status",
404 format!(
405 "terminated_due_to_terminate_condition_resolves_to_error. {}",
406 e
407 ),
408 ));
409 } else {
410 new_job_attrs.push(Attribute::new(
411 "job_terminate_condition_status",
412 "valid",
413 ));
414 if resolution? {
415 should_terminate_job = true;
416 new_job_attrs.push(Attribute::new("action", "recur_job"));
417 new_job_attrs.push(Attribute::new(
418 "creation_status",
419 "terminated_due_to_terminate_condition_resolves_to_true",
420 ));
421 } else {
422 should_terminate_job = false;
423 }
424 }
425 }
426 None => {
427 should_terminate_job = false;
428 }
429 }
430
431 if !should_terminate_job {
432 let new_job = PENDING_JOBS().update(
433 deps.storage,
434 state.current_job_id.u64(),
435 |s| match s {
436 None => Ok(Job {
437 id: state.current_job_id,
438 owner: finished_job.owner.clone(),
439 last_update_time: Uint64::from(env.block.time.seconds()),
440 name: finished_job.name.clone(),
441 description: finished_job.description,
442 labels: finished_job.labels,
443 status: JobStatus::Pending,
444 condition: finished_job.condition.clone(),
445 terminate_condition: finished_job.terminate_condition.clone(),
446 vars: new_vars,
447 requeue_on_evict: finished_job.requeue_on_evict,
448 recurring: finished_job.recurring,
449 msgs: finished_job.msgs.clone(),
450 reward: finished_job.reward,
451 assets_to_withdraw: finished_job.assets_to_withdraw,
452 }),
453 Some(_) => Err(ContractError::JobAlreadyExists {}),
454 },
455 )?;
456
457 state.current_job_id = state.current_job_id.checked_add(Uint64::new(1))?;
458 state.q = state.q.checked_add(Uint64::new(1))?;
459
460 msgs.push(
461 WasmMsg::Execute {
463 contract_addr: account.account.to_string(),
464 msg: to_binary(&warp_account_pkg::ExecuteMsg::Generic(GenericMsg {
465 msgs: vec![CosmosMsg::Bank(BankMsg::Send {
466 to_address: config.fee_collector.to_string(),
467 amount: vec![Coin::new(
468 (fee).u128(),
469 config.fee_denom.clone(),
470 )],
471 })],
472 }))?,
473 funds: vec![],
474 },
475 );
476
477 msgs.push(
478 WasmMsg::Execute {
480 contract_addr: account.account.to_string(),
481 msg: to_binary(&warp_account_pkg::ExecuteMsg::Generic(GenericMsg {
482 msgs: vec![CosmosMsg::Bank(BankMsg::Send {
483 to_address: env.contract.address.to_string(),
484 amount: vec![Coin::new(
485 (new_job.reward).u128(),
486 config.fee_denom,
487 )],
488 })],
489 }))?,
490 funds: vec![],
491 },
492 );
493
494 msgs.push(
495 WasmMsg::Execute {
497 contract_addr: account.account.to_string(),
498 msg: to_binary(&warp_account_pkg::ExecuteMsg::WithdrawAssets(
499 WithdrawAssetsMsg {
500 asset_infos: new_job.assets_to_withdraw,
501 },
502 ))?,
503 funds: vec![],
504 },
505 );
506
507 new_job_attrs.push(Attribute::new("action", "create_job"));
508 new_job_attrs.push(Attribute::new("job_id", new_job.id));
509 new_job_attrs.push(Attribute::new("job_owner", new_job.owner));
510 new_job_attrs.push(Attribute::new("job_name", new_job.name));
511 new_job_attrs.push(Attribute::new(
512 "job_status",
513 serde_json_wasm::to_string(&new_job.status)?,
514 ));
515 new_job_attrs.push(Attribute::new(
516 "job_condition",
517 serde_json_wasm::to_string(&new_job.condition)?,
518 ));
519 new_job_attrs.push(Attribute::new(
520 "job_msgs",
521 serde_json_wasm::to_string(&new_job.msgs)?,
522 ));
523 new_job_attrs.push(Attribute::new("job_reward", new_job.reward));
524 new_job_attrs.push(Attribute::new("job_creation_fee", fee));
525 new_job_attrs.push(Attribute::new(
526 "job_last_updated_time",
527 new_job.last_update_time,
528 ));
529 new_job_attrs.push(Attribute::new("sub_action", "recur_job"));
530 }
531 }
532 }
533
534 STATE.save(deps.storage, &state)?;
535
536 Ok(Response::new()
537 .add_attribute("action", "execute_reply")
538 .add_attribute("job_id", job.id)
539 .add_attributes(res_attrs)
540 .add_attributes(new_job_attrs)
541 .add_messages(msgs))
542 }
543 }
544}