Build a Blog on a Blockchain with Ignite CLI

Build a Blog on a Blockchain with Ignite CLI

Introduction

This tutorial guides you through creating a blog application as a Cosmos SDK blockchain using Ignite CLI. You'll learn to set up types, messages, queries, and write logic for creating, reading, updating, and deleting blog posts.

Creating the Blog Blockchain

  1. Initialize the Blockchain:
ignite scaffold chain blog
cd blog
  1. Define the Post Type:
ignite scaffold type post title body creator id:uint

This step creates a Post type with title, body, creator (all string), and id (unsigned integer).

Implementing CRUD Operations

Creating Posts

Before we start creating posts, we'll define the action and structure that our blockchain will understand for a blog post.

  1. Scaffold Create Message:
ignite scaffold message create-post title body --response id:uint

This message allows users to create posts with a title and body.

Now open your IDE to make some changes to the code.

  1. Append Posts to the Store:

    Now, let's add functionality to store the posts on the blockchain.

Start by creating the file x/blog/keeper/post.go.

Next, implement AppendPost and the following functions in x/blog/keeper/post.go to add posts to the store.

package keeper

import (
    "encoding/binary"

	"cosmossdk.io/store/prefix"
	"github.com/cosmos/cosmos-sdk/runtime"
	sdk "github.com/cosmos/cosmos-sdk/types"

    "blog/x/blog/types"
)

func (k Keeper) AppendPost(ctx context.Context, post types.Post) uint64 {
    count := k.GetPostCount(ctx)
    post.Id = count
    storeAdapter := runtime.KVStoreAdapter(k.storeService.OpenKVStore(ctx))
    store := prefix.NewStore(storeAdapter, types.KeyPrefix(types.PostKey))
    appendedValue := k.cdc.MustMarshal(&post)
    store.Set(GetPostIDBytes(post.Id), appendedValue)
    k.SetPostCount(ctx, count+1)
    return count
}

func (k Keeper) GetPostCount(ctx context.Context) uint64 {
    storeAdapter := runtime.KVStoreAdapter(k.storeService.OpenKVStore(ctx))
    store := prefix.NewStore(storeAdapter, []byte{})
    byteKey := types.KeyPrefix(types.PostCountKey)
    bz := store.Get(byteKey)
    if bz == nil {
        return 0
    }
    return binary.BigEndian.Uint64(bz)
}

func GetPostIDBytes(id uint64) []byte {
    bz := make([]byte, 8)
    binary.BigEndian.PutUint64(bz, id)
    return bz
}

func (k Keeper) SetPostCount(ctx context.Context, count uint64) {
    storeAdapter := runtime.KVStoreAdapter(k.storeService.OpenKVStore(ctx))
    store := prefix.NewStore(storeAdapter, []byte{})
    byteKey := types.KeyPrefix(types.PostCountKey)
    bz := make([]byte, 8)
    binary.BigEndian.PutUint64(bz, count)
    store.Set(byteKey, bz)
}

func (k Keeper) GetPost(ctx context.Context, id uint64) (val types.Post, found bool) {
    storeAdapter := runtime.KVStoreAdapter(k.storeService.OpenKVStore(ctx))
    store := prefix.NewStore(storeAdapter, types.KeyPrefix(types.PostKey))
    b := store.Get(GetPostIDBytes(id))
    if b == nil {
        return val, false
    }
    k.cdc.MustUnmarshal(b, &val)
    return val, true
}
  1. Add Post key prefix:

Navigate to the x/blog/types/keys.go file and add the PostKey and PostCountKey constants inside the existing const block:

    // PostKey is used to uniquely identify posts within the system.
    // It will be used as the beginning of the key for each post, followed bei their unique ID
    PostKey = "Post/value/"

    // PostCountKey this key will be used to keep track of the ID of the latest post added to the store.
    PostCountKey = "Post/count/"
  1. Update Create Post:

Navigate to the x/blog/keeper/msg_server_create_post.go file to update the CreatePost function:

package keeper

import (
    "context"

    sdk "github.com/cosmos/cosmos-sdk/types"

    "blog/x/blog/types"
)

func (k msgServer) CreatePost(goCtx context.Context, msg *types.MsgCreatePost) (*types.MsgCreatePostResponse, error) {
    ctx := sdk.UnwrapSDKContext(goCtx)
    var post = types.Post{
        Creator: msg.Creator,
        Title:   msg.Title,
        Body:    msg.Body,
    }
    id := k.AppendPost(
        ctx,
        post,
    )
    return &types.MsgCreatePostResponse{
        Id: id,
    }, nil
}

Updating Posts

  1. Scaffold Update Message:
ignite scaffold message update-post title body id:uint

This command allows for updating existing posts specified by their ID.

  1. Update Logic

Implement SetPost in x/blog/keeper/post.go for updating posts in the store.

func (k Keeper) SetPost(ctx context.Context, post types.Post) {
    storeAdapter := runtime.KVStoreAdapter(k.storeService.OpenKVStore(ctx))
    store := prefix.NewStore(storeAdapter, types.KeyPrefix(types.PostKey))
    b := k.cdc.MustMarshal(&post)
    store.Set(GetPostIDBytes(post.Id), b)
}

Refine the UpdatePost function in x/blog/keeper/msg_server_update_post.go

package keeper

import (
    "context"
    "fmt"

    errorsmod "cosmossdk.io/errors"
    sdk "github.com/cosmos/cosmos-sdk/types"
    sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"

    "blog/x/blog/types"
)

