tuktuk_cli/cmd/
cron.rs

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