Skip to main content

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 background
Challenge

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 Counter account and the initialize instruction already in place.
  • Rust and Cargo, plus the Anchor CLI you installed on Day 57.
  • The litesvm crate already wired into your Cargo.toml from yesterday’s test setup.

Steps

  1. Open programs/counter/src/lib.rs. Add a second handler called increment below your existing initialize handler. It takes no arguments, reads the counter account as mutable, and bumps the count by one using checked_add so 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(())
}

  1. Below your Initialize accounts struct, add an Increment accounts struct. This is where the constraint lives. The has_one = authority attribute tells Anchor: before this handler runs, confirm the authority field stored inside the counter account matches the authority signer 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>,
}

  1. Rebuild the program so the new instruction discriminator and the updated IDL land in target/deploy/ and target/idl/. The build step also regenerates the typed account and instruction helpers your tests rely on.
anchor build

  1. 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 calls initialize, then increment, then reads the account back and asserts count == 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

Submission

Take a screenshot of your terminal showing both transactions succeeding and the test initialize_then_increment ... ok line. Submit the screenshot here.

Submit your project