warp_controller/
contract.rs

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    //STATE
125    #[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    //CONFIG
144    #[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        // maximum time for evictions
154        pub t_max: Uint64,
155        // minimum time for evictions
156        pub t_min: Uint64,
157        // maximum fee for evictions
158        pub a_max: Uint128,
159        // minimum fee for evictions
160        pub a_min: Uint128,
161        // maximum length of queue modifier for evictions
162        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        //account creation
195        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        //job execution
303        _ => {
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            //assume reward.amount == warp token allowance
350            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                            //send reward to controller
462                            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                            //send reward to controller
479                            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                            //withdraw all assets that are listed
496                            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}