triggr-program 0.1.1

Created with Anchor
Documentation
use anchor_lang::{prelude::*, solana_program::clock::Clock};

use crate::errors::TriggrError;
use crate::state::Statistics;

#[account]
#[derive(Debug)]
pub struct Trigger {
    /// The status of the trigger.
    pub status: Status,

    /// Usage statistics of the trigger.
    pub usage_stats: Statistics,

    /// The authority of the trigger.
    pub authority: Pubkey,

    /// The conditions of the trigger.
    pub conditions: AdjacencyTree,

    /// The timestamp when the trigger was created.
    pub created_at: i64,

    /// The number of tasks associated with the trigger.
    pub task_count: u8,

    // This is like a nonce taken from the user account's trigger_count
    pub own_index: u64,

    /// The lifetime of the trigger.
    pub lifetime: Lifetime,

    /// The title of the workflow associated with the trigger.
    pub workflow_title: String, // max length is 50 bytes
}

impl Trigger {
    pub const MIN_SIZE: usize = 230;

    pub fn get_size(condition_tree: &AdjacencyTree) -> usize {
        let mut size = Self::MIN_SIZE;
        size += condition_tree.nodes.len() * AdjacencyTree::MIN_SIZE;
        size += condition_tree.edges.len() * 2;
        size
    }
}

// Enum representing the status of a trigger
#[derive(Clone, PartialEq, Eq, Debug, AnchorDeserialize, AnchorSerialize)]
pub enum Status {
    Draft,
    Active,
    Disabled,
}

impl Trigger {
    // Method to evaluate the trigger's time conditions
    pub fn evaluate_time(&mut self) -> Result<()> {
        let now: i64 = Clock::get()?.unix_timestamp * 1000; // Convert to milliseconds

        // if no next execution date is set, set it to kickoff time
        if let None = self.lifetime.next_execution_date {
            self.lifetime.set_next_execution_date()?;
            return Ok(());
        }

        if let Some(current_next_execution_date) = self.lifetime.next_execution_date {
            if current_next_execution_date > now {
                return Err(TriggrError::InvalidTimeCondition.into());
            }

            if self.lifetime.recurrence.is_some() {
                // Get the time in milliseconds
                self.lifetime.set_next_execution_date()?;
            } else {
                // Disable the trigger if it's not recurring
                self.status = Status::Disabled;
            }
        }
        Ok(())
    }

    // Constructor for the Trigger struct
    pub fn new(
        authority: Pubkey,
        conditions: AdjacencyTree,
        lifetime: Lifetime,
        workflow_title: String,
        own_index: u64,
    ) -> Self {
        let mut trigger = Self {
            status: Status::Active,
            usage_stats: Statistics {
                execution_count: 0,
                last_executed_at: 0,
            },
            authority,
            conditions,
            created_at: Clock::get().unwrap().unix_timestamp,
            task_count: 0,
            own_index,
            workflow_title,
            lifetime,
        };
        trigger.evaluate_time().unwrap();
        trigger
    }
}

// Enum representing days of the week
#[derive(Clone, PartialEq, Eq, Debug, AnchorDeserialize, AnchorSerialize)]
pub enum Weekday {
    Sunday,
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
}

impl Weekday {
    pub fn as_u8(&self) -> u8 {
        match self {
            Weekday::Sunday => 0,
            Weekday::Monday => 1,
            Weekday::Tuesday => 2,
            Weekday::Wednesday => 3,
            Weekday::Thursday => 4,
            Weekday::Friday => 5,
            Weekday::Saturday => 6,
        }
    }
}

// Enum representing different types of recurrence patterns for a trigger
#[derive(Clone, PartialEq, Eq, Debug, AnchorDeserialize, AnchorSerialize)]
pub enum Recurrence {
    Daily {
        time_of_day: i64, // Time of day in milliseconds from midnight
    },
    Weekly {
        weekdays: Vec<Weekday>, // Days of the week on which the trigger should occur
        time_of_day: i64,       // Time of day in milliseconds from midnight
    },
    Monthly {
        day_of_month: u8, // Day of the month (1-31)
        time_of_day: i64, // Time of day in milliseconds from midnight
    },
}

impl Recurrence {
    pub fn time_of_day(&self) -> i64 {
        let time = match self {
            Recurrence::Daily { time_of_day } => time_of_day,
            Recurrence::Weekly { time_of_day, .. } => time_of_day,
            Recurrence::Monthly { time_of_day, .. } => time_of_day,
        };

        *time
    }
}

// Structure that holds information related to the lifetime of a trigger
#[derive(Clone, Default, PartialEq, Eq, Debug, AnchorDeserialize, AnchorSerialize)]
pub struct Lifetime {
    pub kickoff_time: i64,                // Timestamp of when the trigger started
    pub max_execution_date: Option<i64>,  // Timestamp of when the trigger should expire
    pub next_execution_date: Option<i64>, // Timestamp of the next execution of the trigger
    pub max_execution_count: Option<u8>,  // Maximum number of times the trigger should execute
    pub recurrence: Option<Recurrence>,   // Recurrence pattern of the trigger
}

impl Lifetime {
    pub const MIN_SIZE: usize = 80; // todo: update this

