gmsol_sdk/client/ops/
timelock.rs

1use std::{future::Future, ops::Deref, sync::Arc};
2
3use gmsol_programs::{
4    constants::roles,
5    gmsol_timelock::{
6        accounts::{Executor, InstructionHeader},
7        client::{accounts, args},
8    },
9};
10use gmsol_solana_utils::transaction_builder::TransactionBuilder;
11use gmsol_utils::{instruction::InstructionAccess, oracle::PriceProviderKind, role::RoleKey};
12use solana_sdk::{
13    instruction::{AccountMeta, Instruction},
14    pubkey::Pubkey,
15    signer::Signer,
16    system_program,
17};
18
19use crate::utils::zero_copy::ZeroCopy;
20
21/// Timelock instructions.
22pub trait TimelockOps<C> {
23    /// Initialize [`TimelockConfig`](gmsol_programs::gmsol_timelock::accounts::TimelockConfig) account.
24    fn initialize_timelock_config(
25        &self,
26        store: &Pubkey,
27        initial_delay: u32,
28    ) -> TransactionBuilder<C, Pubkey>;
29
30    /// Increase timelock delay.
31    fn increase_timelock_delay(&self, store: &Pubkey, delta: u32) -> TransactionBuilder<C>;
32
33    /// Initialize [`Executor`] account.
34    fn initialize_executor(
35        &self,
36        store: &Pubkey,
37        role: &str,
38    ) -> crate::Result<TransactionBuilder<C, Pubkey>>;
39
40    /// Create a timelocked instruction buffer for the given instruction.
41    fn create_timelocked_instruction(
42        &self,
43        store: &Pubkey,
44        role: &str,
45        buffer: impl Signer + 'static,
46        instruction: Instruction,
47    ) -> crate::Result<TransactionBuilder<C, Pubkey>>;
48
49    /// Approve timelocked instruction.
50    fn approve_timelocked_instruction(
51        &self,
52        store: &Pubkey,
53        buffer: &Pubkey,
54        role_hint: Option<&str>,
55    ) -> impl Future<Output = crate::Result<TransactionBuilder<C>>>;
56
57    /// Approve timelocked instruction.
58    fn approve_timelocked_instructions(
59        &self,
60        store: &Pubkey,
61        buffers: impl IntoIterator<Item = Pubkey>,
62        role_hint: Option<&str>,
63    ) -> impl Future<Output = crate::Result<TransactionBuilder<C>>>;
64
65    /// Cancel timelocked instruction.
66    fn cancel_timelocked_instruction(
67        &self,
68        store: &Pubkey,
69        buffer: &Pubkey,
70        executor_hint: Option<&Pubkey>,
71        rent_receiver_hint: Option<&Pubkey>,
72    ) -> impl Future<Output = crate::Result<TransactionBuilder<C>>>;
73
74    /// Cancel timelocked instruction.
75    fn cancel_timelocked_instructions(
76        &self,
77        store: &Pubkey,
78        buffers: impl IntoIterator<Item = Pubkey>,
79        executor_hint: Option<&Pubkey>,
80        rent_receiver_hint: Option<&Pubkey>,
81    ) -> impl Future<Output = crate::Result<TransactionBuilder<C>>>;
82
83    /// Execute timelocked instruction.
84    fn execute_timelocked_instruction(
85        &self,
86        store: &Pubkey,
87        buffer: &Pubkey,
88        hint: Option<ExecuteTimelockedInstructionHint<'_>>,
89    ) -> impl Future<Output = crate::Result<TransactionBuilder<C>>>;
90
91    /// Timelock-bypassed revoke role.
92    fn timelock_bypassed_revoke_role(
93        &self,
94        store: &Pubkey,
95        role: &str,
96        address: &Pubkey,
97    ) -> TransactionBuilder<C>;
98
99    /// Timelock-bypassed set expected price provider.
100    fn timelock_bypassed_set_epxected_price_provider(
101        &self,
102        store: &Pubkey,
103        token_map: &Pubkey,
104        token: &Pubkey,
105        new_expected_price_provider: PriceProviderKind,
106    ) -> TransactionBuilder<C>;
107}
108
109impl<C: Deref<Target = impl Signer> + Clone> TimelockOps<C> for crate::Client<C> {
110    fn initialize_timelock_config(
111        &self,
112        store: &Pubkey,
113        initial_delay: u32,
114    ) -> TransactionBuilder<C, Pubkey> {
115        let config = self.find_timelock_config_address(store);
116        let executor = self
117            .find_executor_address(store, roles::ADMIN)
118            .expect("must success");
119        self.timelock_transaction()
120            .anchor_args(args::InitializeConfig {
121                delay: initial_delay,
122            })
123            .anchor_accounts(accounts::InitializeConfig {
124                authority: self.payer(),
125                store: *store,
126                timelock_config: config,
127                executor,
128                wallet: self.find_executor_wallet_address(&executor),
129                store_program: *self.store_program_id(),
130                system_program: system_program::ID,
131            })
132            .output(config)
133    }
134
135    fn increase_timelock_delay(&self, store: &Pubkey, delta: u32) -> TransactionBuilder<C> {
136        self.timelock_transaction()
137            .anchor_args(args::IncreaseDelay { delta })
138            .anchor_accounts(accounts::IncreaseDelay {
139                authority: self.payer(),
140                store: *store,
141                timelock_config: self.find_timelock_config_address(store),
142                store_program: *self.store_program_id(),
143            })
144    }
145
146    fn initialize_executor(
147        &self,
148        store: &Pubkey,
149        role: &str,
150    ) -> crate::Result<TransactionBuilder<C, Pubkey>> {
151        let executor = self.find_executor_address(store, role)?;
152        let wallet = self.find_executor_wallet_address(&executor);
153        Ok(self
154            .timelock_transaction()
155            .anchor_args(args::InitializeExecutor {
156                role: role.to_string(),
157            })
158            .anchor_accounts(accounts::InitializeExecutor {
159                payer: self.payer(),
160                store: *store,
161                executor,
162                wallet,
163                system_program: system_program::ID,
164            })
165            .output(executor))
166    }
167
168    fn create_timelocked_instruction(
169        &self,
170        store: &Pubkey,
171        role: &str,
172        buffer: impl Signer + 'static,
173        mut instruction: Instruction,
174    ) -> crate::Result<TransactionBuilder<C, Pubkey>> {
175        let executor = self.find_executor_address(store, role)?;
176        let instruction_buffer = buffer.pubkey();
177
178        let mut signers = vec![];
179
180        instruction
181            .accounts
182            .iter_mut()
183            .enumerate()
184            .for_each(|(idx, account)| {
185                if account.is_signer {
186                    signers.push(idx as u16);
187                }
188                account.is_signer = false;
189            });
190
191        let num_accounts = instruction
192            .accounts
193            .len()
194            .try_into()
195            .map_err(|_| crate::Error::custom("too many accounts"))?;
196
197        let data_len = instruction
198            .data
199            .len()
200            .try_into()
201            .map_err(|_| crate::Error::custom("data too long"))?;
202
203        let rpc = self
204            .timelock_transaction()
205            .anchor_args(args::CreateInstructionBuffer {
206                num_accounts,
207                data_len,
208                data: instruction.data,
209                signers,
210            })
211            .anchor_accounts(accounts::CreateInstructionBuffer {
212                authority: self.payer(),
213                store: *store,
214                executor,
215                instruction_buffer,
216                instruction_program: instruction.program_id,
217                store_program: *self.store_program_id(),
218                system_program: system_program::ID,
219            })
220            .accounts(instruction.accounts)
221            .owned_signer(Arc::new(buffer))
222            .output(instruction_buffer);
223        Ok(rpc)
224    }
225
226    async fn approve_timelocked_instruction(
227        &self,
228        store: &Pubkey,
229        buffer: &Pubkey,
230        role_hint: Option<&str>,
231    ) -> crate::Result<TransactionBuilder<C>> {
232        let role = match role_hint {
233            Some(role) => role.to_string(),
234            None => {
235                let instruction_header = self
236                    .account::<ZeroCopy<InstructionHeader>>(buffer)
237                    .await?
238                    .ok_or(crate::Error::NotFound)?
239                    .0;
240                let executor = &instruction_header.executor;
241                let executor = self
242                    .account::<ZeroCopy<Executor>>(executor)
243                    .await?
244                    .ok_or(crate::Error::NotFound)?
245                    .0;
246                executor.role_name()?.to_string()
247            }
248        };
249        let executor = self.find_executor_address(store, &role)?;
250        Ok(self
251            .timelock_transaction()
252            .anchor_args(args::ApproveInstruction { role })
253            .anchor_accounts(accounts::ApproveInstruction {
254                authority: self.payer(),
255                store: *store,
256                executor,
257                instruction: *buffer,
258                store_program: *self.store_program_id(),
259            }))
260    }
261
262    async fn approve_timelocked_instructions(
263        &self,
264        store: &Pubkey,
265        buffers: impl IntoIterator<Item = Pubkey>,
266        role_hint: Option<&str>,
267    ) -> crate::Result<TransactionBuilder<C>> {
268        let mut buffers = buffers.into_iter().peekable();
269        let buffer = buffers
270            .peek()
271            .ok_or_else(|| crate::Error::custom("no instructions to appove"))?;
272        let role = match role_hint {
273            Some(role) => role.to_string(),
274            None => {
275                let instruction_header = self
276                    .account::<ZeroCopy<InstructionHeader>>(buffer)
277                    .await?
278                    .ok_or(crate::Error::NotFound)?
279                    .0;
280                let executor = &instruction_header.executor;
281                let executor = self
282                    .account::<ZeroCopy<Executor>>(executor)
283                    .await?
284                    .ok_or(crate::Error::NotFound)?
285                    .0;
286                executor.role_name()?.to_string()
287            }
288        };
289        let executor = self.find_executor_address(store, &role)?;
290        Ok(self
291            .timelock_transaction()
292            .anchor_args(args::ApproveInstructions { role })
293            .anchor_accounts(accounts::ApproveInstructions {
294                authority: self.payer(),
295                store: *store,
296                executor,
297                store_program: *self.store_program_id(),
298            })
299            .accounts(
300                buffers
301                    .map(|pubkey| AccountMeta {
302                        pubkey,
303                        is_signer: false,
304                        is_writable: true,
305                    })
306                    .collect::<Vec<_>>(),
307            ))
308    }
309
310    async fn cancel_timelocked_instruction(
311        &self,
312        store: &Pubkey,
313        buffer: &Pubkey,
314        executor_hint: Option<&Pubkey>,
315        rent_receiver_hint: Option<&Pubkey>,
316    ) -> crate::Result<TransactionBuilder<C>> {
317        let (executor, rent_receiver) = match (executor_hint, rent_receiver_hint) {
318            (Some(executor), Some(rent_receiver)) => (*executor, *rent_receiver),
319            _ => {
320                let instruction_header = self
321                    .account::<ZeroCopy<InstructionHeader>>(buffer)
322                    .await?
323                    .ok_or(crate::Error::NotFound)?
324                    .0;
325                (
326                    instruction_header.executor,
327                    instruction_header.rent_receiver,
328                )
329            }
330        };
331        Ok(self
332            .timelock_transaction()
333            .anchor_args(args::CancelInstruction {})
334            .anchor_accounts(accounts::CancelInstruction {
335                authority: self.payer(),
336                store: *store,
337                executor,
338                rent_receiver,
339                instruction: *buffer,
340                store_program: *self.store_program_id(),
341            }))
342    }
343
344    async fn cancel_timelocked_instructions(
345        &self,
346        store: &Pubkey,
347        buffers: impl IntoIterator<Item = Pubkey>,
348        executor_hint: Option<&Pubkey>,
349        rent_receiver_hint: Option<&Pubkey>,
350    ) -> crate::Result<TransactionBuilder<C>> {
351        let mut buffers = buffers.into_iter().peekable();
352        let buffer = buffers
353            .peek()
354            .ok_or_else(|| crate::Error::custom("no instructions to appove"))?;
355        let (executor, rent_receiver) = match (executor_hint, rent_receiver_hint) {
356            (Some(executor), Some(rent_receiver)) => (*executor, *rent_receiver),
357            _ => {
358                let instruction_header = self
359                    .account::<ZeroCopy<InstructionHeader>>(buffer)
360                    .await?
361                    .ok_or(crate::Error::NotFound)?
362                    .0;
363                (
364                    instruction_header.executor,
365                    instruction_header.rent_receiver,
366                )
367            }
368        };
369        Ok(self
370            .timelock_transaction()
371            .anchor_args(args::CancelInstructions {})
372            .anchor_accounts(accounts::CancelInstructions {
373                authority: self.payer(),
374                store: *store,
375                executor,
376                rent_receiver,
377                store_program: *self.store_program_id(),
378            })
379            .accounts(
380                buffers
381                    .map(|pubkey| AccountMeta {
382                        pubkey,
383                        is_signer: false,
384                        is_writable: true,
385                    })
386                    .collect::<Vec<_>>(),
387            ))
388    }
389
390    async fn execute_timelocked_instruction(
391        &self,
392        store: &Pubkey,
393        buffer: &Pubkey,
394        hint: Option<ExecuteTimelockedInstructionHint<'_>>,
395    ) -> crate::Result<TransactionBuilder<C>> {
396        let (executor, rent_receiver, mut accounts) = match hint {
397            Some(hint) => (
398                *hint.executor,
399                *hint.rent_receiver,
400                hint.accounts.to_owned(),
401            ),
402            None => {
403                let buffer = self
404                    .instruction_buffer(buffer)
405                    .await?
406                    .ok_or(crate::Error::NotFound)?;
407                let executor = buffer.header.executor;
408                let rent_receiver = buffer.header.rent_receiver;
409                (
410                    executor,
411                    rent_receiver,
412                    buffer.accounts().map(AccountMeta::from).collect(),
413                )
414            }
415        };
416
417        let wallet = self.find_executor_wallet_address(&executor);
418
419        accounts
420            .iter_mut()
421            .filter(|a| a.pubkey == wallet)
422            .for_each(|a| a.is_signer = false);
423
424        Ok(self
425            .timelock_transaction()
426            .anchor_args(args::ExecuteInstruction {})
427            .anchor_accounts(accounts::ExecuteInstruction {
428                authority: self.payer(),
429                store: *store,
430                timelock_config: self.find_timelock_config_address(store),
431                executor,
432                wallet,
433                rent_receiver,
434                instruction: *buffer,
435                store_program: *self.store_program_id(),
436            })
437            .accounts(accounts))
438    }
439
440    fn timelock_bypassed_revoke_role(
441        &self,
442        store: &Pubkey,
443        role: &str,
444        address: &Pubkey,
445    ) -> TransactionBuilder<C> {
446        let executor = self
447            .find_executor_address(store, roles::ADMIN)
448            .expect("must success");
449        let wallet = self.find_executor_wallet_address(&executor);
450        self.timelock_transaction()
451            .anchor_args(args::RevokeRole {
452                role: role.to_string(),
453            })
454            .anchor_accounts(accounts::RevokeRole {
455                authority: self.payer(),
456                store: *store,
457                executor,
458                wallet,
459                user: *address,
460                store_program: *self.store_program_id(),
461            })
462    }
463
464    fn timelock_bypassed_set_epxected_price_provider(
465        &self,
466        store: &Pubkey,
467        token_map: &Pubkey,
468        token: &Pubkey,
469        new_expected_price_provider: PriceProviderKind,
470    ) -> TransactionBuilder<C> {
471        let executor = self
472            .find_executor_address(store, RoleKey::MARKET_KEEPER)
473            .expect("must success");
474        let wallet = self.find_executor_wallet_address(&executor);
475        self.timelock_transaction()
476            .anchor_args(args::SetExpectedPriceProvider {
477                new_expected_price_provider: new_expected_price_provider.into(),
478            })
479            .anchor_accounts(accounts::SetExpectedPriceProvider {
480                authority: self.payer(),
481                store: *store,
482                executor,
483                wallet,
484                token_map: *token_map,
485                token: *token,
486                store_program: *self.store_program_id(),
487                system_program: system_program::ID,
488            })
489    }
490}
491
492/// Execute timelocked instruction hint.
493#[derive(Debug)]
494pub struct ExecuteTimelockedInstructionHint<'a> {
495    /// Executor.
496    pub executor: &'a Pubkey,
497    /// Rent receiver.
498    pub rent_receiver: &'a Pubkey,
499    /// Accounts.
500    pub accounts: &'a [AccountMeta],
501}