4 min read

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!