    pub fn set_next_execution_date(&mut self) -> Result<()> {
        if let Some(recurrence) = &self.recurrence {
            // Get the time in milliseconds
            let time_milliseconds = recurrence.time_of_day();

            let mut next_date = self.kickoff_time;

            if self.next_execution_date.is_some() {
                next_date = self.next_execution_date.unwrap();
            }

            // Calculate the next execution date based on the recurrence type
            let next_execution_date = match recurrence {
                Recurrence::Daily { time_of_day: _ } => {
                    // Add one day's worth of milliseconds
                    let next_day = next_date + 86400000;

                    // Remove the time from next_day by rounding down to the nearest day
                    let start_of_next_day = (next_day / 86400000) * 86400000;

                    // Add the desired time_of_day
                    start_of_next_day + time_milliseconds
                }
                Recurrence::Weekly { weekdays, .. } => {
                    let mut next_execution_date = next_date;
                    let mut weekday: u8 = (((next_execution_date / 86400000) + 4) % 7)
                        .try_into()
                        .unwrap();

                    // If the current weekday is in the list but the time has passed,
                    // add a day to start checking from the next day.
                    if weekdays.iter().any(|wd| wd.as_u8() == weekday)
                        && (next_execution_date % 86400000) > time_milliseconds
                    {
                        next_execution_date += 86400000; // Add one day
                        weekday = (weekday + 1) % 7;
                    }

                    // Find the next weekday from the list that matches or exceeds the current weekday
                    while !weekdays.iter().any(|wd| wd.as_u8() == weekday) {
                        next_execution_date += 86400000; // Add one day
                        weekday = (weekday + 1) % 7;
                    }

                    // Round up to the nearest day and add the time of day
                    (next_execution_date / 86400000) * 86400000 + time_milliseconds
                }
                Recurrence::Monthly { day_of_month, .. } => {
                    const SECONDS_IN_DAY: i64 = 86400_000;
                    const DAYS_IN_MONTH: [u8; 12] =
                        [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];

                    // Leap year check
                    let is_leap_year = |year: i64| -> bool {
                        (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
                    };

                    // Calculate the current year, month, and day from the kickoff_time
                    let mut elapsed_days = next_date / SECONDS_IN_DAY;
                    let mut year = 1970;

                    while elapsed_days >= 365 {
                        if is_leap_year(year) {
                            if elapsed_days >= 366 {
                                elapsed_days -= 366;
                                year += 1;
                            } else {
                                break;
                            }
                        } else {
                            elapsed_days -= 365;
                            year += 1;
                        }
                    }

                    let mut month = 0;

                    while elapsed_days >= DAYS_IN_MONTH[month].into() {
                        if month == 1 && is_leap_year(year) {
                            elapsed_days -= 29;
                        } else {
                            elapsed_days -= DAYS_IN_MONTH[month] as i64;
                        }
                        month += 1;
                    }

                    let current_day = elapsed_days + 1;

                    // Adjusting for the day_of_month logic
                    if current_day < *day_of_month as i64 {
                        // Stay in the current month
                    } else if current_day == *day_of_month as i64 {
                        if (next_date % SECONDS_IN_DAY) > time_milliseconds {
                            // It's the same day, but the execution time has already passed
                            month += 1;
                        }
                        // If not, stay in the current month as it hasn't passed the execution time yet
                    } else {
                        // If the current_day is greater than day_of_month
                        month += 1;
                    }

                    if month == 12 {
                        month = 0;
                        year += 1;
                    }

                    // The next execution day is always the desired day_of_month
                    let next_execution_day = *day_of_month as i64;

                    // Calculate the UNIX time for the next execution
                    let total_days_until_next_execution = (year - 1970) * 365 + (year - 1970) / 4
                        - (year - 1970) / 100
                        + (year - 1970) / 400
                        + DAYS_IN_MONTH[..month as usize]
                            .iter()
                            .map(|&d| d as i64)
                            .sum::<i64>()
                        + next_execution_day
                        - 1;

                    total_days_until_next_execution * SECONDS_IN_DAY + time_milliseconds
                }
            };
            // Update the kickoff time and next execution date
            self.next_execution_date = Some(next_execution_date);
        } else {
            // If there's no recurrence, set the kickoff time and next execution date to the same value
            self.next_execution_date = Some(self.kickoff_time)
        }
        Ok(())
    }
}

// Structure that holds information related to a specific condition of the trigger
#[derive(Clone, Default, PartialEq, Eq, Debug, AnchorDeserialize, AnchorSerialize)]
pub struct Condition {
    pub condition_type: u8, // This is the index of the condition type in the program state
    pub condition_data: Vec<u8>,
}

impl Condition {
    pub const MIN_SIZE: usize = 1 + 4 + 8;
}

#[derive(Clone, Default, PartialEq, Eq, Debug, AnchorDeserialize, AnchorSerialize)]
pub struct AdjacencyTree {
    pub nodes: Vec<AdjacencyTreeNode>,
    pub edges: Vec<[u8; 2]>, // (parent_index, child_index) pairs
}

impl AdjacencyTree {
    pub const MIN_SIZE: usize = 4 + 2 + AdjacencyTreeNode::MIN_SIZE + 4;
}

#[derive(Clone, Default, PartialEq, Eq, Debug, AnchorDeserialize, AnchorSerialize)]
pub struct AdjacencyTreeNode {
    pub condition: Condition,   // Condition to evaluate
    pub task_index: Option<u8>, // index of task to execute, only should be present if it's the end of a branch
}

impl AdjacencyTreeNode {
    pub const MIN_SIZE: usize = Condition::MIN_SIZE + 1 + 1;
}