gmsol_timelock/states/
instruction.rs

1use std::cell::{Ref, RefMut};
2
3use anchor_lang::prelude::*;
4use gmsol_store::{
5    utils::pubkey::{optional_address, DEFAULT_PUBKEY},
6    CoreError,
7};
8use gmsol_utils::{instruction::InstructionError, InitSpace};
9
10use crate::states::create_executor_wallet_pda;
11
12pub use gmsol_utils::instruction::{InstructionAccess, InstructionAccount, InstructionAccountFlag};
13
14const MAX_FLAGS: usize = 8;
15
16/// Instruction Header.
17#[account(zero_copy)]
18pub struct InstructionHeader {
19    version: u8,
20    flags: InstructionFlagContainer,
21    wallet_bump: u8,
22    padding_0: [u8; 5],
23    /// Approved ts.
24    approved_at: i64,
25    /// Executor.
26    pub(crate) executor: Pubkey,
27    /// Program ID.
28    program_id: Pubkey,
29    /// Number of accounts.
30    num_accounts: u16,
31    /// Data length.
32    data_len: u16,
33    padding_1: [u8; 12],
34    pub(crate) rent_receiver: Pubkey,
35    approver: Pubkey,
36    reserved: [u8; 64],
37}
38
39impl InstructionHeader {
40    /// Get space.
41    pub(crate) fn init_space(num_accounts: u16, data_len: u16) -> usize {
42        std::mem::size_of::<Self>()
43            + usize::from(data_len)
44            + usize::from(num_accounts) * InstructionAccount::INIT_SPACE
45    }
46
47    /// Approve.
48    pub(crate) fn approve(&mut self, approver: Pubkey) -> Result<()> {
49        require!(!self.is_approved(), CoreError::PreconditionsAreNotMet);
50        require_keys_eq!(
51            self.approver,
52            DEFAULT_PUBKEY,
53            CoreError::PreconditionsAreNotMet
54        );
55
56        require_keys_neq!(approver, DEFAULT_PUBKEY, CoreError::InvalidArgument);
57
58        let clock = Clock::get()?;
59
60        self.flags.set_flag(InstructionFlag::Approved, true);
61        self.approved_at = clock.unix_timestamp;
62        self.approver = approver;
63
64        Ok(())
65    }
66
67    /// Returns whether the instruction is approved.
68    pub fn is_approved(&self) -> bool {
69        self.flags.get_flag(InstructionFlag::Approved)
70    }
71
72    /// Get the approved timestamp.
73    pub fn approved_at(&self) -> Option<i64> {
74        self.is_approved().then_some(self.approved_at)
75    }
76
77    /// Get approver.
78    pub fn apporver(&self) -> Option<&Pubkey> {
79        optional_address(&self.approver)
80    }
81
82    /// Return whether the instruction is executable.
83    pub fn is_executable(&self, delay: u32) -> Result<bool> {
84        let now = Clock::get()?.unix_timestamp;
85        let Some(approved_at) = self.approved_at() else {
86            return Ok(false);
87        };
88        let executable_at = approved_at.saturating_add_unsigned(delay as u64);
89        Ok(now >= executable_at)
90    }
91
92    /// Get executor.
93    pub fn executor(&self) -> &Pubkey {
94        &self.executor
95    }
96
97    /// Get executor wallet.
98    pub fn wallet(&self) -> Result<Pubkey> {
99        match create_executor_wallet_pda(self.executor(), self.wallet_bump, &crate::ID) {
100            Ok(address) => Ok(address),
101            Err(err) => {
102                msg!("[Ix Buffer] failed to create wallet pda: {}", err);
103                err!(CoreError::Internal)
104            }
105        }
106    }
107
108    /// Get rent receiver.
109    pub fn rent_receiver(&self) -> &Pubkey {
110        &self.rent_receiver
111    }
112}
113
114/// Flags of Instruction.
115#[derive(num_enum::IntoPrimitive)]
116#[repr(u8)]
117pub enum InstructionFlag {
118    /// Approved.
119    Approved,
120    // CHECK: cannot have more than `MAX_FLAGS` flags.
121}
122
123gmsol_utils::flags!(InstructionFlag, MAX_FLAGS, u8);
124
125/// Reference to the instruction.
126pub struct InstructionRef<'a> {
127    header: Ref<'a, InstructionHeader>,
128    data: Ref<'a, [u8]>,
129    accounts: Ref<'a, [u8]>,
130}
131
132impl InstructionRef<'_> {
133    pub(crate) fn header(&self) -> &InstructionHeader {
134        &self.header
135    }
136}
137
138/// Instruction Loader.
139pub trait InstructionLoader<'info> {
140    /// Load instruction.
141    fn load_instruction(&self) -> Result<InstructionRef>;
142
143    /// Load and initialize the instruction.
144    #[allow(clippy::too_many_arguments)]
145    fn load_and_init_instruction(
146        &self,
147        executor: Pubkey,
148        wallet_bump: u8,
149        rent_receiver: Pubkey,
150        program_id: Pubkey,
151        data: &[u8],
152        accounts: &[AccountInfo<'info>],
153        signers: &[u16],
154    ) -> Result<InstructionRef>;
155}
156
157impl<'info> InstructionLoader<'info> for AccountLoader<'info, InstructionHeader> {
158    fn load_instruction(&self) -> Result<InstructionRef> {
159        // Check the account.
160        self.load()?;
161
162        let data = self.as_ref().try_borrow_data()?;
163
164        let (_disc, remaining_data) = Ref::map_split(data, |d| d.split_at(8));
165        let (header, remaining_data) = Ref::map_split(remaining_data, |d| {
166            d.split_at(std::mem::size_of::<InstructionHeader>())
167        });
168        let header = Ref::map(header, bytemuck::from_bytes::<InstructionHeader>);
169        let data_len = usize::from(header.data_len);
170        let (data, accounts) = Ref::map_split(remaining_data, |d| d.split_at(data_len));
171
172        let expected_accounts_len = usize::from(header.num_accounts);
173        require_eq!(accounts.len(), expected_accounts_len, CoreError::Internal);
174
175        Ok(InstructionRef {
176            header,
177            data,
178            accounts,
179        })
180    }
181
182    fn load_and_init_instruction(
183        &self,
184        executor: Pubkey,
185        wallet_bump: u8,
186        rent_receiver: Pubkey,
187        program_id: Pubkey,
188        instruction_data: &[u8],
189        instruction_accounts: &[AccountInfo<'info>],
190        signers: &[u16],
191    ) -> Result<InstructionRef> {
192        use gmsol_store::utils::dynamic_access::get_mut;
193
194        // Initialize the header.
195        {
196            let data_len = instruction_data.len().try_into()?;
197            let num_accounts = instruction_accounts.len().try_into()?;
198            let mut header = self.load_init()?;
199            header.wallet_bump = wallet_bump;
200            header.executor = executor;
201            header.program_id = program_id;
202            header.num_accounts = num_accounts;
203            header.data_len = data_len;
204            header.rent_receiver = rent_receiver;
205
206            drop(header);
207
208            self.exit(&crate::ID)?;
209        }
210
211        // Initialize remaining data.
212        {
213            // Check the account.
214            self.load_mut()?;
215
216            let data = self.as_ref().try_borrow_mut_data()?;
217
218            msg!("[Timelock] buffer size: {}", data.len());
219
220            let (_disc, remaining_data) = RefMut::map_split(data, |d| d.split_at_mut(8));
221            let (header, remaining_data) = RefMut::map_split(remaining_data, |d| {
222                d.split_at_mut(std::mem::size_of::<InstructionHeader>())
223            });
224            let header = RefMut::map(header, bytemuck::from_bytes_mut::<InstructionHeader>);
225            let data_len = usize::from(header.data_len);
226            let (mut data, mut accounts) =
227                RefMut::map_split(remaining_data, |d| d.split_at_mut(data_len));
228
229            data.copy_from_slice(instruction_data);
230
231            let wallet = header.wallet()?;
232
233            for (idx, account) in instruction_accounts.iter().enumerate() {
234                let idx_u16: u16 = idx
235                    .try_into()
236                    .map_err(|_| error!(CoreError::InvalidArgument))?;
237                let dst = get_mut::<InstructionAccount>(&mut accounts, idx)
238                    .ok_or_else(|| error!(CoreError::InvalidArgument))?;
239
240                let address = account.key();
241                let is_signer = signers.contains(&idx_u16);
242                if is_signer {
243                    // Currently only the executor wallet is allowed to be a signer.
244                    require_keys_eq!(wallet, address, CoreError::InvalidArgument);
245                }
246
247                dst.pubkey = address;
248                dst.flags
249                    .set_flag(InstructionAccountFlag::Signer, is_signer);
250                dst.flags
251                    .set_flag(InstructionAccountFlag::Writable, account.is_writable);
252            }
253        }
254
255        self.load_instruction()
256    }
257}
258
259impl InstructionAccess for InstructionRef<'_> {
260    fn wallet(&self) -> std::result::Result<Pubkey, InstructionError> {
261        self.header
262            .wallet()
263            .map_err(|_| InstructionError::FailedToGetWallet)
264    }
265
266    fn program_id(&self) -> &Pubkey {
267        &self.header.program_id
268    }
269
270    fn data(&self) -> &[u8] {
271        &self.data
272    }
273
274    fn num_accounts(&self) -> usize {
275        usize::from(self.header.num_accounts)
276    }
277
278    fn accounts(&self) -> impl Iterator<Item = &InstructionAccount> {
279        use gmsol_store::utils::dynamic_access::get;
280
281        let num_accounts = self.num_accounts();
282
283        (0..num_accounts).map(|idx| get(&self.accounts, idx).expect("must exist"))
284    }
285}
286
287/// Utils for using instruction buffer.
288#[cfg(feature = "utils")]
289pub mod utils {
290
291    use anchor_lang::{prelude::Pubkey, AccountDeserialize};
292    use bytes::Bytes;
293    use gmsol_store::utils::de;
294    use gmsol_utils::instruction::InstructionError;
295
296    use super::{InstructionAccess, InstructionHeader};
297
298    /// Instruction Buffer.
299    pub struct InstructionBuffer {
300        /// Get header.
301        pub header: InstructionHeader,
302        data: Bytes,
303        accounts: Bytes,
304    }
305
306    impl InstructionAccess for InstructionBuffer {
307        fn wallet(&self) -> Result<Pubkey, InstructionError> {
308            self.header
309                .wallet()
310                .map_err(|_| InstructionError::FailedToGetWallet)
311        }
312
313        fn program_id(&self) -> &Pubkey {
314            &self.header.program_id
315        }
316
317        fn data(&self) -> &[u8] {
318            &self.data
319        }
320
321        fn num_accounts(&self) -> usize {
322            usize::from(self.header.num_accounts)
323        }
324
325        fn accounts(&self) -> impl Iterator<Item = &super::InstructionAccount> {
326            use gmsol_store::utils::dynamic_access::get;
327
328            let num_accounts = self.num_accounts();
329
330            (0..num_accounts).map(|idx| get(&self.accounts, idx).expect("must exist"))
331        }
332    }
333
334    impl AccountDeserialize for InstructionBuffer {
335        fn try_deserialize(buf: &mut &[u8]) -> anchor_lang::Result<Self> {
336            de::check_discriminator::<InstructionHeader>(buf)?;
337            Self::try_deserialize_unchecked(buf)
338        }
339
340        fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result<Self> {
341            let header = de::try_deserailize_unchecked::<InstructionHeader>(buf)?;
342            let (_disc, data) = buf.split_at(8);
343            let (_header, remaining_data) = data.split_at(std::mem::size_of::<InstructionHeader>());
344            let data_len = usize::from(header.data_len);
345            let (data, accounts) = remaining_data.split_at(data_len);
346            Ok(Self {
347                header,
348                data: Bytes::copy_from_slice(data),
349                accounts: Bytes::copy_from_slice(accounts),
350            })
351        }
352    }
353}