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
This tutorial is tested and works with Ignite v29.0.0
- Initialize the Blockchain:
ignite scaffold chain blog
cd blog
- 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.
- 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.
-
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, []byte(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 := []byte(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 := []byte(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, []byte(types.PostKey))
b := store.Get(GetPostIDBytes(id))
if b == nil {
return val, false
}
k.cdc.MustUnmarshal(b, &val)
return val, true
}
- 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/"
- 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
- Scaffold Update Message:
ignite scaffold message update-post title body id:uint
This command allows for updating existing posts specified by their ID.
- 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, []byte(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
- Scaffold Delete Message:
ignite scaffold message delete-post id:uint
This command enables the deletion of posts by their ID.
- 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, []byte(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
- 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.
- 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 (q queryServer) ShowPost(ctx context.Context, req *types.QueryShowPostRequest) (*types.QueryShowPostResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "invalid request")
}
sdkCtx := sdk.UnwrapSDKContext(ctx)
post, found := q.k.GetPost(sdkCtx, 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 (q queryServer) ListPost(ctx context.Context, req *types.QueryListPostRequest) (*types.QueryListPostResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "invalid request")
}
storeAdapter := runtime.KVStoreAdapter(q.k.storeService.OpenKVStore(ctx))
store := prefix.NewStore(storeAdapter, []byte(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 := q.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
}
- Proto Implementation:
In proto/blog/blog/v1/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.
- Create a Post:
blogd tx blog create-post hello world --from alice --chain-id blog
- View a Post:
blogd q blog show-post 0
- List All Posts:
blogd q blog list-post
- Update a Post:
blogd tx blog update-post "Hello" "Cosmos" 0 --from alice --chain-id blog
- 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.