triggr-program 0.1.1

Created with Anchor
Documentation

pub use anchor_lang::prelude::*;
pub use anchor_lang::{
    solana_program::{instruction::Instruction, program::invoke, program::invoke_signed, pubkey},
    system_program::{transfer, Transfer},
    solana_program::clock::Clock,
    InstructionData,

};
pub use state::*;

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

#[derive(Accounts)]
#[instruction(traverse: Vec<u8>, _trigger_count: u64, _task_count: u8)]
pub struct ExecTask<'info> {
    #[account(mut)]
    initiator: Signer<'info>,

    program_state: Account<'info, ProgramState>,

    /// CHECK: Will only be checked
    #[account()]
    authority: UncheckedAccount<'info>,

    #[account(mut, seeds = ["user".as_bytes(), &authority.key().to_bytes()[..]], bump)]
    //not needed
    user: Account<'info, User>,

    /// CHECK: Wallet account
    #[account(mut, seeds = ["payer".as_bytes(), &authority.key().to_bytes()[..]], bump, constraint = payer.owner == &system_program.key())]
    payer: UncheckedAccount<'info>,

    #[account(
        mut, 
        seeds = ["trigger".as_bytes(), &authority.key().to_bytes()[..], &_trigger_count.to_le_bytes()[..]], 
        bump, 
        constraint = trigger.status == Status::Active @ TriggrError::TriggerNotActive
    )]
    trigger: Box<Account<'info, Trigger>>,

    // the task is not needed, but the account is needed to check the authority
    #[account(mut, seeds = ["task".as_bytes(), &trigger.key().to_bytes()[..], &get_task_index(traverse, &trigger.conditions).to_le_bytes()[..]], bump)]
    task: Account<'info, Task>,

    system_program: Program<'info, System>,
}

pub fn get_task_index(traverse: Vec<u8>, conditions: &AdjacencyTree) -> u8 {
    if traverse.len() == 0 {
        0
    } else {
        conditions.nodes[*traverse.last().unwrap() as usize]
            .task_index
            .unwrap()
    }
}

pub fn handler(
    ctx: Context<ExecTask>,
    traverse: Vec<u8>,
    _trigger_count: u64,
    _task_count: u8,
) -> Result<()> {


    let payer_seeds = &[
        "payer".as_bytes(),
        &ctx.accounts.task.authority.as_ref()[..],
        &[ctx.bumps["payer"]],
    ];

    let payer = &[&payer_seeds[..]];

    let mut remaining_accounts = ctx.remaining_accounts.to_vec();

    if traverse.len() > 0 {
        // 1) Check traverse through conditions is allowed
        for (index, parent_node_index) in traverse.iter().enumerate() {
            // every traverse should begin at trigger 0
            if index == 0 {
                assert_eq!(*parent_node_index, 0);
            }

            // find edge with parent_node_index and traverse[parent_node_index + 1]
            ctx.accounts
                .trigger
                .conditions
                .edges
                .iter()
                .find(|edge| {
                    traverse.get(index + 1).map_or(true, |next_value| {
                        **edge == [*parent_node_index, *next_value]
                    })
                })
                .unwrap_or_else(|| {
                    panic!("Edge with ({}, {}) not found", parent_node_index, index + 1)
                });
        }

        // 2) Check conditions in traverse are met
        for condition_node_index in traverse.iter() {
            let node = &ctx.accounts.trigger.conditions.nodes[*condition_node_index as usize];

            let program_account = remaining_accounts.remove(0);

            let middleware_program_id = ctx.accounts.program_state.middleware[node.condition.condition_type as usize].program_id;

            assert_eq!(middleware_program_id, program_account.key(), "Unexpected middleware program ID found.");

            let middleware = &ctx.accounts.program_state.middleware[node.condition.condition_type as usize]; 

            // The most important accounts to pass should be persisted in the trigger's data and serialized when the trigger is created.
            // Each Trigger program should validate the accounts passed in are the same as the ones in the instruction's data.
            let accounts: Vec<AccountInfo> = remaining_accounts
                .drain(0..middleware.accounts_count as usize)
                .collect();

            let account_metas: Vec<AccountMeta> = accounts
                .into_iter()
                .map(|account_info| AccountMeta {
                    pubkey: *account_info.key,
                    is_signer: account_info.is_signer,
                    is_writable: account_info.is_writable,
                })
                .collect();

            let instruction = Instruction {
                program_id: program_account.key(),
                accounts: account_metas,
                data: node.condition.condition_data.clone(),
            };

            invoke(&instruction, &ctx.remaining_accounts).unwrap();
        }
    }
    // time eval always needs to happen because of kickoff and cutoff times
    Trigger::evaluate_time(&mut *ctx.accounts.trigger)?;

    // 3) Execute the bundle
    for bundle in &ctx.accounts.task.bundles {

        // check if the expiration slot has not passed
        if let Some(expiration_slot) = bundle.expiration_slot {
            let current_slot = Clock::get()?.slot;
            if current_slot > expiration_slot {
                return Err(TriggrError::ExpirationSlotPassed.into());
            }
        }
        
        assert_eq!(bundle.ready, true, "Bundle is not ready");

        bundle.instructions.iter().for_each(|instruction| {
            msg!("Executing instruction: {:?}", instruction.program_id);

            invoke_signed(
                &Instruction::from(instruction), // the instruction contains all the right account
                ctx.remaining_accounts, // only the accounts in the instruction end up being passed in
                payer,
            )
            .unwrap();
        });
    };

    // increment the trigger's execution count
    ctx.accounts.trigger.usage_stats.execution_count += 1;
    ctx.accounts.trigger.usage_stats.last_executed_at = Clock::get()?.unix_timestamp;

    // increment the task's execution count
    ctx.accounts.task.usage_stats.execution_count += 1;
    ctx.accounts.task.usage_stats.last_executed_at = Clock::get()?.unix_timestamp;

    if ctx.accounts.trigger.status != Status::Active {
        let active_triggers = &mut ctx.accounts.user.active_triggers;

        active_triggers.retain(|&x| x != ctx.accounts.trigger.key());

        ctx.accounts.user.active_triggers = active_triggers.clone();

        // the reallocating of the space in the user account is handled when the trigger is closed.
    }
    // todo: close LUT if trigger is no longer valid

    Ok(())
}