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 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}