Skip to main content

antegen_thread_program/instructions/
thread_exec.rs

1use crate::{
2    errors::*,
3    state::{decompile_instruction, CompiledInstructionV0, Signal, PAYER_PUBKEY},
4    *,
5};
6use anchor_lang::{
7    prelude::*,
8    solana_program::program::{get_return_data, invoke_signed},
9};
10
11/// Accounts required by the `thread_exec` instruction.
12#[derive(Accounts)]
13#[instruction(forgo_commission: bool, fiber_cursor: u8)]
14pub struct ThreadExec<'info> {
15    /// The executor sending and paying for the transaction
16    #[account(mut)]
17    pub executor: Signer<'info>,
18
19    /// The thread being executed
20    /// Note: `dup` allows thread to appear in remaining_accounts (from compiled instruction)
21    #[account(
22        mut,
23        dup,
24        seeds = [
25            SEED_THREAD,
26            thread.authority.as_ref(),
27            thread.id.as_slice(),
28        ],
29        bump = thread.bump,
30        constraint = !thread.paused @ AntegenThreadError::ThreadPaused,
31        constraint = !thread.fiber_ids.is_empty() @ AntegenThreadError::InvalidThreadState,
32    )]
33    pub thread: Box<Account<'info, Thread>>,
34
35    /// The fiber to execute (optional - not needed if fiber_cursor == 0 and default fiber exists)
36    /// Seeds validation is done in the instruction body when fiber is Some
37    pub fiber: Option<Box<Account<'info, FiberState>>>,
38
39    /// The config for fee distribution
40    #[account(
41        seeds = [SEED_CONFIG],
42        bump = config.bump,
43    )]
44    pub config: Account<'info, ThreadConfig>,
45
46    // The config admin (for core team fee distribution)
47    /// CHECK: This is validated by the config account
48    #[account(
49        mut,
50        constraint = admin.key().eq(&config.admin) @ AntegenThreadError::InvalidConfigAdmin,
51    )]
52    pub admin: UncheckedAccount<'info>,
53
54    /// Optional nonce account for durable nonces
55    /// CHECK: Only required if thread has nonce account
56    #[account(mut)]
57    pub nonce_account: Option<UncheckedAccount<'info>>,
58
59    /// CHECK: Recent blockhashes sysvar (optional - only required if thread has nonce account)
60    pub recent_blockhashes: Option<UncheckedAccount<'info>>,
61
62    #[account(address = anchor_lang::system_program::ID)]
63    pub system_program: Program<'info, System>,
64}
65
66pub fn thread_exec(
67    ctx: Context<ThreadExec>,
68    forgo_commission: bool,
69    fiber_cursor: u8,
70) -> Result<()> {
71    let clock: Clock = Clock::get()?;
72    let thread: &mut Box<Account<Thread>> = &mut ctx.accounts.thread;
73    let config: &Account<ThreadConfig> = &ctx.accounts.config;
74
75    let executor: &mut Signer = &mut ctx.accounts.executor;
76    let executor_lamports_start: u64 = executor.lamports();
77
78    // Check global pause
79    require!(
80        !ctx.accounts.config.paused,
81        AntegenThreadError::GlobalPauseActive
82    );
83
84    let thread_pubkey = thread.key();
85
86    // Handle close_fiber execution when Signal::Close is set
87    if thread.fiber_signal == Signal::Close {
88        // Decompile and execute the close_fiber (CPIs to thread_delete)
89        let compiled = CompiledInstructionV0::try_from_slice(&thread.close_fiber)?;
90        let instruction = decompile_instruction(&compiled)?;
91
92        msg!("Executing close_fiber to delete thread");
93
94        // Invoke thread_delete via CPI with thread signing as authority
95        thread.sign(|seeds| invoke_signed(&instruction, ctx.remaining_accounts, &[seeds]))?;
96
97        // Thread is now closed by thread_delete, nothing more to do
98        return Ok(());
99    }
100
101    // Check if this is a chained execution (previous fiber signaled Chain)
102    let is_chained = thread.fiber_signal == Signal::Chain;
103
104    // Sync fiber_cursor for chained executions so advance_to_next_fiber works correctly
105    if is_chained {
106        thread.fiber_cursor = fiber_cursor;
107    }
108
109    // Normal fiber execution path
110    // Validate thread is ready for execution (has fibers and valid exec_index)
111    thread.validate_for_execution()?;
112
113    // Handle nonce using trait method
114    thread.advance_nonce_if_required(
115        &thread.to_account_info(),
116        &ctx.accounts.nonce_account,
117        &ctx.accounts.recent_blockhashes,
118    )?;
119
120    // Validate trigger and get elapsed time (skip for chained executions)
121    let time_since_ready = if is_chained {
122        msg!(
123            "Chained execution from fiber_signal={:?}",
124            thread.fiber_signal
125        );
126        0 // No elapsed time for chained executions
127    } else {
128        thread.validate_trigger(&clock, ctx.remaining_accounts, &thread_pubkey)?
129    };
130
131    // Get instruction from default fiber or fiber account
132    let (instruction, _priority_fee, is_inline) =
133        if fiber_cursor == 0 && thread.default_fiber.is_some() {
134            // Use default fiber at index 0
135            let compiled =
136                CompiledInstructionV0::try_from_slice(thread.default_fiber.as_ref().unwrap())?;
137            let mut ix = decompile_instruction(&compiled)?;
138
139            // Replace PAYER_PUBKEY with executor
140            for acc in ix.accounts.iter_mut() {
141                if acc.pubkey.eq(&PAYER_PUBKEY) {
142                    acc.pubkey = executor.key();
143                }
144            }
145
146            (ix, thread.default_fiber_priority_fee, true)
147        } else {
148            // Use fiber account at fiber_cursor
149            let fiber = ctx
150                .accounts
151                .fiber
152                .as_ref()
153                .ok_or(AntegenThreadError::FiberAccountRequired)?;
154
155            // Verify we're loading the correct fiber account
156            let expected_fiber = thread.fiber_at_index(&thread_pubkey, fiber_cursor);
157            require!(
158                fiber.key() == expected_fiber,
159                AntegenThreadError::WrongFiberIndex
160            );
161
162            (
163                fiber.get_instruction(&executor.key())?,
164                fiber.priority_fee,
165                false,
166            )
167        };
168
169    // Invoke the instruction
170    thread.sign(|seeds| invoke_signed(&instruction, ctx.remaining_accounts, &[seeds]))?;
171
172    // Verify the inner instruction did not write data to the executor account
173    require!(
174        executor.data_is_empty(),
175        AntegenThreadError::UnauthorizedWrite
176    );
177
178    // Parse the signal from return data and store for executor to read
179    let signal: Signal = match get_return_data() {
180        None => Signal::None,
181        Some((program_id, return_data)) => {
182            if program_id.eq(&instruction.program_id) {
183                Signal::try_from_slice(return_data.as_slice()).unwrap_or(Signal::None)
184            } else {
185                Signal::None
186            }
187        }
188    };
189
190    // Calculate and distribute payments when chain ends (signal != Chain)
191    // This ensures fees are calculated once at the end of a chain, capturing total balance change
192    if signal != Signal::Chain {
193        let balance_change = executor.lamports() as i64 - executor_lamports_start as i64;
194        let payments =
195            config.calculate_payments(time_since_ready, balance_change, forgo_commission);
196
197        // Log execution timing and commission details
198        if forgo_commission && payments.executor_commission.eq(&0) {
199            let effective_commission = config.calculate_effective_commission(time_since_ready);
200            let forgone = config.calculate_executor_fee(effective_commission);
201            msg!(
202                "Executed {}s after trigger, forgoing {} commission",
203                time_since_ready,
204                forgone
205            );
206        } else {
207            msg!("Executed {}s after trigger", time_since_ready);
208        }
209
210        // Distribute payments using thread trait
211        thread.distribute_payments(
212            &thread.to_account_info(),
213            &executor.to_account_info(),
214            &ctx.accounts.admin.to_account_info(),
215            &payments,
216        )?;
217    }
218
219    // Store signal for executor to read after simulation
220    thread.fiber_signal = signal.clone();
221
222    // For Immediate triggers: auto-close after fiber completes (unless chaining)
223    // Since Immediate triggers set next = i64::MAX after execution,
224    // there's no reason to keep the thread alive - auto-delete to reclaim rent
225    if matches!(thread.trigger, Trigger::Immediate { .. }) && signal != Signal::Chain {
226        thread.fiber_signal = Signal::Close;
227    }
228
229    match &signal {
230        Signal::Next { index } => {
231            thread.fiber_cursor = *index;
232        }
233        Signal::UpdateTrigger { trigger } => {
234            thread.trigger = trigger.clone();
235            thread.advance_to_next_fiber();
236        }
237        Signal::None => {
238            thread.advance_to_next_fiber();
239        }
240        _ => {}
241    }
242
243    // Update schedule for next execution (skip for chained - only first fiber updates schedule)
244    if !is_chained {
245        thread.update_schedule(&clock, ctx.remaining_accounts, &thread_pubkey)?;
246    }
247
248    // Update fiber tracking
249    if !is_inline {
250        let fiber = ctx
251            .accounts
252            .fiber
253            .as_mut()
254            .ok_or(AntegenThreadError::FiberAccountRequired)?;
255        fiber.last_executed = clock.unix_timestamp;
256        fiber.exec_count += 1;
257    }
258
259    thread.exec_count += 1;
260    thread.last_executor = executor.key();
261    thread.last_error_time = None;
262
263    Ok(())
264}