1use anchor_lang::{prelude::*, solana_program::program::invoke_signed};
2use gmsol_store::{
3    program::GmsolStore,
4    states::{Seed, Store, MAX_ROLE_NAME_LEN},
5    utils::{fixed_str::fixed_str_to_bytes, CpiAuthenticate, CpiAuthentication, WithStore},
6    CoreError,
7};
8
9use crate::{
10    roles,
11    states::{
12        config::TimelockConfig, Executor, ExecutorWalletSigner, InstructionAccess,
13        InstructionHeader, InstructionLoader,
14    },
15};
16
17#[derive(Accounts)]
19#[instruction(num_accounts: u16, data_len: u16)]
20pub struct CreateInstructionBuffer<'info> {
21    #[account(mut)]
23    pub authority: Signer<'info>,
24    pub store: UncheckedAccount<'info>,
27    #[account(has_one = store)]
29    pub executor: AccountLoader<'info, Executor>,
30    #[account(
32        init,
33        payer = authority,
34        space = 8 + InstructionHeader::init_space(num_accounts, data_len),
35    )]
36    pub instruction_buffer: AccountLoader<'info, InstructionHeader>,
37    pub instruction_program: UncheckedAccount<'info>,
40    pub store_program: Program<'info, GmsolStore>,
42    pub system_program: Program<'info, System>,
44}
45
46pub(crate) fn unchecked_create_instruction_buffer<'info>(
50    ctx: Context<'_, '_, 'info, 'info, CreateInstructionBuffer<'info>>,
51    num_accounts: u16,
52    data_len: u16,
53    data: &[u8],
54    signers: &[u16],
55) -> Result<()> {
56    let remaining_accounts = ctx.remaining_accounts;
57    let num_accounts = usize::from(num_accounts);
58    require_gte!(
59        remaining_accounts.len(),
60        num_accounts,
61        CoreError::InvalidArgument
62    );
63
64    require_eq!(
65        data.len(),
66        usize::from(data_len),
67        CoreError::InvalidArgument
68    );
69
70    let wallet_bump = {
71        let executor = ctx.accounts.executor.load()?;
72        let wallet_bump = executor.wallet_bump;
73        msg!(
74            "[Timelock] creating instruction buffer for program {}, with executor `{}`",
75            ctx.accounts.instruction_program.key,
76            executor.role_name()?,
77        );
78        wallet_bump
79    };
80
81    ctx.accounts.instruction_buffer.load_and_init_instruction(
82        ctx.accounts.executor.key(),
83        wallet_bump,
84        ctx.accounts.authority.key(),
85        ctx.accounts.instruction_program.key(),
86        data,
87        &remaining_accounts[0..num_accounts],
88        signers,
89    )?;
90
91    Ok(())
92}
93
94impl<'info> WithStore<'info> for CreateInstructionBuffer<'info> {
95    fn store_program(&self) -> AccountInfo<'info> {
96        self.store_program.to_account_info()
97    }
98
99    fn store(&self) -> AccountInfo<'info> {
100        self.store.to_account_info()
101    }
102}
103
104impl<'info> CpiAuthentication<'info> for CreateInstructionBuffer<'info> {
105    fn authority(&self) -> AccountInfo<'info> {
106        self.authority.to_account_info()
107    }
108
109    fn on_error(&self) -> Result<()> {
110        err!(CoreError::PermissionDenied)
111    }
112}
113
114#[derive(Accounts)]
116#[instruction(role: String)]
117pub struct ApproveInstruction<'info> {
118    pub authority: Signer<'info>,
120    pub store: UncheckedAccount<'info>,
123    #[account(
125        has_one = store,
126        constraint = executor.load()?.role_name()? == role.as_str() @ CoreError::InvalidArgument,
127        seeds = [
128            Executor::SEED,
129            store.key.as_ref(),
130            &fixed_str_to_bytes::<MAX_ROLE_NAME_LEN>(&role)?,
131        ],
132        bump = executor.load()?.bump,
133    )]
134    pub executor: AccountLoader<'info, Executor>,
135    #[account(mut, has_one = executor)]
137    pub instruction: AccountLoader<'info, InstructionHeader>,
138    pub store_program: Program<'info, GmsolStore>,
140}
141
142pub(crate) fn approve_instruction(ctx: Context<ApproveInstruction>, role: &str) -> Result<()> {
144    validate_timelocked_role(&ctx, role)?;
145    ctx.accounts
146        .instruction
147        .load_mut()?
148        .approve(ctx.accounts.authority.key())
149}
150
151impl<'info> WithStore<'info> for ApproveInstruction<'info> {
152    fn store_program(&self) -> AccountInfo<'info> {
153        self.store_program.to_account_info()
154    }
155
156    fn store(&self) -> AccountInfo<'info> {
157        self.store.to_account_info()
158    }
159}
160
161impl<'info> CpiAuthentication<'info> for ApproveInstruction<'info> {
162    fn authority(&self) -> AccountInfo<'info> {
163        self.authority.to_account_info()
164    }
165
166    fn on_error(&self) -> Result<()> {
167        err!(CoreError::PermissionDenied)
168    }
169}
170
171#[derive(Accounts)]
173#[instruction(role: String)]
174pub struct ApproveInstructions<'info> {
175    pub authority: Signer<'info>,
177    pub store: UncheckedAccount<'info>,
180    #[account(
182        has_one = store,
183        constraint = executor.load()?.role_name()? == role.as_str() @ CoreError::InvalidArgument,
184        seeds = [
185            Executor::SEED,
186            store.key.as_ref(),
187            &fixed_str_to_bytes::<MAX_ROLE_NAME_LEN>(&role)?,
188        ],
189        bump = executor.load()?.bump,
190    )]
191    pub executor: AccountLoader<'info, Executor>,
192    pub store_program: Program<'info, GmsolStore>,
194}
195
196pub(crate) fn approve_instructions<'info>(
198    ctx: Context<'_, '_, 'info, 'info, ApproveInstructions<'info>>,
199    role: &str,
200) -> Result<()> {
201    validate_timelocked_role(&ctx, role)?;
202
203    let executor = ctx.accounts.executor.key();
204    let approver = ctx.accounts.authority.key();
205    for account in ctx.remaining_accounts {
206        require!(account.is_writable, ErrorCode::AccountNotMutable);
207        let loader = AccountLoader::<InstructionHeader>::try_from(account)?;
208        require_keys_eq!(
209            *loader.load()?.executor(),
210            executor,
211            CoreError::InvalidArgument
212        );
213        loader.load_mut()?.approve(approver)?;
214    }
215
216    Ok(())
217}
218
219impl<'info> WithStore<'info> for ApproveInstructions<'info> {
220    fn store_program(&self) -> AccountInfo<'info> {
221        self.store_program.to_account_info()
222    }
223
224    fn store(&self) -> AccountInfo<'info> {
225        self.store.to_account_info()
226    }
227}
228
229impl<'info> CpiAuthentication<'info> for ApproveInstructions<'info> {
230    fn authority(&self) -> AccountInfo<'info> {
231        self.authority.to_account_info()
232    }
233
234    fn on_error(&self) -> Result<()> {
235        err!(CoreError::PermissionDenied)
236    }
237}
238
239#[derive(Accounts)]
241pub struct CancelInstruction<'info> {
242    pub authority: Signer<'info>,
244    pub store: UncheckedAccount<'info>,
247    #[account(has_one = store)]
249    pub executor: AccountLoader<'info, Executor>,
250    #[account(mut)]
253    pub rent_receiver: UncheckedAccount<'info>,
254    #[account(
256        mut,
257        has_one = executor,
258        has_one = rent_receiver,
259        close = rent_receiver,
260    )]
261    pub instruction: AccountLoader<'info, InstructionHeader>,
262    pub store_program: Program<'info, GmsolStore>,
264}
265
266pub(crate) fn unchecked_cancel_instruction(_ctx: Context<CancelInstruction>) -> Result<()> {
270    Ok(())
271}
272
273impl<'info> WithStore<'info> for CancelInstruction<'info> {
274    fn store_program(&self) -> AccountInfo<'info> {
275        self.store_program.to_account_info()
276    }
277
278    fn store(&self) -> AccountInfo<'info> {
279        self.store.to_account_info()
280    }
281}
282
283impl<'info> CpiAuthentication<'info> for CancelInstruction<'info> {
284    fn authority(&self) -> AccountInfo<'info> {
285        self.authority.to_account_info()
286    }
287
288    fn on_error(&self) -> Result<()> {
289        err!(CoreError::PermissionDenied)
290    }
291}
292
293#[derive(Accounts)]
295pub struct CancelInstructions<'info> {
296    pub authority: Signer<'info>,
298    pub store: UncheckedAccount<'info>,
301    #[account(has_one = store)]
303    pub executor: AccountLoader<'info, Executor>,
304    #[account(mut)]
307    pub rent_receiver: UncheckedAccount<'info>,
308    pub store_program: Program<'info, GmsolStore>,
310}
311
312pub(crate) fn unchecked_cancel_instructions<'info>(
316    ctx: Context<'_, '_, 'info, 'info, CancelInstructions<'info>>,
317) -> Result<()> {
318    let executor = ctx.accounts.executor.key();
319    let rent_receiver = ctx.accounts.rent_receiver.key();
320
321    for account in ctx.remaining_accounts {
322        require!(account.is_writable, ErrorCode::AccountNotMutable);
323        let loader = AccountLoader::<InstructionHeader>::try_from(account)?;
324
325        {
326            let header = loader.load()?;
327            require_keys_eq!(*header.executor(), executor, CoreError::InvalidArgument);
328            require_keys_eq!(
329                *header.rent_receiver(),
330                rent_receiver,
331                CoreError::InvalidArgument
332            );
333        }
334
335        loader.close(ctx.accounts.rent_receiver.to_account_info())?;
336    }
337
338    Ok(())
339}
340
341impl<'info> WithStore<'info> for CancelInstructions<'info> {
342    fn store_program(&self) -> AccountInfo<'info> {
343        self.store_program.to_account_info()
344    }
345
346    fn store(&self) -> AccountInfo<'info> {
347        self.store.to_account_info()
348    }
349}
350
351impl<'info> CpiAuthentication<'info> for CancelInstructions<'info> {
352    fn authority(&self) -> AccountInfo<'info> {
353        self.authority.to_account_info()
354    }
355
356    fn on_error(&self) -> Result<()> {
357        err!(CoreError::PermissionDenied)
358    }
359}
360
361#[derive(Accounts)]
363pub struct ExecuteInstruction<'info> {
364    pub authority: Signer<'info>,
366    pub store: AccountLoader<'info, Store>,
368    #[account(has_one = store)]
370    pub timelock_config: AccountLoader<'info, TimelockConfig>,
371    #[account(has_one = store)]
373    pub executor: AccountLoader<'info, Executor>,
374    #[account(
378        mut,
379        seeds = [Executor::WALLET_SEED, executor.key().as_ref()],
380        bump = executor.load()?.wallet_bump,
381    )]
382    pub wallet: UncheckedAccount<'info>,
383    #[account(mut)]
386    pub rent_receiver: UncheckedAccount<'info>,
387    #[account(
389        mut,
390        has_one = executor,
391        has_one = rent_receiver,
392        close = rent_receiver,
393    )]
394    pub instruction: AccountLoader<'info, InstructionHeader>,
395    pub store_program: Program<'info, GmsolStore>,
397}
398
399pub(crate) fn unchecked_execute_instruction(ctx: Context<ExecuteInstruction>) -> Result<()> {
403    let remaining_accounts = ctx.remaining_accounts;
404
405    let instruction = ctx.accounts.instruction.load_instruction()?;
406
407    {
409        let store = ctx.accounts.store.load()?;
410        let approver = instruction
411            .header()
412            .apporver()
413            .ok_or_else(|| error!(CoreError::PreconditionsAreNotMet))?;
414        let timelocked_role = roles::timelocked_role(ctx.accounts.executor.load()?.role_name()?);
415        require!(
416            store.has_role(approver, &timelocked_role)?,
417            CoreError::PreconditionsAreNotMet
418        );
419    }
420
421    let delay = ctx.accounts.timelock_config.load()?.delay();
422    require!(
423        instruction.header().is_executable(delay)?,
424        CoreError::PreconditionsAreNotMet
425    );
426
427    let signer = ExecutorWalletSigner::new(
428        ctx.accounts.executor.key(),
429        ctx.accounts.executor.load()?.wallet_bump,
430    );
431
432    invoke_signed(
433        &instruction.to_instruction(false).map_err(|err| {
434            msg!("Instruction error: {}", err);
435            error!(CoreError::InvalidArgument)
436        })?,
437        remaining_accounts,
438        &[&signer.as_seeds()],
439    )?;
440    Ok(())
441}
442
443impl<'info> WithStore<'info> for ExecuteInstruction<'info> {
444    fn store_program(&self) -> AccountInfo<'info> {
445        self.store_program.to_account_info()
446    }
447
448    fn store(&self) -> AccountInfo<'info> {
449        self.store.to_account_info()
450    }
451}
452
453impl<'info> CpiAuthentication<'info> for ExecuteInstruction<'info> {
454    fn authority(&self) -> AccountInfo<'info> {
455        self.authority.to_account_info()
456    }
457
458    fn on_error(&self) -> Result<()> {
459        err!(CoreError::PermissionDenied)
460    }
461}
462
463fn validate_timelocked_role<'info>(
464    ctx: &Context<impl CpiAuthenticate<'info>>,
465    role: &str,
466) -> Result<()> {
467    let timelocked_role = roles::timelocked_role(role);
468    CpiAuthenticate::only(ctx, &timelocked_role)?;
469    msg!(
470        "[Timelock] approving `{}` instruction by a `{}`",
471        role,
472        timelocked_role
473    );
474    Ok(())
475}