func (k msgServer) UpdatePost(goCtx context.Context, msg *types.MsgUpdatePost) (*types.MsgUpdatePostResponse, error) {
    ctx := sdk.UnwrapSDKContext(goCtx)
    var post = types.Post{
        Creator: msg.Creator,
        Id:      msg.Id,
        Title:   msg.Title,
        Body:    msg.Body,
    }
    val, found := k.GetPost(ctx, msg.Id)
    if !found {
        return nil, errorsmod.Wrap(sdkerrors.ErrKeyNotFound, fmt.Sprintf("key %d doesn't exist", msg.Id))
    }
    if msg.Creator != val.Creator {
        return nil, errorsmod.Wrap(sdkerrors.ErrUnauthorized, "incorrect owner")
    }
    k.SetPost(ctx, post)
    return &types.MsgUpdatePostResponse{}, nil
}

Deleting Posts

  1. Scaffold Delete Message:
ignite scaffold message delete-post id:uint

This command enables the deletion of posts by their ID.

  1. Delete Logic:

Implement RemovePost in x/blog/keeper/post.go to delete posts from the store.

func (k Keeper) RemovePost(ctx context.Context, id uint64) {
    storeAdapter := runtime.KVStoreAdapter(k.storeService.OpenKVStore(ctx))
    store := prefix.NewStore(storeAdapter, types.KeyPrefix(types.PostKey))
    store.Delete(GetPostIDBytes(id))
}

Add the according logic to x/blog/keeper/msg_server_delete_post:

package keeper

import (
    "context"
    "fmt"

    errorsmod "cosmossdk.io/errors"
    sdk "github.com/cosmos/cosmos-sdk/types"
    sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"

    "blog/x/blog/types"
)

func (k msgServer) DeletePost(goCtx context.Context, msg *types.MsgDeletePost) (*types.MsgDeletePostResponse, error) {
    ctx := sdk.UnwrapSDKContext(goCtx)
    val, found := k.GetPost(ctx, msg.Id)
    if !found {
        return nil, errorsmod.Wrap(sdkerrors.ErrKeyNotFound, fmt.Sprintf("key %d doesn't exist", msg.Id))
    }
    if msg.Creator != val.Creator {
        return nil, errorsmod.Wrap(sdkerrors.ErrUnauthorized, "incorrect owner")
    }
    k.RemovePost(ctx, msg.Id)
    return &types.MsgDeletePostResponse{}, nil
}

Reading Posts

  1. Scaffold Query Messages:
ignite scaffold query show-post id:uint --response post:Post
ignite scaffold query list-post --response post:Post --paginated

These queries allow for retrieving a single post by ID and listing all posts with pagination.

  1. Query Implementation:

Implement ShowPost in x/blog/keeper/query_show_post.go.

package keeper

import (
    "context"

    sdk "github.com/cosmos/cosmos-sdk/types"
    sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"

    "blog/x/blog/types"
)

func (k Keeper) ShowPost(goCtx context.Context, req *types.QueryShowPostRequest) (*types.QueryShowPostResponse, error) {
    if req == nil {
        return nil, status.Error(codes.InvalidArgument, "invalid request")
    }

    ctx := sdk.UnwrapSDKContext(goCtx)
    post, found := k.GetPost(ctx, req.Id)
    if !found {
        return nil, sdkerrors.ErrKeyNotFound
    }

    return &types.QueryShowPostResponse{Post: post}, nil
}

Implement ListPost in x/blog/keeper/query_list_post.go.

package keeper

import (
	"context"

	"cosmossdk.io/store/prefix"
	"github.com/cosmos/cosmos-sdk/runtime"
	"github.com/cosmos/cosmos-sdk/types/query"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

	"blog/x/blog/types"
)

func (k Keeper) ListPost(ctx context.Context, req *types.QueryListPostRequest) (*types.QueryListPostResponse, error) {
	if req == nil {
		return nil, status.Error(codes.InvalidArgument, "invalid request")
	}

	storeAdapter := runtime.KVStoreAdapter(k.storeService.OpenKVStore(ctx))
	store := prefix.NewStore(storeAdapter, types.KeyPrefix(types.PostKey))

	var posts []types.Post
	pageRes, err := query.Paginate(store, req.Pagination, func(key []byte, value []byte) error {
		var post types.Post
		if err := k.cdc.Unmarshal(value, &post); err != nil {
			return err
		}

		posts = append(posts, post)
		return nil
	})

	if err != nil {
		return nil, status.Error(codes.Internal, err.Error())
	}

	return &types.QueryListPostResponse{Post: posts, Pagination: pageRes}, nil
}
  1. Proto Implementation:

In proto/blog/blog/query.proto,
add the repeated keyword to return a list of posts in QueryListPostResponse and include the option
[(gogoproto.nullable) = false] in QueryShowPostResponse and QueryListPostResponse to generate the field without a pointer.

message QueryShowPostResponse {
  Post post = 1 [(gogoproto.nullable) = false];
}

message QueryListPostResponse {
  // highlight-next-line
  repeated Post post = 1 [(gogoproto.nullable) = false];
  cosmos.base.query.v1beta1.PageResponse pagination = 2;
}

Start the Chain

Start the blockchain by running:

ignite chain serve

Interacting with the Blog

Open a new Terminal window and interact with the blog chain.

  1. Create a Post:
blogd tx blog create-post hello world --from alice --chain-id blog
  1. View a Post:
blogd q blog show-post 0
  1. List All Posts:
blogd q blog list-post
  1. Update a Post:
blogd tx blog update-post "Hello" "Cosmos" 0 --from alice --chain-id blog
  1. Delete a Post:
blogd tx blog delete-post 0 --from alice  --chain-id blog

Summary

Congratulations on completing the Blog tutorial! You've successfully built a functional blockchain application using Ignite and Cosmos SDK. This tutorial equipped you with the skills to generate code for key blockchain operations and implement business-specific logic in a blockchain context. Continue developing your skills and expanding your blockchain applications with the next tutorials.