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<UncheckedAccount<'info>>,
50
51    /// CHECK: Rent sysvar (optional - only required for durable nonce threads)
52    pub rent: Option<UncheckedAccount<'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
69    // Check if nonce account is provided for durable nonce thread
70    let create_durable_thread = ctx.accounts.nonce_account.is_some();
71
72    if create_durable_thread {
73        // Validate that required sysvars are provided for nonce creation
74        let nonce_account = ctx.accounts.nonce_account.as_ref().unwrap();
75        let recent_blockhashes = ctx.accounts.recent_blockhashes.as_ref().ok_or(error!(
76            crate::errors::AntegenThreadError::InvalidNonceAccount
77        ))?;
78        let rent_program = ctx.accounts.rent.as_ref().ok_or(error!(
79            crate::errors::AntegenThreadError::InvalidNonceAccount
80        ))?;
81
82        let rent: Rent = Rent::get()?;
83        let nonce_account_size: usize = State::size();
84        let nonce_lamports: u64 = rent.minimum_balance(nonce_account_size);
85
86        create_nonce_account(
87            CpiContext::new(
88                anchor_lang::system_program::ID,
89                CreateNonceAccount {
90                    from: payer.to_account_info(),
91                    nonce: nonce_account.to_account_info(),
92                    recent_blockhashes: recent_blockhashes.to_account_info(),
93                    rent: rent_program.to_account_info(),
94                },
95            ),
96            nonce_lamports,
97            &thread.key(),
98        )?;
99
100        thread.nonce_account = nonce_account.key();
101    } else {
102        thread.nonce_account = crate::ID;
103    }
104
105    // Initialize the thread
106    let clock = Clock::get().unwrap();
107    let current_timestamp = clock.unix_timestamp;
108
109    thread.version = CURRENT_THREAD_VERSION;
110    thread.authority = authority.key();
111    thread.bump = ctx.bumps.thread;
112    thread.created_at = current_timestamp;
113    thread.name = id.to_name();
114    thread.id = id.into();
115    thread.paused = false;
116    thread.trigger = trigger.clone();
117
118    // Initialize schedule based on trigger type
119    // Use created_at as initial prev value for proper fee calculation on first execution
120    let thread_pubkey = thread.key();
121    thread.schedule = match &trigger {
122        Trigger::Account { .. } => Schedule::OnChange { prev: 0 },
123        Trigger::Cron {
124            schedule, jitter, ..
125        } => {
126            let base_next =
127                next_timestamp(current_timestamp, schedule.clone()).unwrap_or(current_timestamp);
128            // Apply jitter to initial trigger time
129            let jitter_offset =
130                crate::utils::calculate_jitter_offset(current_timestamp, &thread_pubkey, *jitter);
131            let next = base_next.saturating_add(jitter_offset);
132            Schedule::Timed {
133                prev: current_timestamp, // Use creation time as initial prev
134                next,
135            }
136        }
137        Trigger::Immediate { .. } => Schedule::Timed {
138            prev: current_timestamp, // Use creation time as initial prev
139            next: current_timestamp,
140        },
141        Trigger::Slot { slot } => Schedule::Block {
142            prev: clock.slot, // Use current slot as initial prev
143            next: *slot,
144        },
145        Trigger::Epoch { epoch } => Schedule::Block {
146            prev: clock.epoch, // Use current epoch as initial prev
147            next: *epoch,
148        },
149        Trigger::Interval {
150            seconds, jitter, ..
151        } => {
152            let base_next = current_timestamp.saturating_add(*seconds);
153            // Apply jitter to initial trigger time
154            let jitter_offset =
155                crate::utils::calculate_jitter_offset(current_timestamp, &thread_pubkey, *jitter);
156            let next = base_next.saturating_add(jitter_offset);
157            Schedule::Timed {
158                prev: current_timestamp, // Use creation time as initial prev
159                next,
160            }
161        }
162        Trigger::Timestamp { unix_ts, .. } => Schedule::Timed {
163            prev: current_timestamp, // Use creation time as initial prev
164            next: *unix_ts,
165        },
166    };
167
168    // Use thread's PDA seeds for signer seeds
169    let signer_seeds = vec![vec![
170        SEED_THREAD.to_vec(),
171        thread.authority.to_bytes().to_vec(),
172        thread.id.clone(),
173    ]];
174
175    // Handle optional initial instruction
176    if let Some(instruction) = initial_instruction {
177        // Store default fiber inline in thread account
178        let instruction: Instruction = instruction.into();
179
180        // Compile the instruction
181        let compiled = compile_instruction(instruction, signer_seeds.clone())?;
182        let compiled_bytes = borsh::to_vec(&compiled)?;
183
184        // Store inline in thread
185        thread.default_fiber = Some(compiled_bytes);
186        thread.default_fiber_priority_fee = priority_fee.unwrap_or(0);
187        thread.fiber_next_id = 1; // Next fiber will be at index 1
188        thread.fiber_ids = vec![0]; // Fiber 0 exists (inline)
189    } else {
190        // No initial instruction, create empty thread
191        thread.default_fiber = None;
192        thread.default_fiber_priority_fee = 0;
193        thread.fiber_next_id = 0; // Next fiber will be at index 0
194        thread.fiber_ids = Vec::new();
195    }
196
197    thread.fiber_cursor = 0;
198    thread.exec_count = 0; // Initialize execution counter
199    thread.last_executor = Pubkey::default(); // Initialize with default for load balancing
200
201    // Initialize fiber_signal to None (no pending signal)
202    thread.fiber_signal = Signal::None;
203
204    // Build and store pre-compiled thread_delete instruction for self-closing
205    let delete_ix = Instruction {
206        program_id: crate::ID,
207        accounts: crate::accounts::ThreadDelete {
208            authority: thread_pubkey,   // thread signs as authority
209            close_to: thread.authority, // rent goes to owner
210            thread: thread_pubkey,
211        }
212        .to_account_metas(None),
213        data: crate::instruction::DeleteThread {}.data(),
214    };
215
216    let compiled = compile_instruction(delete_ix, signer_seeds)?;
217    thread.close_fiber = borsh::to_vec(&compiled)?;
218
219    // Transfer SOL from payer to the thread.
220    transfer(
221        CpiContext::new(
222            anchor_lang::system_program::ID,
223            Transfer {
224                from: payer.to_account_info(),
225                to: thread.to_account_info(),
226            },
227        ),
228        amount,
229    )?;
230
231    Ok(())
232}