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
- 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, 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
}
- 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, 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
- 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, 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
- 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 (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
}
- 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.
- 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.