croncat_agents/
contract.rs

1use crate::distributor::*;
2use crate::error::ContractError;
3use crate::error::ContractError::InvalidConfigurationValue;
4use crate::external::*;
5use crate::msg::*;
6use crate::state::*;
7#[cfg(not(feature = "library"))]
8use cosmwasm_std::entry_point;
9use cosmwasm_std::{
10    has_coins, to_binary, Addr, Attribute, Binary, Coin, Deps, DepsMut, Empty, Env, MessageInfo,
11    Order, QuerierWrapper, Response, StdError, StdResult, Storage, Uint64,
12};
13use croncat_sdk_agents::msg::{
14    AgentInfo, AgentResponse, AgentTaskResponse, ApprovedAgentAddresses, GetAgentIdsResponse,
15    TaskStats, UpdateConfig,
16};
17use croncat_sdk_agents::types::{Agent, AgentNominationStatus, AgentStatus, Config};
18use croncat_sdk_core::internal_messages::agents::{AgentOnTaskCompleted, AgentOnTaskCreated};
19use croncat_sdk_core::types::{DEFAULT_PAGINATION_FROM_INDEX, DEFAULT_PAGINATION_LIMIT};
20use cw2::set_contract_version;
21use std::cmp::min;
22
23pub(crate) const CONTRACT_NAME: &str = "crate:croncat-agents";
24pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
25
26#[cfg_attr(not(feature = "library"), entry_point)]
27pub fn instantiate(
28    deps: DepsMut,
29    _env: Env,
30    info: MessageInfo,
31    msg: InstantiateMsg,
32) -> Result<Response, ContractError> {
33    let InstantiateMsg {
34        pause_admin,
35        version,
36        croncat_manager_key,
37        croncat_tasks_key,
38        agent_nomination_duration,
39        min_tasks_per_agent,
40        min_coins_for_agent_registration,
41        agents_eject_threshold,
42        min_active_agent_count,
43        public_registration,
44        allowed_agents,
45    } = msg;
46
47    validate_config_non_zero_u16(agent_nomination_duration, "agent_nomination_duration")?;
48    validate_config_non_zero_u16(min_active_agent_count, "min_active_agent_count")?;
49    validate_config_non_zero_u64(min_tasks_per_agent, "min_tasks_per_agent")?;
50    validate_config_non_zero_u64(agents_eject_threshold, "agents_eject_threshold")?;
51    validate_config_non_zero_u64(
52        min_coins_for_agent_registration,
53        "min_coins_for_agent_registration",
54    )?;
55
56    // Validate all entries
57    let validated_allowed_agents = if let Some(agent_addrs) = &allowed_agents {
58        map_validate(&deps, agent_addrs)?
59    } else {
60        vec![]
61    };
62
63    let owner_addr = info.sender.clone();
64
65    // Validate pause_admin
66    // MUST: only be contract address
67    // MUST: not be same address as factory owner (DAO)
68    // Any factory action should be done by the owner_addr
69    // NOTE: different networks have diff bech32 prefix lengths. Capturing min/max here
70    if !(61usize..=74usize).contains(&pause_admin.to_string().len()) {
71        return Err(ContractError::InvalidPauseAdmin {});
72    }
73
74    let config = &Config {
75        min_tasks_per_agent: min_tasks_per_agent.unwrap_or(DEFAULT_MIN_TASKS_PER_AGENT),
76        croncat_factory_addr: info.sender,
77        croncat_manager_key,
78        croncat_tasks_key,
79        agent_nomination_block_duration: agent_nomination_duration
80            .unwrap_or(DEFAULT_NOMINATION_BLOCK_DURATION),
81        owner_addr,
82        pause_admin,
83        agents_eject_threshold: agents_eject_threshold.unwrap_or(DEFAULT_AGENTS_EJECT_THRESHOLD),
84        min_coins_for_agent_registration: min_coins_for_agent_registration
85            .unwrap_or(DEFAULT_MIN_COINS_FOR_AGENT_REGISTRATION),
86        min_active_agent_count: min_active_agent_count.unwrap_or(DEFAULT_MIN_ACTIVE_AGENT_COUNT),
87        public_registration,
88    };
89
90    // Store the approved agents if public registration is closed
91    // due to initial, progressive decentralization.
92    if !public_registration {
93        for agent_addr in validated_allowed_agents {
94            APPROVED_AGENTS
95                .save(deps.storage, &agent_addr, &Empty {})
96                .unwrap();
97        }
98    }
99
100    CONFIG.save(deps.storage, config)?;
101    PAUSED.save(deps.storage, &false)?;
102    AGENTS_ACTIVE.save(deps.storage, &vec![])?; // Init active agents empty vector
103    set_contract_version(
104        deps.storage,
105        CONTRACT_NAME,
106        version.unwrap_or_else(|| CONTRACT_VERSION.to_string()),
107    )?;
108    AGENT_NOMINATION_STATUS.save(
109        deps.storage,
110        &AgentNominationStatus {
111            start_height_of_nomination: None,
112            tasks_created_from_last_nomination: 0,
113        },
114    )?;
115
116    Ok(Response::new()
117        .add_attribute("action", "instantiate")
118        .add_attribute("owner", config.owner_addr.to_string()))
119}
120
121#[cfg_attr(not(feature = "library"), entry_point)]
122pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
123    match msg {
124        QueryMsg::GetAgent { account_id } => to_binary(&query_get_agent(deps, env, account_id)?),
125        QueryMsg::GetAgentIds { from_index, limit } => {
126            to_binary(&query_get_agent_ids(deps, from_index, limit)?)
127        }
128        QueryMsg::GetApprovedAgentAddresses { from_index, limit } => to_binary(
129            &query_get_approved_agent_addresses(deps, from_index, limit)?,
130        ),
131        QueryMsg::GetAgentTasks { account_id } => {
132            to_binary(&query_get_agent_tasks(deps, env, account_id)?)
133        }
134        QueryMsg::Config {} => to_binary(&CONFIG.load(deps.storage)?),
135        QueryMsg::Paused {} => to_binary(&PAUSED.load(deps.storage)?),
136    }
137}
138
139#[cfg_attr(not(feature = "library"), entry_point)]
140pub fn execute(
141    deps: DepsMut,
142    env: Env,
143    info: MessageInfo,
144    msg: ExecuteMsg,
145) -> Result<Response, ContractError> {
146    match msg {
147        ExecuteMsg::RegisterAgent { payable_account_id } => {
148            register_agent(deps, info, env, payable_account_id)
149        }
150        ExecuteMsg::UnregisterAgent { from_behind } => {
151            unregister_agent(deps.storage, &deps.querier, &info.sender, from_behind)
152        }
153        ExecuteMsg::UpdateAgent { payable_account_id } => {
154            update_agent(deps, info, env, payable_account_id)
155        }
156        ExecuteMsg::CheckInAgent {} => accept_nomination_agent(deps, info, env),
157        ExecuteMsg::OnTaskCreated(msg) => on_task_created(env, deps, info, msg),
158        ExecuteMsg::OnTaskCompleted(msg) => on_task_completed(deps, info, msg),
159        ExecuteMsg::UpdateConfig { config } => execute_update_config(deps, info, config),
160        ExecuteMsg::Tick {} => execute_tick(deps, env),
161        ExecuteMsg::PauseContract {} => execute_pause(deps, info),
162        ExecuteMsg::UnpauseContract {} => execute_unpause(deps, info),
163        ExecuteMsg::AddAgentToWhitelist { agent_address } => {
164            execute_add_agent_to_whitelist(env, deps, info, agent_address)
165        }
166        ExecuteMsg::RemoveAgentFromWhitelist { agent_address } => {
167            execute_remove_agent_from_whitelist(env, deps, info, agent_address)
168        }
169    }
170}
171
172fn query_get_agent(deps: Deps, env: Env, account_id: String) -> StdResult<AgentResponse> {
173    let account_id = deps.api.addr_validate(&account_id)?;
174
175    let agent = AGENTS.may_load(deps.storage, &account_id)?;
176
177    let a = if let Some(a) = agent {
178        a
179    } else {
180        return Ok(AgentResponse { agent: None });
181    };
182
183    let config: Config = CONFIG.load(deps.storage)?;
184    let rewards =
185        croncat_manager_contract::query_agent_rewards(&deps.querier, &config, account_id.as_str())?;
186    let agent_status = get_agent_status(deps.storage, env, &account_id)
187        // Return wrapped error if there was a problem
188        .map_err(|err| StdError::GenericErr {
189            msg: err.to_string(),
190        })?;
191
192    let stats = AGENT_STATS
193        .may_load(deps.storage, &account_id)?
194        .unwrap_or_default();
195    let agent_response = AgentResponse {
196        agent: Some(AgentInfo {
197            status: agent_status,
198            payable_account_id: a.payable_account_id,
199            balance: rewards,
200            register_start: a.register_start,
201            last_executed_slot: stats.last_executed_slot,
202            completed_block_tasks: Uint64::from(stats.completed_block_tasks),
203            completed_cron_tasks: Uint64::from(stats.completed_cron_tasks),
204            missed_blocked_tasks: Uint64::from(stats.missed_blocked_tasks),
205            missed_cron_tasks: Uint64::from(stats.missed_cron_tasks),
206        }),
207    };
208    Ok(agent_response)
209}
210
211/// Get a list of agent addresses
212fn query_get_agent_ids(
213    deps: Deps,
214    from_index: Option<u64>,
215    limit: Option<u64>,
216) -> StdResult<GetAgentIdsResponse> {
217    let active_loaded: Vec<Addr> = AGENTS_ACTIVE.load(deps.storage)?;
218    let active = active_loaded
219        .into_iter()
220        .skip(from_index.unwrap_or(DEFAULT_PAGINATION_FROM_INDEX) as usize)
221        .take(limit.unwrap_or(DEFAULT_PAGINATION_LIMIT) as usize)
222        .collect();
223    let pending: Vec<Addr> = AGENTS_PENDING
224        .iter(deps.storage)?
225        .skip(from_index.unwrap_or(DEFAULT_PAGINATION_FROM_INDEX) as usize)
226        .take(limit.unwrap_or(DEFAULT_PAGINATION_LIMIT) as usize)
227        .collect::<StdResult<Vec<Addr>>>()?;
228
229    Ok(GetAgentIdsResponse { active, pending })
230}
231
232/// Get a list of the approved agent addresses
233/// This is only relevant when Config's `public_registration` value is true
234fn query_get_approved_agent_addresses(
235    deps: Deps,
236    from_index: Option<u64>,
237    limit: Option<u64>,
238) -> StdResult<ApprovedAgentAddresses> {
239    let agent_addresses = APPROVED_AGENTS
240        .keys(deps.storage, None, None, Order::Ascending)
241        .skip(from_index.unwrap_or(DEFAULT_PAGINATION_FROM_INDEX) as usize)
242        .take(limit.unwrap_or(DEFAULT_PAGINATION_LIMIT) as usize)
243        .collect::<Result<Vec<Addr>, StdError>>();
244
245    Ok(ApprovedAgentAddresses {
246        approved_addresses: agent_addresses.unwrap(),
247    })
248}
249
250fn query_get_agent_tasks(deps: Deps, env: Env, account_id: String) -> StdResult<AgentTaskResponse> {
251    let account_id = deps.api.addr_validate(&account_id)?;
252    let active = AGENTS_ACTIVE.load(deps.storage)?;
253    if !active.contains(&account_id) {
254        return Ok(AgentTaskResponse {
255            stats: TaskStats {
256                num_cron_tasks: Uint64::zero(),
257                num_block_tasks: Uint64::zero(),
258            },
259        });
260    }
261    let config: Config = CONFIG.load(deps.storage)?;
262
263    let (block_slots, cron_slots) = croncat_tasks_contract::query_tasks_slots(deps, &config)?;
264    if block_slots == 0 && cron_slots == 0 {
265        return Ok(AgentTaskResponse {
266            stats: TaskStats {
267                num_cron_tasks: Uint64::zero(),
268                num_block_tasks: Uint64::zero(),
269            },
270        });
271    }
272    AGENT_TASK_DISTRIBUTOR
273        .get_agent_tasks(
274            &deps,
275            &env,
276            account_id,
277            (Some(block_slots), Some(cron_slots)),
278        )
279        .map_err(|err| StdError::generic_err(err.to_string()))
280}
281
282/// If registration is public, adds any account as an agent that will be able to execute tasks.
283/// If registration is restricted to the whitelist, it's consulted.
284/// Registering allows for rewards accruing with micro-payments which will accumulate to more long-term.
285///
286/// Optional Parameters:
287/// "payable_account_id" - Allows a different account id to be specified, so a user can receive funds at a different account than the agent account.
288fn register_agent(
289    deps: DepsMut,
290    info: MessageInfo,
291    env: Env,
292    payable_account_id: Option<String>,
293) -> Result<Response, ContractError> {
294    if !info.funds.is_empty() {
295        return Err(ContractError::NoFundsShouldBeAttached);
296    }
297    if PAUSED.load(deps.storage)? {
298        return Err(ContractError::ContractPaused);
299    }
300    let c = CONFIG.load(deps.storage)?;
301    let account = info.sender;
302
303    // Check if registration is public, return error if the calling agent isn't allowed
304    if !c.public_registration && !APPROVED_AGENTS.has(deps.storage, &account) {
305        return Err(ContractError::UnapprovedAgent {});
306    }
307
308    // REF: https://github.com/CosmWasm/cw-tokens/tree/main/contracts/cw20-escrow
309    // Check if native token balance is sufficient for a few txns, in this case 4 txns
310    let agent_wallet_balances = deps.querier.query_all_balances(account.clone())?;
311
312    // Get the denom from the manager contract
313    let manager_config = croncat_manager_contract::query_manager_config(&deps.as_ref(), &c)?;
314
315    let agents_needs_coin = Coin::new(
316        c.min_coins_for_agent_registration.into(),
317        manager_config.native_denom,
318    );
319    if !has_coins(&agent_wallet_balances, &agents_needs_coin) || agent_wallet_balances.is_empty() {
320        return Err(ContractError::InsufficientFunds {
321            amount_needed: agents_needs_coin,
322        });
323    }
324
325    let payable_id = if let Some(addr) = payable_account_id {
326        deps.api.addr_validate(&addr)?
327    } else {
328        account.clone()
329    };
330
331    let mut active_agents_vec: Vec<Addr> = AGENTS_ACTIVE
332        .may_load(deps.storage)?
333        .ok_or(ContractError::NoActiveAgents)?;
334    let total_agents = active_agents_vec.len();
335    let agent_status = if total_agents == 0 {
336        active_agents_vec.push(account.clone());
337        AGENTS_ACTIVE.save(deps.storage, &active_agents_vec)?;
338        AgentStatus::Active
339    } else {
340        AGENTS_PENDING.push_back(deps.storage, &account)?;
341        AgentStatus::Pending
342    };
343
344    let storage = deps.storage;
345    AGENTS.update(
346        storage,
347        &account,
348        |a: Option<Agent>| -> Result<_, ContractError> {
349            match a {
350                // make sure that account isn't already added
351                Some(_) => Err(ContractError::AgentAlreadyRegistered),
352                None => {
353                    Ok(Agent {
354                        payable_account_id: payable_id,
355                        // REF: https://github.com/CosmWasm/cosmwasm/blob/main/packages/std/src/types.rs#L57
356                        register_start: env.block.time,
357                    })
358                }
359            }
360        },
361    )?;
362    AGENT_STATS.save(
363        storage,
364        &account,
365        &AgentStats {
366            last_executed_slot: env.block.height,
367            completed_block_tasks: 0,
368            completed_cron_tasks: 0,
369            missed_blocked_tasks: 0,
370            missed_cron_tasks: 0,
371        },
372    )?;
373    Ok(Response::new()
374        .add_attribute("action", "register_agent")
375        .add_attribute("agent_status", agent_status.to_string()))
376}
377
378/// Update agent details, specifically the payable account id for an agent.
379fn update_agent(
380    deps: DepsMut,
381    info: MessageInfo,
382    _env: Env,
383    payable_account_id: String,
384) -> Result<Response, ContractError> {
385    let payable_account_id = deps.api.addr_validate(&payable_account_id)?;
386    if PAUSED.load(deps.storage)? {
387        return Err(ContractError::ContractPaused);
388    }
389
390    AGENTS.update(
391        deps.storage,
392        &info.sender,
393        |a: Option<Agent>| -> Result<_, ContractError> {
394            match a {
395                Some(agent) => {
396                    let mut ag = agent;
397                    ag.payable_account_id = payable_account_id;
398                    Ok(ag)
399                }
400                None => Err(ContractError::AgentNotRegistered {}),
401            }
402        },
403    )?;
404
405    Ok(Response::new().add_attribute("action", "update_agent"))
406}
407
408/// Allows an agent to accept a nomination within a certain amount of time to become an active agent.
409fn accept_nomination_agent(
410    deps: DepsMut,
411    info: MessageInfo,
412    env: Env,
413) -> Result<Response, ContractError> {
414    // Compare current time and Config's agent_nomination_begin_time to see if agent can join
415    let c: Config = CONFIG.load(deps.storage)?;
416
417    let mut active_agents: Vec<Addr> = AGENTS_ACTIVE.load(deps.storage)?;
418    let mut pending_queue_iter = AGENTS_PENDING.iter(deps.storage)?;
419    // Agent must be in the pending queue
420    // Get the position in the pending queue
421    let agent_position = pending_queue_iter
422        .position(|a| a.map_or_else(|_| false, |v| info.sender == v))
423        .ok_or(ContractError::AgentNotPending)?;
424    let agent_nomination_status = AGENT_NOMINATION_STATUS.load(deps.storage)?;
425    // edge case if last agent left
426    if active_agents.is_empty() && agent_position == 0 {
427        active_agents.push(info.sender.clone());
428        AGENTS_ACTIVE.save(deps.storage, &active_agents)?;
429
430        AGENTS_PENDING.pop_front(deps.storage)?;
431        AGENT_NOMINATION_STATUS.save(
432            deps.storage,
433            &AgentNominationStatus {
434                start_height_of_nomination: None,
435                tasks_created_from_last_nomination: 0,
436            },
437        )?;
438        return Ok(Response::new()
439            .add_attribute("action", "accept_nomination_agent")
440            .add_attribute("new_agent", info.sender.as_str()));
441    }
442
443    // It works out such that the time difference between when this is called,
444    // and the agent nomination begin time can be divided by the nomination
445    // duration and we get an integer. We use that integer to determine if an
446    // agent is allowed to get let in. If their position in the pending queue is
447    // less than or equal to that integer, they get let in.
448    let max_index = max_agent_nomination_index(&c, env, agent_nomination_status)?
449        .ok_or(ContractError::TryLaterForNomination)?;
450    let kicked_agents = if agent_position as u64 <= max_index {
451        // Make this agent active
452        // Update state removing from pending queue
453        let kicked_agents: Vec<Addr> = {
454            let mut kicked = Vec::with_capacity(agent_position);
455            for _ in 0..=agent_position {
456                let agent = AGENTS_PENDING.pop_front(deps.storage)?;
457                // Since we already iterated over it - we know it exists
458                let kicked_agent;
459                unsafe {
460                    kicked_agent = agent.unwrap_unchecked();
461                }
462                kicked.push(kicked_agent);
463            }
464            kicked
465        };
466
467        // and adding to active queue
468        active_agents.push(info.sender.clone());
469        AGENTS_ACTIVE.save(deps.storage, &active_agents)?;
470
471        // and update the config, setting the nomination begin time to None,
472        // which indicates no one will be nominated until more tasks arrive
473        AGENT_NOMINATION_STATUS.save(
474            deps.storage,
475            &AgentNominationStatus {
476                start_height_of_nomination: None,
477                tasks_created_from_last_nomination: 0,
478            },
479        )?;
480        kicked_agents
481    } else {
482        return Err(ContractError::TryLaterForNomination);
483    };
484    // Find difference
485    Ok(Response::new()
486        .add_attribute("action", "accept_nomination_agent")
487        .add_attribute("new_agent", info.sender.as_str())
488        .add_attribute("kicked_agents", format!("{kicked_agents:?}")))
489}
490
491/// Removes the agent from the active set of AGENTS.
492/// Withdraws all reward balances to the agent payable account id.
493/// In case it fails to unregister pending agent try to set `from_behind` to true
494fn unregister_agent(
495    storage: &mut dyn Storage,
496    querier: &QuerierWrapper<Empty>,
497    agent_id: &Addr,
498    from_behind: Option<bool>,
499) -> Result<Response, ContractError> {
500    if PAUSED.load(storage)? {
501        return Err(ContractError::ContractPaused);
502    }
503    let config: Config = CONFIG.load(storage)?;
504    let agent = AGENTS
505        .may_load(storage, agent_id)?
506        .ok_or(ContractError::AgentNotRegistered {})?;
507
508    // Remove from the list of active agents if the agent in this list
509    let mut active_agents: Vec<Addr> = AGENTS_ACTIVE.load(storage)?;
510    if let Some(index) = active_agents.iter().position(|addr| addr == agent_id) {
511        //Notify the balancer agent has been removed, to rebalance itself
512        AGENT_STATS.remove(storage, agent_id);
513        active_agents.remove(index);
514        AGENTS_ACTIVE.save(storage, &active_agents)?;
515    } else {
516        // Agent can't be both in active and pending vector
517        // Remove from the pending queue
518        let mut return_those_agents: Vec<Addr> =
519            Vec::with_capacity((AGENTS_PENDING.len(storage)? / 2) as usize);
520        if from_behind.unwrap_or(false) {
521            while let Some(addr) = AGENTS_PENDING.pop_front(storage)? {
522                if addr.eq(agent_id) {
523                    break;
524                } else {
525                    return_those_agents.push(addr);
526                }
527            }
528            for ag in return_those_agents.iter().rev() {
529                AGENTS_PENDING.push_front(storage, ag)?;
530            }
531        } else {
532            while let Some(addr) = AGENTS_PENDING.pop_back(storage)? {
533                if addr.eq(agent_id) {
534                    break;
535                } else {
536                    return_those_agents.push(addr);
537                }
538            }
539            for ag in return_those_agents.iter().rev() {
540                AGENTS_PENDING.push_back(storage, ag)?;
541            }
542        }
543    }
544    let msg = croncat_manager_contract::create_withdraw_rewards_submsg(
545        querier,
546        &config,
547        agent_id.as_str(),
548        agent.payable_account_id.to_string(),
549    )?;
550    AGENTS.remove(storage, agent_id);
551
552    let responses = Response::new()
553        // Send withdraw rewards message to manager contract
554        .add_message(msg)
555        .add_attribute("action", "unregister_agent")
556        .add_attribute("account_id", agent_id);
557
558    Ok(responses)
559}
560
561pub fn execute_update_config(
562    deps: DepsMut,
563    info: MessageInfo,
564    msg: UpdateConfig,
565) -> Result<Response, ContractError> {
566    // Deconstruct, so we don't miss any fields
567    let UpdateConfig {
568        croncat_manager_key,
569        croncat_tasks_key,
570        min_tasks_per_agent,
571        agent_nomination_duration,
572        min_coins_for_agent_registration,
573        agents_eject_threshold,
574        min_active_agent_count,
575        public_registration,
576    } = msg;
577
578    CONFIG.update(deps.storage, |config| {
579        validate_config_non_zero_u16(agent_nomination_duration, "agent_nomination_duration")?;
580        validate_config_non_zero_u16(min_active_agent_count, "min_active_agent_count")?;
581        validate_config_non_zero_u64(min_tasks_per_agent, "min_tasks_per_agent")?;
582        validate_config_non_zero_u64(agents_eject_threshold, "agents_eject_threshold")?;
583        validate_config_non_zero_u64(
584            min_coins_for_agent_registration,
585            "min_coins_for_agent_registration",
586        )?;
587
588        if info.sender != config.owner_addr {
589            return Err(ContractError::Unauthorized {});
590        }
591
592        // The public_registration field is a one-way boolean, used for progressive decentralization
593        // If update is attempting to turn true to false, this is prohibited
594        if config.public_registration && public_registration == Some(false) {
595            return Err(ContractError::DecentralizationEnabled {});
596        }
597
598        let new_config = Config {
599            owner_addr: config.owner_addr,
600            pause_admin: config.pause_admin,
601            croncat_factory_addr: config.croncat_factory_addr,
602            croncat_manager_key: croncat_manager_key.unwrap_or(config.croncat_manager_key),
603            croncat_tasks_key: croncat_tasks_key.unwrap_or(config.croncat_tasks_key),
604            min_tasks_per_agent: min_tasks_per_agent.unwrap_or(config.min_tasks_per_agent),
605            agent_nomination_block_duration: agent_nomination_duration
606                .unwrap_or(config.agent_nomination_block_duration),
607            min_coins_for_agent_registration: min_coins_for_agent_registration
608                .unwrap_or(DEFAULT_MIN_COINS_FOR_AGENT_REGISTRATION),
609            agents_eject_threshold: agents_eject_threshold
610                .unwrap_or(DEFAULT_AGENTS_EJECT_THRESHOLD),
611            min_active_agent_count: min_active_agent_count
612                .unwrap_or(DEFAULT_MIN_ACTIVE_AGENT_COUNT),
613            public_registration: public_registration.unwrap_or(config.public_registration),
614        };
615        Ok(new_config)
616    })?;
617
618    // When progressive decentralization begins and public registration is open,
619    // we won't need the allowed agent list, so we'll clear it.
620    if public_registration == Some(true) {
621        APPROVED_AGENTS.clear(deps.storage);
622    }
623
624    Ok(Response::new().add_attribute("action", "update_config"))
625}
626
627/// Note that we ran into problems with clippy here
628/// See https://github.com/CronCats/cw-croncat/pull/415
629#[allow(clippy::op_ref)]
630fn get_agent_status(
631    storage: &dyn Storage,
632    env: Env,
633    account_id: &Addr,
634) -> Result<AgentStatus, ContractError> {
635    let c: Config = CONFIG.load(storage)?;
636    let active = AGENTS_ACTIVE.load(storage)?;
637
638    // Pending
639    let mut pending_iter = AGENTS_PENDING.iter(storage)?;
640    // If agent is pending, Check if they should get nominated to checkin to become active
641    let agent_position = if let Some(pos) = pending_iter.position(|address| {
642        if let Ok(addr) = address {
643            &addr == account_id
644        } else {
645            false
646        }
647    }) {
648        pos
649    } else {
650        // Check for active
651        if active.contains(account_id) {
652            return Ok(AgentStatus::Active);
653        } else {
654            return Err(ContractError::AgentNotRegistered {});
655        }
656    };
657
658    // Edge case if last agent unregistered
659    if active.is_empty() && agent_position == 0 {
660        return Ok(AgentStatus::Nominated);
661    };
662
663    // Load config's task ratio, total tasks, active agents, and AGENT_NOMINATION_BEGIN_TIME.
664    // Then determine if this agent is considered "Nominated" and should call CheckInAgent
665    let max_agent_index =
666        max_agent_nomination_index(&c, env, AGENT_NOMINATION_STATUS.load(storage)?)?;
667    let agent_status = match max_agent_index {
668        Some(max_idx) if agent_position as u64 <= max_idx => AgentStatus::Nominated,
669        _ => AgentStatus::Pending,
670    };
671    Ok(agent_status)
672}
673
674/// Calculate the biggest index of nomination for pending agents
675fn max_agent_nomination_index(
676    cfg: &Config,
677    env: Env,
678    agent_nomination_status: AgentNominationStatus,
679) -> StdResult<Option<u64>> {
680    let block_height = env.block.height;
681
682    let agents_by_tasks_created = agent_nomination_status
683        .tasks_created_from_last_nomination
684        .saturating_div(cfg.min_tasks_per_agent);
685    let agents_by_height = agent_nomination_status
686        .start_height_of_nomination
687        .map_or(0, |start_height| {
688            (block_height - start_height) / cfg.agent_nomination_block_duration as u64
689        });
690    let agents_to_pass = min(agents_by_tasks_created, agents_by_height);
691    if agents_to_pass == 0 {
692        Ok(None)
693    } else {
694        Ok(Some(agents_to_pass - 1))
695    }
696}
697
698pub fn execute_tick(deps: DepsMut, env: Env) -> Result<Response, ContractError> {
699    let block_height = env.block.height;
700    let config = CONFIG.load(deps.storage)?;
701    let mut attributes = vec![];
702    let mut submessages = vec![];
703    let agents_active = AGENTS_ACTIVE.load(deps.storage)?;
704    let total_remove_agents: usize = agents_active.len();
705    let mut total_removed = 0;
706
707    for agent_id in agents_active {
708        let skip = (config.min_active_agent_count as usize) >= total_remove_agents - total_removed;
709        if !skip {
710            let stats = AGENT_STATS
711                .load(deps.storage, &agent_id)
712                .unwrap_or_default();
713            if block_height > stats.last_executed_slot + config.agents_eject_threshold {
714                let resp = unregister_agent(deps.storage, &deps.querier, &agent_id, None)
715                    .unwrap_or_default();
716                // Save attributes and messages
717                attributes.extend_from_slice(&resp.attributes);
718                submessages.extend_from_slice(&resp.messages);
719                total_removed += 1;
720            }
721        }
722    }
723
724    // Check if there isn't any active or pending agents
725    if AGENTS_ACTIVE.load(deps.storage)?.is_empty() && AGENTS_PENDING.is_empty(deps.storage)? {
726        attributes.push(Attribute::new("lifecycle", "tick_failure"))
727    }
728    let response = Response::new()
729        .add_attribute("action", "tick")
730        .add_attributes(attributes)
731        .add_submessages(submessages);
732    Ok(response)
733}
734
735pub fn execute_pause(deps: DepsMut, info: MessageInfo) -> Result<Response, ContractError> {
736    if PAUSED.load(deps.storage)? {
737        return Err(ContractError::ContractPaused);
738    }
739    let config = CONFIG.load(deps.storage)?;
740    if info.sender != config.pause_admin {
741        return Err(ContractError::Unauthorized);
742    }
743    PAUSED.save(deps.storage, &true)?;
744    Ok(Response::new().add_attribute("action", "pause_contract"))
745}
746
747pub fn execute_unpause(deps: DepsMut, info: MessageInfo) -> Result<Response, ContractError> {
748    if !PAUSED.load(deps.storage)? {
749        return Err(ContractError::ContractUnpaused);
750    }
751    let config = CONFIG.load(deps.storage)?;
752    if info.sender != config.owner_addr {
753        return Err(ContractError::Unauthorized);
754    }
755    PAUSED.save(deps.storage, &false)?;
756    Ok(Response::new().add_attribute("action", "unpause_contract"))
757}
758
759pub fn execute_add_agent_to_whitelist(
760    _env: Env,
761    deps: DepsMut,
762    info: MessageInfo,
763    agent_address: String,
764) -> Result<Response, ContractError> {
765    let config = CONFIG.may_load(deps.storage)?.unwrap();
766    // Ensure the owner is calling
767    if config.owner_addr != info.sender {
768        return Err(ContractError::Unauthorized);
769    }
770
771    let validated_agent_address = deps.api.addr_validate(agent_address.as_str())?;
772    APPROVED_AGENTS.save(deps.storage, &validated_agent_address, &Empty {})?;
773
774    Ok(Response::new().add_attribute("action", "add_agent_to_whitelist"))
775}
776
777pub fn execute_remove_agent_from_whitelist(
778    _env: Env,
779    deps: DepsMut,
780    info: MessageInfo,
781    agent_address: String,
782) -> Result<Response, ContractError> {
783    let config = CONFIG.may_load(deps.storage)?.unwrap();
784    // Ensure the owner is calling
785    if config.owner_addr != info.sender {
786        return Err(ContractError::Unauthorized);
787    }
788
789    let validated_agent_address = deps.api.addr_validate(agent_address.as_str())?;
790    APPROVED_AGENTS.remove(deps.storage, &validated_agent_address);
791
792    Ok(Response::new().add_attribute("action", "remove_agent_to_whitelist"))
793}
794
795fn on_task_created(
796    env: Env,
797    deps: DepsMut,
798    info: MessageInfo,
799    _: AgentOnTaskCreated,
800) -> Result<Response, ContractError> {
801    let config = CONFIG.may_load(deps.storage)?.unwrap();
802    croncat_tasks_contract::assert_caller_is_tasks_contract(&deps.querier, &config, &info.sender)?;
803
804    AGENT_NOMINATION_STATUS.update(deps.storage, |mut status| -> StdResult<_> {
805        if status.start_height_of_nomination.is_none() {
806            status.start_height_of_nomination = Some(env.block.height)
807        }
808        Ok(AgentNominationStatus {
809            start_height_of_nomination: status.start_height_of_nomination,
810            tasks_created_from_last_nomination: status.tasks_created_from_last_nomination + 1,
811        })
812    })?;
813
814    let response = Response::new().add_attribute("action", "on_task_created");
815    Ok(response)
816}
817
818fn on_task_completed(
819    deps: DepsMut,
820    info: MessageInfo,
821    args: AgentOnTaskCompleted,
822) -> Result<Response, ContractError> {
823    let config = CONFIG.may_load(deps.storage)?.unwrap();
824
825    croncat_manager_contract::assert_caller_is_manager_contract(
826        &deps.querier,
827        &config,
828        &info.sender,
829    )?;
830    let mut stats = AGENT_STATS.load(deps.storage, &args.agent_id)?;
831
832    if args.is_block_slot_task {
833        stats.completed_block_tasks += 1;
834    } else {
835        stats.completed_cron_tasks += 1;
836    }
837    AGENT_STATS.save(deps.storage, &args.agent_id, &stats)?;
838
839    let response = Response::new().add_attribute("action", "on_task_completed");
840    Ok(response)
841}
842
843/// Validating a non-zero value for u64
844fn validate_non_zero(num: u64, field_name: &str) -> Result<(), ContractError> {
845    if num == 0u64 {
846        Err(InvalidConfigurationValue {
847            field: field_name.to_string(),
848        })
849    } else {
850        Ok(())
851    }
852}
853
854/// Resources indicate that trying to use generics in this case is not the correct path
855/// This will cast into a u64 and proceed to validate
856fn validate_config_non_zero_u16(
857    opt_num: Option<u16>,
858    field_name: &str,
859) -> Result<(), ContractError> {
860    if let Some(num) = opt_num {
861        validate_non_zero(num as u64, field_name)
862    } else {
863        Ok(())
864    }
865}
866
867fn validate_config_non_zero_u64(
868    opt_num: Option<u64>,
869    field_name: &str,
870) -> Result<(), ContractError> {
871    if let Some(num) = opt_num {
872        validate_non_zero(num, field_name)
873    } else {
874        Ok(())
875    }
876}
877
878// Thank you cw1 for the handy function
879// pub fn map_validate(deps: &DepsMut, admins: &Vec<Addr>) -> StdResult<Vec<Addr>> {
880pub fn map_validate(deps: &DepsMut, agents: &[String]) -> StdResult<Vec<Addr>> {
881    agents
882        .iter()
883        .map(|addr| deps.api.addr_validate(addr.as_str()))
884        .collect()
885}