DEV Community

Siddhant Chavan
Siddhant Chavan

Posted on

How I built a Counter program in Anchor and learned to trust my tests

My first Anchor program finally started feeling like a real on-chain application when I stopped thinking of accounts as “database rows” and started understanding how ownership, constraints, and tests work together.

The first thing Anchor made clear was the account context.

[derive(Accounts)]

pub struct Initialize<'info> {
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}

The Accounts struct is where Anchor changes the way you think compared to a Web2 backend.

Instead of manually validating requests, allocating storage, and checking permissions, you describe the rules. counter is the on-chain state account, authority is the wallet signing the transaction, and system_program handles account creation. Anchor uses this information to generate the validation logic before your instruction runs.

The initialize handler is surprisingly small:

pub fn initialize(ctx: Context) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.authority = ctx.accounts.authority.key();
counter.count = 0;
Ok(())
}

ctx.accounts gives direct access to the accounts passed into the transaction. Because Anchor already verified the accounts, the handler only focuses on business logic: storing the owner and setting the initial value.

Then came the increment instruction:

pub fn increment(ctx: Context) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count = counter.count
.checked_add(1)
.ok_or(ProgramError::ArithmeticOverflow)?;
Ok(())
}

The important part was not the increment itself, but the constraint:

[account(mut, has_one = authority)]

pub counter: Account<'info, Counter>,

has_one = authority guarantees that the signer calling increment is the same wallet stored as the counter owner. The check happens before my code executes.

My tests became the proof that these guarantees actually worked.

The happy path test checks that initialization creates the correct state:

assert_eq!(parsed.count, 0);
assert_eq!(parsed.authority, authority.pubkey());

If this fails, something is wrong with account creation or state initialization.

The failure test checks that unauthorized users cannot modify someone else’s counter:

let result = svm.send_transaction(bad_tx);

assert!(
result.is_err(),
"increment should fail when signed by the wrong authority"
);

If this fails, the program is allowing a wallet that does not own the counter to update it.

The most interesting experiment was breaking the program on purpose.

I changed:

checked_add(1)

to:

checked_add(2)

The program still ran, but the test caught the bug:

assertion left == right failed
left: 2
right: 1

That was the moment I understood why tests matter on-chain. A green test is not just a checkmark — it is evidence that a specific rule is still protected.

Next week, I would build on this by adding more realistic account relationships, better error handling, and connecting the Anchor program with a frontend client.

100DaysOfSolana #solana #rust #anchor #testing

Top comments (0)