croncat_manager/
contract.rs

1use std::collections::HashSet;
2
3#[cfg(not(feature = "library"))]
4use cosmwasm_std::entry_point;
5use cosmwasm_std::{
6    from_binary, to_binary, Addr, Attribute, BankMsg, Binary, Deps, DepsMut, Env, MessageInfo,
7    Reply, Response, StdError, StdResult, SubMsg, Uint128, WasmMsg,
8};
9use croncat_sdk_core::internal_messages::agents::AgentWithdrawOnRemovalArgs;
10use croncat_sdk_core::internal_messages::manager::{ManagerCreateTaskBalance, ManagerRemoveTask};
11use croncat_sdk_manager::msg::{AgentWithdrawCallback, ManagerExecuteMsg::ProxyCallForwarded};
12use croncat_sdk_manager::types::{TaskBalance, TaskBalanceResponse, UpdateConfig};
13use croncat_sdk_tasks::types::{Interval, Task, TaskExecutionInfo, TaskInfo};
14use cw2::set_contract_version;
15use cw_utils::{may_pay, parse_reply_execute_data};
16
17use crate::balances::{
18    add_fee_rewards, execute_owner_withdraw, execute_receive_cw20, execute_refill_native_balance,
19    execute_refill_task_cw20, execute_user_withdraw, query_users_balances, sub_user_cw20,
20};
21use crate::error::ContractError;
22use crate::helpers::{
23    assert_caller_is_agent_contract, attached_natives, calculate_required_natives,
24    check_if_sender_is_tasks, check_ready_for_execution, create_bank_send_message,
25    create_task_completed_msg, finalize_task, gas_with_fees, get_agents_addr, get_tasks_addr,
26    is_after_boundary, is_before_boundary, parse_reply_msg, process_queries, query_agent,
27    recalculate_cw20, remove_task_balance, replace_values, task_sub_msgs,
28};
29use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
30use crate::state::{
31    Config, QueueItem, AGENT_REWARDS, CONFIG, LAST_TASK_EXECUTION_INFO, PAUSED, REPLY_QUEUE,
32    TASKS_BALANCES, TREASURY_BALANCE,
33};
34use crate::ContractError::InvalidPercentage;
35
36pub(crate) const CONTRACT_NAME: &str = "crate:croncat-manager";
37pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
38
39pub(crate) const DEFAULT_FEE: u16 = 5;
40
41/// reply id from tasks contract
42pub(crate) const TASK_REPLY: u64 = u64::from_be_bytes(*b"croncat1");
43
44/// Instantiate
45/// First contract method before it runs on the chains
46/// See [`InstantiateMsg`] for more details
47/// `gas_price` and `owner_id` getting validated
48///
49/// Response: every [`Config`] field as attributes
50#[cfg_attr(not(feature = "library"), entry_point)]
51pub fn instantiate(
52    deps: DepsMut,
53    _env: Env,
54    info: MessageInfo,
55    msg: InstantiateMsg,
56) -> Result<Response, ContractError> {
57    // Deconstruct so we don't miss fields
58    let InstantiateMsg {
59        version,
60        croncat_tasks_key,
61        croncat_agents_key,
62        pause_admin,
63        gas_price,
64        treasury_addr,
65        cw20_whitelist,
66    } = msg;
67
68    // require minimum balance for denom establishment
69    if info.funds.is_empty() {
70        return Err(ContractError::EmptyBalance {});
71    }
72
73    // Get the denom from the only balance available at time of this instantiation
74    // Factory will pass along 1 unit for us to safely know what native denom is.
75    // We can't use "deps.querier.query_bonded_denom" because it will error on ICS chains
76    let funds_denom = info
77        .funds
78        .iter()
79        .find(|coin| coin.amount == Uint128::new(1))
80        .map(|coin| coin.denom.clone());
81
82    let denom = if let Some(d) = funds_denom {
83        d
84    } else {
85        return Err(ContractError::EmptyBalance {});
86    };
87
88    // Check if we attached some funds in native denom, add them into treasury
89    let treasury_funds = may_pay(&info, denom.as_str());
90    if treasury_funds.is_err() {
91        return Err(ContractError::RedundantFunds {});
92    }
93    TREASURY_BALANCE.save(deps.storage, &treasury_funds.unwrap())?;
94
95    let gas_price = gas_price.unwrap_or_default();
96    // Make sure gas_price is valid
97    if !gas_price.is_valid() {
98        return Err(ContractError::InvalidGasPrice {});
99    }
100
101    let owner_addr = info.sender.clone();
102
103    // Validate pause_admin
104    // MUST: only be contract address
105    // MUST: not be same address as factory owner (DAO)
106    // Any factory action should be done by the owner_addr
107    // NOTE: different networks have diff bech32 prefix lengths. Capturing min/max here
108    if !(61usize..=74usize).contains(&pause_admin.to_string().len()) {
109        return Err(ContractError::InvalidPauseAdmin {});
110    }
111
112    let cw20_whitelist: Vec<Addr> = cw20_whitelist
113        .unwrap_or_default()
114        .into_iter()
115        .map(Addr::unchecked)
116        .collect();
117
118    let config = Config {
119        owner_addr,
120        pause_admin,
121        croncat_factory_addr: info.sender,
122        croncat_tasks_key,
123        croncat_agents_key,
124        agent_fee: DEFAULT_FEE,
125        treasury_fee: DEFAULT_FEE,
126        gas_price,
127        cw20_whitelist,
128        native_denom: denom,
129        limit: 100,
130        treasury_addr: treasury_addr
131            .map(|human| deps.api.addr_validate(&human))
132            .transpose()?,
133    };
134
135    // Update state
136    CONFIG.save(deps.storage, &config)?;
137    PAUSED.save(deps.storage, &false)?;
138    LAST_TASK_EXECUTION_INFO.save(deps.storage, &TaskExecutionInfo::default())?;
139    set_contract_version(
140        deps.storage,
141        CONTRACT_NAME,
142        version.unwrap_or_else(|| CONTRACT_VERSION.to_string()),
143    )?;
144
145    Ok(Response::new()
146        .add_attribute("action", "instantiate")
147        .add_attribute("owner_id", config.owner_addr.to_string()))
148}
149
150#[cfg_attr(not(feature = "library"), entry_point)]
151pub fn execute(
152    deps: DepsMut,
153    env: Env,
154    info: MessageInfo,
155    msg: ExecuteMsg,
156) -> Result<Response, ContractError> {
157    match msg {
158        ExecuteMsg::UpdateConfig(msg) => execute_update_config(deps, info, *msg),
159        ExecuteMsg::ProxyCall { task_hash } => execute_proxy_call(deps, env, info, task_hash),
160        ExecuteMsg::ProxyBatch(proxy_calls) => execute_proxy_batch(info, env, proxy_calls),
161        ExecuteMsg::ProxyCallForwarded {
162            agent_addr,
163            task_hash,
164        } => execute_proxy_call_forwarded(deps, env, info, task_hash, agent_addr),
165        ExecuteMsg::Receive(msg) => execute_receive_cw20(deps, info, msg),
166        ExecuteMsg::RefillTaskBalance { task_hash } => {
167            execute_refill_native_balance(deps, info, task_hash)
168        }
169        ExecuteMsg::RefillTaskCw20Balance { task_hash, cw20 } => {
170            execute_refill_task_cw20(deps, info, task_hash, cw20)
171        }
172        ExecuteMsg::CreateTaskBalance(msg) => execute_create_task_balance(deps, info, *msg),
173        ExecuteMsg::RemoveTask(msg) => execute_remove_task(deps, info, msg),
174        ExecuteMsg::OwnerWithdraw {} => execute_owner_withdraw(deps, info),
175        ExecuteMsg::UserWithdraw { limit } => execute_user_withdraw(deps, info, limit),
176        ExecuteMsg::AgentWithdraw(args) => execute_withdraw_agent_rewards(deps, info, args),
177        ExecuteMsg::PauseContract {} => execute_pause(deps, info),
178        ExecuteMsg::UnpauseContract {} => execute_unpause(deps, info),
179    }
180}
181
182fn execute_remove_task(
183    deps: DepsMut,
184    info: MessageInfo,
185    msg: ManagerRemoveTask,
186) -> Result<Response, ContractError> {
187    let config = CONFIG.load(deps.storage)?;
188    check_if_sender_is_tasks(&deps.querier, &config, &info.sender)?;
189    let task_owner = msg.sender;
190    let task_balance = TASKS_BALANCES.load(deps.storage, &msg.task_hash)?;
191    let coins_transfer = remove_task_balance(
192        deps.storage,
193        task_balance,
194        &task_owner,
195        &config.native_denom,
196        &msg.task_hash,
197    )?;
198
199    let bank_send = BankMsg::Send {
200        to_address: task_owner.into_string(),
201        amount: coins_transfer,
202    };
203    Ok(Response::new()
204        .add_attribute("action", "remove_task")
205        .add_message(bank_send))
206}
207
208fn execute_proxy_call_internal(
209    deps: DepsMut,
210    env: Env,
211    info: MessageInfo,
212    task_hash: Option<String>,
213    agent_fwd_addr: Option<Addr>,
214) -> Result<Response, ContractError> {
215    let config: Config = CONFIG.load(deps.storage)?;
216
217    let agent_addr = if let Some(a) = agent_fwd_addr {
218        // we MUST confirm info.sender is THIS contract if agent addr fwded
219        if env.contract.address == info.sender {
220            a
221        } else {
222            return Err(ContractError::Unauthorized {});
223        }
224    } else {
225        info.sender
226    };
227    let agents_addr = get_agents_addr(&deps.querier, &config)?;
228    let tasks_addr = get_tasks_addr(&deps.querier, &config)?;
229
230    // Check if agent is active,
231    // Then get a task
232    let current_task: croncat_sdk_tasks::types::TaskResponse = if let Some(hash) = task_hash {
233        // For evented case - check the agent is active, then may the best agent win
234        let agent_response: croncat_sdk_agents::msg::AgentResponse =
235            deps.querier.query_wasm_smart(
236                agents_addr,
237                &croncat_sdk_agents::msg::QueryMsg::GetAgent {
238                    account_id: agent_addr.to_string(),
239                },
240            )?;
241        if agent_response.agent.map_or(true, |agent| {
242            agent.status != croncat_sdk_agents::types::AgentStatus::Active
243        }) {
244            return Err(ContractError::AgentNotActive {});
245        }
246
247        // A hash means agent is attempting to execute evented task
248        let task_data: croncat_sdk_tasks::types::TaskResponse = deps.querier.query_wasm_smart(
249            tasks_addr.clone(),
250            &croncat_sdk_tasks::msg::TasksQueryMsg::Task { task_hash: hash },
251        )?;
252
253        // Check the task is evented
254        if let Some(task) = task_data.clone().task {
255            let t = Task {
256                owner_addr: task.owner_addr,
257                interval: task.interval,
258                boundary: task.boundary,
259                stop_on_fail: task.stop_on_fail,
260                amount_for_one_task: task.amount_for_one_task,
261                actions: task.actions,
262                queries: task.queries.unwrap_or_default(),
263                transforms: task.transforms,
264                version: task.version,
265            };
266            if !t.is_evented() {
267                return Err(ContractError::NoTaskForAgent {});
268            }
269        }
270        task_data
271    } else {
272        // For scheduled case - check only active agents that are allowed tasks
273        let agent_tasks: croncat_sdk_agents::msg::AgentTaskResponse =
274            deps.querier.query_wasm_smart(
275                agents_addr,
276                &croncat_sdk_agents::msg::QueryMsg::GetAgentTasks {
277                    account_id: agent_addr.to_string(),
278                },
279            )?;
280        if agent_tasks.stats.num_block_tasks.is_zero() && agent_tasks.stats.num_cron_tasks.is_zero()
281        {
282            return Err(ContractError::NoTaskForAgent {});
283        }
284
285        // get a scheduled task
286        deps.querier.query_wasm_smart(
287            tasks_addr.clone(),
288            &croncat_sdk_tasks::msg::TasksQueryMsg::CurrentTask {},
289        )?
290    };
291
292    let Some(mut task) = current_task.task else {
293        // No task
294        return Err(ContractError::NoTask {});
295    };
296    let task_hash = task.task_hash.to_owned();
297    let task_version = task.version.to_owned();
298
299    // check if ready between boundary (if any)
300    if is_before_boundary(&env.block, Some(&task.boundary)) {
301        return Err(ContractError::TaskNotReady {});
302    }
303    if is_after_boundary(&env.block, Some(&task.boundary)) {
304        // End task
305        return end_task(
306            deps,
307            task,
308            config,
309            agent_addr,
310            tasks_addr,
311            Some(vec![
312                Attribute::new("lifecycle", "task_ended"),
313                Attribute::new("task_hash", task_hash),
314                Attribute::new("task_version", task_version),
315            ]),
316            true,
317        );
318    }
319
320    if task.queries.is_some() {
321        // Process all the queries
322        let query_responses = process_queries(&deps, &task)?;
323        if !query_responses.is_empty() {
324            replace_values(&mut task, query_responses)?;
325        }
326
327        // Recalculate cw20 usage and re-check for self-calls
328        let invalidated_after_transform = if let Ok(amounts) =
329            recalculate_cw20(&task, &config, deps.as_ref(), &env.contract.address)
330        {
331            task.amount_for_one_task.cw20 = amounts;
332            false
333        } else {
334            true
335        };
336
337        // Need to re-check if task has enough cw20's
338        // because it could have been changed through transform
339        let task_balance = TASKS_BALANCES.load(deps.storage, task_hash.as_bytes())?;
340        if invalidated_after_transform
341            || task_balance
342                .verify_enough_cw20(task.amount_for_one_task.cw20.clone(), Uint128::new(1))
343                .is_err()
344        {
345            // Task is no longer valid
346            return end_task(
347                deps,
348                task,
349                config,
350                agent_addr,
351                tasks_addr,
352                Some(vec![
353                    Attribute::new("lifecycle", "task_invalidated"),
354                    Attribute::new("task_hash", task_hash),
355                    Attribute::new("task_version", task_version),
356                ]),
357                false,
358            );
359        }
360    }
361
362    let sub_msgs = task_sub_msgs(&task);
363    let queue_item = QueueItem {
364        task: task.clone(),
365        agent_addr,
366        failures: Default::default(),
367    };
368
369    REPLY_QUEUE.save(deps.storage, &queue_item)?;
370
371    // Save latest task execution info
372    // This is a simple register, allowing the receiving contract to query
373    // and discover details about the latest task sent
374
375    let last_task_execution_info = TaskExecutionInfo {
376        block_height: env.block.height,
377        tx_info: env.transaction,
378        task_hash: task.task_hash,
379        owner_addr: task.owner_addr,
380        amount_for_one_task: task.amount_for_one_task,
381        version: task_version,
382    };
383
384    LAST_TASK_EXECUTION_INFO.save(deps.storage, &last_task_execution_info)?;
385
386    Ok(Response::new()
387        .add_attribute("action", "proxy_call")
388        .add_submessages(sub_msgs))
389}
390
391/// Regular direct entrypoint for proxy_call task execution
392fn execute_proxy_call(
393    deps: DepsMut,
394    env: Env,
395    info: MessageInfo,
396    task_hash: Option<String>,
397) -> Result<Response, ContractError> {
398    let paused = PAUSED.load(deps.storage)?;
399    check_ready_for_execution(&info, paused)?;
400
401    execute_proxy_call_internal(deps, env, info, task_hash, None)
402}
403
404/// Forwarded entrypoint for proxy_call task execution via Batch
405/// Enables info.sender to carry in optimistic batch setup
406fn execute_proxy_call_forwarded(
407    deps: DepsMut,
408    env: Env,
409    info: MessageInfo,
410    task_hash: Option<String>,
411    agent_addr: Addr,
412) -> Result<Response, ContractError> {
413    let paused = PAUSED.load(deps.storage)?;
414    check_ready_for_execution(&info, paused)?;
415
416    execute_proxy_call_internal(deps, env, info, task_hash, Some(agent_addr))
417}
418
419/// Based on how tasks could fail & how batching task proxy_call can result in many tasks not
420/// executing at desired time, this method makes and effort to wrap a single signed TX into
421/// an optimistic batch. SubMsgs provide the only way to optimistically attempt all proxy call
422/// transaction/calls. Future CosmosSDK changes could allow this method not to be needed.
423fn execute_proxy_batch(
424    info: MessageInfo,
425    env: Env,
426    proxy_calls: Vec<Option<String>>,
427) -> Result<Response, ContractError> {
428    let mut sub_msgs = Vec::with_capacity(proxy_calls.len());
429    let mut unique_hashes = HashSet::new();
430    let agent_addr = info.sender;
431
432    for task_hash in proxy_calls {
433        // Not handling reply, as the individual proxy_call's will handle appropriately
434        let msg = SubMsg::new(WasmMsg::Execute {
435            // We can ONLY call ourselves
436            contract_addr: env.contract.address.to_string(),
437            // Instead of huge matcher, we require ONLY the proxy_call case
438            msg: to_binary(&ProxyCallForwarded {
439                task_hash: task_hash.clone(),
440                agent_addr: agent_addr.clone(),
441            })?,
442            funds: vec![],
443        });
444
445        match task_hash {
446            Some(th) => {
447                // dedupe task hashes because we dont want DoS single tasks
448                if unique_hashes.insert(th) {
449                    sub_msgs.push(msg);
450                }
451            }
452            None => {
453                sub_msgs.push(msg);
454            }
455        }
456    }
457
458    Ok(Response::new().add_submessages(sub_msgs))
459}
460
461fn end_task(
462    deps: DepsMut,
463    task: TaskInfo,
464    config: Config,
465    agent_addr: Addr,
466    tasks_addr: Addr,
467    attrs: Option<Vec<Attribute>>,
468    reimburse_only: bool,
469) -> Result<Response, ContractError> {
470    // Sub gas/fee from native
471    let gas_with_fees = if reimburse_only {
472        gas_with_fees(task.amount_for_one_task.gas, 0u64)?
473    } else {
474        gas_with_fees(
475            task.amount_for_one_task.gas,
476            (task.amount_for_one_task.agent_fee + task.amount_for_one_task.treasury_fee) as u64,
477        )?
478    };
479    let native_for_gas_required = task
480        .amount_for_one_task
481        .gas_price
482        .calculate(gas_with_fees)
483        .unwrap();
484    let mut task_balance = TASKS_BALANCES.load(deps.storage, task.task_hash.as_bytes())?;
485    task_balance.native_balance = task_balance
486        .native_balance
487        .checked_sub(Uint128::new(native_for_gas_required))
488        .map_err(StdError::overflow)?;
489
490    // Account for fees, to reimburse agent for efforts
491    // TODO: Need to NOT add fee for non-reimberse
492    add_fee_rewards(
493        deps.storage,
494        task.amount_for_one_task.gas,
495        &task.amount_for_one_task.gas_price,
496        &agent_addr,
497        task.amount_for_one_task.agent_fee,
498        task.amount_for_one_task.treasury_fee,
499        reimburse_only,
500    )?;
501
502    // refund the final balances to task owner
503    let coins_transfer = remove_task_balance(
504        deps.storage,
505        task_balance,
506        &task.owner_addr,
507        &config.native_denom,
508        task.task_hash.as_bytes(),
509    )?;
510    let msg = croncat_sdk_core::internal_messages::tasks::TasksRemoveTaskByManager {
511        task_hash: task.task_hash.into_bytes(),
512    }
513    .into_cosmos_msg(tasks_addr)?;
514    let bank_send = BankMsg::Send {
515        to_address: task.owner_addr.into_string(),
516        amount: coins_transfer,
517    };
518    Ok(Response::new()
519        .add_attribute("action", "end_task")
520        .add_attributes(attrs.unwrap_or_default())
521        .add_message(msg)
522        .add_message(bank_send))
523}
524
525/// Execute: UpdateConfig
526/// Used by contract owner to update config or pause contract
527///
528/// Returns updated [`Config`]
529pub fn execute_update_config(
530    deps: DepsMut,
531    info: MessageInfo,
532    msg: UpdateConfig,
533) -> Result<Response, ContractError> {
534    CONFIG.update(deps.storage, |mut config| {
535        // Deconstruct, so we don't miss any fields
536        let UpdateConfig {
537            agent_fee,
538            treasury_fee,
539            gas_price,
540            croncat_tasks_key,
541            croncat_agents_key,
542            treasury_addr,
543            cw20_whitelist,
544        } = msg;
545
546        if info.sender != config.owner_addr {
547            return Err(ContractError::Unauthorized {});
548        }
549
550        let updated_agent_fee = if let Some(agent_fee) = agent_fee {
551            // Validate it
552            validate_percentage_value(&agent_fee, "agent_fee")?;
553            agent_fee
554        } else {
555            // Use current value in config
556            config.agent_fee
557        };
558
559        let updated_treasury_fee = if let Some(treasury_fee) = treasury_fee {
560            validate_percentage_value(&treasury_fee, "treasury_fee")?;
561            treasury_fee
562        } else {
563            config.treasury_fee
564        };
565
566        let gas_price = gas_price.unwrap_or(config.gas_price);
567        if !gas_price.is_valid() {
568            return Err(ContractError::InvalidGasPrice {});
569        }
570
571        let treasury_addr = if let Some(human) = treasury_addr {
572            Some(deps.api.addr_validate(&human)?)
573        } else {
574            config.treasury_addr
575        };
576
577        let cw20_whitelist: Vec<Addr> = cw20_whitelist
578            .unwrap_or_default()
579            .into_iter()
580            .map(|human| deps.api.addr_validate(&human))
581            .collect::<StdResult<_>>()?;
582
583        config.cw20_whitelist.extend(cw20_whitelist);
584
585        let new_config = Config {
586            owner_addr: config.owner_addr,
587            pause_admin: config.pause_admin,
588            croncat_factory_addr: config.croncat_factory_addr,
589            croncat_tasks_key: croncat_tasks_key.unwrap_or(config.croncat_tasks_key),
590            croncat_agents_key: croncat_agents_key.unwrap_or(config.croncat_agents_key),
591            agent_fee: updated_agent_fee,
592            treasury_fee: updated_treasury_fee,
593            gas_price,
594            cw20_whitelist: config.cw20_whitelist,
595            native_denom: config.native_denom,
596            limit: config.limit,
597            treasury_addr,
598        };
599        Ok(new_config)
600    })?;
601
602    Ok(Response::new().add_attribute("action", "update_config"))
603}
604
605fn execute_create_task_balance(
606    deps: DepsMut,
607    info: MessageInfo,
608    msg: ManagerCreateTaskBalance,
609) -> Result<Response, ContractError> {
610    let config = CONFIG.load(deps.storage)?;
611    check_if_sender_is_tasks(&deps.querier, &config, &info.sender)?;
612    let (native, ibc) = attached_natives(&config.native_denom, info.funds)?;
613    let cw20 = msg.cw20;
614    if let Some(attached_cw20) = &cw20 {
615        sub_user_cw20(deps.storage, &msg.sender, attached_cw20)?;
616    }
617    let tasks_balance = TaskBalance {
618        native_balance: native,
619        cw20_balance: cw20,
620        ibc_balance: ibc,
621    };
622    // Let's check if task has enough attached balance
623    {
624        let gas_with_fees = gas_with_fees(
625            msg.amount_for_one_task.gas,
626            (config.agent_fee + config.treasury_fee) as u64,
627        )?;
628        let native_for_gas_required = config.gas_price.calculate(gas_with_fees).unwrap();
629        let (native_for_sends_required, ibc_required) =
630            calculate_required_natives(msg.amount_for_one_task.coin, &config.native_denom)?;
631        tasks_balance.verify_enough_attached(
632            Uint128::from(native_for_gas_required) + native_for_sends_required,
633            msg.amount_for_one_task.cw20,
634            ibc_required,
635            msg.recurring,
636            &config.native_denom,
637        )?;
638    }
639    TASKS_BALANCES.save(deps.storage, &msg.task_hash, &tasks_balance)?;
640
641    Ok(Response::new().add_attribute("action", "create_task_balance"))
642}
643
644/// Allows an agent to withdraw all rewards, paid to the specified payable account id.
645fn execute_withdraw_agent_rewards(
646    deps: DepsMut,
647    info: MessageInfo,
648    args: Option<AgentWithdrawOnRemovalArgs>,
649) -> Result<Response, ContractError> {
650    //assert if contract is ready for execution
651    let paused = PAUSED.load(deps.storage)?;
652    check_ready_for_execution(&info, paused)?;
653    let config: Config = CONFIG.load(deps.storage)?;
654
655    let agent_id: Addr;
656    let payable_account_id: Addr;
657    let mut fail_on_zero_balance = true;
658
659    if let Some(arg) = args {
660        assert_caller_is_agent_contract(&deps.querier, &config, &info.sender)?;
661        agent_id = Addr::unchecked(arg.agent_id);
662        payable_account_id = Addr::unchecked(arg.payable_account_id);
663        fail_on_zero_balance = false;
664    } else {
665        agent_id = info.sender;
666        let agent = query_agent(&deps.querier, &config, agent_id.to_string())?
667            .agent
668            .ok_or(ContractError::NoRewardsOwnerAgentFound {})?;
669        payable_account_id = agent.payable_account_id;
670    }
671    let agent_rewards = AGENT_REWARDS
672        .may_load(deps.storage, &agent_id)?
673        .unwrap_or_default();
674
675    AGENT_REWARDS.remove(deps.storage, &agent_id);
676
677    let mut msgs = vec![];
678    // This will send all token balances to Agent
679    let msg = create_bank_send_message(
680        &payable_account_id,
681        &config.native_denom,
682        agent_rewards.u128(),
683    )?;
684
685    if !agent_rewards.is_zero() {
686        msgs.push(msg);
687    } else if fail_on_zero_balance {
688        return Err(ContractError::NoWithdrawRewardsAvailable {});
689    }
690
691    Ok(Response::new()
692        .add_messages(msgs)
693        .set_data(to_binary(&AgentWithdrawCallback {
694            agent_id: agent_id.to_string(),
695            amount: agent_rewards,
696            payable_account_id: payable_account_id.to_string(),
697        })?)
698        .add_attribute("action", "withdraw_rewards")
699        .add_attribute("payment_account_id", &payable_account_id)
700        .add_attribute("rewards", agent_rewards))
701}
702
703pub fn execute_pause(deps: DepsMut, info: MessageInfo) -> Result<Response, ContractError> {
704    if PAUSED.load(deps.storage)? {
705        return Err(ContractError::ContractPaused);
706    }
707    let config = CONFIG.load(deps.storage)?;
708    if info.sender != config.pause_admin {
709        return Err(ContractError::Unauthorized {});
710    }
711    PAUSED.save(deps.storage, &true)?;
712    Ok(Response::new().add_attribute("action", "pause_contract"))
713}
714
715pub fn execute_unpause(deps: DepsMut, info: MessageInfo) -> Result<Response, ContractError> {
716    if !PAUSED.load(deps.storage)? {
717        return Err(ContractError::ContractUnpaused);
718    }
719    let config = CONFIG.load(deps.storage)?;
720    if info.sender != config.owner_addr {
721        return Err(ContractError::Unauthorized {});
722    }
723    PAUSED.save(deps.storage, &false)?;
724    Ok(Response::new().add_attribute("action", "unpause_contract"))
725}
726
727#[cfg_attr(not(feature = "library"), entry_point)]
728pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
729    match msg {
730        QueryMsg::Config {} => to_binary(&CONFIG.load(deps.storage)?),
731        QueryMsg::Paused {} => to_binary(&PAUSED.load(deps.storage)?),
732        QueryMsg::TreasuryBalance {} => to_binary(&TREASURY_BALANCE.load(deps.storage)?),
733        QueryMsg::UsersBalances {
734            address,
735            from_index,
736            limit,
737        } => to_binary(&query_users_balances(deps, address, from_index, limit)?),
738        QueryMsg::TaskBalance { task_hash } => to_binary(&TaskBalanceResponse {
739            balance: TASKS_BALANCES.may_load(deps.storage, task_hash.as_bytes())?,
740        }),
741        QueryMsg::AgentRewards { agent_id } => to_binary(
742            &AGENT_REWARDS
743                .may_load(deps.storage, &Addr::unchecked(agent_id))?
744                .unwrap_or(Uint128::zero()),
745        ),
746    }
747}
748
749#[cfg_attr(not(feature = "library"), entry_point)]
750pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result<Response, ContractError> {
751    match msg.id {
752        TASK_REPLY => {
753            let execute_data = parse_reply_execute_data(msg)?;
754            let remove_task_msg: Option<ManagerRemoveTask> =
755                from_binary(&execute_data.data.unwrap())?;
756            let Some(msg) = remove_task_msg else {
757                return Ok(Response::new());
758            };
759            let config = CONFIG.load(deps.storage)?;
760            let task_owner = msg.sender;
761            let task_balance = TASKS_BALANCES.load(deps.storage, &msg.task_hash)?;
762            let coins_transfer = remove_task_balance(
763                deps.storage,
764                task_balance,
765                &task_owner,
766                &config.native_denom,
767                &msg.task_hash,
768            )?;
769
770            let bank_send = BankMsg::Send {
771                to_address: task_owner.into_string(),
772                amount: coins_transfer,
773            };
774            Ok(Response::new().add_message(bank_send))
775        }
776        _ => {
777            let mut queue_item = REPLY_QUEUE.load(deps.storage)?;
778            let last = parse_reply_msg(deps.storage, &mut queue_item, msg);
779            if last {
780                let failures: Vec<Attribute> = queue_item
781                    .failures
782                    .iter()
783                    .map(|(idx, failure)| Attribute::new(format!("action{}_failure", idx), failure))
784                    .collect();
785                let config = CONFIG.load(deps.storage)?;
786                let complete_msg = create_task_completed_msg(
787                    &deps.querier,
788                    &config,
789                    &queue_item.agent_addr,
790                    !matches!(queue_item.task.interval, Interval::Cron(_)),
791                )?;
792                Ok(finalize_task(deps, queue_item)?
793                    .add_message(complete_msg)
794                    .add_attributes(failures))
795            } else {
796                Ok(Response::new())
797            }
798        }
799    }
800}
801
802/// Validate when a given value should be a reasonable percentage.
803/// Due to low native token prices on some chains, we must allow for
804/// greater than 100% in order to be sustainable, and have gone with
805/// a max of 10,000% after internal discussion and looking at the numbers.
806/// Since it's unsigned, don't check for negatives
807fn validate_percentage_value(val: &u16, field_name: &str) -> Result<(), ContractError> {
808    if val > &10_000u16 {
809        Err(InvalidPercentage {
810            field: field_name.to_string(),
811        })
812    } else {
813        Ok(())
814    }
815}