antegen_thread_program/instructions/
thread_create.rs

1use crate::{
2    state::{compile_instruction, Schedule, SerializableInstruction, Signal, Trigger},
3    utils::next_timestamp,
4    *,
5};
6use anchor_lang::{
7    prelude::*,
8    solana_program::instruction::Instruction,
9    system_program::{create_nonce_account, transfer, CreateNonceAccount, Transfer},
10    InstructionData, ToAccountMetas,
11};
12use solana_nonce::state::State;
13
14/// Accounts required by the `thread_create` instruction.
15///
16/// For simple thread creation (no durable nonce), only authority, payer, thread, and system_program are needed.
17/// For durable nonce threads, also provide nonce_account, recent_blockhashes, and rent.
18#[derive(Accounts)]
19#[instruction(amount: u64, id: ThreadId, trigger: Trigger, initial_instruction: Option<SerializableInstruction>, priority_fee: Option<u64>)]
20pub struct ThreadCreate<'info> {
21    /// CHECK: the authority (owner) of the thread. Allows for program ownership
22    #[account()]
23    pub authority: Signer<'info>,
24
25    /// The payer for account initializations.
26    #[account(mut)]
27    pub payer: Signer<'info>,
28
29    /// The thread to be created.
30    #[account(
31        init,
32        seeds = [
33            SEED_THREAD,
34            authority.key().as_ref(),
35            id.as_ref(),
36        ],
37        bump,
38        payer = payer,
39        space = 8 + Thread::INIT_SPACE
40    )]
41    pub thread: Account<'info, Thread>,
42
43    /// CHECK: Nonce account (optional - only for durable nonce threads)
44    /// When provided, recent_blockhashes and rent must also be provided.
45    #[account(mut)]
46    pub nonce_account: Option<Signer<'info>>,
47
48    /// CHECK: Recent blockhashes sysvar (optional - only required for durable nonce threads)
49    pub recent_blockhashes: Option<AccountInfo<'info>>,
50
51    /// CHECK: Rent sysvar (optional - only required for durable nonce threads)
52    pub rent: Option<AccountInfo<'info>>,
53
54    pub system_program: Program<'info, System>,
55}
56
57pub fn thread_create(
58    ctx: Context<ThreadCreate>,
59    amount: u64,
60    id: ThreadId,
61    trigger: Trigger,
62    initial_instruction: Option<SerializableInstruction>,
63    priority_fee: Option<u64>,
64) -> Result<()> {
65    let authority: &Signer = &ctx.accounts.authority;
66    let payer: &Signer = &ctx.accounts.payer;
67    let thread: &mut Account<Thread> = &mut ctx.accounts.thread;
68    let system_program: &Program<System> = &ctx.accounts.system_program;
69
70    // Check if nonce account is provided for durable nonce thread
71    let create_durable_thread = ctx.accounts.nonce_account.is_some();
72
73    if create_durable_thread {
74        // Validate that required sysvars are provided for nonce creation
75        let nonce_account = ctx.accounts.nonce_account.as_ref().unwrap();
76        let recent_blockhashes = ctx.accounts.recent_blockhashes.as_ref().ok_or(error!(
77            crate::errors::AntegenThreadError::InvalidNonceAccount
78        ))?;
79        let rent_program = ctx.accounts.rent.as_ref().ok_or(error!(
80            crate::errors::AntegenThreadError::InvalidNonceAccount
81        ))?;
82
83        let rent: Rent = Rent::get()?;
84        let nonce_account_size: usize = State::size();
85        let nonce_lamports: u64 = rent.minimum_balance(nonce_account_size);
86
87        create_nonce_account(
88            CpiContext::new(
89                system_program.to_account_info(),
90                CreateNonceAccount {
91                    from: payer.to_account_info(),
92                    nonce: nonce_account.to_account_info(),
93                    recent_blockhashes: recent_blockhashes.to_account_info(),
94                    rent: rent_program.to_account_info(),
95                },
96            ),
97            nonce_lamports,
98            &thread.key(),
99        )?;
100
101        thread.nonce_account = nonce_account.key();
102    } else {
103        thread.nonce_account = crate::ID;
104    }
105
106    // Initialize the thread
107    let clock = Clock::get().unwrap();
108    let current_timestamp = clock.unix_timestamp;
109
110    thread.version = CURRENT_THREAD_VERSION;
111    thread.authority = authority.key();
112    thread.bump = ctx.bumps.thread;
113    thread.created_at = current_timestamp;
114    thread.name = id.to_name();
115    thread.id = id.into();
116    thread.paused = false;
117    thread.trigger = trigger.clone();
118
119    // Initialize schedule based on trigger type
120    // Use created_at as initial prev value for proper fee calculation on first execution
121    let thread_pubkey = thread.key();
122    thread.schedule = match &trigger {
123        Trigger::Account { .. } => Schedule::OnChange { prev: 0 },
124        Trigger::Cron {
125            schedule, jitter, ..
126        } => {
127            let base_next =
128                next_timestamp(current_timestamp, schedule.clone()).unwrap_or(current_timestamp);
129            // Apply jitter to initial trigger time
130            let jitter_offset =
131                crate::utils::calculate_jitter_offset(current_timestamp, &thread_pubkey, *jitter);
132            let next = base_next.saturating_add(jitter_offset);
133            Schedule::Timed {
134                prev: current_timestamp, // Use creation time as initial prev
135                next,
136            }
137        }
138        Trigger::Immediate { .. } => Schedule::Timed {
139            prev: current_timestamp, // Use creation time as initial prev
140            next: current_timestamp,
141        },
142        Trigger::Slot { slot } => Schedule::Block {
143            prev: clock.slot, // Use current slot as initial prev
144            next: *slot,
145        },
146        Trigger::Epoch { epoch } => Schedule::Block {
147            prev: clock.epoch, // Use current epoch as initial prev
148            next: *epoch,
149        },
150        Trigger::Interval {
151            seconds, jitter, ..
152        } => {
153            let base_next = current_timestamp.saturating_add(*seconds);
154            // Apply jitter to initial trigger time
155            let jitter_offset =
156                crate::utils::calculate_jitter_offset(current_timestamp, &thread_pubkey, *jitter);
157            let next = base_next.saturating_add(jitter_offset);
158            Schedule::Timed {
159                prev: current_timestamp, // Use creation time as initial prev
160                next,
161            }
162        }
163        Trigger::Timestamp { unix_ts, .. } => Schedule::Timed {
164            prev: current_timestamp, // Use creation time as initial prev
165            next: *unix_ts,
166        },
167    };
168
169    // Use thread's PDA seeds for signer seeds
170    let signer_seeds = vec![vec![
171        SEED_THREAD.to_vec(),
172        thread.authority.to_bytes().to_vec(),
173        thread.id.clone(),
174    ]];
175
176    // Handle optional initial instruction
177    if let Some(instruction) = initial_instruction {
178        // Store default fiber inline in thread account
179        let instruction: Instruction = instruction.into();
180
181        // Compile the instruction
182        let compiled = compile_instruction(instruction, signer_seeds.clone())?;
183        let compiled_bytes = compiled.try_to_vec()?;
184
185        // Store inline in thread
186        thread.default_fiber = Some(compiled_bytes);
187        thread.default_fiber_priority_fee = priority_fee.unwrap_or(0);
188        thread.fiber_next_id = 1; // Next fiber will be at index 1
189        thread.fiber_ids = vec![0]; // Fiber 0 exists (inline)
190    } else {
191        // No initial instruction, create empty thread
192        thread.default_fiber = None;
193        thread.default_fiber_priority_fee = 0;
194        thread.fiber_next_id = 0; // Next fiber will be at index 0
195        thread.fiber_ids = Vec::new();
196    }
197
198    thread.fiber_cursor = 0;
199    thread.exec_count = 0; // Initialize execution counter
200    thread.last_executor = Pubkey::default(); // Initialize with default for load balancing
201
202    // Initialize fiber_signal to None (no pending signal)
203    thread.fiber_signal = Signal::None;
204
205    // Build and store pre-compiled thread_delete instruction for self-closing
206    let delete_ix = Instruction {
207        program_id: crate::ID,
208        accounts: crate::accounts::ThreadDelete {
209            authority: thread_pubkey,   // thread signs as authority
210            close_to: thread.authority, // rent goes to owner
211            thread: thread_pubkey,
212        }
213        .to_account_metas(None),
214        data: crate::instruction::DeleteThread {}.data(),
215    };
216
217    let compiled = compile_instruction(delete_ix, signer_seeds)?;
218    thread.close_fiber = compiled.try_to_vec()?;
219
220    // Transfer SOL from payer to the thread.
221    transfer(
222        CpiContext::new(
223            system_program.to_account_info(),
224            Transfer {
225                from: payer.to_account_info(),
226                to: thread.to_account_info(),
227            },
228        ),
229        amount,
230    )?;
231
232    Ok(())
233}