# 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:**
```bash
ignite scaffold chain blog
cd blog
```
2. **Define the Post Type:**
```bash
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**
1. **Scaffold Create Message**
```bash
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.
2. **Append Posts to the Store:**
Create the file `x/blog/keeper/post.go`.
Implement `AppendPost` and following functions in `x/blog/keeper/post.go` to add posts to the store.
```go title="x/blog/keeper/post.go"
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 sdk.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 sdk.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 sdk.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 sdk.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
}
```
3. **Add Post key prefix:**
Add the `PostKey` and `PostCountKey` to the `x/blog/types/keys.go` file:
```go title="x/blog/types/keys.go"
// 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/"
```
4. **Update Create Post:**
Update the `x/blog/keeper/msg_server_create_post.go` file with the `CreatePost` function:
```go title="x/blog/keeper/msg_server_create_post.go"
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:**
```bash
ignite scaffold message update-post title body id:uint
```
This command allows for updating existing posts specified by their ID.
2. **Update Logic**
Implement `SetPost` in `x/blog/keeper/post.go` for updating posts in the store.
```go title="x/blog/keeper/post.go"
func (k Keeper) SetPost(ctx sdk.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`
```go title="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:**
```bash
ignite scaffold message delete-post id:uint
```
This command enables the deletion of posts by their ID.
2. **Delete Logic:**
Implement RemovePost in `x/blog/keeper/post.go` to delete posts from the store.
```go title="x/blog/keeper/post.go"
func (k Keeper) RemovePost(ctx sdk.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`:
```go title="x/blog/keeper/msg_server_delete_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) 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:**
```bash title="proto/blog/blog/query.proto"
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.
2. **Query Implementation:**
Implement `ShowPost` in `x/blog/keeper/query_show_post.go`.
```go title="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`.
```go title="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
}
```
3. **Proto Implementation:**
In `proto/blog/blog/query.proto`
Add a `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.
```proto title="proto/blog/blog/query.proto"
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
```bash
ignite chain serve
```
**Interacting with the Blog**
Open a new Terminal window and interact with the blog chain.
1. **Create a Post:**
```bash
blogd tx blog create-post hello world --from alice --chain-id blog
```
2. **View a Post:**
```bash
blogd q blog show-post 0
```
3. **List All Posts:**
```bash
blogd q blog list-post
```
4. **Update a Post:**
```bash
blogd tx blog update-post "Hello" "Cosmos" 0 --from alice --chain-id blog
```
5. **Delete a Post:**
```bash
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.