order |
---|
1 |
By following this beginner tutorial, you will end up with a simple blog app that is powered by the Cosmos SDK.
For this tutorial you will use Starport v0.15.0, an easy to use tool for building blockchains. To install starport
into /usr/local/bin
, run the following command:
curl https://get.starport.network/starport@v0.15.0! | bash
You can also use Starport v0.15.0 on the web in a browser-based IDE. Learn more about other ways to install Starport.
Get started! The first step is to install the starport
CLI tool.
After starport
is installed, use it to create the initial app structure inside a directory named blog
:
starport app github.com/example/blog
One of the main features of Starport is code generation. The command above has generated a directory structure with a working blockchain application. Starport can also add data types to your app with starport type
command. To see it in action, follow the poll application tutorial. In this guide, however, you will create those files manually to understand how it all works under the hood.
Take a quick look at what Starport has generated for us:
The app/app.go
file imports and configures SDK modules and creates a constructor for the application that extends a basic SDK application among other things. This app will use only a couple standard modules bundled with Cosmos SDK (including auth
for dealing with accounts and bank
for handling coin transfers) and one module (x/blog
) that will contain custom functionality.
In cmd
directory you have source files of two programs for interacting with our application: blogd
starts a full-node for your blockchain and enables you to query the full-node, either to update the state by sending a transaction or to read it via a query.
This blog app will store data in a persistent key-value store. Similarly to most key-value stores, you can retrieve, delete, update, and loop through keys to obtain the values you are interested in.
Create a simple blog-like application and define the first proto type, the Post
in the post.proto
file.
Create the post.proto
file.
// proto/blog/post.proto
syntax = "proto3";
package example.blog.blog;
option go_package = "github.com/example/blog/x/blog/types";
import "gogoproto/gogo.proto";
message Post {
string creator = 1;
string id = 2;
string title = 3;
string body = 4;
}
message MsgCreatePost {
string creator = 1;
string title = 2;
string body = 3;
}
The code above defines the three properties of a post: Creator, Title, Body and ID. We generate unique global IDs for each post and also store them as strings.
Posts in the key-value store will look like this:
"post-0": {
"Creator": "cosmos18cd5t4msvp2lpuvh99rwglrmjrrw9qx5h3f3gz",
"Title": "This is a post!",
"Body": "Welcome to my blog app.",
"ID": "0"
},
"post-1": {
...
}
Right now the store is empty. Next, define how the user adds a posts.
With the Cosmos SDK, users can interact with your app with either a CLI (blogd
) or by sending HTTP requests. Let's define the CLI command first. Users should be able to type blogd tx blog create-post 'This is a post!' 'Welcome to my blog app.' --from=user1
to add a post to your store. The create-post
subcommand hasn’t been defined yet--let’s do it now.
Open the CLI transaction file x/blog/client/cli/tx.go
.
In the import
block, make sure to import these four packages:
// x/blog/client/cli/tx.go
import (
"fmt"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/client/tx"
// "github.com/cosmos/cosmos-sdk/client/flags"
"github.com/example/blog/x/blog/types"
)
This file already contains the function GetTxCmd
which defines custom blogd
commands. We will add the custom create-post
command to our blogd
by first adding GetCmdCreatePost
to blogTxCmd
.
// this line is used by starport scaffolding # 1
cmd.AddCommand(CmdCreatePost())
At the end of the file, let's define GetCmdCreatePost
itself.
func CmdCreatePost() *cobra.Command {
cmd := &cobra.Command{
Use: "create-post [title] [body]",
Short: "Creates a new post",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
argsTitle := string(args[0])
argsBody := string(args[1])
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}
msg := types.NewMsgCreatePost(clientCtx.GetFromAddress().String(), string(argsTitle), string(argsBody))
if err := msg.ValidateBasic(); err != nil {
return err
}
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
},
}
flags.AddTxFlagsToCmd(cmd)
return cmd
}
The function above defines what happens when you run the create-post
subcommand. create-post
takes two arguments [title] [body]
, creates a message NewMsgCreatePost
(with title as args[0]
and args[1]
) and broadcasts this message to be processed in your application.
This is a common pattern in the SDK: users make changes to the store by broadcasting messages. Both CLI commands and HTTP requests create messages that can be broadcasted in order for state transition to occur.
Define NewMsgCreatePost
in a new file you should create as x/blog/types/messages_post.go
.
// x/blog/types/messages_post.go
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
var _ sdk.Msg = &MsgCreatePost{}
Similarly to the post proto, MsgCreatePost
contains our post definition.
func NewMsgCreatePost(creator string, title string, body string) *MsgCreatePost {
return &MsgCreatePost{
Creator: creator,
Title: title,
Body: body,
}
}
NewMsgCreatePost
is a constructor function that creates the MsgCreatePost
message. The following five functions have to be defined to implement the Msg
interface. They allow you to perform validation that doesn’t require access to the store (like checking for empty values), etc.
// Route ...
func (msg MsgCreatePost) Route() string {
return RouterKey
}
// Type ...
func (msg MsgCreatePost) Type() string {
return "CreatePost"
}
// GetSigners ...
func (msg *MsgCreatePost) GetSigners() []sdk.AccAddress {
creator, err := sdk.AccAddressFromBech32(msg.Creator)
if err != nil {
panic(err)
}
return []sdk.AccAddress{creator}
}
// GetSignBytes ...
func (msg *MsgCreatePost) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
// ValidateBasic ...
func (msg *MsgCreatePost) ValidateBasic() error {
_, err := sdk.AccAddressFromBech32(msg.Creator)
if err != nil {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid creator address (%s)", err)
}
return nil
}
Going back to GetCmdCreatePost
in x/blog/client/cli/tx.go
, you'll see MsgCreatePost
being created and broadcast with GenerateOrBroadcastMsgs
.
After being broadcast, the messages are processed by an important part of the application, called handlers.
You should already have the function NewHandler
defined which lists all available handlers. Modify it to include a new function called handleMsgCreatePost
.
//x/blog/handler.go
switch msg := msg.(type) {
case *types.MsgCreatePost:
return handleMsgCreatePost(ctx, k, msg)
default:
Create the handler in handler_post.go
file
Define the function handleMsgCreatePost
in a new file handler_post.go
:
// x/blog/handler_post.go
package blog
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/example/blog/x/blog/keeper"
"github.com/example/blog/x/blog/types"
)
func handleMsgCreatePost(ctx sdk.Context, k keeper.Keeper, msg *types.MsgCreatePost) (*sdk.Result, error) {
k.CreatePost(ctx, *msg)
return &sdk.Result{Events: ctx.EventManager().ABCIEvents()}, nil
}
After creating a post object with creator, ID and title, the message handler calls k.CreatePost(ctx, post)
. “k” stands for Keeper, an abstraction used by the SDK that writes data to the store. Define the CreatePost
keeper function in a new keeper/post.go
file.
First, create a new file post.go
in the keeper/
directory.
Then, add a CreatePost
function that takes two arguments: a context and a post. Also, GetPostCount
and SetPostCount functions
.
// x/blog/keeper/post.go
package keeper
import (
"strconv"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/example/blog/x/blog/types"
)
// GetPostCount get the total number of post
func (k Keeper) GetPostCount(ctx sdk.Context) int64 {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.PostCountKey))
byteKey := types.KeyPrefix(types.PostCountKey)
bz := store.Get(byteKey)
// Count doesn't exist: no element
if bz == nil {
return 0
}
// Parse bytes
count, err := strconv.ParseInt(string(bz), 10, 64)
if err != nil {
// Panic because the count should be always formattable to int64
panic("cannot decode count")
}
return count
}
// SetPostCount set the total number of post
func (k Keeper) SetPostCount(ctx sdk.Context, count int64) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.PostCountKey))
byteKey := types.KeyPrefix(types.PostCountKey)
bz := []byte(strconv.FormatInt(count, 10))
store.Set(byteKey, bz)
}
func (k Keeper) CreatePost(ctx sdk.Context, msg types.MsgCreatePost) {
// Create the post
count := k.GetPostCount(ctx)
var post = types.Post{
Creator: msg.Creator,
Id: strconv.FormatInt(count, 10),
Title: msg.Title,
Body: msg.Body,
}
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.PostKey))
key := types.KeyPrefix(types.PostKey + post.Id)
value := k.cdc.MustMarshalBinaryBare(&post)
store.Set(key, value)
// Update post count
k.SetPostCount(ctx, count+1)
}
func (k Keeper) GetPost(ctx sdk.Context, key string) types.Post {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.PostKey))
var post types.Post
k.cdc.MustUnmarshalBinaryBare(store.Get(types.KeyPrefix(types.PostKey + key)), &post)
return post
}
func (k Keeper) HasPost(ctx sdk.Context, id string) bool {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.PostKey))
return store.Has(types.KeyPrefix(types.PostKey + id))
}
func (k Keeper) GetPostOwner(ctx sdk.Context, key string) string {
return k.GetPost(ctx, key).Creator
}
func (k Keeper) GetAllPost(ctx sdk.Context) (msgs []types.Post) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.PostKey))
iterator := sdk.KVStorePrefixIterator(store, types.KeyPrefix(types.PostKey))
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var msg types.Post
k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &msg)
msgs = append(msgs, msg)
}
return
}
CreatePost
creates a key by concatenating a post prefix with an ID. If you look back at how our store looks, you’ll notice keys have prefixes, which is why post-0bae9f7d-20f8-4b51-9d5c-af9103177d66
contained the prefix post-
. The reason for this is you have one store, but you might want to keep different types of objects in it, like posts and users. Prefixing keys with post-
and user-
allows you to share one storage space between different types of objects.
To define the post prefix add the following code:
// x/blog/types/keys.go
package types
const (
// Other constants...
// PostPrefix is used for keys in the KV store
PostKey= "Post-value-"
PostCountKey= "Post-count-"
)
Finally, store.Set(key, value)
writes our post to the store.
Two last things to do is tell our encoder how the MsgCreatePost
is converted to bytes.
// x/blog/types/codec.go
package types
import (
"github.com/cosmos/cosmos-sdk/codec"
cdctypes "github.com/cosmos/cosmos-sdk/codec/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)
func RegisterCodec(cdc *codec.LegacyAmino) {
// this line is used by starport scaffolding # 2
cdc.RegisterConcrete(&MsgCreatePost{}, "blog/CreatePost", nil)
}
func RegisterInterfaces(registry cdctypes.InterfaceRegistry) {
// this line is used by starport scaffolding # 3
registry.RegisterImplementations((*sdk.Msg)(nil),
&MsgCreatePost{},
)
}
var (
amino = codec.NewLegacyAmino()
ModuleCdc = codec.NewAminoCodec(amino)
)
Now you are ready to build and start the app and create some posts.
To launch the application run:
starport serve
This command installs dependencies, builds and initializes the app, and runs servers. You can also do it manually:
First, create a Makefile
in your /blog
root directory
PACKAGES=$(shell go list ./... | grep -v '/simulation')
VERSION := $(shell echo $(shell git describe --tags) | sed 's/^v//')
COMMIT := $(shell git log -1 --format='%H')
ldflags = -X github.com/cosmos/cosmos-sdk/version.Name=blog \
-X github.com/cosmos/cosmos-sdk/version.ServerName=blogd \
-X github.com/cosmos/cosmos-sdk/version.Version=$(VERSION) \
-X github.com/cosmos/cosmos-sdk/version.Commit=$(COMMIT)
BUILD_FLAGS := -ldflags '$(ldflags)'
all: install
install: go.sum
@echo "--> Installing blogd"
@go install -mod=readonly $(BUILD_FLAGS) ./cmd/blogd
go.sum: go.mod
@echo "--> Ensure dependencies have not been modified"
GO111MODULE=on go mod verify
test:
@go test -mod=readonly $(PACKAGES)
go mod tidy
cleans up dependencies.make
builds your app and creates a binary in your go path:blogd
.- Initialization scripts in the
Makefile
removes data directories, configures your app and generates two accounts. By default your app stores data in your home directory in~/.blogd
. The script removes them, so every time you have a clean state. blogd start
launches your app. After a couple of seconds you will see hashes of blocks being generated. Leave this terminal window open and open a new one.
Note: depending on your OS and firewall settings, you may have to accept a prompt asking if your application's binary (blogd
in this case) can accept external connections.
Run the following command to create a post:
blogd tx blog create-post "My first post" "This is a post\!" --from=alice
“My first post” is a title for our post and --from=alice
tells the program who is creating this post. alice
is a label for your pair of keys used to sign the transaction, created by the initialization script located within the /Makefile
previously. Keys are stored in ~/.blogd
.
After running the command and confirming it, you will see an object with “txhash” property with a value like 4B7B68DEACC7CDF3243965A449095B4AB895C9D9BDF0516725BF2173794A9B3C
.
To verify that the transaction has been processed, open a browser and visit the following URL (make sure to replace 4B7B6...
with the value of your txhash but make sure to have the 0x
prefix):
http://localhost:26657/tx?hash=0x4B7B68DEACC7CDF3243965A449095B4AB895C9D9BDF0516725BF2173794A9B3C
Also check out a basic block overview at
http://localhost:12345/#/blocks
Congratulations! You have just created and launched your custom blockchain and sent the first transaction 🎉
blogd tx blog create-post 'Hello!' 'My first post' --from=user1
ERROR: unknown command "create-post" for "blog"
Make sure you’ve added cmd.AddCommand(CmdCreatePost())
, to func GetTxCmd
in x/blog/client/cli/tx.go
.
blogd tx blog create-post 'Hello!' 'My first post' --from=user1
ERROR: unrecognized blog message type
Make sure you have added
case *types.MsgCreatePost: return handleMsgCreatePost(ctx, k, msg)
to func NewHandler
in x/blog/handler.go
blogd tx blog create-post Hello! --from=user1
panic: Cannot encode unregistered concrete type types.MsgCreatePost.
Make sure you’ve added cdc.RegisterConcrete(MsgCreatePost{}, "blog/CreatePost", nil)
to func RegisterCodec
in x/blog/types/codec.go
.
Error: rpc error: code = NotFound desc = account cosmos1t3rafxvy3ggluchm5sjzetj9wt50eq9hjay6f2 not found: key not found
Make sure that you wait for the first block to be created after bootstrapping a chain again.