triggr/instructions/
exec_task.rs

1
2pub use anchor_lang::prelude::*;
3pub use anchor_lang::{
4    solana_program::{instruction::Instruction, program::invoke, program::invoke_signed, pubkey},
5    system_program::{transfer, Transfer},
6    solana_program::clock::Clock,
7    InstructionData,
8
9};
10pub use state::*;
11
12use crate::errors::TriggrError;
13use crate::state;
14
15#[derive(Accounts)]
16#[instruction(traverse: Vec<u8>, _trigger_count: u64, _task_count: u8)]
17pub struct ExecTask<'info> {
18    #[account(mut)]
19    initiator: Signer<'info>,
20
21    program_state: Account<'info, ProgramState>,
22
23    /// CHECK: Will only be checked
24    #[account()]
25    authority: UncheckedAccount<'info>,
26
27    #[account(mut, seeds = ["user".as_bytes(), &authority.key().to_bytes()[..]], bump)]
28    //not needed
29    user: Account<'info, User>,
30
31    /// CHECK: Wallet account
32    #[account(mut, seeds = ["payer".as_bytes(), &authority.key().to_bytes()[..]], bump, constraint = payer.owner == &system_program.key())]
33    payer: UncheckedAccount<'info>,
34
35    #[account(
36        mut, 
37        seeds = ["trigger".as_bytes(), &authority.key().to_bytes()[..], &_trigger_count.to_le_bytes()[..]], 
38        bump, 
39        constraint = trigger.status == Status::Active @ TriggrError::TriggerNotActive
40    )]
41    trigger: Box<Account<'info, Trigger>>,
42
43    // the task is not needed, but the account is needed to check the authority
44    #[account(mut, seeds = ["task".as_bytes(), &trigger.key().to_bytes()[..], &get_task_index(traverse, &trigger.conditions).to_le_bytes()[..]], bump)]
45    task: Account<'info, Task>,
46
47    system_program: Program<'info, System>,
48}
49
50pub fn get_task_index(traverse: Vec<u8>, conditions: &AdjacencyTree) -> u8 {
51    if traverse.len() == 0 {
52        0
53    } else {
54        conditions.nodes[*traverse.last().unwrap() as usize]
55            .task_index
56            .unwrap()
57    }
58}
59
60pub fn handler(
61    ctx: Context<ExecTask>,
62    traverse: Vec<u8>,
63    _trigger_count: u64,
64    _task_count: u8,
65) -> Result<()> {
66
67
68    let payer_seeds = &[
69        "payer".as_bytes(),
70        &ctx.accounts.task.authority.as_ref()[..],
71        &[ctx.bumps["payer"]],
72    ];
73
74    let payer = &[&payer_seeds[..]];
75
76    let mut remaining_accounts = ctx.remaining_accounts.to_vec();
77
78    if traverse.len() > 0 {
79        // 1) Check traverse through conditions is allowed
80        for (index, parent_node_index) in traverse.iter().enumerate() {
81            // every traverse should begin at trigger 0
82            if index == 0 {
83                assert_eq!(*parent_node_index, 0);
84            }
85
86            // find edge with parent_node_index and traverse[parent_node_index + 1]
87            ctx.accounts
88                .trigger
89                .conditions
90                .edges
91                .iter()
92                .find(|edge| {
93                    traverse.get(index + 1).map_or(true, |next_value| {
94                        **edge == [*parent_node_index, *next_value]
95                    })
96                })
97                .unwrap_or_else(|| {
98                    panic!("Edge with ({}, {}) not found", parent_node_index, index + 1)
99                });
100        }
101
102        // 2) Check conditions in traverse are met
103        for condition_node_index in traverse.iter() {
104            let node = &ctx.accounts.trigger.conditions.nodes[*condition_node_index as usize];
105
106            let program_account = remaining_accounts.remove(0);
107
108            let middleware_program_id = ctx.accounts.program_state.middleware[node.condition.condition_type as usize].program_id;
109
110            assert_eq!(middleware_program_id, program_account.key(), "Unexpected middleware program ID found.");
111
112            let middleware = &ctx.accounts.program_state.middleware[node.condition.condition_type as usize]; 
113
114            // The most important accounts to pass should be persisted in the trigger's data and serialized when the trigger is created.
115            // Each Trigger program should validate the accounts passed in are the same as the ones in the instruction's data.
116            let accounts: Vec<AccountInfo> = remaining_accounts
117                .drain(0..middleware.accounts_count as usize)
118                .collect();
119
120            let account_metas: Vec<AccountMeta> = accounts
121                .into_iter()
122                .map(|account_info| AccountMeta {
123                    pubkey: *account_info.key,
124                    is_signer: account_info.is_signer,
125                    is_writable: account_info.is_writable,
126                })
127                .collect();
128
129            let instruction = Instruction {
130                program_id: program_account.key(),
131                accounts: account_metas,
132                data: node.condition.condition_data.clone(),
133            };
134
135            invoke(&instruction, &ctx.remaining_accounts).unwrap();
136        }
137    }
138    // time eval always needs to happen because of kickoff and cutoff times
139    Trigger::evaluate_time(&mut *ctx.accounts.trigger)?;
140
141    // 3) Execute the bundle
142    for bundle in &ctx.accounts.task.bundles {
143
144        // check if the expiration slot has not passed
145        if let Some(expiration_slot) = bundle.expiration_slot {
146            let current_slot = Clock::get()?.slot;
147            if current_slot > expiration_slot {
148                return Err(TriggrError::ExpirationSlotPassed.into());
149            }
150        }
151        
152        assert_eq!(bundle.ready, true, "Bundle is not ready");
153
154        bundle.instructions.iter().for_each(|instruction| {
155            msg!("Executing instruction: {:?}", instruction.program_id);
156
157            invoke_signed(
158                &Instruction::from(instruction), // the instruction contains all the right account
159                ctx.remaining_accounts, // only the accounts in the instruction end up being passed in
160                payer,
161            )
162            .unwrap();
163        });
164    };
165
166    // increment the trigger's execution count
167    ctx.accounts.trigger.usage_stats.execution_count += 1;
168    ctx.accounts.trigger.usage_stats.last_executed_at = Clock::get()?.unix_timestamp;
169
170    // increment the task's execution count
171    ctx.accounts.task.usage_stats.execution_count += 1;
172    ctx.accounts.task.usage_stats.last_executed_at = Clock::get()?.unix_timestamp;
173
174    if ctx.accounts.trigger.status != Status::Active {
175        let active_triggers = &mut ctx.accounts.user.active_triggers;
176
177        active_triggers.retain(|&x| x != ctx.accounts.trigger.key());
178
179        ctx.accounts.user.active_triggers = active_triggers.clone();
180
181        // the reallocating of the space in the user account is handled when the trigger is closed.
182    }
183    // todo: close LUT if trigger is no longer valid
184
185    Ok(())
186}