tuktuk_cli/cmd/
cron.rs

1use std::str::FromStr;
2
3use anchor_client::anchor_lang::InstructionData;
4use clap::{Args, Subcommand};
5use serde::Serialize;
6use solana_sdk::{
7    instruction::Instruction, pubkey::Pubkey, signer::Signer, system_instruction::transfer,
8};
9use tuktuk::cron;
10use tuktuk_program::{
11    compile_transaction,
12    cron::{
13        accounts::{CronJobNameMappingV0, CronJobV0, UserCronJobsV0},
14        client::{accounts::QueueCronTasksV0, args::QueueCronTasksV0 as QueueCronTasksV0Args},
15        types::InitializeCronJobArgsV0,
16    },
17    types::QueueTaskArgsV0,
18    TaskQueueV0, TransactionSourceV0, TriggerV0,
19};
20use tuktuk_sdk::prelude::*;
21
22use super::task_queue::TaskQueueArg;
23use crate::{
24    client::{send_instructions, CliClient},
25    cmd::Opts,
26    result::{anyhow, Result},
27    serde::{print_json, serde_pubkey},
28};
29
30#[derive(Debug, Args)]
31pub struct CronCmd {
32    #[command(subcommand)]
33    pub cmd: Cmd,
34}
35
36#[derive(Debug, Subcommand)]
37pub enum Cmd {
38    Create {
39        #[arg(long)]
40        authority: Option<Pubkey>,
41        #[command(flatten)]
42        task_queue: TaskQueueArg,
43        #[arg(long)]
44        schedule: String,
45        #[arg(long)]
46        name: String,
47        #[arg(long, value_parser = clap::value_parser!(u8).range(0..=15))]
48        free_tasks_per_transaction: u8,
49        #[arg(long, value_parser = clap::value_parser!(u8).range(1..=15))]
50        num_tasks_per_queue_call: u8,
51        #[arg(long, help = "Initial funding amount in lamports", default_value = "0")]
52        funding_amount: u64,
53    },
54    Get {
55        #[command(flatten)]
56        cron: CronArg,
57    },
58    Fund {
59        #[command(flatten)]
60        cron: CronArg,
61        #[arg(long, help = "Amount to fund the cron job with, in lamports")]
62        amount: u64,
63    },
64    Requeue {
65        #[command(flatten)]
66        cron: CronArg,
67        #[arg(
68            long,
69            help = "Force requeue even if the cron job doesn't think it is removed from queue",
70            default_value = "false"
71        )]
72        force: bool,
73    },
74    Close {
75        #[command(flatten)]
76        cron: CronArg,
77    },
78    List {},
79}
80
81#[derive(Debug, Args)]
82pub struct CronArg {
83    #[arg(long = "cron-name", name = "cron-name")]
84    pub name: Option<String>,
85    #[arg(long = "cron-id", name = "cron-id")]
86    pub id: Option<u32>,
87    #[arg(long = "cron-pubkey", name = "cron-pubkey")]
88    pub pubkey: Option<String>,
89}
90
91impl CronArg {
92    pub async fn get_pubkey(&self, client: &CliClient) -> Result<Option<Pubkey>> {
93        let authority = client.payer.pubkey();
94
95        if let Some(pubkey) = &self.pubkey {
96            // Use the provided pubkey directly
97            Ok(Some(Pubkey::from_str(pubkey)?))
98        } else if let Some(id) = self.id {
99            Ok(Some(tuktuk::cron::cron_job_key(&authority, id)))
100        } else if let Some(name) = &self.name {
101            let mapping: CronJobNameMappingV0 = client
102                .as_ref()
103                .anchor_account(&cron::name_mapping_key(&authority, name))
104                .await?
105                .ok_or_else(|| anyhow::anyhow!("Cron job name mapping not found"))?;
106            Ok(Some(mapping.cron_job))
107        } else {
108            Ok(None)
109        }
110    }
111}
112
113impl CronCmd {
114    async fn fund_cron_job_ix(
115        client: &CliClient,
116        cron_job_key: &Pubkey,
117        amount: u64,
118    ) -> Result<Instruction> {
119        let ix = transfer(&client.payer.pubkey(), cron_job_key, amount);
120        Ok(ix)
121    }
122
123    async fn requeue_cron_job_ix(client: &CliClient, cron_job_key: &Pubkey) -> Result<Instruction> {
124        let cron_job: CronJobV0 = client
125            .rpc_client
126            .anchor_account(cron_job_key)
127            .await?
128            .ok_or_else(|| anyhow::anyhow!("Cron job not found: {}", cron_job_key))?;
129        let task_queue: TaskQueueV0 = client
130            .rpc_client
131            .anchor_account(&cron_job.task_queue)
132            .await?
133            .ok_or_else(|| anyhow::anyhow!("Task queue not found: {}", cron_job.task_queue))?;
134        let id = task_queue
135            .next_available_task_id()
136            .ok_or_else(|| anyhow::anyhow!("No available task id"))?;
137        let remaining_accounts = (cron_job.current_transaction_id
138            ..cron_job.current_transaction_id + cron_job.num_tasks_per_queue_call as u32)
139            .map(|i| {
140                Pubkey::find_program_address(
141                    &[
142                        b"cron_job_transaction",
143                        cron_job_key.as_ref(),
144                        &i.to_le_bytes(),
145                    ],
146                    &tuktuk_program::cron::ID,
147                )
148                .0
149            })
150            .collect::<Vec<Pubkey>>();
151        let (queue_tx, _) = compile_transaction(
152            vec![Instruction {
153                program_id: tuktuk_program::cron::ID,
154                accounts: [
155                    QueueCronTasksV0 {
156                        cron_job: *cron_job_key,
157                        task_queue: cron_job.task_queue,
158                        task_return_account_1: tuktuk::cron::task_return_account_1_key(
159                            cron_job_key,
160                        ),
161                        task_return_account_2: tuktuk::cron::task_return_account_2_key(
162                            cron_job_key,
163                        ),
164                        system_program: solana_sdk::system_program::ID,
165                    }
166                    .to_account_metas(None),
167                    remaining_accounts
168                        .iter()
169                        .map(|pubkey| AccountMeta::new_readonly(*pubkey, false))
170                        .collect::<Vec<AccountMeta>>(),
171                ]
172                .concat(),
173                data: QueueCronTasksV0Args {}.data(),
174            }],
175            vec![],
176        )?;
177        let trunc_name = cron_job.name.chars().take(32).collect::<String>();
178        Ok(tuktuk::task::queue(
179            client.rpc_client.as_ref(),
180            client.payer.pubkey(),
181            client.payer.pubkey(),
182            cron_job.task_queue,
183            QueueTaskArgsV0 {
184                id,
185                description: format!("queue {}", trunc_name),
186                trigger: TriggerV0::Now,
187                transaction: TransactionSourceV0::CompiledV0(queue_tx),
188                crank_reward: None,
189                free_tasks: cron_job.num_tasks_per_queue_call + 1,
190            },
191        )
192        .await?
193        .1)
194    }
195
196    pub async fn run(&self, opts: Opts) -> Result {
197        match &self.cmd {
198            Cmd::Create {
199                authority,
200                task_queue,
201                schedule,
202                name,
203                free_tasks_per_transaction,
204                funding_amount,
205                num_tasks_per_queue_call,
206            } => {
207                let client = opts.client().await?;
208                let task_queue_key = task_queue.get_pubkey(&client).await?.ok_or_else(|| {
209                    anyhow::anyhow!(
210                        "Must provide task-queue-name, task-queue-id, or task-queue-pubkey"
211                    )
212                })?;
213
214                let (key, ix) = tuktuk::cron::create(
215                    client.rpc_client.as_ref(),
216                    client.payer.pubkey(),
217                    client.payer.pubkey(),
218                    InitializeCronJobArgsV0 {
219                        name: name.clone(),
220                        schedule: schedule.clone(),
221                        free_tasks_per_transaction: *free_tasks_per_transaction,
222                        num_tasks_per_queue_call: *num_tasks_per_queue_call,
223                    },
224                    *authority,
225                    task_queue_key,
226                )
227                .await?;
228
229                let fund_ix = Self::fund_cron_job_ix(&client, &key, *funding_amount).await?;
230
231                send_instructions(
232                    client.rpc_client.clone(),
233                    &client.payer,
234                    client.opts.ws_url().as_str(),
235                    &[fund_ix, ix],
236                    &[],
237                )
238                .await?;
239
240                let cron_job: CronJobV0 = client
241                    .as_ref()
242                    .anchor_account(&key)
243                    .await?
244                    .ok_or_else(|| anyhow::anyhow!("Task queue not found: {}", key))?;
245                let cron_job_balance = client.rpc_client.get_balance(&key).await?;
246
247                print_json(&CronJob {
248                    pubkey: key,
249                    id: cron_job.id,
250                    name: name.clone(),
251                    user_cron_jobs: cron_job.user_cron_jobs,
252                    task_queue: cron_job.task_queue,
253                    authority: cron_job.authority,
254                    free_tasks_per_transaction: cron_job.free_tasks_per_transaction,
255                    schedule: cron_job.schedule,
256                    current_exec_ts: cron_job.current_exec_ts,
257                    current_transaction_id: cron_job.current_transaction_id,
258                    next_transaction_id: cron_job.next_transaction_id,
259                    balance: cron_job_balance,
260                    num_tasks_per_queue_call: *num_tasks_per_queue_call,
261                    removed_from_queue: cron_job.removed_from_queue,
262                })?;
263            }
264            Cmd::Get { cron } => {
265                let client = opts.client().await?;
266                let cron_job_key = cron.get_pubkey(&client).await?.ok_or_else(|| {
267                    anyhow::anyhow!("Must provide cron-name, cron-id, or cron-pubkey")
268                })?;
269                let cron_job: CronJobV0 = client
270                    .rpc_client
271                    .anchor_account(&cron_job_key)
272                    .await?
273                    .ok_or_else(|| anyhow::anyhow!("Cron job not found: {}", cron_job_key))?;
274
275                let cron_job_balance = client.rpc_client.get_balance(&cron_job_key).await?;
276                let serializable = CronJob {
277                    pubkey: cron_job_key,
278                    id: cron_job.id,
279                    user_cron_jobs: cron_job.user_cron_jobs,
280                    task_queue: cron_job.task_queue,
281                    authority: cron_job.authority,
282                    free_tasks_per_transaction: cron_job.free_tasks_per_transaction,
283                    schedule: cron_job.schedule,
284                    current_exec_ts: cron_job.current_exec_ts,
285                    current_transaction_id: cron_job.current_transaction_id,
286                    next_transaction_id: cron_job.next_transaction_id,
287                    name: cron_job.name,
288                    balance: cron_job_balance,
289                    num_tasks_per_queue_call: cron_job.num_tasks_per_queue_call,
290                    removed_from_queue: cron_job.removed_from_queue,
291                };
292                print_json(&serializable)?;
293            }
294            Cmd::Requeue { cron, force } => {
295                let client = opts.client().await?;
296                let cron_job_key = cron.get_pubkey(&client).await?.ok_or_else(|| {
297                    anyhow::anyhow!("Must provide cron-name, cron-id, or cron-pubkey")
298                })?;
299                let cron_job: CronJobV0 = client
300                    .rpc_client
301                    .anchor_account(&cron_job_key)
302                    .await?
303                    .ok_or_else(|| anyhow::anyhow!("Cron job not found: {}", cron_job_key))?;
304
305                if cron_job.removed_from_queue || *force {
306                    let ix = Self::requeue_cron_job_ix(&client, &cron_job_key).await?;
307                    send_instructions(
308                        client.rpc_client.clone(),
309                        &client.payer,
310                        client.opts.ws_url().as_str(),
311                        &[ix],
312                        &[],
313                    )
314                    .await?;
315                } else {
316                    println!("Cron job does not need to be requeued");
317                }
318            }
319            Cmd::Fund { cron, amount } => {
320                let client = opts.client().await?;
321                let cron_job_key = cron.get_pubkey(&client).await?.ok_or_else(|| {
322                    anyhow::anyhow!("Must provide cron-name, cron-id, or cron-pubkey")
323                })?;
324
325                let cron_job: CronJobV0 = client
326                    .rpc_client
327                    .anchor_account(&cron_job_key)
328                    .await?
329                    .ok_or_else(|| anyhow::anyhow!("Cron job not found: {}", cron_job_key))?;
330
331                let fund_ix = Self::fund_cron_job_ix(&client, &cron_job_key, *amount).await?;
332                let mut ixs = vec![fund_ix];
333
334                if cron_job.removed_from_queue {
335                    ixs.push(Self::requeue_cron_job_ix(&client, &cron_job_key).await?);
336                }
337
338                send_instructions(
339                    client.rpc_client.clone(),
340                    &client.payer,
341                    client.opts.ws_url().as_str(),
342                    &ixs,
343                    &[],
344                )
345                .await?;
346            }
347            Cmd::Close { cron } => {
348                let client: CliClient = opts.client().await?;
349                let cron_job_key = cron.get_pubkey(&client).await?.ok_or_else(|| {
350                    anyhow::anyhow!("Must provide cron-name, cron-id, or cron-pubkey")
351                })?;
352                let cron_job: CronJobV0 = client
353                    .rpc_client
354                    .anchor_account(&cron_job_key)
355                    .await?
356                    .ok_or_else(|| anyhow::anyhow!("Task queue not found: {}", cron_job_key))?;
357
358                let ix = tuktuk::cron::close(
359                    client.as_ref(),
360                    cron_job_key,
361                    client.payer.pubkey(),
362                    Some(cron_job.authority),
363                    Some(client.payer.pubkey()),
364                )
365                .await?;
366                send_instructions(
367                    client.rpc_client.clone(),
368                    &client.payer,
369                    client.opts.ws_url().as_str(),
370                    &[ix],
371                    &[],
372                )
373                .await?;
374            }
375            Cmd::List {} => {
376                let client = opts.client().await?;
377                let user_cron_jobs_pubkey = cron::user_cron_jobs_key(&client.payer.pubkey());
378
379                let user_cron_jobs: UserCronJobsV0 = client
380                    .as_ref()
381                    .anchor_account(&user_cron_jobs_pubkey)
382                    .await?
383                    .ok_or_else(|| anyhow!("User cron jobs account not found"))?;
384                let cron_job_keys = tuktuk::cron::keys(&client.payer.pubkey(), &user_cron_jobs)?;
385                let cron_jobs = client
386                    .as_ref()
387                    .anchor_accounts::<CronJobV0>(&cron_job_keys)
388                    .await?;
389
390                let mut json_cron_jobs = Vec::new();
391                for (pubkey, maybe_cron_job) in cron_jobs {
392                    if let Some(cron_job) = maybe_cron_job {
393                        let cron_job_balance = client.rpc_client.get_balance(&pubkey).await?;
394                        json_cron_jobs.push(CronJob {
395                            pubkey,
396                            id: cron_job.id,
397                            user_cron_jobs: cron_job.user_cron_jobs,
398                            task_queue: cron_job.task_queue,
399                            authority: cron_job.authority,
400                            free_tasks_per_transaction: cron_job.free_tasks_per_transaction,
401                            schedule: cron_job.schedule,
402                            current_exec_ts: cron_job.current_exec_ts,
403                            current_transaction_id: cron_job.current_transaction_id,
404                            next_transaction_id: cron_job.next_transaction_id,
405                            removed_from_queue: cron_job.removed_from_queue,
406                            name: cron_job.name,
407                            balance: cron_job_balance,
408                            num_tasks_per_queue_call: cron_job.num_tasks_per_queue_call,
409                        });
410                    }
411                }
412                print_json(&json_cron_jobs)?;
413            }
414        }
415
416        Ok(())
417    }
418}
419
420#[derive(Serialize)]
421pub struct CronJob {
422    #[serde(with = "serde_pubkey")]
423    pub pubkey: Pubkey,
424    pub id: u32,
425    #[serde(with = "serde_pubkey")]
426    pub user_cron_jobs: Pubkey,
427    #[serde(with = "serde_pubkey")]
428    pub task_queue: Pubkey,
429    #[serde(with = "serde_pubkey")]
430    pub authority: Pubkey,
431    pub free_tasks_per_transaction: u8,
432    pub schedule: String,
433    pub name: String,
434    pub current_exec_ts: i64,
435    pub current_transaction_id: u32,
436    pub next_transaction_id: u32,
437    pub num_tasks_per_queue_call: u8,
438    pub removed_from_queue: bool,
439    pub balance: u64,
440}