Anchor - Tutorial 2
Finally getting into the hang of things.
anchor init --javascript base2
cd base2
What do we want to do? From docs
Here we have a simple Counter program, where anyone can create a counter, but only the assigned authority can increment it.
Right on.
use anchor_lang::prelude::*;
/*
* Here we have a simple **Counter** program, where anyone can
* create a counter, but only the assigned **authority** can
* increment it.
*/
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
#[program]
mod base2 {
use super::*;
pub fn create(ctx: Context<Create>, authority: Pubkey) -> ProgramResult {
let counter = &mut ctx.accounts.counter;
counter.authority = authority;
counter.count = 0;
Ok(())
}
pub fn increment(ctx: Context<Increment>) -> ProgramResult {
let counter = &mut ctx.accounts.counter;
counter.count += 1;
Ok(())
}
}
#[derive(Accounts)]
pub struct Create<'info> {
// why 40?
#[account(init, payer = user, space = 8 + 40)]
pub counter: Account<'info, Counter>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Increment<'info> {
/*
* has_one: enforces the constraint that
* Increment.counter.authority == Increment.authority.key
*
* Signer: type : anchor_lang : This enforces the constraint
* that the authority account signed the transaction. However,
* anchor doesn't fetch the data on that account.
*/
#[account(mut, has_one = authority)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
#[account]
pub struct Counter {
/*
* authority : field : here : field is of type `Pubkey`, which is
* a solana_program type
*/
pub authority: Pubkey,
pub count: u64,
}
The code is basically almost identical to the previous tutorial, expect for the addition of the authority field. While going through this, I also came across some other useful blog posts
Starting with Solana, Part 2 - Anchor’s Account Macros

where other Daniel Imfeld breaks down the anchor protocol macros. Quite a good read.
After this I rewrote the test script to the below
const assert = require('assert');
const anchor = require('@project-serum/anchor');
const { SystemProgram } = anchor.web3;
const chai = require('chai');
const expect = chai.expect;
chai.use(require('chai-as-promised'));
const delay = ms => new Promise(res => setTimeout(res, ms));
describe('base2', () => {
// Use a local provider.
const provider = anchor.Provider.local();
// Configure the client to use the local cluster.
anchor.setProvider(provider);
// Counter for the tests.
const counterAcctKeypair = anchor.web3.Keypair.generate();
// Program for the tests.
const program = anchor.workspace.Basic2;
it('Creates a counter', async () => {
await program.rpc.create(provider.wallet.publicKey, {
accounts: {
counter: counterAcctKeypair.publicKey,
user: provider.wallet.publicKey,
systemProgram: SystemProgram.programId,
},
signers: [counterAcctKeypair],
});
const counterAccount = await program.account.counter.fetch(counterAcctKeypair.publicKey);
assert.ok(counterAccount.authority.equals(provider.wallet.publicKey));
assert.ok(counterAccount.count.toNumber() === 0);
});
it('Increments a counter', async () => {
await program.rpc.increment({
accounts: {
counter: counterAcctKeypair.publicKey,
authority: provider.wallet.publicKey,
},
});
const counterAccount = await program.account.counter.fetch(counterAcctKeypair.publicKey);
assert.ok(counterAccount.authority.equals(provider.wallet.publicKey));
assert.ok(counterAccount.count.toNumber() == 1);
});
it('Throws an error when the wrong authority is used and does not increment', async()=> {
const secondWalletKeypair = anchor.web3.Keypair.generate();
await expect(program.rpc.increment({
accounts: {
counter: counterAcctKeypair.publicKey,
authority: secondWalletKeypair.publicKey,
},
})).to.be.rejectedWith(Error);
const counterAccount = await program.account.counter.fetch(counterAcctKeypair.publicKey);
assert.ok(counterAccount.authority.equals(provider.wallet.publicKey));
assert.ok(counterAccount.count.toNumber() == 1);
});
// The below fails when the delay is not used
// Error: failed to send transaction: Transaction simulation failed: This transaction has already been processed
// This happens from the client not updating its 'recent blockhash', so this would lead to double spend attack
it('Increments a counter 2nd time', async () => {
await delay(1000);
await program.rpc.increment({
accounts: {
counter: counterAcctKeypair.publicKey,
authority: provider.wallet.publicKey,
},
});
const counterAccount = await program.account.counter.fetch(counterAcctKeypair.publicKey);
assert.ok(counterAccount.authority.equals(provider.wallet.publicKey));
assert.ok(counterAccount.count.toNumber() == 2);
});
});
Key changes:
- I hate how the tutorial names everything the same, like counter: Counter etc, when I don't think they mean exactly that. So I renamed counter to counterAcctKeypair, because that's what it's supposed to be.
- I added a test for the Error, ie when you send in the incorrect authority, an error should be thrown
- I also added a test for double increment.. and found the test harness requires a delay in order to get this to run properly.
const assert = require('assert');
const anchor = require('@project-serum/anchor');
const { SystemProgram } = anchor.web3;
const chai = require('chai');
const expect = chai.expect;
chai.use(require('chai-as-promised'));
const delay = ms => new Promise(res => setTimeout(res, ms));
describe('base2', () => {
// Use a local provider.
const provider = anchor.Provider.local();
// Configure the client to use the local cluster.
anchor.setProvider(provider);
// Counter for the tests.
const counterAcctKeypair = anchor.web3.Keypair.generate();
// Program for the tests.
const program = anchor.workspace.Basic2;
it('Creates a counter', async () => {
await program.rpc.create(provider.wallet.publicKey, {
accounts: {
counter: counterAcctKeypair.publicKey,
user: provider.wallet.publicKey,
systemProgram: SystemProgram.programId,
},
signers: [counterAcctKeypair],
});
const counterAccount = await program.account.counter.fetch(counterAcctKeypair.publicKey);
assert.ok(counterAccount.authority.equals(provider.wallet.publicKey));
assert.ok(counterAccount.count.toNumber() === 0);
});
it('Increments a counter', async () => {
await program.rpc.increment({
accounts: {
counter: counterAcctKeypair.publicKey,
authority: provider.wallet.publicKey,
},
});
const counterAccount = await program.account.counter.fetch(counterAcctKeypair.publicKey);
assert.ok(counterAccount.authority.equals(provider.wallet.publicKey));
assert.ok(counterAccount.count.toNumber() == 1);
});
it('Throws an error when the wrong authority is used and does not increment', async()=> {
const secondWalletKeypair = anchor.web3.Keypair.generate();
await expect(program.rpc.increment({
accounts: {
counter: counterAcctKeypair.publicKey,
authority: secondWalletKeypair.publicKey,
},
})).to.be.rejectedWith(Error);
const counterAccount = await program.account.counter.fetch(counterAcctKeypair.publicKey);
assert.ok(counterAccount.authority.equals(provider.wallet.publicKey));
assert.ok(counterAccount.count.toNumber() == 1);
});
// The below fails when the delay is not used
// Error: failed to send transaction: Transaction simulation failed: This transaction has already been processed
// This happens from the client not updating its 'recent blockhash', so this would lead to double spend attack
it('Increments a counter 2nd time', async () => {
await delay(1000);
await program.rpc.increment({
accounts: {
counter: counterAcctKeypair.publicKey,
authority: provider.wallet.publicKey,
},
});
const counterAccount = await program.account.counter.fetch(counterAcctKeypair.publicKey);
assert.ok(counterAccount.authority.equals(provider.wallet.publicKey));
assert.ok(counterAccount.count.toNumber() == 2);
});
});
Overall, I felt like I started getting the hang of things a little more with this tutorial.. on to the next!