WEB3DEV

Cover image for Crie um módulo de blog usando o Cosmos SDK
Adriano P. Araujo
Adriano P. Araujo

Posted on

Crie um módulo de blog usando o Cosmos SDK

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            

Enter fullscreen mode Exit fullscreen mode

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"`

}

Enter fullscreen mode Exit fullscreen mode

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"

)

Enter fullscreen mode Exit fullscreen mode

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})
},
}
}
Enter fullscreen mode Exit fullscreen mode

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

}
Enter fullscreen mode Exit fullscreen mode

        

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

}

Enter fullscreen mode Exit fullscreen mode

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

}

Enter fullscreen mode Exit fullscreen mode

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"

)

Enter fullscreen mode Exit fullscreen mode

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)

}

}

}

Enter fullscreen mode Exit fullscreen mode

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

}

Enter fullscreen mode Exit fullscreen mode

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

}

Enter fullscreen mode Exit fullscreen mode

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-"

)

Enter fullscreen mode Exit fullscreen mode

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()

}

Enter fullscreen mode Exit fullscreen mode

Lançamento

Agora estamos prontos para compilar e iniciar nosso aplicativo e criar alguns posts.

Para iniciar seu aplicativo, execute:


              starport serve

Enter fullscreen mode Exit fullscreen mode

Este comando instala dependências, compila e inicializa o aplicativo, executando os servidores. Você também pode fazer isso manualmente:

  1. go mod tidy limpa as dependências.

  2. make compila seu aplicativo e cria dois binários em seu caminho: blogd e blogcli.

  3. 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.

  4. 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

Enter fullscreen mode Exit fullscreen mode

“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)

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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"

)

Enter fullscreen mode Exit fullscreen mode

"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"

Enter fullscreen mode Exit fullscreen mode

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.

Enter fullscreen mode Exit fullscreen mode

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.

Oldest comments (0)