Add an increment instruction and test both calls end to end
Build the Solana version of an authenticated update route: create a counter, let its owner increment it, and prove both calls work in one end-to-end test.
Add an increment instruction and test both calls end to end
Today, you are introducing your first account constraint, a one-line guarantee that the wallet calling increment is the same wallet that created the counter application you built yesterday. Anchor enforces it for you at the macro layer, so by the time your handler runs the check has already passed. Tomorrow you will deliberately break that constraint to see the failure path. Today you prove the happy path works for both instructions back to back.
The Challenge
What you’ll need
- The Anchor project from Day 58 with the
Counteraccount and theinitializeinstruction already in place. - Rust and Cargo, plus the Anchor CLI you installed on Day 57.
- The litesvm crate already wired into your
Cargo.tomlfrom yesterday’s test setup.
Steps
- Open
programs/counter/src/lib.rs. Add a second handler calledincrementbelow your existinginitializehandler. It takes no arguments, reads the counter account as mutable, and bumps the count by one usingchecked_addso an overflow returns an error instead of panicking.
pub fn increment(ctx: Context<Increment>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count = counter.count
.checked_add(1)
.ok_or(ProgramError::ArithmeticOverflow)?;
Ok(())
}
- Below your
Initializeaccounts struct, add anIncrementaccounts struct. This is where the constraint lives. Thehas_one = authorityattribute tells Anchor: before this handler runs, confirm theauthorityfield stored inside thecounteraccount matches theauthoritysigner passed in this transaction. If they do not match, the transaction fails before your code executes.
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(mut, has_one = authority)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
- Rebuild the program so the new instruction discriminator and the updated IDL land in
target/deploy/andtarget/idl/. The build step also regenerates the typed account and instruction helpers your tests rely on.
anchor build
- Open the test file you created on Day 58 at
programs/counter/tests/counter.rs. Replace its entire contents with the version below: the same imports, plus a single test that covers both instructions end to end. It callsinitialize, thenincrement, then reads the account back and assertscount == 1.
use anchor_lang::{
solana_program::system_program,
AccountDeserialize, InstructionData, ToAccountMetas,
};
use litesvm::LiteSVM;
use solana_instruction::Instruction;
use solana_keypair::Keypair;
use solana_signer::Signer;
use solana_transaction::Transaction;
#[test]
fn initialize_then_increment() {
let mut svm = LiteSVM::new();
let program_id = counter::ID;
let so_path = concat!(env!("CARGO_MANIFEST_DIR"), "/../../target/deploy/counter.so");
svm.add_program_from_file(program_id, so_path).unwrap();
let authority = Keypair::new();
svm.airdrop(&authority.pubkey(), 1_000_000_000).unwrap();
let counter_kp = Keypair::new();
// 1) initialize
let init_ix = Instruction {
program_id,
accounts: counter::accounts::Initialize {
counter: counter_kp.pubkey(),
authority: authority.pubkey(),
system_program: system_program::ID,
}
.to_account_metas(None),
data: counter::instruction::Initialize {}.data(),
};
let bh = svm.latest_blockhash();
let tx = Transaction::new_signed_with_payer(
&[init_ix],
Some(&authority.pubkey()),
&[&authority, &counter_kp],
bh,
);
svm.send_transaction(tx).unwrap();
// 2) increment
let inc_ix = Instruction {
program_id,
accounts: counter::accounts::Increment {
counter: counter_kp.pubkey(),
authority: authority.pubkey(),
}
.to_account_metas(None),
data: counter::instruction::Increment {}.data(),
};
let bh = svm.latest_blockhash();
let tx = Transaction::new_signed_with_payer(
&[inc_ix],
Some(&authority.pubkey()),
&[&authority],
bh,
);
svm.send_transaction(tx).unwrap();
// 3) read and assert
let account = svm.get_account(&counter_kp.pubkey()).unwrap();
let parsed = counter::Counter::try_deserialize(&mut account.data.as_slice()).unwrap();
assert_eq!(parsed.count, 1);
assert_eq!(parsed.authority, authority.pubkey());
}
Run it
anchor build && cargo test -p counter -- --nocapture
What Just Happened
You doubled the surface area of your program with a few extra lines, but the part that matters is not the line count. It is the shape. A program is just a collection of named instructions, each one a pure function from (accounts, args) to (updated accounts, log output, return value). Whether you have one instruction or fifty, the wiring is the same: a handler in the #[program] module, an accounts struct that declares what gets passed in, and a transaction on the client side that names the instruction and supplies those accounts. That is the entire mental model, and now you have it for two instructions instead of one.
The other thing you did, almost as a side effect, was your first piece of authorization. The has_one = authority constraint is the Solana equivalent of the line in your backend route that says if request.user.id !== resource.authority_id: return 403. The difference is that you did not write it in your handler. You declared it on the accounts struct and Anchor enforces it before the handler even loads. Declarative authorization, checked by the runtime, with a typed Rust struct you cannot accidentally skip. Tomorrow you will write the test that proves a stranger’s signer gets rejected, which is what makes today’s happy-path test meaningful in the first place.
Running both calls in a single test also taught you something subtle about LiteSVM: state persists across transactions inside one LiteSVM instance, the same way it would on a real cluster. The counter account you created in step one was still there when you incremented it in step two. Your test is now a tiny end-to-end simulation, not just a unit test of one handler in isolation.
Resources
- Anchor account constraints reference
- Testing Anchor programs with LiteSVM
- litesvm::LiteSVM API docs
- anchor-lang crate documentation
Submission
Take a screenshot of your terminal showing both transactions succeeding and the test initialize_then_increment ... ok line. Submit the screenshot here.