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 tidylimpa as dependências.makecompila seu aplicativo e cria dois binários em seu caminho:blogdeblogcli.Os scripts de inicialização no
Makefileremovem 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~/.blogde~/.blogcli. O script os remove, portanto, toda vez você terá um estado limpo.blogd startpublica 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.


Top comments (0)