Skip to main content
Version: v28

Creating posts

In this chapter, we will be focusing on the process of handling a "create post" message. This involves the use of a special type of function known as a keeper method. Keeper methods are responsible for interacting with the blockchain and modifying its state based on the instructions provided in a message.

When a "create post" message is received, the corresponding keeper method will be called and passed the message as an argument. The keeper method can then use the various getter and setter functions provided by the store object to retrieve and modify the current state of the blockchain. This allows the keeper method to effectively process the "create post" message and make the necessary updates to the blockchain.

In order to keep the code for accessing and modifying the store object clean and separate from the logic implemented in the keeper methods, we will create a new file called post.go. This file will contain functions that are specifically designed to handle operations related to creating and managing posts within the blockchain.

Appending posts to the store

x/blog/keeper/post.go
package keeper

import (
"encoding/binary"

"blog/x/blog/types"

"cosmossdk.io/store/prefix"
"github.com/cosmos/cosmos-sdk/runtime"
sdk "github.com/cosmos/cosmos-sdk/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
}

This code defines a function called AppendPost which belongs to a Keeper type. The Keeper type is responsible for interacting with the blockchain and modifying its state in response to various messages.

The AppendPost function takes in two arguments: a Context object and a Post object. The Context object is a standard parameter in many functions in the Cosmos SDK and is used to provide contextual information about the current state of the blockchain, such as the current block height. The Post object represents a post that will be added to the blockchain.

The function begins by retrieving the current post count using the GetPostCount method. You will implement this method in the next step as it has not been implemented yet. This method is called on the Keeper object and takes in a Context object as an argument. It returns the current number of posts that have been added to the blockchain.

Next, the function sets the ID of the new post to be the current post count, so that each post has a unique identifier. It does this by assigning the value of count to the Id field of the Post object.

The function then creates a new store object using the prefix.NewStore function. The prefix.NewStore function takes in two arguments: the KVStore associated with the provided context and a key prefix for the Post objects. The KVStore is a key-value store that is used to persist data on the blockchain, and the key prefix is used to differentiate the Post objects from other types of objects that may be stored in the same KVStore.

The function then serializes the Post object using the cdc.MustMarshal function and stores it in the blockchain using the Set method of the store object. The cdc.MustMarshal function is part of the Cosmos SDK's encoding/decoding library and is used to convert the Post object into a byte slice that can be stored in the KVStore. The Set method is called on the store object and takes in two arguments: a key and a value. In this case, the key is a byte slice generated by the GetPostIDBytes function and the value is the serialized Post object. You will implement this method in the next step as it has not been implemented yet.

Finally, the function increments the post count by one and updates the blockchain state using the SetPostCount method. You will implement this method in the next step as it has not been implemented yet. This method is called on the Keeper object and takes in a Context object and a new post count as arguments. It updates the current post count in the blockchain to be the new post count provided.

The function then returns the ID of the newly created post, which is the current post count before it was incremented. This allows the caller of the function to know the ID of the post that was just added to the blockchain.

To complete the implementation of AppendPost, the following tasks need to be performed:

  • Define PostKey, which will be used to store and retrieve posts from the database.
  • Implement GetPostCount, which will retrieve the current number of posts stored in the database.
  • Implement GetPostIDBytes, which will convert a post ID to a byte array.
  • Implement SetPostCount, which will update the post count stored in the database.

Post key prefix

In the file keys.go, let's define the PostKey prefix as follows:

x/blog/types/keys.go
const (
PostKey = "Post/value/"
)

This prefix will be used to uniquely identify a post within the system. It will be used as the beginning of the key for each post, followed by the ID of the post to create a unique key for each post.

Getting the post count

In the file post.go, let's define the GetPostCount function as follows:

x/blog/keeper/post.go
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)
}

This code defines a function named GetPostCount that belongs to the Keeper struct. The function takes in a single argument, a context object ctx of type sdk.Context, and returns a value of type uint64.

The function begins by creating a new store using the key-value store in the context and an empty byte slice as the prefix. It then defines a byte slice byteKey using the KeyPrefix function from the types package, which takes in the PostCountKey. You will define PostCountKey in the next step.

The function then retrieves the value at the key byteKey in the store using the Get method and stores it in a variable bz.

Next, the function checks if the value at byteKey is nil using an if statement. If it is nil, meaning that the key does not exist in the store, the function returns 0. This indicates that there are no elements or posts associated with the key.

