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    lookup_tables: Vec<Pubkey>,
74) -> Result<()> {
75    let authority: &Signer = &ctx.accounts.authority;
76    let payer: &Signer = &ctx.accounts.payer;
77    let thread: &mut Account<Thread> = &mut ctx.accounts.thread;
78
79    // Check if nonce account is provided for durable nonce thread
80    let create_durable_thread = ctx.accounts.nonce_account.is_some();
81
82    if create_durable_thread {
83        // Validate that required sysvars are provided for nonce creation
84        let nonce_account = ctx.accounts.nonce_account.as_ref().unwrap();
85        let recent_blockhashes = ctx.accounts.recent_blockhashes.as_ref().ok_or(error!(
86            crate::errors::AntegenThreadError::InvalidNonceAccount
87        ))?;
88        let rent_program = ctx.accounts.rent.as_ref().ok_or(error!(
89            crate::errors::AntegenThreadError::InvalidNonceAccount
90        ))?;
91
92        let rent: Rent = Rent::get()?;
93        let nonce_account_size: usize = State::size();
94        let nonce_lamports: u64 = rent.minimum_balance(nonce_account_size);
95
96        create_nonce_account(
97            CpiContext::new(
98                anchor_lang::system_program::ID,
99                CreateNonceAccount {
100                    from: payer.to_account_info(),
101                    nonce: nonce_account.to_account_info(),
102                    recent_blockhashes: recent_blockhashes.to_account_info(),
103                    rent: rent_program.to_account_info(),
104                },
105            ),
106            nonce_lamports,
107            &thread.key(),
108        )?;
109
110        thread.nonce_account = nonce_account.key();
111    } else {
112        thread.nonce_account = crate::ID;
113    }
114
115    // Initialize the thread
116    let clock = Clock::get().unwrap();
117    let current_timestamp = clock.unix_timestamp;
118
119    thread.version = CURRENT_THREAD_VERSION;
120    thread.authority = authority.key();
121    thread.bump = ctx.bumps.thread;
122    thread.created_at = current_timestamp;
123    thread.name = id.to_name();
124    thread.id = id.into();
125    thread.paused = paused.unwrap_or(false);
126    thread.trigger = trigger.clone();
127
128    // Initialize schedule based on trigger type
129    // Use created_at as initial prev value for proper fee calculation on first execution
130    let thread_pubkey = thread.key();
131    thread.schedule = match &trigger {
132        Trigger::Account { .. } => Schedule::OnChange { prev: 0 },
133        Trigger::Cron {
134            schedule, jitter, ..
135        } => {
136            let base_next =
137                next_timestamp(current_timestamp, schedule.clone()).unwrap_or(current_timestamp);
138            let jitter_offset =
139                crate::utils::calculate_jitter_offset(current_timestamp, &thread_pubkey, *jitter);
140            let next = base_next.saturating_add(jitter_offset);
141            Schedule::Timed {
142                prev: current_timestamp,
143                next,
144            }
145        }
146        Trigger::Immediate { .. } => Schedule::Timed {
147            prev: current_timestamp,
148            next: current_timestamp,
149        },
150        Trigger::Slot { slot } => Schedule::Block {
151            prev: clock.slot,
152            next: *slot,
153        },
154        Trigger::Epoch { epoch } => Schedule::Block {
155            prev: clock.epoch,
156            next: *epoch,
157        },
158        Trigger::Interval {
159            seconds, jitter, ..
160        } => {
161            let base_next = current_timestamp.saturating_add(*seconds);
162            let jitter_offset =
163                crate::utils::calculate_jitter_offset(current_timestamp, &thread_pubkey, *jitter);
164            let next = base_next.saturating_add(jitter_offset);
165            Schedule::Timed {
166                prev: current_timestamp,
167                next,
168            }
169        }
170        Trigger::Timestamp { unix_ts, .. } => Schedule::Timed {
171            prev: current_timestamp,
172            next: *unix_ts,
173        },
174    };
175
176    thread.exec_count = 0;
177    thread.last_executor = Pubkey::default();
178    thread.fiber_signal = Signal::None;
179
180    // Build and store pre-compiled thread_close instruction for self-closing
181    let close_ix = Instruction {
182        program_id: crate::ID,
183        accounts: crate::accounts::ThreadClose {
184            authority: thread_pubkey,   // thread signs as authority
185            close_to: thread.authority, // rent goes to owner
186            thread: thread_pubkey,
187            fiber_program: Some(antegen_fiber_program::ID),
188        }
189        .to_account_metas(None),
190        data: crate::instruction::CloseThread {}.data(),
191    };
192
193    let compiled = compile_instruction(close_ix)?;
194    thread.close_fiber = borsh::to_vec(&compiled)?;
195
196    // Transfer SOL from payer to the thread BEFORE fiber CPI
197    // (thread PDA needs lamports to pre-fund fiber creation)
198    transfer(
199        CpiContext::new(
200            anchor_lang::system_program::ID,
201            Transfer {
202                from: payer.to_account_info(),
203                to: thread.to_account_info(),
204            },
205        ),
206        amount,
207    )?;
208
209    // Optionally create fiber index 0 via CPI to fiber program
210    if let Some(instruction) = instruction {
211        // Prevent thread_delete instructions in fibers (same check as fiber_create)
212        if instruction.program_id.eq(&crate::ID)
213            && instruction.data.len().ge(&8)
214            && instruction.data[..8].eq(crate::instruction::DeleteThread::DISCRIMINATOR)
215        {
216            return Err(AntegenThreadError::InvalidInstruction.into());
217        }
218
219        // Require fiber and fiber_program accounts
220        let fiber = ctx
221            .accounts
222            .fiber
223            .as_ref()
224            .ok_or(AntegenThreadError::MissingFiberAccount)?;
225        let fiber_program = ctx
226            .accounts
227            .fiber_program
228            .as_ref()
229            .ok_or(AntegenThreadError::MissingFiberAccount)?;
230
231        let priority_fee = priority_fee.unwrap_or(0);
232
233        // Conditional pre-funding: only pre-fund if fiber account is not yet initialized
234        if fiber.to_account_info().data_len() == 0 {
235            let space = 8 + antegen_fiber_program::state::FiberVersionedState::INIT_SPACE;
236            let rent_lamports = Rent::get()?.minimum_balance(space);
237            **thread.to_account_info().try_borrow_mut_lamports()? -= rent_lamports;
238            **fiber.to_account_info().try_borrow_mut_lamports()? += rent_lamports;
239        }
240
241        thread.sign(|seeds| {
242            antegen_fiber_program::cpi::create(
243                CpiContext::new_with_signer(
244                    fiber_program.key(),
245                    antegen_fiber_program::cpi::accounts::Create {
246                        thread: thread.to_account_info(),
247                        fiber: fiber.to_account_info(),
248                        system_program: ctx.accounts.system_program.to_account_info(),
249                    },
250                    &[seeds],
251                ),
252                0, // fiber_index = 0
253                instruction.clone(),
254                priority_fee,
255                lookup_tables.clone(),
256            )
257        })?;
258
259        thread.fiber_next_id = 1;
260        thread.fiber_ids = vec![0];
261        thread.fiber_cursor = 0;
262    } else {
263        // No default fiber — users add fibers separately via create_fiber
264        thread.fiber_next_id = 0;
265        thread.fiber_ids = Vec::new();
266        thread.fiber_cursor = 0;
267    }
268
269    Ok(())
270}