Aprenda a criar um aplicativo de blog simples usando o Cosmos SDK
O tutorial original pode ser encontrado na documentação do Cosmos SDK aqui .
Introdução
Seguindo este tutorial, você aprenderá como criar um aplicativo de blog simples desenvolvido com o Cosmos SDK.
Começando
Vamos começar! A primeira etapa é instalar a ferramenta starport CLI.
Depois do starport
instalado, use-o para criar a estrutura inicial do aplicativo dentro de um diretório chamado blog
:
starport app github.com/example/blog
Uma das principais características do Starport é a geração de código. O comando acima gerou uma estrutura de diretório com um aplicativo blockchain em funcionamento. O Starport também pode adicionar tipos de dados ao seu aplicativo com o comando starport type
. Para vê-lo em ação, siga o tutorial poll application tutorial. Neste guia, no entanto, criaremos esses arquivos manualmente para entender como tudo funciona debaixo do capô.
Visão geral
Vamos dar uma olhada rápida no que o Starport gerou para nós. O arquivo app/app.go
importa e configura módulos SDK e cria um construtor para nosso aplicativo que estende um aplicativo SDK básico, entre outras coisas. Este aplicativo usará apenas alguns módulos-padrão agrupados com o Cosmos SDK (incluindo auth
para lidar com contas e bank
para lidar com transferências de moedas) e um módulo ( x/blog
) que conterá uma funcionalidade personalizada.
No diretório cmd
temos os arquivos fonte de dois programas para interagir com nossa aplicação: blogd
inicia um nó completo para a sua blockchain e blogcli
permite que você consulte este nó completo, seja para atualizar o estado enviando uma transação ou para lê-lo através de uma consulta.
Este aplicativo de blog armazenará dados em um armazenamento de valor-chave persistente . Da mesma forma que a maioria dos armazenamentos de valor-chave, você pode recuperar, excluir, atualizar e percorrer as chaves para obter os valores nos quais está interessado.
Estaremos criando um aplicativo simples, semelhante a um blog, então vamos definir o primeiro tipo, o Post
.
x/blog/types/TypesPost.go
package type
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
type Post struct {
Creator sdk.AccAddress `json:"creator" yaml:"creator"`
ID string `json:"id" yaml:"id"`
Title string `json:"title" yaml:"title"`
Body string `json:"body" yaml:"body"`
}
O código acima define as três propriedades de um post: Criador, Título e ID. O SDK fornece tipos úteis para representar coisas como endereços, então usamos sdk.AccAddress para Creator (Criador). Title (Título) é armazenado como uma string. Por fim, geramos IDs globais exclusivos para cada postagem e também os armazenamos como strings.
As postagens em nosso armazenamento de valor-chave ficarão assim:
"post-0bae9f7d-20f8-4b51-9d5c-af9103177d66": {
"Creator": "cosmos18cd5t4msvp2lpuvh99rwglrmjrrw9qx5h3f3gz",
"Title": "Isso é uma postagem!",
"Body": "Bem vindo ao meu blog.",
"ID": "0bae9f7d-20f8-4b51-9d5c-af9103177d66"
},
"post-8c6d8cd4-b4c9-4ba3-a683-e894db3f2605": {
...
}
No momento o armazenamento está vazio. Vamos descobrir como adicionar postagens.
Com o Cosmos SDK, os usuários podem interagir com seu aplicativo com uma CLI (blogcli
) ou enviando solicitações HTTP. Vamos definir o comando CLI primeiro. Os usuários devem poder digitar blogcli tx blog create-post 'Isso é uma postagem!' 'Bem vindo ao meu blog.' --from=user1
para adicionar uma postagem ao nosso blog. O subcomando create-post
ainda não foi definido - vamos fazê-lo agora.
x/blog/client/cli/tx.go
No bloco import
, certifique-se de importar estes cinco pacotes adicionais:
import (
// Importações existentes...
"bufio"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
sdk "github.com/cosmos/cosmos-sdk/types"
)
Este arquivo já contém o func GetTxCmd
que define os comandos personalizados . Adicionaremos o comando personalizado create-post
ao nosso blogcli
adicionando primeiro GetCmdCreatePost
ao blogTxCmd
.
blogTxCmd.AddCommand(flags.PostCommands(
GetCmdCreatePost(cdc),
)...)
No final do arquivo, vamos definir GetCmdCreatePost
.
package cli
import (
"bufio"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
"github.com/sdk-tutorials/starport-blog/blog/x/blog/types")
func GetCmdCreatePost(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "create-post [title] [body]",
Short: "Crie uma nova postagem",
Args: cobra.MinimumNArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
argsTitle := string(args[0])
argsBody := string(args[1])
cliCtx := context.NewCLIContext().WithCodec(cdc)
inBuf := bufio.NewReader(cmd.InOrStdin())
txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
msg := types.NewMsgCreatePost(cliCtx.GetFromAddress(), argsTitle, argsBody)
err := msg.ValidateBasic()
if err != nil {
return err
}
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
}
A função acima define o que acontece quando você executa o subcomando create-post
. create-post
recebe dois argumentos [title] [body]
, cria uma mensagem NewMsgCreatePost
( com títulos args[0]
e args[1]
) e transmite essa mensagem para ser processada em seu aplicativo.
Esse é um padrão comum no SDK: os usuários fazem alterações no blog transmitindo mensagens . Tanto os comandos CLI quanto as requisições HTTP criam mensagens que podem ser transmitidas para que ocorra a transição de estado.
x/blog/types/MsgCreatePost.go
Vamos definir NewMsgCreatePost
em um novo arquivo que você deve criar como x/blog/types/MsgCreatePost.go
.
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/google/uuid"
)
var _ sdk.Msg = &MsgCreatePost{}
type MsgCreatePost struct {
ID string
Creator sdk.AccAddress `json:"creator" yaml:"creator"`
Title string `json:"title" yaml:"title"`
Body string `json:"body" yaml:"body"`
}
func NewMsgCreatePost(creator sdk.AccAddress, title string, body string) MsgCreatePost {
return MsgCreatePost{
ID: uuid.New().String(),
Creator: creator,
Title: title,
Body: body,
}
}
func (msg MsgCreatePost) Route() string {
return RouterKey
}
func (msg MsgCreatePost) Type() string {
return "CreatePost"
}
func (msg MsgCreatePost) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{sdk.AccAddress(msg.Creator)}
}
func (msg MsgCreatePost) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
func (msg MsgCreatePost) ValidateBasic() error {
if msg.Creator.Empty() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "creator can't be empty")
}
return nil
}
Da mesma forma que o struct da postagem, MsgCreatePost
contém as propriedades do criador e do título. Não incluímos a propriedade ID, porque MsgCreatePost
define apenas os dados que aceitamos do usuário - estaremos gerando um ID automaticamente na próxima etapa.
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/google/uuid"
)
var _ sdk.Msg = &MsgCreatePost{}
type MsgCreatePost struct {
ID string
Creator sdk.AccAddress `json:"creator" yaml:"creator"`
Title string `json:"title" yaml:"title"`
Body string `json:"body" yaml:"body"`
}
func NewMsgCreatePost(creator sdk.AccAddress, title string, body string) MsgCreatePost {
return MsgCreatePost{
ID: uuid.New().String(),
Creator: creator,
Title: title,
Body: body,
}
}
func (msg MsgCreatePost) Route() string {
return RouterKey
}
func (msg MsgCreatePost) Type() string {
return "CreatePost"
}
func (msg MsgCreatePost) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{sdk.AccAddress(msg.Creator)}
}
func (msg MsgCreatePost) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
func (msg MsgCreatePost) ValidateBasic() error {
if msg.Creator.Empty() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "creator can't be empty")
}
return nil
}
NewMsgCreatePost
é uma função construtora que cria a mensagem MsgCreatePost
. As cinco funções a seguir devem ser definidas para implementar a interface Msg
. Elas permitem que você execute a validação que não requer acesso ao armazenamento(como verificar valores vazios), etc.
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/google/uuid"
)
var _ sdk.Msg = &MsgCreatePost{}
type MsgCreatePost struct {
ID string
Creator sdk.AccAddress `json:"creator" yaml:"creator"`
Title string `json:"title" yaml:"title"`
Body string `json:"body" yaml:"body"`
}
func NewMsgCreatePost(creator sdk.AccAddress, title string, body string) MsgCreatePost {
return MsgCreatePost{
ID: uuid.New().String(),
Creator: creator,
Title: title,
Body: body,
}
}
func (msg MsgCreatePost) Route() string {
return RouterKey
}
func (msg MsgCreatePost) Type() string {
return "CreatePost"
}
func (msg MsgCreatePost) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{sdk.AccAddress(msg.Creator)}
}
func (msg MsgCreatePost) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
func (msg MsgCreatePost) ValidateBasic() error {
if msg.Creator.Empty() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "creator can't be empty")
}
return nil
}
Voltando ao GetCmdCreatePost
no x/blog/client/cli/tx.go
, você verá que MsgCreatePost
está sendo criado e transmitido com GenerateOrBroadcastMsgs
.
Após serem transmitidas, as mensagens são processadas por uma parte importante da aplicação, chamada de handlers .
x/blog/handler.go
Comece importando seus novos tipos do blog que criamos:
import (
// Importações existentes...
"github.com/example/blog/x/blog/types"
)
Você já deve ter definido func NewHandle
, a qual lista todos os manipuladores disponíveis. Modifique-a para incluir uma nova função chamada handleMsgCreatePost
.
package blog
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/sdk-tutorials/starport-blog/blog/x/blog/keeper"
"github.com/sdk-tutorials/starport-blog/blog/x/blog/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
// NewHandler ...
func NewHandler(k keeper.Keeper) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) {
ctx = ctx.WithEventManager(sdk.NewEventManager())
switch msg := msg.(type) {
// Essa linha é usada pelo starport scaffolding
case types.MsgCreateComment:
return handleMsgCreateComment(ctx, k, msg)
case types.MsgCreatePost:
return handleMsgCreatePost(ctx, k, msg)
default:
errMsg := fmt.Sprintf("unrecognized %s message type: %T", types.ModuleName, msg)
return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, errMsg)
}
}
}
Agora vamos definir handleMsgCreatePost
:
package blog
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/sdk-tutorials/starport-blog/blog/x/blog/keeper"
"github.com/sdk-tutorials/starport-blog/blog/x/blog/types"
)
func handleMsgCreatePost(ctx sdk.Context, k keeper.Keeper, msg types.MsgCreatePost) (*sdk.Result, error) {
var post = types.Post{
Creator: msg.Creator,
ID: msg.ID,
Title: msg.Title,
Body: msg.Body,
}
k.CreatePost(ctx, post)
return &sdk.Result{Events: ctx.EventManager().Events()}, nil
}
Neste handler, você cria um objeto Post
(o tipo da postagem foi definido na primeira etapa). Você preenche o objeto de postagem com criador, título e o corpo da mensagem ( msg.Creator
, msg.Title
) e usa a ID exclusiva msg.Body
que foi gerada no tx.go
com NewMsgCreatePost()uuid.New().String()
Depois de criar um objeto de postagem com criador, ID e título, o handler de mensagens chama k.CreatePost(ctx, post)
. “k” significa Keeper , uma abstração usada pelo SDK que grava dados no armazenamento. Vamos definir uma função keeper CreatePost
.
x/blog/keeper/keeper.go
Adicione uma função CreatePost
que receba dois argumentos: um context e um post.
package keeper
import (
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/sdk-tutorials/starport-blog/blog/x/blog/types"
)
func (k Keeper) CreatePost(ctx sdk.Context, post types.Post) {
store := ctx.KVStore(k.storeKey)
key := []byte(types.PostPrefix + post.ID)
value := k.cdc.MustMarshalBinaryLengthPrefixed(post)
store.Set(key, value)
}
func listPost(ctx sdk.Context, k Keeper) ([]byte, error) {
var postList []types.Post
store := ctx.KVStore(k.storeKey)
iterator := sdk.KVStorePrefixIterator(store, []byte(types.PostPrefix))
for ; iterator.Valid(); iterator.Next() {
var post types.Post
k.cdc.MustUnmarshalBinaryLengthPrefixed(store.Get(iterator.Key()), &post)
postList = append(postList, post)
}
res := codec.MustMarshalJSONIndent(k.cdc, postList)
return res, nil
}
CreatePost
cria uma chave concatenando um prefixo de postagem com um ID. Se você observar a aparência do nosso armazenamento, notará que as chaves têm prefixos, e é por isso que post-0bae9f7d-20f8-4b51-9d5c-af9103177d66
contêm o prefixo post-
. A razão para isso é que você tem um armazenamento, mas pode querer manter diferentes tipos de objetos nele, como postagens e usuários. Prefixar chaves com post-
e user-
permite que você compartilhe um espaço de armazenamento entre diferentes tipos de objetos.
x/blog/types/key.go
Para definir o prefixo do post, adicione o seguinte código:
package types
const (
// ModuleName é o nome do módulo
ModuleName = "blog"
// StoreKey será usado quando for criado o KVStore
StoreKey = ModuleName
// RouterKey será usado para rotear as msgs
RouterKey = ModuleName
// QuerierRoute será usado para as queries das msgs
QuerierRoute = ModuleName
)
const (
PostPrefix = "post-"
)
const (
CommentPrefix = "comment-"
)
x/blog/types/codec.go
Finalmente, store.Set(key, value)
escreve nosso post para o blog. As duas últimas coisas a fazer é dizer ao nosso codificador como nosso MsgCreatePost
é convertido em bytes.
package types
import (
"github.com/cosmos/cosmos-sdk/codec"
)
// RegisterCodec registra tipos concretos no codec
func RegisterCodec(cdc *codec.Codec) {
// esta linha é usada pelo starport scaffolding
cdc.RegisterConcrete(MsgCreateComment{}, "blog/CreateComment", nil)
cdc.RegisterConcrete(MsgCreatePost{}, "blog/CreatePost", nil)
}
// ModuleCdc define o módulo codec
var ModuleCdc *codec.Codec
func init() {
ModuleCdc = codec.New()
RegisterCodec(ModuleCdc)
codec.RegisterCrypto(ModuleCdc)
ModuleCdc.Seal()
}
Lançamento
Agora estamos prontos para compilar e iniciar nosso aplicativo e criar alguns posts.
Para iniciar seu aplicativo, execute:
starport serve
Este comando instala dependências, compila e inicializa o aplicativo, executando os servidores. Você também pode fazer isso manualmente:
go mod tidy
limpa as dependências.make
compila seu aplicativo e cria dois binários em seu caminho:blogd
eblogcli
.Os scripts de inicialização no
Makefile
removem os diretórios de dados, e configuram seu aplicativo gerando duas contas. Por padrão, seu aplicativo armazena dados em seu diretório inicial em~/.blogd
e~/.blogcli
. O script os remove, portanto, toda vez você terá um estado limpo.blogd start
publica seu aplicativo. Após alguns segundos, você verá hashes de blocos sendo gerados. Deixe esta janela do terminal aberta e abra uma nova.
Observação: dependendo das configurações do sistema operacional e do firewall, talvez seja necessário aceitar uma solicitação perguntando se o binário do seu aplicativo (neste caso o blogd
) pode aceitar conexões externas.
Execute o seguinte comando para criar uma postagem:
blogcli tx blog create-post " 'Minha primeira postagem" "Isso é uma postagem\!" --from=user1
“Isto é um post!” é um título para nosso post e --from=user1
diz ao programa quem está criando este post. user1
é um rótulo para o par de chaves usado para assinar a transação, criado pelo script de inicialização localizado anteriormente em /Makefile
. As chaves são armazenadas em ~/.blogcli
.
Depois de executar o comando e confirmá-lo, você verá um objeto com a propriedade “txhash” com um valor como CA1491B39384A4F29E568F62B156E0F2D0601507EF499CE1B8F3930BAFE7F03C
.
Para verificar se a transação foi processada, abra um navegador e visite o seguinte URL (certifique-se de substituir CA14…
pelo valor do seu txhash, mas certifique-se de manter o prefixo 0x
):
[http://localhost:26657/tx?hash=0xCA1491B39384A4F29E568F62B156E0F2D0601507EF499CE1B8F3930BAFE7F03C](http://localhost:26657/tx?hash=0xCA1491B39384A4F29E568F62B156E0F2D0601507EF499CE1B8F3930BAFE7F03C)
Parabéns! Você acabou de criar e publicar sua blockchain personalizada e enviar a primeira transação 🎉
Erros potenciais e suas soluções
Não é possível encontrar o módulo que fornece o pacote
x/blog/client/cli/tx.go:12:2: cannot find module providing package github.com/cosmos/cosmos-sdk/client/utils: import lookup disabled by -mod=readonly
x/blog/client/cli/tx.go:75:59: undefined: sdk
Certifique-se de importar todos os pacotes necessários em x/blog/client/cli/tx.go
:
import (
// ...
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
)
"create-post" é um comando desconhecido para "blog"
blogcli tx blog create-post 'Olá!' 'Minha primeira postagem' --from=user1
ERROR: unknown command "create-post" for "blog"
Certifique-se de ter adicionado GetCmdCreatePost(cdc)
, func GetTxCmd
no x/blog/client/cli/tx.go
.
Não é possível codificar o tipo concreto não registrado
blogcli tx blog create-post Hello! --from=user1
panic: Cannot encode unregistered concrete type types.MsgCreatePost.
Certifique-se de ter adicionado cdc.RegisterConcrete(MsgCreatePost{}, "blog/CreatePost", nil)
em func RegisterCodecx/blog/types/codec.go
Conclusão
Parabéns, agora você tem um aplicativo de blog em funcionamento que armazena seus dados no Cosmos!
Este artigo foi escrito por Figment e traduzido por Adriano P. de Araujo. O original em inglês pode ser encontrado aqui.
Latest comments (0)