If the value at byteKey is not nil, the function uses the binary package's BigEndian type to parse the bytes in bz and returns the resulting uint64 value. The BigEndian type is used to interpret the bytes in bz as a big-endian encoded unsigned 64-bit integer. The Uint64 method converts the bytes to a uint64 value and returns it.

GetPostCount function is used to retrieve the total number of posts stored in the key-value store, represented as a uint64 value.

In the file keys.go, let's define the PostCountKey as follows:

x/blog/types/keys.go
const (
PostCountKey = "Post/count/"
)

This key will be used to keep track of the ID of the latest post added to the store.

Converting post ID to bytes

Now, let's implement GetPostIDBytes, which will convert a post ID to a byte array.

x/blog/keeper/post.go
func GetPostIDBytes(id uint64) []byte {
bz := make([]byte, 8)
binary.BigEndian.PutUint64(bz, id)
return bz
}

GetPostIDBytes takes in a value id of type uint64 and returns a value of type []byte.

The function starts by creating a new byte slice bz with a length of 8 using the make built-in function. It then uses the binary package's BigEndian type to encode the value of id as a big-endian encoded unsigned integer and store the result in bz using the PutUint64 method. Finally, the function returns the resulting byte slice bz.

This function can be used to convert a post ID, represented as a uint64, to a byte slice that can be used as a key in a key-value store. The binary.BigEndian.PutUint64 function encodes the uint64 value of id as a big-endian encoded unsigned integer and stores the resulting bytes in the []byte slice bz. The resulting byte slice can then be used as a key in the store.

Updating the post count

Implement SetPostCount in post.go, which will update the post count stored in the database.

x/blog/keeper/post.go
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)
}

This code defines a function SetPostCount in the Keeper struct. The function takes in a context ctx of type sdk.Context and a value count of type uint64, and does not return a value.

The function first creates a new store by calling the NewStore function from the prefix package and passing in the key-value store from the context and an empty byte slice as the prefix. It stores the resulting store in a variable named store.

Next, the function defines a byte slice byteKey using the KeyPrefix function from the types package and passing in the PostCountKey. The KeyPrefix function returns a byte slice with the given key as a prefix.

The function then creates a new byte slice bz with a length of 8 using the make built-in function. It then uses the binary package's BigEndian type to encode the value of count as a big-endian encoded unsigned integer and store the result in bz using the PutUint64 method.

Finally, the function calls the Set method on the store variable, passing in byteKey and bz as arguments. This sets the value at the key byteKey in the store to the value bz.

This function can be used to update the count of posts stored in the database. It does this by converting the uint64 value of count to a byte slice using the binary.BigEndian.PutUint64 function, and then storing the resulting byte slice at the key byteKey in the store using the Set method.

Now that you have implemented the code for creating blog posts, you can proceed to implement the keeper method that is invoked when the "create post" message is processed.

Handling the "create post" message

x/blog/keeper/msg_server_create_post.go
package keeper

import (
"context"

"blog/x/blog/types"

sdk "github.com/cosmos/cosmos-sdk/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
}

The CreatePost function is a message handler for the MsgCreatePost message type. It is responsible for creating a new post on the blockchain based on the information provided in the MsgCreatePost message.

The function first retrieves the Cosmos SDK context from the Go context using the sdk.UnwrapSDKContext function. It then creates a new Post object using the Creator, Title, and Body fields from the MsgCreatePost message.

Next, the function calls the AppendPost method on the msgServer object (which is of the Keeper type) and passes in the Cosmos SDK context and the new Post object as arguments. The AppendPost method is responsible for adding the new post to the blockchain and returning the ID of the new post.

Finally, the function returns a MsgCreatePostResponse object that contains the ID of the new post. It also returns a nil error, indicating that the operation was successful.

Summary

Great job! You have successfully implemented the logic for writing blog posts to the blockchain store and the keeper method that will be called when a "create post" message is processed.

The AppendPost keeper method retrieves the current post count, sets the ID of the new post to be the current post count, serializes the post object, and stores it in the blockchain using the Set method of the store object. The key for the post in the store is a byte slice generated by the GetPostIDBytes function and the value is the serialized post object. The function then increments the post count by one and updates the blockchain state using the SetPostCount method.

The CreatePost handler method receives a MsgCreatePost message containing the data for the new post, creates a new Post object using this data, and passes it to the AppendPost keeper method to be added to the blockchain. It then returns a MsgCreatePostResponse object containing the ID of the newly created post.

By implementing these methods, you have successfully implemented the necessary logic for handling "create post" messages and adding posts to the blockchain.