Building a blog on Solana with Anchor
Tags: blockchain, javascript, rust, web3
Published: 2022-01-04 18:19:55

## Prerequisites

This tutorial assumes you already have some basic knowledge of working with Anchor. There are a few good guides out there to help you get started with this:

- Nader Dabit's guide to [Solana Fullstack development](https://dev.to/dabit3/the-complete-guide-to-full-stack-solana-development-with-react-anchor-rust-and-phantom-3291?utm_source=pocket_mylist)
- Brian Friel's [Learning How to Build on Solana](https://www.brianfriel.xyz/learning-how-to-build-on-solana/) and guide to [Program Derived Addresses](https://www.brianfriel.xyz/understanding-program-derived-addresses/).

These tutorials cover everything you'll need to know to get started so I won't cover what they already have. Please refer to these for guides for a basic intro and to help get setup with Anchor - with this tutorial I'm simply building on the shoulders of these fine sers. 

You can find the source code for this tutorial [here](https://github.com/Findiglay/anchor-blog).

## A blog on a blockchain?

The idea to build an on-chain "Blog" (note the inverted commas) came from a livestream interview with Armani Ferrante where he explained that maximum account space on Solana is 10MB and gave the example of a blog as something that could hypothetically be built on the Solana blockchain. I thought this would be an interesting learning exercise as it would require understanding how to:

1. Create a program that can control multiple related accounts via [Program Derived Addresses](https://docs.solana.com/developing/programming-model/calling-between-programs#program-derived-addresses).
2. Implement [constraints](https://project-serum.github.io/anchor/tutorials/tutorial-2.html) so that only the author of the blog can create new posts for a certain PDA.

Please note that this really is just a learning exercise and it's not intended to be deployed to Mainnet - especially given the fact that [keeping an account alive](https://docs.solana.com/developing/programming-model/accounts) for a single ~10kb post will cost a decent amount in rent (~$10 depending on the current price of SOL). 

Solana is not really built for this kind of storage (at least [not currently](https://twitter.com/GenesysGo/status/1477999830173687813)). There are certainly more cost efficient ways to build an on-chain blog which I'll aim to cover in future posts.

## The program

### 1. Initializing a blog

Let's get started with our program. Once you've bootstrapped your new Anchor project with `$ anchor init anchor-blog`, open the `programs/anchor-blog/src/lib.rs` and add the following code to the bottom of this file in order to define our Blog account struct:

```rust
#[account]
#[derive(Default)]
pub struct Blog {
    pub bump: u8,
    pub post_count: u8,
    pub authority: Pubkey,
}
```
Here we define a counter `post_count` property which will record the number posts in the blog and an `authority` property which will define who can create new posts for this blog.  

If you have read Brian Friel's post on [Program Derived Addresses](https://www.brianfriel.xyz/understanding-program-derived-addresses/), you'll know that the `bump` property on this account indicates that it will be a Program Derived Address - that is the account will be owned by the executing `anchor_blog` program rather than by a public/private keypair.

Next, let's define our instructions and method for our blog initialization and then I will explain how this will work. First update the `Initialize` instruction with the following:

```rust
#[derive(Accounts)]
#[instruction(blog_account_bump: u8)]
pub struct Initialize<'info> {
    #[account(
        init,
        seeds = [
            b"blog_v0".as_ref(),
            user.key().as_ref(),
        ],
        bump = blog_account_bump,
        payer = user
    )]
    blog_account: Account<'info, Blog>,
    #[account(mut)]
    user: Signer<'info>,
    system_program: Program<'info, System>,
}
```
Our blog account will have a PDA derived from the seeds "blog_v0" and the signing user's public address. Importantly, this means that each user will have a unique PDA for their blog account.

Next, update our program's initialize method with the following:
```rust
pub fn initialize(ctx: Context<Initialize>, blog_account_bump: u8) -> ProgramResult {
    ctx.accounts.blog_account.bump = blog_account_bump;
    ctx.accounts.blog_account.authority = *ctx.accounts.user.to_account_info().key;
    Ok(())
}
```
The key here is that we are assigning the user key as the authority. In future, only the authority will be able to create posts for this blog.

Also note the asterix `*` in this assignment. For users new to Rust, this is a [dereference operator](https://doc.rust-lang.org/book/ch15-02-deref.html?highlight=dereference%20operator#following-the-pointer-to-the-value-with-the-dereference-operator). It simply ensures that we are using the value of the key here rather than the pointer.

Now that we have written part of our program, lets write some tests to ensure our blog will initialize correctly.

In order to be sure we can test different scenarios, we are going to add some helper methods. Create a `tests/helpers.ts` file and add the following:
```ts
import * as anchor from "@project-serum/anchor";
import { AnchorBlog } from "../target/types/anchor_blog";

export function getProgram(
  provider: anchor.Provider
): anchor.Program<AnchorBlog> {
  const idl = require("../target/idl/anchor_blog.json");
  const programID = new anchor.web3.PublicKey(idl.metadata.address);
  return new anchor.Program(idl, programID, provider);
}

export function getProvider(
  connection: anchor.web3.Connection,
  keypair: anchor.web3.Keypair
): anchor.Provider {
  // @ts-expect-error
  const wallet = new anchor.Wallet(keypair);
  return new anchor.Provider(
    connection,
    wallet,
    anchor.Provider.defaultOptions()
  );
}

export async function requestAirdrop(
  connection: anchor.web3.Connection,
  publicKey: anchor.web3.PublicKey
): Promise<void> {
  const airdropSignature = await connection.requestAirdrop(
    publicKey,
    anchor.web3.LAMPORTS_PER_SOL * 20
  );
  await connection.confirmTransaction(airdropSignature);
}

``` 
Next replace the boilerplate code in `tests/anchor-blog.ts` with the following:
```ts
import assert from "assert";
import * as anchor from "@project-serum/anchor";
import * as helpers from "./helpers";

describe("anchor-blog", async () => {
  // Configure the client to use the local cluster.
  const connection = new anchor.web3.Connection(
    "http://localhost:8899",
    anchor.Provider.defaultOptions().preflightCommitment
  );

  const provider = helpers.getProvider(
    connection,
    anchor.web3.Keypair.generate()
  );
  const program = helpers.getProgram(provider);

  const [blogAccount, blogAccountBump] =
    await anchor.web3.PublicKey.findProgramAddress(
      [Buffer.from("blog_v0"), provider.wallet.publicKey.toBuffer()],
      program.programId
    );

  before(async () => {
    await helpers.requestAirdrop(connection, provider.wallet.publicKey);
  });

  it("Initializes with 0 entries", async () => {
    await program.rpc.initialize(blogAccountBump, {
      accounts: {
        blogAccount,
        user: provider.wallet.publicKey,
        systemProgram: anchor.web3.SystemProgram.programId,
      },
    });

    const blogState = await program.account.blog.fetch(blogAccount);

    assert.equal(0, blogState.postCount);
  });
});

```

Now that we are ready to run a test, build your project:
```
$ anchor build
```

Make sure that your program ID is updated and run:
```
$ anchor test
```

### 2. Creating a post

Now that we can initialize a blog, let's implement our method for creating a post. We will start by defining our Post account struct in the `programs/anchor-blog/src/lib.rs` file:

```rust
#[account]
#[derive(Default)]
pub struct Post {
    pub authority: Pubkey,
    pub bump: u8,
    pub entry: u8,
    pub title: String,
    pub body: String,
}
```
We are going to keep this simple but feel free to improvise and add more fields if you are feeling adventurous. Each Post account will have a title, body and an entry number.

Next let's define our instructions for the `create_post` RPC method:
```rust
#[derive(Accounts)]
#[instruction(post_account_bump: u8, title: String, body: String)]
pub struct CreatePost<'info> {
    #[account(mut, has_one = authority)]
    pub blog_account: Account<'info, Blog>,
    #[account(
        init,
        seeds = [
            b"post".as_ref(),
            blog_account.key().as_ref(),
            &[blog_account.post_count as u8].as_ref()
        ],
        bump = post_account_bump,
        payer = authority,
        space = 10000
    )]
    pub post_account: Account<'info, Post>,
    pub authority: Signer<'info>,
    pub system_program: Program<'info, System>
}
``` 
As defined by the `seeds` array, each Post account will have a PDA derived from the Blog account's public key (which is also a PDA) and the Blog account's `post_count` value.

We assign 10kb to `space` which is an arbitary value here and simply ensures that we will have enough space for a hypthetically sizeable blog article.

Note that a Blog account must already have been initialized to create a post and must be provided here. We also add a constraint `has_one = authority` to require that the Blog account's authority signs this instruction. This will ensure that:
```rust
CreatePost.blog_account.authority == CreatePost.authority.key
```

Finally, let's define our `create_post` RPC method:
```rust
pub fn create_post(ctx: Context<CreatePost>, post_account_bump: u8, title: String, body: String) -> ProgramResult {
    ctx.accounts.post_account.bump = post_account_bump;
    ctx.accounts.post_account.authority = *ctx.accounts.authority.to_account_info().key;
    ctx.accounts.post_account.title = title;
    ctx.accounts.post_account.body = body;
    ctx.accounts.post_account.entry = ctx.accounts.blog_account.post_count;
    ctx.accounts.blog_account.post_count += 1;
    Ok(())
}
```
This should be more or less self-explanatory. I'll simply point out here that we are also updating the `blog_account` by assigning the current `post_count` value to be this Post's entry value, before also incrementing the Blog's `post_count` by one with `ctx.accounts.blog_account.post_count += 1;`.

Let's now add another test to `tests/anchor-blog.ts` to see if our `create_post` method is working. First, get the PDA address and bump in the describe block next to where we previosuly retrived our Blog account PDA:
```ts
const [firstPostAccount, firstPostAccountBump] =
  await anchor.web3.PublicKey.findProgramAddress(
    [
      Buffer.from("post"),
      blogAccount.toBuffer(),
      new anchor.BN(0).toArrayLike(Buffer),
    ],
    program.programId
  );
```
And then add the following test:

```ts
it("Creates a post and increments the post count", async () => {
  const title = "Hello World";
  const body = "gm, this is a test post";

  await program.rpc.createPost(firstPostAccountBump, title, body, {
    accounts: {
      blogAccount,
      postAccount: firstPostAccount,
      authority: provider.wallet.publicKey,
      systemProgram: anchor.web3.SystemProgram.programId,
    },
  });

  const blogState = await program.account.blog.fetch(blogAccount);
  const postState = await program.account.post.fetch(firstPostAccount);

  assert.equal(title, postState.title);
  assert.equal(body, postState.body);
  assert.equal(0, postState.entry);
  assert.equal(1, blogState.postCount);
});
```
Rebuild your project with `$ anchor build` and run `anchor test` (you may need to check the program ID has not changed but it will likely be the same). 

We also want to be sure that only the Blog's authority can create a Post. Let's test that with the following:
```ts
it("Requires the correct signer to create a post", async () => {
  const title = "Hello World";
  const body = "gm, this is an unauthorized post";

  const [secondPostAccount, secondPostAccountBump] =
    await anchor.web3.PublicKey.findProgramAddress(
      [
        Buffer.from("post"),
        blogAccount.toBuffer(),
        new anchor.BN(1).toArrayLike(Buffer),
      ],
      program.programId
    );
  const newKeypair = anchor.web3.Keypair.generate();
  await helpers.requestAirdrop(connection, newKeypair.publicKey);
  const newProvider = helpers.getProvider(connection, newKeypair);
  const newProgram = helpers.getProgram(newProvider);

  let error;

  try {
    await newProgram.rpc.createPost(secondPostAccountBump, title, body, {
      accounts: {
        blogAccount,
        postAccount: secondPostAccount,
        authority: provider.wallet.publicKey,
        systemProgram: anchor.web3.SystemProgram.programId,
      },
    });
  } catch (err) {
    error = err;
  } finally {
    assert.equal(error.message, "Signature verification failed");
  }
});
```

### 3. Update a post

For our final method we want to be able to update a post. First, define our instructions:
```rust
#[derive(Accounts)]
#[instruction(tite: String, body: String)]
pub struct UpdatePost<'info> {
    #[account(mut, has_one = authority)]
    pub blog_account: Account<'info, Blog>,
    #[account(mut, has_one = authority)]
    pub post_account: Account<'info, Post>,
    pub authority: Signer<'info>,
}
```
This method will use the same `has_one = authority` constraint as the `create_post` method, but because our `post_account` already exists our instructions are a little simpler this time.

Now we can add our `update_post` method:
```rust
pub fn update_post(ctx: Context<UpdatePost>, title: String, body: String) -> ProgramResult {
    ctx.accounts.post_account.title = title;
    ctx.accounts.post_account.body = body;
    Ok(())
}
```
And add some tests to make sure it works:

```ts
it("Updates a post", async () => {
  const title = "Hello World Update";
  const body = "gm, this post has been updated";

  await program.rpc.updatePost(title, body, {
    accounts: {
      blogAccount,
      postAccount: firstPostAccount,
      authority: provider.wallet.publicKey,
    },
  });

  const blogState = await program.account.blog.fetch(blogAccount);
  const postState = await program.account.post.fetch(firstPostAccount);

  assert.equal(1, blogState.postCount);
  assert.equal(title, postState.title);
  assert.equal(body, postState.body);
});

it("Requires the correct signer to update a post", async () => {
  const title = "Hello World Update";
  const body = "gm, this post has been updated";

  const newKeypair = anchor.web3.Keypair.generate();
  await helpers.requestAirdrop(connection, newKeypair.publicKey);
  const newProvider = helpers.getProvider(connection, newKeypair);
  const newProgram = helpers.getProgram(newProvider);

  let error;

  try {
    await newProgram.rpc.updatePost(title, body, {
      accounts: {
        blogAccount,
        postAccount: firstPostAccount,
        authority: provider.wallet.publicKey,
      },
    });
  } catch (err) {
    error = err;
  } finally {
    assert.equal(error?.message, "Signature verification failed");
  }
});
```
And that's it! Remember to rebuild your project. If you want to see a very bare-bones example of how to create and load posts you can check out the basic app example here: https://github.com/Findiglay/anchor-blog/tree/main/app.