croncat_sdk_tasks/
types.rs

1use std::{fmt::Display, str::FromStr};
2
3use cosmwasm_schema::cw_serde;
4use cosmwasm_std::{
5    Addr, Binary, CosmosMsg, Empty, Env, Timestamp, TransactionInfo, Uint64, WasmQuery,
6};
7use cron_schedule::Schedule;
8use croncat_mod_generic::types::PathToValue;
9pub use croncat_sdk_core::types::AmountForOneTask;
10use cw20::Cw20Coin;
11use hex::ToHex;
12use sha2::{Digest, Sha256};
13
14#[cw_serde]
15pub struct Config {
16    /// Address of the contract owner
17    pub owner_addr: Addr,
18
19    /// A multisig admin whose sole responsibility is to pause the contract in event of emergency.
20    /// Must be a different contract address than DAO, cannot be a regular keypair
21    /// Does not have the ability to unpause, must rely on the DAO to assess the situation and act accordingly
22    pub pause_admin: Addr,
23
24    /// Address of the factory contract
25    pub croncat_factory_addr: Addr,
26
27    /// Chain name to add prefix to the task_hash
28    pub chain_name: String,
29
30    /// Assigned by Factory, denotes the version of this contract (CW2 spec) & used as the task verion as well.
31    pub version: String,
32
33    /// Name of the key for raw querying Manager address from the factory
34    pub croncat_manager_key: (String, [u8; 2]),
35
36    /// Name of the key for raw querying Agents address from the factory
37    pub croncat_agents_key: (String, [u8; 2]),
38
39    /// Time in nanos for each bucket of tasks
40    pub slot_granularity_time: u64,
41
42    /// Gas needed to cover proxy call without any action
43    pub gas_base_fee: u64,
44
45    /// Gas needed to cover single non-wasm task's Action
46    pub gas_action_fee: u64,
47
48    /// Gas needed to cover single query
49    pub gas_query_fee: u64,
50
51    /// Gas limit, to make sure task won't lock contract
52    pub gas_limit: u64,
53}
54
55/// Request to create a task
56#[cw_serde]
57pub struct TaskRequest {
58    pub interval: Interval,
59    pub boundary: Option<Boundary>,
60    pub stop_on_fail: bool,
61    pub actions: Vec<Action>,
62    pub queries: Option<Vec<CosmosQuery>>,
63    pub transforms: Option<Vec<Transform>>,
64
65    /// How much of cw20 coin is attached to this task
66    /// This will be taken from the manager's contract temporary "Users balance"
67    /// and attached directly to the task's balance.
68    ///
69    /// Note: Unlike other coins ( which get refunded to the task creator in the same transaction as task removal)
70    /// cw20's will get moved back to the temporary "Users balance".
71    /// This is done primarily to save up gas from executing another contract during `proxy_call`
72    pub cw20: Option<Cw20Coin>,
73}
74
75/// Defines the spacing of execution
76/// NOTES:
77/// - Block Height Based: Once, Immediate, Block
78/// - Timestamp Based: Once, Cron
79/// - No Epoch support directly, advised to use block heights instead
80#[cw_serde]
81pub enum Interval {
82    /// For when this is a non-recurring future scheduled TXN
83    Once,
84
85    /// The ugly batch schedule type, in case you need to exceed single TXN gas limits, within fewest block(s)
86    Immediate,
87
88    /// Allows timing based on block intervals rather than timestamps
89    Block(u64),
90
91    /// Crontab Spec String
92    Cron(String),
93}
94
95impl Interval {
96    pub fn next(
97        &self,
98        env: &Env,
99        boundary: &Boundary,
100        slot_granularity_time: u64,
101    ) -> (u64, SlotType) {
102        match (self, boundary) {
103            // If Once, return the first block within a specific range that can be triggered 1 time.
104            // If Immediate, return the first block within a specific range that can be triggered immediately, potentially multiple times.
105            (Interval::Once, Boundary::Height(boundary_height))
106            | (Interval::Immediate, Boundary::Height(boundary_height)) => (
107                get_next_block_limited(env, boundary_height),
108                SlotType::Block,
109            ),
110            // If Once, return the first time within a specific range that can be triggered 1 time.
111            // If Immediate, return the first time within a specific range that can be triggered immediately, potentially multiple times.
112            (Interval::Once, Boundary::Time(boundary_time))
113            | (Interval::Immediate, Boundary::Time(boundary_time)) => {
114                (get_next_time_in_window(env, boundary_time), SlotType::Cron)
115            }
116            // return the first block within a specific range that can be triggered 1 or more times based on timestamps.
117            // Uses crontab spec
118            (Interval::Cron(crontab), Boundary::Time(boundary_time)) => (
119                get_next_cron_time(env, boundary_time, crontab, slot_granularity_time),
120                SlotType::Cron,
121            ),
122            // return the block within a specific range that can be triggered 1 or more times based on block heights.
123            // Uses block offset (Example: Block(100) will trigger every 100 blocks)
124            // So either:
125            // - Boundary specifies a start/end that block offsets can be computed from
126            // - Block offset will truncate to specific modulo offsets
127            (Interval::Block(block), Boundary::Height(boundary_height)) => (
128                get_next_block_by_offset(env.block.height, boundary_height, *block),
129                SlotType::Block,
130            ),
131            // If interval is cron it means boundary is [`BoundaryTime`], and rest of the items is height
132            _ => unreachable!(),
133        }
134    }
135
136    pub fn is_valid(&self) -> bool {
137        match self {
138            Interval::Once | Interval::Immediate | Interval::Block(_) => true,
139            Interval::Cron(crontab) => {
140                let s = Schedule::from_str(crontab);
141                s.is_ok()
142            }
143        }
144    }
145}
146
147/// Start and end block or timestamp when task should be executed for the last time
148#[cw_serde]
149pub enum Boundary {
150    Height(BoundaryHeight),
151    Time(BoundaryTime),
152}
153
154impl Boundary {
155    pub fn is_block(&self) -> bool {
156        matches!(self, Boundary::Height(_))
157    }
158}
159
160#[cw_serde]
161pub struct BoundaryHeight {
162    pub start: Option<Uint64>,
163    pub end: Option<Uint64>,
164}
165
166#[cw_serde]
167pub struct BoundaryTime {
168    pub start: Option<Timestamp>,
169    pub end: Option<Timestamp>,
170}
171
172#[cw_serde]
173pub struct Action<T = Empty> {
174    /// Supported CosmosMsgs only!
175    pub msg: CosmosMsg<T>,
176
177    /// The gas needed to safely process the execute msg
178    pub gas_limit: Option<u64>,
179}
180
181/// Transforms of the tasks actions
182#[cw_serde]
183pub struct Transform {
184    /// Action index to update
185    /// first action would be "0"
186    pub action_idx: u64,
187
188    /// Query index of the new data for this action
189    /// first query would be "0"
190    pub query_idx: u64,
191
192    /// Action key path to the value that should get replaced
193    /// for example:
194    /// X: {Y: {Z: value}}
195    /// \[X,Y,Z\] to reach that value
196    pub action_path: PathToValue,
197    /// Query response key's path to the value that needs to be taken to replace value from the above
198    /// for example query gave that response:
199    /// A: {B: {C: value}}
200    /// In order to reach a value \[A,B,C\] should be used as input
201    pub query_response_path: PathToValue,
202}
203
204#[cw_serde]
205pub struct Task {
206    /// Entity responsible for this task, can change task details
207    pub owner_addr: Addr,
208
209    /// Scheduling definitions
210    pub interval: Interval,
211    pub boundary: Boundary,
212
213    /// Defines if this task can continue until balance runs out
214    pub stop_on_fail: bool,
215
216    /// The cosmos message to call, if time or rules are met
217    pub actions: Vec<Action>,
218    /// A prioritized list of messages that can be chained decision matrix
219    /// required to complete before task action
220    /// Rules MUST return the ResolverResponse type
221    pub queries: Vec<CosmosQuery>,
222    pub transforms: Vec<Transform>,
223
224    // allows future backward compat
225    pub version: String,
226
227    // computed amounts / fees
228    pub amount_for_one_task: AmountForOneTask,
229    // pub
230}
231
232impl Task {
233    /// Get the hash of a task based on parameters
234    pub fn to_hash(&self, prefix: &str) -> String {
235        let message = format!(
236            "{:?}{:?}{:?}{:?}{:?}{:?}",
237            self.owner_addr,
238            self.interval,
239            self.boundary,
240            self.actions,
241            self.queries,
242            self.transforms
243        );
244
245        let hash = Sha256::digest(message.as_bytes());
246        let encoded: String = hash.encode_hex();
247
248        // Return prefixed hash, since multi-chain tasks require simpler identification
249        // Using the specified native_denom, if none, no prefix
250        // Example:
251        // No prefix:   fca49b82eb84818215768293c9e57e7d4194a7c862538e1dedb4516bf2dff0ca (No longer used/stored)
252        // with prefix: stars:82eb84818215768293c9e57e7d4194a7c862538e1dedb4516bf2dff0ca
253        // with prefix: longnetwork:818215768293c9e57e7d4194a7c862538e1dedb4516bf2dff0ca
254        let (_, l) = encoded.split_at(prefix.len() + 1);
255        format!("{}:{}", prefix, l)
256    }
257
258    /// Get the hash of a task based on parameters
259    pub fn to_hash_vec(&self, prefix: &str) -> Vec<u8> {
260        self.to_hash(prefix).into_bytes()
261    }
262
263    pub fn recurring(&self) -> bool {
264        !matches!(self.interval, Interval::Once)
265    }
266
267    pub fn is_evented(&self) -> bool {
268        !self.queries.is_empty()
269            && (self.interval == Interval::Once || self.interval == Interval::Immediate)
270    }
271
272    pub fn into_response(self, prefix: &str) -> TaskResponse {
273        let task_hash = self.to_hash(prefix);
274
275        let queries = if !self.queries.is_empty() {
276            Some(self.queries)
277        } else {
278            None
279        };
280
281        TaskResponse {
282            task: Some(TaskInfo {
283                task_hash,
284                owner_addr: self.owner_addr,
285                interval: self.interval,
286                boundary: self.boundary,
287                stop_on_fail: self.stop_on_fail,
288                amount_for_one_task: self.amount_for_one_task,
289                actions: self.actions,
290                queries,
291                transforms: self.transforms,
292                version: self.version,
293            }),
294        }
295    }
296}
297
298/// Query given module contract with a message
299#[cw_serde]
300pub struct CroncatQuery {
301    /// This is address of the queried module contract.
302    /// For the addr can use one of our croncat-mod-* contracts, or custom contracts
303    pub contract_addr: String,
304    pub msg: Binary,
305    /// For queries with `check_result`: query return value should be formatted as a:
306    /// [`QueryResponse`](mod_sdk::types::QueryResponse)
307    pub check_result: bool,
308}
309
310/// Query given module contract with a message
311#[cw_serde]
312pub enum CosmosQuery<T = WasmQuery> {
313    // For optionally checking results, esp for modules
314    Croncat(CroncatQuery),
315
316    // For covering native wasm query cases (Smart, Raw)
317    Wasm(T),
318}
319
320#[cw_serde]
321pub struct SlotTasksTotalResponse {
322    pub block_tasks: u64,
323    pub cron_tasks: u64,
324    pub evented_tasks: u64,
325}
326
327#[cw_serde]
328pub struct CurrentTaskInfoResponse {
329    pub total: Uint64,
330    pub last_created_task: Timestamp,
331}
332
333#[cw_serde]
334pub struct TaskInfo {
335    pub task_hash: String,
336
337    pub owner_addr: Addr,
338
339    pub interval: Interval,
340    pub boundary: Boundary,
341
342    pub stop_on_fail: bool,
343    pub amount_for_one_task: AmountForOneTask,
344
345    pub actions: Vec<Action>,
346    pub queries: Option<Vec<CosmosQuery>>,
347    pub transforms: Vec<Transform>,
348    pub version: String,
349}
350#[cw_serde]
351pub struct TaskResponse {
352    pub task: Option<TaskInfo>,
353}
354
355/// This struct is used in two important places.
356/// On the tasks contract, when [`create_task`](crate::msg::TasksExecuteMsg::CreateTask) is called, this struct
357/// is returned in the binary data field of the response.
358/// It's also saved on the manager contract before [`proxy_call`](https://docs.rs/croncat-sdk-manager/latest/croncat_sdk_manager/msg/enum.ManagerExecuteMsg.html#variant.ProxyCall), allowing for validation
359/// on the receiving contract, by doing a raw query to the manager's state key [LAST_TASK_EXECUTION_INFO_KEY](https://docs.rs/croncat-sdk-manager/latest/croncat_sdk_manager/types/constant.LAST_TASK_EXECUTION_INFO_KEY.html).
360#[cw_serde]
361pub struct TaskExecutionInfo {
362    pub block_height: u64,
363    pub tx_info: Option<TransactionInfo>,
364    pub task_hash: String,
365    pub owner_addr: Addr,
366    pub amount_for_one_task: AmountForOneTask,
367    pub version: String,
368}
369
370impl Default for TaskExecutionInfo {
371    fn default() -> Self {
372        Self {
373            block_height: u64::default(),
374            tx_info: None,
375            task_hash: String::default(),
376            owner_addr: Addr::unchecked(""),
377            amount_for_one_task: AmountForOneTask::default(),
378            version: String::default(),
379        }
380    }
381}
382
383#[cw_serde]
384pub struct SlotHashesResponse {
385    pub block_id: u64,
386    pub block_task_hash: Vec<String>,
387    pub time_id: u64,
388    pub time_task_hash: Vec<String>,
389}
390
391#[cw_serde]
392pub struct SlotIdsResponse {
393    pub time_ids: Vec<u64>,
394    pub block_ids: Vec<u64>,
395}
396
397#[cw_serde]
398pub enum SlotType {
399    Block,
400    Cron,
401}
402
403impl Display for SlotType {
404    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
405        match self {
406            SlotType::Block => write!(f, "block"),
407            SlotType::Cron => write!(f, "cron"),
408        }
409    }
410}
411
412/// Get the next block within the boundary
413fn get_next_block_limited(env: &Env, boundary_height: &BoundaryHeight) -> u64 {
414    let current_block_height = env.block.height;
415
416    let next_block_height = match boundary_height.start {
417        // shorthand - remove 1 since it adds 1 later
418        Some(id) if current_block_height < id.u64() => id.u64() - 1,
419        _ => current_block_height,
420    };
421
422    match boundary_height.end {
423        // stop if passed end height
424        Some(end) if current_block_height > end.u64() => 0,
425
426        // we ONLY want to catch if we're passed the end block height
427        Some(end) if next_block_height > end.u64() => end.u64(),
428
429        // immediate needs to return this block + 1
430        _ => next_block_height + 1,
431    }
432}
433
434/// Get the next time within the boundary
435/// Does not shift the timestamp, to allow better windowed event boundary
436fn get_next_time_in_window(env: &Env, boundary: &BoundaryTime) -> u64 {
437    let current_block_time = env.block.time.nanos();
438
439    let next_block_time = match boundary.start {
440        Some(id) if current_block_time < id.nanos() => id.nanos(),
441        _ => current_block_time,
442    };
443
444    match boundary.end {
445        // stop if passed end time
446        Some(end) if current_block_time > end.nanos() => 0,
447
448        // we ONLY want to catch if we're passed the end block time
449        Some(end) if next_block_time > end.nanos() => end.nanos(),
450
451        // immediate needs to return this time
452        _ => next_block_time,
453    }
454}
455
456/// Either:
457/// - Boundary specifies a start/end that block offsets can be computed from
458/// - Block offset will truncate to specific modulo offsets
459pub(crate) fn get_next_block_by_offset(
460    block_height: u64,
461    boundary_height: &BoundaryHeight,
462    block: u64,
463) -> u64 {
464    let current_block_height = block_height;
465    let modulo_block = if block > 0 {
466        current_block_height.saturating_sub(current_block_height % block) + block
467    } else {
468        return 0;
469    };
470
471    let next_block_height = match boundary_height.start {
472        Some(id) if current_block_height < id.u64() => {
473            let rem = id.u64() % block;
474            if rem > 0 {
475                id.u64().saturating_sub(rem) + block
476            } else {
477                id.u64()
478            }
479        }
480        _ => modulo_block,
481    };
482
483    match boundary_height.end {
484        // stop if passed end height
485        Some(end) if current_block_height > end.u64() => 0,
486
487        // we ONLY want to catch if we're passed the end block height
488        Some(end) => {
489            let end_height = if let Some(rem) = end.u64().checked_rem(block) {
490                end.u64().saturating_sub(rem)
491            } else {
492                end.u64()
493            };
494            // we ONLY want to catch if we're passed the end block height
495            if next_block_height > end_height {
496                0
497            } else {
498                next_block_height
499            }
500        }
501        None => next_block_height,
502    }
503}
504
505/// Get the slot number (in nanos) of the next task according to boundaries
506/// Unless current slot is the end slot, don't put in the current slot
507fn get_next_cron_time(
508    env: &Env,
509    boundary: &BoundaryTime,
510    crontab: &str,
511    slot_granularity_time: u64,
512) -> u64 {
513    let current_block_ts = env.block.time.nanos();
514    let current_block_slot =
515        current_block_ts.saturating_sub(current_block_ts % slot_granularity_time);
516
517    // get earliest possible time
518    let current_ts = match boundary.start {
519        Some(ts) if current_block_ts < ts.nanos() => ts.nanos(),
520        _ => current_block_ts,
521    };
522
523    // receive time from schedule, calculate slot for this time
524    let schedule = Schedule::from_str(crontab).unwrap();
525    let next_ts = schedule.next_after(&current_ts).unwrap();
526    let next_ts_slot = next_ts.saturating_sub(next_ts % slot_granularity_time);
527
528    // put task in the next slot if next_ts_slot in the current slot
529    let next_slot = if next_ts_slot == current_block_slot {
530        next_ts_slot + slot_granularity_time
531    } else {
532        next_ts_slot
533    };
534
535    match boundary.end {
536        Some(end) if current_block_ts > end.nanos() => 0,
537        Some(end) => {
538            let end_slot = end
539                .nanos()
540                .saturating_sub(end.nanos() % slot_granularity_time);
541            u64::min(end_slot, next_slot)
542        }
543        _ => next_slot,
544    }
545}
546
547#[cfg(test)]
548mod test {
549    use cosmwasm_std::{testing::mock_env, Addr, CosmosMsg, Timestamp, Uint64, WasmMsg};
550    use croncat_sdk_core::types::{AmountForOneTask, GasPrice};
551    use hex::ToHex;
552    use sha2::{Digest, Sha256};
553
554    use crate::types::{Action, BoundaryHeight, CosmosQuery, CroncatQuery, Transform};
555
556    use super::{Boundary, BoundaryTime, Interval, SlotType, Task};
557
558    const TWO_MINUTES: u64 = 120_000_000_000;
559
560    #[test]
561    fn is_valid_test() {
562        let once = Interval::Once;
563        assert!(once.is_valid());
564
565        let immediate = Interval::Immediate;
566        assert!(immediate.is_valid());
567
568        let block = Interval::Block(100);
569        assert!(block.is_valid());
570
571        let cron_correct = Interval::Cron("1 * * * * *".to_string());
572        assert!(cron_correct.is_valid());
573
574        let cron_wrong = Interval::Cron("1 * * * * * *".to_string());
575        assert!(cron_wrong.is_valid());
576    }
577
578    #[test]
579    fn hashing() {
580        let task = Task {
581            owner_addr: Addr::unchecked("bob"),
582            interval: Interval::Block(5),
583            boundary: Boundary::Height(BoundaryHeight {
584                start: Some(Uint64::new(4)),
585                end: None,
586            }),
587            stop_on_fail: false,
588            amount_for_one_task: AmountForOneTask {
589                cw20: None,
590                coin: [None, None],
591                gas: 100,
592                agent_fee: u16::default(),
593                treasury_fee: u16::default(),
594                gas_price: GasPrice::default(),
595            },
596            actions: vec![Action {
597                msg: CosmosMsg::Wasm(WasmMsg::ClearAdmin {
598                    contract_addr: "alice".to_string(),
599                }),
600                gas_limit: Some(5),
601            }],
602            queries: vec![CosmosQuery::Croncat(CroncatQuery {
603                msg: Default::default(),
604                contract_addr: "addr".to_owned(),
605                check_result: true,
606            })],
607            transforms: vec![Transform {
608                action_idx: 0,
609                query_idx: 0,
610                action_path: vec![].into(),
611                query_response_path: vec![].into(),
612            }],
613            version: String::from(""),
614        };
615
616        let message = format!(
617            "{:?}{:?}{:?}{:?}{:?}{:?}",
618            task.owner_addr,
619            task.interval,
620            task.boundary,
621            task.actions,
622            task.queries,
623            task.transforms
624        );
625
626        let hash = Sha256::digest(message.as_bytes());
627
628        let encode: String = hash.encode_hex();
629        let prefix = "atom";
630        let (_, l) = encode.split_at(prefix.len() + 1);
631        let encoded = format!("{}:{}", prefix, l);
632        let bytes = encoded.clone().into_bytes();
633
634        // Tests
635        assert_eq!(encoded, task.to_hash(prefix));
636        assert_eq!(bytes, task.to_hash_vec(prefix));
637    }
638
639    #[test]
640    fn interval_get_next_block_limited() {
641        // (input, input, outcome, outcome)
642        let cases: Vec<(Interval, Boundary, u64, SlotType)> = vec![
643            // Once cases
644            (
645                Interval::Once,
646                Boundary::Height(BoundaryHeight {
647                    start: Some(Uint64::new(12345)),
648                    end: None,
649                }),
650                12346,
651                SlotType::Block,
652            ),
653            (
654                Interval::Once,
655                Boundary::Height(BoundaryHeight {
656                    start: Some(Uint64::new(12348)),
657                    end: None,
658                }),
659                12348,
660                SlotType::Block,
661            ),
662            (
663                Interval::Once,
664                Boundary::Height(BoundaryHeight {
665                    start: Some(Uint64::new(12345)),
666                    end: Some(Uint64::new(12346)),
667                }),
668                12346,
669                SlotType::Block,
670            ),
671            (
672                Interval::Once,
673                Boundary::Height(BoundaryHeight {
674                    start: Some(Uint64::new(12345)),
675                    end: Some(Uint64::new(12340)),
676                }),
677                0,
678                SlotType::Block,
679            ),
680            // Immediate cases
681            (
682                Interval::Immediate,
683                Boundary::Height(BoundaryHeight {
684                    start: Some(Uint64::new(12345)),
685                    end: None,
686                }),
687                12346,
688                SlotType::Block,
689            ),
690            (
691                Interval::Immediate,
692                Boundary::Height(BoundaryHeight {
693                    start: Some(Uint64::new(12348)),
694                    end: None,
695                }),
696                12348,
697                SlotType::Block,
698            ),
699            (
700                Interval::Immediate,
701                Boundary::Height(BoundaryHeight {
702                    start: Some(Uint64::new(12345)),
703                    end: Some(Uint64::new(12346)),
704                }),
705                12346,
706                SlotType::Block,
707            ),
708            (
709                Interval::Immediate,
710                Boundary::Height(BoundaryHeight {
711                    start: Some(Uint64::new(12345)),
712                    end: Some(Uint64::new(12340)),
713                }),
714                0,
715                SlotType::Block,
716            ),
717        ];
718        // Check all these cases
719        for (interval, boundary, outcome_block, outcome_slot_kind) in cases.iter() {
720            let env = mock_env();
721            let (next_id, slot_kind) = interval.next(&env, boundary, 1);
722            assert_eq!(outcome_block, &next_id);
723            assert_eq!(outcome_slot_kind, &slot_kind);
724        }
725    }
726
727    #[test]
728    fn interval_get_next_block_by_offset() {
729        // (input, input, outcome, outcome)
730        let cases: Vec<(Interval, Boundary, u64, SlotType)> = vec![
731            // strictly modulo cases
732            (
733                Interval::Block(1),
734                Boundary::Height(BoundaryHeight {
735                    start: Some(Uint64::new(12345)),
736                    end: None,
737                }),
738                12346,
739                SlotType::Block,
740            ),
741            (
742                Interval::Block(10),
743                Boundary::Height(BoundaryHeight {
744                    start: Some(Uint64::new(12345)),
745                    end: None,
746                }),
747                12350,
748                SlotType::Block,
749            ),
750            (
751                Interval::Block(100),
752                Boundary::Height(BoundaryHeight {
753                    start: Some(Uint64::new(12345)),
754                    end: None,
755                }),
756                12400,
757                SlotType::Block,
758            ),
759            (
760                Interval::Block(1000),
761                Boundary::Height(BoundaryHeight {
762                    start: Some(Uint64::new(12345)),
763                    end: None,
764                }),
765                13000,
766                SlotType::Block,
767            ),
768            (
769                Interval::Block(10000),
770                Boundary::Height(BoundaryHeight {
771                    start: Some(Uint64::new(12345)),
772                    end: None,
773                }),
774                20000,
775                SlotType::Block,
776            ),
777            (
778                Interval::Block(100000),
779                Boundary::Height(BoundaryHeight {
780                    start: Some(Uint64::new(12345)),
781                    end: None,
782                }),
783                100000,
784                SlotType::Block,
785            ),
786            // with another start
787            (
788                Interval::Block(1),
789                Boundary::Height(BoundaryHeight {
790                    start: Some(Uint64::new(12348)),
791                    end: None,
792                }),
793                12348,
794                SlotType::Block,
795            ),
796            (
797                Interval::Block(10),
798                Boundary::Height(BoundaryHeight {
799                    start: Some(Uint64::new(12360)),
800                    end: None,
801                }),
802                12360,
803                SlotType::Block,
804            ),
805            (
806                Interval::Block(10),
807                Boundary::Height(BoundaryHeight {
808                    start: Some(Uint64::new(12364)),
809                    end: None,
810                }),
811                12370,
812                SlotType::Block,
813            ),
814            (
815                Interval::Block(100),
816                Boundary::Height(BoundaryHeight {
817                    start: Some(Uint64::new(12364)),
818                    end: None,
819                }),
820                12400,
821                SlotType::Block,
822            ),
823            // modulo + boundary end
824            (
825                Interval::Block(1),
826                Boundary::Height(BoundaryHeight {
827                    start: Some(Uint64::new(12345)),
828                    end: Some(Uint64::new(12345)),
829                }),
830                0,
831                SlotType::Block,
832            ),
833            (
834                Interval::Block(10),
835                Boundary::Height(BoundaryHeight {
836                    start: Some(Uint64::new(12345)),
837                    end: Some(Uint64::new(12355)),
838                }),
839                12350,
840                SlotType::Block,
841            ),
842            (
843                Interval::Block(100),
844                Boundary::Height(BoundaryHeight {
845                    start: Some(Uint64::new(12345)),
846                    end: Some(Uint64::new(12355)),
847                }),
848                0,
849                SlotType::Block,
850            ),
851            (
852                Interval::Block(100),
853                Boundary::Height(BoundaryHeight {
854                    start: Some(Uint64::new(12345)),
855                    end: Some(Uint64::new(12300)),
856                }),
857                0,
858                SlotType::Block,
859            ),
860            (
861                Interval::Block(100),
862                Boundary::Height(BoundaryHeight {
863                    start: Some(Uint64::new(12345)),
864                    end: Some(Uint64::new(12545)),
865                }),
866                12400,
867                SlotType::Block,
868            ),
869            (
870                Interval::Block(100),
871                Boundary::Height(BoundaryHeight {
872                    start: Some(Uint64::new(11345)),
873                    end: Some(Uint64::new(11545)),
874                }),
875                0,
876                SlotType::Block,
877            ),
878            (
879                Interval::Block(100_000),
880                Boundary::Height(BoundaryHeight {
881                    start: Some(Uint64::new(12345)),
882                    end: Some(Uint64::new(120355)),
883                }),
884                100_000,
885                SlotType::Block,
886            ),
887            // wrong block interval
888            (
889                Interval::Block(100_000),
890                Boundary::Height(BoundaryHeight {
891                    start: Some(Uint64::new(12345)),
892                    end: Some(Uint64::new(12355)),
893                }),
894                0,
895                SlotType::Block,
896            ),
897            (
898                Interval::Block(0),
899                Boundary::Height(BoundaryHeight {
900                    start: Some(Uint64::new(12345)),
901                    end: Some(Uint64::new(12355)),
902                }),
903                0,
904                SlotType::Block,
905            ),
906        ];
907
908        // Check all these cases
909        let env = mock_env();
910        for (interval, boundary, outcome_block, outcome_slot_kind) in cases.iter() {
911            let (next_id, slot_kind) = interval.next(&env, boundary, 1);
912            assert_eq!(outcome_block, &next_id);
913            assert_eq!(outcome_slot_kind, &slot_kind);
914        }
915    }
916
917    #[test]
918    fn interval_get_next_cron_time() {
919        // (input, input, outcome, outcome)
920        // test the case when slot_granularity_time == 1
921        let cases: Vec<(Interval, Boundary, u64, SlotType)> = vec![
922            (
923                Interval::Cron("* * * * * *".to_string()),
924                Boundary::Time(BoundaryTime {
925                    start: Some(Timestamp::from_nanos(1_571_797_419_879_305_533)),
926                    end: None,
927                }),
928                1_571_797_420_000_000_000, // current time in nanos is 1_571_797_419_879_305_533
929                SlotType::Cron,
930            ),
931            (
932                Interval::Cron("1 * * * * *".to_string()),
933                Boundary::Time(BoundaryTime {
934                    start: Some(Timestamp::from_nanos(1_571_797_419_879_305_533)),
935                    end: None,
936                }),
937                1_571_797_441_000_000_000,
938                SlotType::Cron,
939            ),
940            (
941                Interval::Cron("* 0 * * * *".to_string()),
942                Boundary::Time(BoundaryTime {
943                    start: Some(Timestamp::from_nanos(1_571_797_419_879_305_533)),
944                    end: None,
945                }),
946                1_571_799_600_000_000_000,
947                SlotType::Cron,
948            ),
949            (
950                Interval::Cron("15 0 * * * *".to_string()),
951                Boundary::Time(BoundaryTime {
952                    start: Some(Timestamp::from_nanos(1_571_797_419_879_305_533)),
953                    end: None,
954                }),
955                1_571_799_615_000_000_000,
956                SlotType::Cron,
957            ),
958            // with another start
959            (
960                Interval::Cron("15 0 * * * *".to_string()),
961                Boundary::Time(BoundaryTime {
962                    start: Some(Timestamp::from_nanos(1_471_799_600_000_000_000)),
963                    end: None,
964                }),
965                1_571_799_615_000_000_000,
966                SlotType::Cron,
967            ),
968            (
969                Interval::Cron("15 0 * * * *".to_string()),
970                Boundary::Time(BoundaryTime {
971                    start: Some(Timestamp::from_nanos(1_571_799_600_000_000_000)),
972                    end: None,
973                }),
974                1_571_799_615_000_000_000,
975                SlotType::Cron,
976            ),
977            (
978                Interval::Cron("15 0 * * * *".to_string()),
979                Boundary::Time(BoundaryTime {
980                    start: Some(Timestamp::from_nanos(1_571_799_700_000_000_000)),
981                    end: None,
982                }),
983                1_571_803_215_000_000_000,
984                SlotType::Cron,
985            ),
986            // cases when a boundary has end
987            // current slot is the end slot
988            (
989                Interval::Cron("* * * * * *".to_string()),
990                Boundary::Time(BoundaryTime {
991                    start: Some(Timestamp::from_nanos(1_571_797_419_879_305_533)),
992                    end: Some(Timestamp::from_nanos(1_571_797_419_879_305_533)),
993                }),
994                1_571_797_419_879_305_533,
995                SlotType::Cron,
996            ),
997            // the next slot is after the end, return end slot
998            (
999                Interval::Cron("* * * * * *".to_string()),
1000                Boundary::Time(BoundaryTime {
1001                    start: Some(Timestamp::from_nanos(1_571_797_419_879_305_533)),
1002                    end: Some(Timestamp::from_nanos(1_571_797_419_879_305_535)),
1003                }),
1004                1_571_797_419_879_305_535,
1005                SlotType::Cron,
1006            ),
1007            // next slot in boundaries
1008            (
1009                Interval::Cron("* * * * * *".to_string()),
1010                Boundary::Time(BoundaryTime {
1011                    start: Some(Timestamp::from_nanos(1_571_797_419_879_305_533)),
1012                    end: Some(Timestamp::from_nanos(1_571_797_420_000_000_000)),
1013                }),
1014                1_571_797_420_000_000_000,
1015                SlotType::Cron,
1016            ),
1017            // the task has ended
1018            (
1019                Interval::Cron("* * * * * *".to_string()),
1020                Boundary::Time(BoundaryTime {
1021                    start: Some(Timestamp::from_nanos(1_571_797_419_879_305_533)),
1022                    end: Some(Timestamp::from_nanos(1_571_797_419_879_305_532)),
1023                }),
1024                0,
1025                SlotType::Cron,
1026            ),
1027            (
1028                Interval::Cron("15 0 * * * *".to_string()),
1029                Boundary::Time(BoundaryTime {
1030                    start: Some(Timestamp::from_nanos(1_471_799_600_000_000_000)),
1031                    end: Some(Timestamp::from_nanos(1_471_799_600_000_000_000)),
1032                }),
1033                0,
1034                SlotType::Cron,
1035            ),
1036            (
1037                Interval::Cron("1 * * * * *".to_string()),
1038                Boundary::Time(BoundaryTime {
1039                    start: Some(Timestamp::from_nanos(1_471_797_441_000_000_000)),
1040                    end: Some(Timestamp::from_nanos(1_671_797_441_000_000_000)),
1041                }),
1042                1_571_797_441_000_000_000,
1043                SlotType::Cron,
1044            ),
1045        ];
1046        // Check all these cases
1047        for (interval, boundary, outcome_time, outcome_slot_kind) in cases.iter() {
1048            let env = mock_env();
1049            let (next_id, slot_kind) = interval.next(&env, boundary, 1);
1050            assert_eq!(outcome_time, &next_id);
1051            assert_eq!(outcome_slot_kind, &slot_kind);
1052        }
1053
1054        // slot_granularity_time == 120_000_000_000 ~ 2 minutes
1055        let cases: Vec<(Interval, Boundary, u64, SlotType)> = vec![
1056            (
1057                Interval::Cron("* * * * * *".to_string()),
1058                Boundary::Time(BoundaryTime {
1059                    start: Some(Timestamp::from_nanos(1_571_797_419_879_305_533)),
1060                    end: None,
1061                }),
1062                // the timestamp is in the current slot, so we take the next slot
1063                1_571_797_420_000_000_000_u64
1064                    .saturating_sub(1_571_797_420_000_000_000 % TWO_MINUTES)
1065                    + TWO_MINUTES, // current time in nanos is 1_571_797_419_879_305_533
1066                SlotType::Cron,
1067            ),
1068            (
1069                Interval::Cron("1 * * * * *".to_string()),
1070                Boundary::Time(BoundaryTime {
1071                    start: Some(Timestamp::from_nanos(1_571_797_419_879_305_533)),
1072                    end: None,
1073                }),
1074                1_571_797_440_000_000_000,
1075                SlotType::Cron,
1076            ),
1077            (
1078                Interval::Cron("* 0 * * * *".to_string()),
1079                Boundary::Time(BoundaryTime {
1080                    start: Some(Timestamp::from_nanos(1_571_797_419_879_305_533)),
1081                    end: None,
1082                }),
1083                1_571_799_600_000_000_000,
1084                SlotType::Cron,
1085            ),
1086            (
1087                Interval::Cron("15 0 * * * *".to_string()),
1088                Boundary::Time(BoundaryTime {
1089                    start: Some(Timestamp::from_nanos(1_571_797_419_879_305_533)),
1090                    end: None,
1091                }),
1092                1_571_799_600_000_000_000,
1093                SlotType::Cron,
1094            ),
1095            // with another start
1096            (
1097                Interval::Cron("15 0 * * * *".to_string()),
1098                Boundary::Time(BoundaryTime {
1099                    start: Some(Timestamp::from_nanos(1_471_799_600_000_000_000)),
1100                    end: None,
1101                }),
1102                1_571_799_600_000_000_000,
1103                SlotType::Cron,
1104            ),
1105            (
1106                Interval::Cron("15 0 * * * *".to_string()),
1107                Boundary::Time(BoundaryTime {
1108                    start: Some(Timestamp::from_nanos(1_571_799_600_000_000_000)),
1109                    end: None,
1110                }),
1111                1_571_799_600_000_000_000,
1112                SlotType::Cron,
1113            ),
1114            (
1115                Interval::Cron("15 0 * * * *".to_string()),
1116                Boundary::Time(BoundaryTime {
1117                    start: Some(Timestamp::from_nanos(1_571_799_700_000_000_000)),
1118                    end: None,
1119                }),
1120                1_571_803_200_000_000_000,
1121                SlotType::Cron,
1122            ),
1123            // cases when a boundary has end
1124            // boundary end in the current slot
1125            (
1126                Interval::Cron("* * * * * *".to_string()),
1127                Boundary::Time(BoundaryTime {
1128                    start: Some(Timestamp::from_nanos(1_571_797_419_879_305_533)),
1129                    end: Some(Timestamp::from_nanos(1_571_797_419_879_305_535)),
1130                }),
1131                1_571_797_320_000_000_000,
1132                SlotType::Cron,
1133            ),
1134            // next slot in boundaries
1135            (
1136                Interval::Cron("1 * * * * *".to_string()),
1137                Boundary::Time(BoundaryTime {
1138                    start: Some(Timestamp::from_nanos(1_571_797_419_879_305_533)),
1139                    end: Some(Timestamp::from_nanos(1_571_797_560_000_000_000)),
1140                }),
1141                1_571_797_440_000_000_000,
1142                SlotType::Cron,
1143            ),
1144            // next slot after the end, return end slot
1145            (
1146                Interval::Cron("1 * * * * *".to_string()),
1147                Boundary::Time(BoundaryTime {
1148                    start: Some(Timestamp::from_nanos(1_571_797_419_879_305_533)),
1149                    end: Some(Timestamp::from_nanos(1_571_797_420_000_000_000)),
1150                }),
1151                1_571_797_320_000_000_000,
1152                SlotType::Cron,
1153            ),
1154            // the task has ended
1155            (
1156                Interval::Cron("* * * * * *".to_string()),
1157                Boundary::Time(BoundaryTime {
1158                    start: Some(Timestamp::from_nanos(1_571_797_419_879_305_533)),
1159                    end: Some(Timestamp::from_nanos(1_571_797_419_879_305_532)),
1160                }),
1161                0,
1162                SlotType::Cron,
1163            ),
1164        ];
1165        // Check all these cases
1166        for (interval, boundary, outcome_time, outcome_slot_kind) in cases.iter() {
1167            let env = mock_env();
1168            let (next_id, slot_kind) = interval.next(&env, boundary, TWO_MINUTES);
1169            assert_eq!(outcome_time, &next_id);
1170            assert_eq!(outcome_slot_kind, &slot_kind);
1171        }
1172    }
1173}