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 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 vec![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 vec![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 vec![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}