Skip to main content

antegen_thread_program/instructions/
thread_create.rs

1use crate::{
2    errors::AntegenThreadError,
3    state::{compile_instruction, Schedule, SerializableInstruction, Signal, ThreadSeeds, Trigger},
4    utils::next_timestamp,
5    *,
6};
7use anchor_lang::{
8    prelude::*,
9    solana_program::instruction::Instruction,
10    system_program::{create_nonce_account, transfer, CreateNonceAccount, Transfer},
11    InstructionData, ToAccountMetas,
12};
13use solana_nonce::state::State;
14
15/// Accounts required by the `thread_create` instruction.
16///
17/// For simple thread creation (no durable nonce), only authority, payer, thread, and system_program are needed.
18/// For durable nonce threads, also provide nonce_account, recent_blockhashes, and rent.
19#[derive(Accounts)]
20#[instruction(amount: u64, id: ThreadId)]
21pub struct ThreadCreate<'info> {
22    /// CHECK: the authority (owner) of the thread. Allows for program ownership
23    #[account()]
24    pub authority: Signer<'info>,
25
26    /// The payer for account initializations.
27    #[account(mut)]
28    pub payer: Signer<'info>,
29
30    /// The thread to be created.
31    #[account(
32        init_if_needed,
33        seeds = [
34            SEED_THREAD,
35            authority.key().as_ref(),
36            id.as_ref(),
37        ],
38        bump,
39        payer = payer,
40        space = 8 + Thread::INIT_SPACE
41    )]
42    pub thread: Account<'info, Thread>,
43
44    /// CHECK: Nonce account (optional - only for durable nonce threads)
45    /// When provided, recent_blockhashes and rent must also be provided.
46    #[account(mut)]
47    pub nonce_account: Option<Signer<'info>>,
48
49    /// CHECK: Recent blockhashes sysvar (optional - only required for durable nonce threads)
50    pub recent_blockhashes: Option<UncheckedAccount<'info>>,
51
52    /// CHECK: Rent sysvar (optional - only required for durable nonce threads)
53    pub rent: Option<UncheckedAccount<'info>>,
54
55    pub system_program: Program<'info, System>,
56
57    /// CHECK: Fiber account (optional — only when creating with an instruction)
58    #[account(mut)]
59    pub fiber: Option<UncheckedAccount<'info>>,
60
61    /// Fiber Program (optional — only required when fiber is provided)
62    pub fiber_program: Option<Program<'info, antegen_fiber_program::program::AntegenFiber>>,
63}
64
65pub fn thread_create(
66    ctx: Context<ThreadCreate>,
67    amount: u64,
68    id: ThreadId,
69    trigger: Trigger,
70    paused: Option<bool>,
71    instruction: Option<SerializableInstruction>,
72    priority_fee: Option<u64>,
73) -> Result<()> {
74    let authority: &Signer = &ctx.accounts.authority;
75    let payer: &Signer = &ctx.accounts.payer;
76    let thread: &mut Account<Thread> = &mut ctx.accounts.thread;
77
78    // Check if nonce account is provided for durable nonce thread
79    let create_durable_thread = ctx.accounts.nonce_account.is_some();
80
81    if create_durable_thread {
82        // Validate that required sysvars are provided for nonce creation
83        let nonce_account = ctx.accounts.nonce_account.as_ref().unwrap();
84        let recent_blockhashes = ctx.accounts.recent_blockhashes.as_ref().ok_or(error!(
85            crate::errors::AntegenThreadError::InvalidNonceAccount
86        ))?;
87        let rent_program = ctx.accounts.rent.as_ref().ok_or(error!(
88            crate::errors::AntegenThreadError::InvalidNonceAccount
89        ))?;
90
91        let rent: Rent = Rent::get()?;
92        let nonce_account_size: usize = State::size();
93        let nonce_lamports: u64 = rent.minimum_balance(nonce_account_size);
94
95        create_nonce_account(
96            CpiContext::new(
97                anchor_lang::system_program::ID,
98                CreateNonceAccount {
99                    from: payer.to_account_info(),
100                    nonce: nonce_account.to_account_info(),
101                    recent_blockhashes: recent_blockhashes.to_account_info(),
102                    rent: rent_program.to_account_info(),
103                },
104            ),
105            nonce_lamports,
106            &thread.key(),
107        )?;
108
109        thread.nonce_account = nonce_account.key();
110    } else {
111        thread.nonce_account = crate::ID;
112    }
113
114    // Initialize the thread
115    let clock = Clock::get().unwrap();
116    let current_timestamp = clock.unix_timestamp;
117
118    thread.version = CURRENT_THREAD_VERSION;
119    thread.authority = authority.key();
120    thread.bump = ctx.bumps.thread;
121    thread.created_at = current_timestamp;
122    thread.name = id.to_name();
123    thread.id = id.into();
124    thread.paused = paused.unwrap_or(false);
125    thread.trigger = trigger.clone();
126
127    // Initialize schedule based on trigger type
128    // Use created_at as initial prev value for proper fee calculation on first execution
129    let thread_pubkey = thread.key();
130    thread.schedule = match &trigger {
131        Trigger::Account { .. } => Schedule::OnChange { prev: 0 },
132        Trigger::Cron {
133            schedule, jitter, ..
134        } => {
135            let base_next =
136                next_timestamp(current_timestamp, schedule.clone()).unwrap_or(current_timestamp);
137            let jitter_offset =
138                crate::utils::calculate_jitter_offset(current_timestamp, &thread_pubkey, *jitter);
139            let next = base_next.saturating_add(jitter_offset);
140            Schedule::Timed {
141                prev: current_timestamp,
142                next,
143            }
144        }
145        Trigger::Immediate { .. } => Schedule::Timed {
146            prev: current_timestamp,
147            next: current_timestamp,
148        },
149        Trigger::Slot { slot } => Schedule::Block {
150            prev: clock.slot,
151            next: *slot,
152        },
153        Trigger::Epoch { epoch } => Schedule::Block {
154            prev: clock.epoch,
155            next: *epoch,
156        },
157        Trigger::Interval {
158            seconds, jitter, ..
159        } => {
160            let base_next = current_timestamp.saturating_add(*seconds);
161            let jitter_offset =
162                crate::utils::calculate_jitter_offset(current_timestamp, &thread_pubkey, *jitter);
163            let next = base_next.saturating_add(jitter_offset);
164            Schedule::Timed {
165                prev: current_timestamp,
166                next,
167            }
168        }
169        Trigger::Timestamp { unix_ts, .. } => Schedule::Timed {
170            prev: current_timestamp,
171            next: *unix_ts,
172        },
173    };
174
175    thread.exec_count = 0;
176    thread.last_executor = Pubkey::default();
177    thread.fiber_signal = Signal::None;
178
179    // Build and store pre-compiled thread_close instruction for self-closing
180    let close_ix = Instruction {
181        program_id: crate::ID,
182        accounts: crate::accounts::ThreadClose {
183            authority: thread_pubkey,   // thread signs as authority
184            close_to: thread.authority, // rent goes to owner
185            thread: thread_pubkey,
186            fiber_program: Some(antegen_fiber_program::ID),
187        }
188        .to_account_metas(None),
189        data: crate::instruction::CloseThread {}.data(),
190    };
191
192    let compiled = compile_instruction(close_ix)?;
193    thread.close_fiber = borsh::to_vec(&compiled)?;
194
195    // Transfer SOL from payer to the thread BEFORE fiber CPI
196    // (thread PDA needs lamports to pre-fund fiber creation)
197    transfer(
198        CpiContext::new(
199            anchor_lang::system_program::ID,
200            Transfer {
201                from: payer.to_account_info(),
202                to: thread.to_account_info(),
203            },
204        ),
205        amount,
206    )?;
207
208    // Optionally create fiber index 0 via CPI to fiber program
209    if let Some(instruction) = instruction {
210        // Prevent thread_delete instructions in fibers (same check as fiber_create)
211        if instruction.program_id.eq(&crate::ID)
212            && instruction.data.len().ge(&8)
213            && instruction.data[..8].eq(crate::instruction::DeleteThread::DISCRIMINATOR)
214        {
215            return Err(AntegenThreadError::InvalidInstruction.into());
216        }
217
218        // Require fiber and fiber_program accounts
219        let fiber = ctx
220            .accounts
221            .fiber
222            .as_ref()
223            .ok_or(AntegenThreadError::MissingFiberAccount)?;
224        let fiber_program = ctx
225            .accounts
226            .fiber_program
227            .as_ref()
228            .ok_or(AntegenThreadError::MissingFiberAccount)?;
229
230        let priority_fee = priority_fee.unwrap_or(0);
231
232        // Conditional pre-funding: only pre-fund if fiber account is not yet initialized
233        if fiber.to_account_info().data_len() == 0 {
234            let space = 8 + antegen_fiber_program::state::FiberState::INIT_SPACE;
235            let rent_lamports = Rent::get()?.minimum_balance(space);
236            **thread.to_account_info().try_borrow_mut_lamports()? -= rent_lamports;
237            **fiber.to_account_info().try_borrow_mut_lamports()? += rent_lamports;
238        }
239
240        thread.sign(|seeds| {
241            antegen_fiber_program::cpi::create_fiber(
242                CpiContext::new_with_signer(
243                    fiber_program.key(),
244                    antegen_fiber_program::cpi::accounts::FiberCreate {
245                        thread: thread.to_account_info(),
246                        fiber: fiber.to_account_info(),
247                        system_program: ctx.accounts.system_program.to_account_info(),
248                    },
249                    &[seeds],
250                ),
251                0, // fiber_index = 0
252                instruction.clone(),
253                priority_fee,
254            )
255        })?;
256
257        thread.fiber_next_id = 1;
258        thread.fiber_ids = vec![0];
259        thread.fiber_cursor = 0;
260    } else {
261        // No default fiber — users add fibers separately via create_fiber
262        thread.fiber_next_id = 0;
263        thread.fiber_ids = Vec::new();
264        thread.fiber_cursor = 0;
265    }
266
267    Ok(())
268}