In this guide, we will learn how programs can create and manage threadsdirectly using CPIs.
Learn how to create a thread via CPI.
Build an automated counter program that increments itself.
Secure our program endpoints against spam and unwanted callers.
All code are open source and tested, feel free to grab and fork the examples.
1. Building a counter program
anchorinitcountercdcounter
Let's begin by opening up the program file located at programs/counter/src/lib.rs. Here, we'll build a simple counter program that tracks an incrementing integer.
struct Counter – We start by defining a Counter account to hold our counter state.
fn increment() – We declare an instruction to increment the counter value.
struct Increment<'info> – We create an Anchor accounts struct to set up constraints on the increment instruction. We will see in the next section, how to use the thread and thread_authority fields.
use anchor_lang::prelude::*;// 1️⃣ We define an account to hold our counter state#[account]pubstructCounter {pub current_value:u64, // the value of the counterpub updated_at:i64, // last time the counter has been updated}#[program]pubmod counter {use super::*;// 2️⃣ We define an instruction to mutate the `Counter`pubfnincrement(ctx:Context<Increment>) ->Result<()> { ctx.accounts.counter.current_value = ctx.accounts.counter.current_value.checked_add(1).unwrap(); ctx.accounts.counter.updated_at =Clock::get().unwrap().unix_timestamp;Ok(()) }}/// Seed for `Counter` account Program Derived Address/// ⚠ Make sure it matches whatever you are using on the client-sidepubconst SEED_COUNTER:&[u8] =b"counter";// 3️⃣ We define constraints for the `increment` instruction with Anchor macros#[derive(Accounts)]pubstructIncrement<'info> {/// The counter account. #[account(mut, seeds = [SEED_COUNTER], bump)]pub counter:Account<'info, Counter>,/// Verify that only this thread can execute the Increment Instruction #[account(signer, constraint = thread.authority.eq(&thread_authority.key()))]pub thread:Account<'info, Thread>,/// The Thread Admin/// The authority that was used as a seed to derive the thread address/// `thread_authority` should equal `thread.thread_authority` #[account(seeds = [THREAD_AUTHORITY_SEED], bump)]pub thread_authority:SystemAccount<'info>,}
2. Getting familiar with the thread program
In the previous guide, we created threads using the Typescript SDK. In Solana, everything is an account and threads are no different. We were just asking the Clockwork thread program to create thread accounts for us. Similar to the Token Program maintained by the Solana team, the Clockwork Thread Program is a program deployed and maintained by the Clockwork team.
Instead of submitting transactions to the RPC, we can very well interact with that program using CPIs. Here's an example of instructions provided by the thread program via the Clockwork SDK for programs.
thread_create – Create a Thread with a target instruction to run.
thread_delete – Delete a Thread and withdraw the funds.
thread_update – Update a Thread settings; instructions, triggers, etc.
3. Creating a thread via CPI
We will take the increment counter instruction and instead of running it by ourselves, we create a thread and make it increment the counter on our behalf. Let's create that thread, instead of crafting the instructions by hand, let's install theClockwork SDK:
cargoaddclockwork-sdk@~2.0.1
Let's head back to our program file located at programs/counter/src/lib.rs, what follows are the typical steps to create a thread via CPI:
target_ix – We start by defining the instruction to run by our thread.
trigger – We define the conditions for our thread to wake and execute.
clockwork_sdk::cpi::thread_create – We use this helper to create thread CPI.
use anchor_lang::prelude::*;use anchor_lang::InstructionData;use anchor_lang::solana_program::{ instruction::Instruction, native_token::LAMPORTS_PER_SOL, system_program};// 0️⃣ Import the Clockwork SDK.use clockwork_sdk::state::{Thread, ThreadAccount};...pubmod counter {...pubfnincrement(ctx:Context<Increment>) ->Result<()> { ... }pubfninitialize(ctx:Context<Initialize>, thread_id:Vec<u8>)->Result<()> {// Get accounts.let system_program =&ctx.accounts.system_program;let clockwork_program =&ctx.accounts.clockwork_program;let payer =&ctx.accounts.payer;let thread =&ctx.accounts.thread;let thread_authority =&ctx.accounts.thread_authority;let counter =&mut ctx.accounts.counter;// 1️⃣ Prepare an instruction to automate. // In this case, we will automate the Increment instruction.let target_ix =Instruction { program_id: ID, accounts:crate::accounts::Increment { counter: counter.key(), thread: thread.key(), thread_authority: thread_authority.key(), }.to_account_metas(Some(true)), data:crate::instruction::Increment {}.data(), };// 2️⃣ Define a trigger for the thread.let trigger = clockwork_sdk::state::Trigger::Cron { schedule:"*/10 * * * * * *".into(), skippable:true, };// 3️⃣ Create a Thread via CPIlet bump =*ctx.bumps.get("thread_authority").unwrap(); clockwork_sdk::cpi::thread_create( CpiContext::new_with_signer( clockwork_program.to_account_info(), clockwork_sdk::cpi::ThreadCreate { payer: payer.to_account_info(), system_program: system_program.to_account_info(), thread: thread.to_account_info(), authority: thread_authority.to_account_info(), },&[&[THREAD_AUTHORITY_SEED, &[bump]]], ), LAMPORTS_PER_SOL, // amount thread_id, // idvec![target_ix.into()], // instructions trigger, // trigger )?;Ok(()) }}/// Seed for deriving the `Counter` account PDA.pubconst SEED_COUNTER:&[u8] =b"counter";/// Seed for thread_authority pda/// ⚠️ Make sure it matches whatever you are using on the client-sidepubconst THREAD_AUTHORITY_SEED:&[u8] =b"authority";#[derive(Accounts)]#[instruction(thread_id:Vec<u8 >)]pubstructInitialize<'info> {/// The counter account to initialize. #[account( init, payer = payer, seeds = [SEED_COUNTER], bump, space =8+ std::mem::size_of::< Counter> (), )]pub counter:Account<'info, Counter>,/// The signer who will pay to initialize the program./// (not to be confused with the thread executions). #[account(mut)]pub payer:Signer<'info>,/// The Clockwork thread program. #[account(address = clockwork_sdk::ID)]pub clockwork_program:Program<'info, clockwork_sdk::ThreadProgram>,/// The Solana system program. #[account(address = system_program::ID)]pub system_program:Program<'info, System>,/// Address to assign to the newly created thread. #[account(mut, address =Thread::pubkey(thread_authority.key(), thread_id))]pub thread:SystemAccount<'info>,/// The pda that will own and manage the thread. #[account(seeds = [THREAD_AUTHORITY_SEED], bump)]pub thread_authority:SystemAccount<'info>,}
Finally, the trickiest part is to define our Initialize instruction constraints with Anchor macros properly. Note that the thread authority is a PDA account. Only this program has the authority to administrate the thread; pause, start, create, delete the thread, etc.
4. Testing our automation
Let's add a test case to initialize our program and get it running. Here, we will simply calculate the required PDAs and call our program's Initialize instruction. From there, our program will create a thread and begin running all on its own.
...import { ClockworkProvider } from"@clockwork-xyz/sdk";constprovider=anchor.AnchorProvider.env();anchor.setProvider(provider);constwallet=provider.wallet;constprogram=anchor.workspace.Counter asProgram<Counter>;constclockworkProvider=ClockworkProvider.fromAnchorProvider(provider);it("It increments every 10 seconds",async () => { // 1️⃣ Prepare thread addressconstthreadId="counter";const [threadAuthority] =PublicKey.findProgramAddressSync(// Make sure it matches on the prog side [anchor.utils.bytes.utf8.encode("authority")],program.programId );const [threadAddress,threadBump] =clockworkProvider.getThreadPDA(threadAuthority, threadId)// 2️⃣ Ask our program to initialize a thread via CPI// and thus become the admin of that threadawaitprogram.methods.initialize(Buffer.from(threadId)).accounts({ payer:wallet.publicKey, systemProgram:SystemProgram.programId, clockworkProgram:clockworkProvider.threadProgram.programId, thread: threadAddress, threadAuthority: threadAuthority, counter: counter, }).rpc();}
Finally, let's run the test using anchor test. You modify the test to print the thread address and look up the thread in your favorite Solana explorer. You should see the counter being auto-increment every 10 seconds by our thread.
Key Learnings
Programs can create threads via CPIs.
When creating threads via CPIs, use a "program authority" PDA to act as the owner of the thread and manage its permissions.
Appendix
This guide was written using the following environment dependencies.
Dependency
Version
Anchor
v0.26.0
Clockwork
v2.0.1
Clockwork TS SDK
v0.3.0
Rust
v1.65.0
Solana
v1.14.16
Ubuntu
v20.04
Learn more
A complete copy of all code provided in this guide can be found in the examples repoon GitHub.