一、快速入门
Ignite CLI version: v0.26.1
在本教程中,我们将使用一个模块创建一个区块链,该模块允许我们从区块链中写入和读取数据。这个模块将实现创建和阅读博客文章的功能,类似于博客应用程序。最终用户将能够提交新的博客文章,并查看区块链上现有文章的列表。本教程将指导您完成创建和使用此模块与区块链交互的过程。
本教程的目标是提供创建反馈循环的逐步说明,该反馈循环允许您向区块链提交数据并从区块链读取该数据。在本教程结束时,您将实现一个完整的反馈循环,并能够使用它与区块链进行交互。
首先,用Ignite CLI创建一个新的博客区块链:
$ ignite scaffold chain blog
为了创建使用区块链的博客应用程序,我们需要定义应用程序的需求。我们希望应用程序在区块链上存储Post
类型的对象。这些对象应该有两个属性:title
和body
。
除了在区块链上存储文章外,我们还希望为用户提供对这些文章执行CRUD(创建、读取、更新和删除)操作的能力。这将允许用户创建新的文章,阅读现有文章,更新现有文章的内容,以及删除不再需要的文章。
Ignite CLI的特性之一是能够生成实现基本CRUD功能的代码。这是通过使用脚手架命令来实现的,可以使用这些命令快速生成在应用程序中创建、读取、更新和删除数据所需的代码。
Ignite CLI能够为存储在不同类型数据结构中的数据生成代码。这包括列表(lists,按递增整数索引的数据集合)、映射(maps,按自定义键索引的集合)和简单类型(singles,数据的单个实例)。通过使用这些不同的数据结构,可以定制应用程序以满足特定的需求。例如,如果您正在构建一个博客应用程序,您可能希望使用一个列表来存储所有文章,每个文章都用一个整数作为索引。或者,您也可以使用一个map根据每个文章的唯一标题来索引它,或者使用一个map来存储单个文章。数据结构的选择取决于应用程序的特定需求。
除了您选择的数据结构之外,Ignite CLI还要求您提供它将为其生成代码的数据类型的名称,以及描述数据类型的字段。例如,如果您正在创建一个博客应用程序,您可能想要创建一个名为Post
的类型,其中包含文章的title
和body
字段。Ignite CLI将使用这些信息生成必要的代码,用于在应用程序中创建、读取、更新和删除这种类型的数据。
进入blog
目录,执行ignite scaffold list
命令:
$ cd blog
$ ignite scaffold type post title body creator id:uint
现在您已经使用Ignite CLI为您的应用程序生成了代码,让我们回顾一下它创建了什么。Ignite CLI 将为您指定的数据结构和数据类型生成代码,以及操作这些数据所需的基本CRUD操作的代码。这段代码将为您的应用程序提供坚实的基础,您可以进一步定制它以满足您的特定需求。通过检查 ignite CLI 生成的代码,您可以确保它满足您的需求,并更好地理解如何使用该工具构建应用程序。
Ignite CLI在proto/blog/
目录中生成了几个文件和修改。这些包括:
-
post.proto
: 这是一个协议缓冲文件,定义Post
类型,包含字段title
、body
、id
和creator
。 -
tx.proto
: 该文件已被修改为包含三个RPCs (远程过程调用):CreatePost
、UpdatePost
和DeletePost
。每个 RPC 都对应一个Cosmos SDK消息,可用于在文章上执行相应的CRUD操作。 -
query.proto
:该文件已被修改为包含两个查询:Post
和PostAll
。Post
查询可用于根据其ID检索单个文章,而PostAll
查询可用于检索经过分页的文章列表。 -
genesis.proto
: 该文件已被修改为在模块的起源状态中包含posts,该模块定义了区块链第一次启动时的初始状态。
Ignite CLI还在x/blog/keeper
目录中生成了几个新文件,为应用程序实现了特定于CRUD的逻辑。这些包括:
msg_server_post.go
: 这个文件实现了CreatePost
、UpdatePost
和DeletePost
消息的keeper方法。这些方法在模块处理相应的消息时被调用,它们处理每个CRUD操作的特定逻辑。query_post.go
: 该文件实现Post
和PostAl
l查询,它们分别用于按ID检索单个文章或分页的文章列表。post.go
: 该文件实现了keeper方法所依赖的底层函数。这些功能包括将文章追加(添加)到商店、获取单个文章、获取文章数以及管理应用程序中的文章所需的其他操作。
总的来说,这些文件为博客应用程序的CRUD功能提供了必要的实现。它们处理每个CRUD操作的特定逻辑,以及这些操作所依赖的底层函数。
在x/blog/types
目录中创建和修改的文件:
-
messages_post.go
: 这个新文件包含Cosmos SDK消息构造函数和相关方法,如Route()
,Type()
,GetSigners()
,GetSignBytes()
, 和ValidateBasic()
。 -
keys.go
: 该文件已被修改为包含用于存储博客文章的key前缀。通过使用key前缀,我们可以确保博客文章的数据与数据库中的其他类型的数据是分开的,并且在需要时可以轻松访问。 -
genesis.go
: 该文件被修改为定义博客模块的初始(创世)状态,以及验证初始状态的Validate()
函数。这是设置区块链的重要步骤,因为它定义了初始数据,并确保它根据应用程序的规则有效。 -
codec.go
: 修改此文件以向编码器注册我们的消息类型,允许它们在通过网络传输时正确地序列化和反序列化。
此外,*.pb.go
文件由*.proto
生成。它们包含应用程序使用的消息、rpc和查询的类型定义。这些文件是使用协议缓冲区(protobuf)工具从*.proto
生成的,它允许我们以语言无关的方式定义数据的结构。
Ignite CLI通过创建和修改几个文件,为x/blog/client/cli
CLI目录添加了一些功能。
tx_post.go
: 创建此文件是为了实现CLI命令,用于广播blog模块中包含消息的事务。这些命令允许用户使用Ignite CLI轻松地向区块链发送消息。query_post.go
:该文件用于实现CLI命令查询blog模块。这些命令允许用户从区块链检索信息,例如博客文章列表。tx.go
: 该文件被修改为向链的二进制文件中添加广播事务的CLI命令。query.go
: 该文件还被修改为将用于查询链的CLI命令添加到链的二进制中。
如您所见,ignite scaffold list
命令已经生成并修改了许多源代码文件。这些文件定义了消息的类型、处理消息时执行的逻辑,以及将所有内容连接在一起的连接。这包括创建、更新和删除博客文章的逻辑,以及检索此信息所需的查询。
要查看实际生成的代码,我们需要启动区块链。我们可以通过使用ignite chain serve
命令来做到这一点,该命令将为我们构建、初始化和启动区块链:
ignite chain serve
一旦区块链开始运行,我们就可以使用二进制文件与它交互,并查看代码如何处理创建、更新和删除博客文章。我们还可以看到它如何处理和响应查询。这将使我们更好地理解应用程序是如何工作的,并允许我们测试它的功能。
当ignite chain service
在一个终端窗口中运行时,打开另一个终端,并使用链的二进制文件在区块链上创建一个新的博客文章:
blogd tx blog create-post 'Hello, World!' 'This is a blog post' --from alice
当使用--from
标志指定将用于签署事务的帐户时,确保指定的帐户可用是很重要的。在开发环境中,您可以在ignite chain serve
命令的输出中或在config.yml
文件中看到可用帐户的列表。
同样值得注意的是,在广播事务时需要使用--from
标志。该标志指定将用于签署事务的帐户,这是事务处理中的关键步骤。没有有效的签名,交易将不会被区块链接受。因此,确保使用--from
标志指定的帐户可用是很重要的。
在事务成功广播之后,您可以查询区块链以获得博客文章列表。为此,您可以使用blogd q blog list-post
命令,该命令将返回已添加到区块链的所有博客文章的分页列表。
通过查询区块链,您可以验证您的交易是否成功处理,博客文章是否已添加到链中。此外,您还可以使用其他查询命令检索关于区块链上其他数据的信息,例如帐户、余额和治理建议。
让我们通过改变body
内容来修改刚刚创建的博客文章。为此,我们可以使用blogd tx blog update-post
命令,该命令允许我们更新区块链上现有的博客文章。在运行这个命令时,我们需要指定我们想要修改的博客文章的ID,以及我们想要使用的新的正文内容。运行此命令后,交易将广播到区块链,博客文章将使用新的正文内容进行更新。
现在我们已经用新内容更新了博客文章,让我们再次查询区块链以查看更改。为此,我们可以使用blogd q blog list-post
命令,该命令将返回区块链上所有博客文章的列表。通过再次运行此命令,我们可以在列表中看到更新的博客文章,并且可以验证我们所做的更改已成功应用于区块链。
让我们试着用Bob的账户删除一篇博客文章。但是,由于博客文章是使用Alice的帐户创建的,我们可以期望区块链检查用户是否被授权删除文章。在这种情况下,由于Bob不是文章的作者,他的交易应该被区块链拒绝。
要删除一篇博客文章,我们可以使用blogd tx blog delete-post
命令,它允许我们删除区块链上现有的博客文章。在运行此命令时,我们需要指定要删除的博客文章的ID,以及要用于签署事务的帐户。在本例中,我们将使用Bob的帐户来签署交易。
运行此命令后,事务将广播到区块链。但是,由于Bob不是文章的作者,区块链应该拒绝他的交易,博客文章也不会被删除。这是区块链如何执行规则和权限的示例,它表明只有经过授权的用户才能对区块链进行更改。
现在,让我们再次尝试删除博客文章,但这次使用Alice的帐户。既然Alice是这篇博客文章的作者,她应该有权删除它。
为了检查Alice是否成功删除了博客文章,我们可以再次查询区块链以获得文章列表。
恭喜你成功完成了使用Ignite CLI创建博客的教程!按照说明,您已经学习了如何创建一个新的区块链,为具有CRUD功能的“post”类型生成代码,启动一个本地区块链,并测试您的博客的功能。
现在您已经有了一个简单应用程序的工作示例,您可以使用Ignite生成的代码进行实验,看看更改如何影响应用程序的行为。这是一项很有价值的技能,因为它将允许您定制应用程序以满足特定需求并改进应用程序的功能。您可以尝试更改数据结构或数据类型,或向代码添加其他字段或功能。
在接下来的教程中,我们将仔细研究Ignite生成的代码,以便更好地理解如何构建区块链。通过自己编写一些代码,我们可以更深入地了解Ignite的工作原理,以及如何使用它在区块链上创建应用程序。这将帮助我们更多地了解Ignite CLI的功能,以及如何使用它来构建健壮而强大的应用程序。请关注这些教程,并准备好与Ignite一起深入区块链的世界!
二、Blog – In-depth tutorial
在本教程中,您将学习如何从头开始使用Ignite CLI创建Cosmos SDK区块链的博客应用程序。这意味着您将负责设置必要的类型、消息和查询,并编写在区块链上创建、读取、更新和删除博客文章的逻辑。
您将构建的应用程序的功能将与Ignite CLI命令Ignite scaffold list post title body
生成的功能相同,但您将手动执行,以便更深入地了解该过程。通过本教程,您将学习如何使用Ignite CLI在Cosmos SDK区块链上构建一个博客应用程序。
2.1 创建结构
使用以下命令创建一个新的区块链:
# blog 以 example 代替
ignite scaffold chain blog
这将创建一个名为blog/
的新目录,其中包含区块链应用程序所需的文件和目录。接下来,通过运行以下命令导航到新创建的目录:
$ cd blog
因为你的应用程序将存储和操作博客文章,你将需要创建一个Post类型来表示这些文章。您可以使用以下Ignite CLI命令:
$ ignite scaffold type post title body creator id:uint
这将创建一个带有四个字段的Post
类型:string
类型的title
, body
, creator
和uint
类型的id
。
在使用Ignite的代码脚手架命令后,将更改提交到Git等版本控制系统是一个很好的实践。这将允许您区分Ignite自动做出的更改和开发人员手动做出的更改,还允许您在必要时回滚更改。您可以使用以下命令将更改提交到Git:
2.1.1 创建消息
接下来,您将为您的博客文章实现CRUD(创建、读取、更新和删除)操作。因为创建、更新和删除操作会改变应用程序的状态,所以它们被认为是写操作。
在Cosmos SDK区块链中,通过广播包含触发状态转换的消息的交易(transactions)来改变状态。要创建广播和处理带有“create post”消息的交易的逻辑,您可以使用以下Ignite CLI命令:
ignite scaffold message create-post title body --response id:uint
这将创建一个“create post”消息,包含两个字段:title
和body
,它们都是string
类型。post将存储在类似于列表的数据结构的键值存储中,其中它们的索引是一个递增的整数ID。当一个新文章被创建时,它将被分配一个整数ID。--response
标志用于返回uint
类型的id
,作为对“create post”消息的响应。
要在应用程序中更新特定的博客文章,需要创建一个名为“update post”的消息,该消息接受三个参数:title
、body
和id
。uint
类型的id
参数对于指定要更新的博客文章是必要的。您可以使用Ignite CLI命令创建此消息:
ignite scaffold message update-post title body id:uint
要在应用程序中删除特定的博客文章,需要创建一个名为“delete post”的消息,该消息只接受要删除的文章的id
。您可以使用Ignite CLI命令创建此消息:
ignite scaffold message delete-post id:uint
2.1.2 创建查询
查询(Queries)允许用户从区块链状态检索信息。在您的应用程序中,您将有两个查询:“show post”和“list post”。“show post”查询将允许用户通过ID检索特定的文章,而“list post”查询将返回所有存储的文章的分页列表。
创建“show post”查询,可以使用下面的Ignite CLI命令:
ignite scaffold query show-post id:uint --response post:Post
该查询将接受类型为uint
的id
作为参数,并将返回类型为Post
的post
作为响应。
要创建“list post”查询,您可以使用以下Ignite CLI命令:
ignite scaffold query list-post --response post:Post --paginated
该查询将在分页输出中返回Post
类型的post
。--paginated
标志表示查询应该以分页格式返回结果,允许用户一次检索特定页面的结果。
总结
祝贺您完成了区块链应用程序的初始设置!您已经成功地创建了一个“post”数据类型,并生成了处理三种类型的消息(创建、更新和删除)和两种类型的查询(列出和显示文章)所需的代码。
但是,此时,您所创建的消息将不会触发任何状态转换,并且您所创建的查询将不会返回任何结果。这是因为Ignite只为这些特性生成样板代码,而实现必要的逻辑以使它们发挥作用则取决于您。
在本教程的下一章中,您将学习如何实现消息处理和查询逻辑以完成区块链应用程序。这将涉及编写代码来处理您创建的消息和查询,并使用它们来修改或从区块链的状态中检索数据。在此过程结束时,您将在Cosmos SDK区块链上拥有一个功能齐全的博客应用程序。
2.2 创建文章
在本章中,我们将关注处理“create post”消息的过程。这涉及到使用一种特殊类型的函数,称为keeper方法。Keeper方法负责与区块链交互,并根据消息中提供的指令修改其状态
。
当接收到“create post”消息时,将调用相应的keeper方法并将该消息作为参数传递。然后,keeper方法可以使用store对象提供的各种getter和setter函数来检索和修改区块链的当前状态。这允许keeper方法有效地处理“create post”消息,并对区块链进行必要的更新。
为了保持访问和修改store对象的代码整洁,并与keeper方法中实现的逻辑分离,我们将创建一个名为post.go
的新文件。该文件将包含专门设计用于处理与在区块链中创建和管理文章相关的操作的函数。
2.2.1 添加文章到 store
# x/blog/keeper/post.go
package keeper
import (
"encoding/binary"
"blog/x/blog/types"
"github.com/cosmos/cosmos-sdk/store/prefix"
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
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.PostKey))
appendedValue := k.cdc.MustMarshal(&post)
store.Set(GetPostIDBytes(post.Id), appendedValue)
k.SetPostCount(ctx, count+1)
return count
}
这段代码定义了一个名为AppendPost
的函数,它属于Keeper
类型。Keeper
类型负责与区块链交互,并修改其状态以响应各种消息。
AppendPost
函数有两个参数:一个Context
对象和一个Post
对象。Context
对象是Cosmos SDK中许多函数中的标准参数,用于提供有关区块链当前状态的上下文信息,例如当前块高度。Post
对象表示将添加到区块链的文章。
该函数首先使用GetPostCount
方法检索当前的文章计数。您将在下一步中实现此方法,因为它还没有实现。该方法在Keeper
对象上调用,并接受Context
对象作为参数。它返回已添加到区块链的当前文章数。
接下来,该函数将新文章的ID
设置为当前文章数,这样每个文章都有唯一的标识符。它通过将count
的值分配给Post
对象的Id
字段来实现这一点。
函数然后使用prefix.NewStore
创建一个新的store对象。prefix.NewStore
函数接受两个参数:与所提供的上下文关联的KVStore
和Post
对象的键前缀。KVStore
是一个键值存储,用于在区块链上持久化数据,KeyPrefix
用于将Post
对象与可能存储在同一KVStore
中的其他类型对象区分开来。
该函数然后使用cdc.MustMarshal
函数序列化Post
对象,并使用store
对象的Set
方法将其存储在区块链中。cdc.MustMarshal
函数是Cosmos SDK encoding/decoding (编码/解码库)的一部分,用于将Post
对象转换为可以存储在KVStore
中的字节片。Se
t方法在store
对象上被调用,它有两个参数:一个键和一个值。在本例中,键是由GetPostIDBytes
函数生成的字节片,值是序列化的Post
对象。您将在下一步中实现此方法,因为它还没有实现。
最后,使用SetPostCount
方法将 post 计数加 1,并更新区块链状态。您将在下一个步骤中实现该方法,因为它还没有实现。这个方法在Keeper
对象上被调用,并在Context
对象和一个新的post 计数作为参数。它更新了区块链中的当前 post 计数,成为新的 post 计数。
然后,函数返回新创建的post的ID,这是当前的post计数,然后再增加。这允许调用函数的调用者知道被添加到区块链的post的ID。
为了完成AppendPost
的实现,需要执行以下任务:
- 定义
PostKey
,它将用于存储并从数据库中检索 post。 - 实现
GetPostCount
,它将检索存储在数据库中的当前存储数。 - 实现
GetPostIDBytes
,它将将post ID转换为一个字节数组。 - 实现
SetPostCount
,它将更新存储在数据库中的post计数。
Post key prefix
在文件keys.go
中。我们来定义PostKey
前缀如下:
# x/blog/types/keys.go
const (
PostKey = "Post/value/"
)
这个前缀将用于在系统中唯一标识一个文章 。它将被用作每个 post 的键的开始,然后是 post 的ID,为每个文章创建一个唯一的键。
获得 post 计数
在文件post.go
中。我们来定义GetPostCount
函数如下:
# x/blog/keeper/post.go
func (k Keeper) GetPostCount(ctx sdk.Context) uint64 {
store := prefix.NewStore(ctx.KVStore(k.storeKey), []byte{})
byteKey := types.KeyPrefix(types.PostCountKey)
bz := store.Get(byteKey)
if bz == nil {
return 0
}
return binary.BigEndian.Uint64(bz)
}
这段代码定义了一个名为GetPostCount
的函数,它属于Keeper
结构体。该函数只接受一个参数,即sdk.Context
的上下文对象ctx
。,返回类型为uint64
的值。
该函数首先使用上下文中的键值存储和空字节片作为前缀创建一个新store
。然后,它使用types包中的KeyPrefix函数定义一个字节片byteKey,该函数接受PostCountKey
。您将在下一步中定义PostCountKey。
然后,该函数使用store
的Get
方法检索 store 中键byteKey
处的值,并将其存储在变量bz
中。
接下来,该函数使用if
语句检查byteKe
y的值是否为nil
。如果它为nil
,意味着该键在store
中不存在,则函数返回0
。这表明没有与该键关联的元素或文章。
如果byteKey
的值不是nil
,该函数使用二进制包的BigEndian
类型解析bz
中的字节,并返回结果的uint64
值。BigEndian
类型用于将bz
中的字节解释为大端序编码的无符号64位整数。Uint64
方法将字节转换为uint64
值并返回。
GetPostCount
函数用于检索键值store
中存储的post
的总数,以uint64
值表示。
在文件keys.go
中,让我们定义PostCountKey
如下:
# x/blog/types/keys.go
const (
PostCountKey = "Post/count/"
)
此key 将用于跟踪添加到 store 的最新文章的ID。
将文章ID转换为字节
现在,让我们实现GetPostIDBytes
,它将post ID转换为字节数组。
# x/blog/keeper/post.go
func GetPostIDBytes(id uint64) []byte {
bz := make([]byte, 8)
binary.BigEndian.PutUint64(bz, id)
return bz
}
GetPostIDBytes
接收类型为uint64
的值id
,并返回类型为[]byte
的值。
该函数首先使用make
内置函数创建一个长度为8
的新字节切片bz
。然后,它使用binary
包的BigEndian
类型将id
的值编码为大端序编码的无符号整数,并使用PutUint64
方法将结果存储在bz
中。最后,函数返回得到的字节切片bz
。
此函数可用于将post ID(表示为uint64)转换为字节切片(byte slice),该字节切片可用作键值存储中的键。binary.BigEndian.PutUint64
函数将id
的uint64
值编码为大端序无符号整数,并将结果字节存储在[]字节片bz中。然后,生成的字节片可以用作存储中的键。
更新post count
在post.go
中实现SetPostCount
,它将更新存储在数据库中的文章计数。
# x/blog/keeper/post.go
func (k Keeper) SetPostCount(ctx sdk.Context, count uint64) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), []byte{})
byteKey := types.KeyPrefix(types.PostCountKey)
bz := make([]byte, 8)
binary.BigEndian.PutUint64(bz, count)
store.Set(byteKey, bz)
}
这段代码在Keeper
结构中定义了一个函数SetPostCount
。该函数接受类型为sdk.Context
的ctx
。上下文和类型为uint64
的值count
,并且不返回值。
该函数首先从前缀包调用NewStore
函数,并从上下文传入键值store和一个空字节切片作为前缀,从而创建一个新 store。它将结果存储在名为store
的变量中。
接下来,该函数使用types
包中的KeyPrefix
函数定义一个字节切片byteKey
,并传入PostCountKey
。KeyPrefix
函数返回一个以给定键作为前缀的字节切片。
然后,该函数使用make
内置函数创建一个长度为8
的新字节切片bz
。然后,它使用binary
包的BigEndian
类型将count
的值编码为大端序编码的无符号整数,并使用PutUint64
方法将结果存储在bz
中。
最后,函数调用store
变量的Set
方法,将byteKey
和bz
作为参数传入。这将store
中键byteKey
处的值设置为值bz
。
这个函数可以用来更新存储在数据库中的文章的计数。它通过使用binary.BigEndian.PutUint64
将count
的uint64
值转换为字节切片来实现这一点,然后使用Set
方法将结果字节切片存储在store 中的键byteKey
处。
现在您已经实现了创建博客文章的代码,接下来可以实现keeper
方法,该方法在处理“create post”消息时被调用。
2.2.2 处理“create post”消息
# 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
}
CreatePost
函数是MsgCreatePost
消息类型的消息处理程序。它负责根据MsgCreatePost
消息中提供的信息在区块链上创建一个新的文章。
该函数首先使用sdk.UnwrapSDKContext
函数 从Go上下文检索Cosmos SDK context
。U然后,它使用MsgCreatePost
消息中的Creator
、Title
和Body
字段创建一个新的Post
对象。
接下来,该函数调用msgServer
对象(属于Keeper类型)上的AppendPost
方法,并传入Cosmos SDK context 和新的Post
对象作为参数。AppendPost
方法负责将新文章添加到区块链并返回新文章的ID
。
最后,该函数返回一个MsgCreatePostResponse
对象,其中包含新文章的ID
。它还返回一个nil
错误,表示操作成功。
测试
$ exampled tx example create-post 'Hello, world!' 'This is a blog post' --from alice
总结
伟大的工作!您已经成功实现了将博客文章写入区块链存储的逻辑,以及在处理“create post”消息时将调用的keeper方法。
AppendPost
keeper方法检索当前的文章计数,将新文章的ID
设置为当前的文章计数,序列化文章对象,并使用store
对象的Set
方法将其存储在区块链中。存储中post的键是由GetPostIDBytes
函数生成的字节切片,值是序列化的post
对象。然后,该函数将发布计数加1,并使用SetPostCount
方法更新区块链状态。
CreatePost
处理程序方法接收包含新文章数据的MsgCreatePost
消息,使用该数据创建一个新的post
对象,并将其传递给AppendPost
keeper方法以添加到区块链。然后它返回一个MsgCreatePostResponse
对象,其中包含新创建的文章的ID
。
通过实现这些方法,您已经成功地实现了处理“创建文章”消息和向区块链添加文章的必要逻辑。
2.3 更新文章
在本章中,我们将关注处理“update post”消息的过程。
要更新文章,您需要使用“Get”操作从存储中检索特定的文章,修改值,然后使用“Set”操作将更新的文章写回存储中。
让我们首先实现一个getter和一个setter逻辑。
2.3.1 获取文章
在post.go
中实现GetPost
keeper方法:
# x/blog/keeper/post.go
func (k Keeper) GetPost(ctx sdk.Context, id uint64) (val types.Post, found bool) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.PostKey))
b := store.Get(GetPostIDBytes(id))
if b == nil {
return val, false
}
k.cdc.MustUnmarshal(b, &val)
return val, true
}
GetPost
接受两个参数:一个上下文ctx
和一个类型为uint64
的id
,表示要检索的post
的id
。它返回一个types.Post
结构,包含文章的值,以及一个布尔值,指示是否在数据库中找到了文章。
该函数首先使用prefix.NewStore
方法创建store
,从上下文类型中传入键值store 。types.KeyPrefix
函数应用于类型的types.PostKey
常量作为参数。然后,它尝试使用store.Get
方法从存储中检索文章,将post的ID作为字节切片传入。如果在存储中没有找到post,则返回空types.Post
类型结构和布尔值false
。
如果在存储(store)中找到post
,该函数使用cdc.MustUnmarshal
方法将检索到的字节切片解组为types.Post
类型,传入val
变量的指针作为参数。然后它返回val
结构体和一个布尔值true
,表示在数据库中找到了文章。
2.3.2 设置文章
在post.go
中实现SetPost
keeper方法:
# x/blog/keeper/post.go
func (k Keeper) SetPost(ctx sdk.Context, post types.Post) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.PostKey))
b := k.cdc.MustMarshal(&post)
store.Set(GetPostIDBytes(post.Id), b)
}
SetPost
接受两个参数:一个context ctx
和一个types.Post
结构,其中包含Post的更新值。该函数不返回任何东西。
该函数首先使用prefix.NewStore
方法创建存储,从上下文和类型中传入键值 store。应用于types.PostKey
常量的types.KeyPrefix
函数作为参数。然后它使用cdc.MustMarshal
将更新后的post结构封送到字节切片中,将指向post结构体的指针作为参数传入。最后,它使用store.Set
方法更新 store 中的文章,将post的ID
作为字节切片传入,并将编码后的post结构作为参数传入。
2.3.3 更新文章
# x/blog/keeper/msg_server_update_post.go
package keeper
import (
"context"
"fmt"
"blog/x/blog/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
func (k msgServer) UpdatePost(goCtx context.Context, msg *types.MsgUpdatePost) (*types.MsgUpdatePostResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
var post = types.Post{
Creator: msg.Creator,
Id: msg.Id,
Title: msg.Title,
Body: msg.Body,
}
val, found := k.GetPost(ctx, msg.Id)
if !found {
return nil, sdkerrors.Wrap(sdkerrors.ErrKeyNotFound, fmt.Sprintf("key %d doesn't exist", msg.Id))
}
if msg.Creator != val.Creator {
return nil, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "incorrect owner")
}
k.SetPost(ctx, post)
return &types.MsgUpdatePostResponse{}, nil
}
UpdatePost
接受上下文和消息MsgUpdatePost
作为输入,并返回响应MsgUpdatePostResponse
和一个错误。该函数首先使用提供的msg.Id
从数据库中检索帖子的当前值,并检查post是否存在以及msg.Creator
是否与帖子的当前所有者相同。如果其中一个检查失败,它将返回一个错误。如果两个检查都通过,它就用msg
中提供的新值更新数据库中的post,并返回一个没有错误的响应。
2.4 删除文章
在本章中,我们将关注处理“delete post”消息的过程。
2.4.1 删除文章
# x/blog/keeper/post.go
func (k Keeper) RemovePost(ctx sdk.Context, id uint64) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.PostKey))
store.Delete(GetPostIDBytes(id))
}
RemovePost
函数接受两个参数:上下文对象ctx
和无符号整数id
。该函数通过删除与给定id
相关联的键值对来从键值sotre 中删除帖子。键值存储是使用store
变量访问的,store
变量是通过使用prefix
包创建的,该前缀包使用上下文的键值存储和基于PostKey
常量的前缀创建一个新的存储。然后在存储对象上调用Delete
方法,使用GetPostIDBytes
函数将id
转换为字节切片作为要删除的键。
2.4.2 Deleting posts
# x/blog/keeper/msg_server_delete_post.go
package keeper
import (
"context"
"fmt"
"blog/x/blog/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
func (k msgServer) DeletePost(goCtx context.Context, msg *types.MsgDeletePost) (*types.MsgDeletePostResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
val, found := k.GetPost(ctx, msg.Id)
if !found {
return nil, sdkerrors.Wrap(sdkerrors.ErrKeyNotFound, fmt.Sprintf("key %d doesn't exist", msg.Id))
}
if msg.Creator != val.Creator {
return nil, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "incorrect owner")
}
k.RemovePost(ctx, msg.Id)
return &types.MsgDeletePostResponse{}, nil
}
DeletePost
接受两个参数:类型为context的上下文goCtx
。上下文和指向类型为*types.MsgDeletePost
的消息的指针。该函数返回一个指向类型MsgDeletePostRespons
e消息的指针和一个错误。
2.5 展示一篇文章
在本章中,您将在您的博客应用程序中实现一个特性,使用户能够通过其唯一ID
检索单个博客文章。当每篇博客文章创建并存储在区块链上时,这个 ID 被分配给它。通过添加这个查询功能,用户可以通过指定自己的 ID 轻松检索特定的博客文章。
2.5.1 Show post
让我们实现ShowPost
keeper方法,当用户对区块链应用程序进行查询并指定所需帖子的ID时,将调用该方法。
# x/blog/keeper/query_show_post.go
package keeper
import (
"context"
"blog/x/blog/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (k Keeper) ShowPost(goCtx context.Context, req *types.QueryShowPostRequest) (*types.QueryShowPostResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "invalid request")
}
ctx := sdk.UnwrapSDKContext(goCtx)
post, found := k.GetPost(ctx, req.Id)
if !found {
return nil, sdkerrors.ErrKeyNotFound
}
return &types.QueryShowPostResponse{Post: post}, nil
}
ShowPost
是一个从区块链的状态中检索单个post对象的函数。它包含两个参数:名为goCtx的context.Context
对象和指向types.QueryShowPostRequest
类型的指针,称为req
。它返回一个指向types.QueryShowPostResponse
类型的指针和一个错误。
该函数首先检查req
参数是否为nil
。如果是,它将。使用google.golang.org/grpc/status
包中的status.Error
函数,返回一个带有code
为InvalidArgument
的error
,并状态返回消息“invalid request”
如果req
参数不是nil
,则该函数将使用sdk.UnwrapSDKContext
函数 从context.Context
unwrap sdk.Context
对象。然后,它使用GetPost
函数从区块链的状态中检索具有指定Id
的post
对象,并通过检查found
的布尔变量的值来检查是否找到了该post。如果没有找到文章,它将返回一个类型为sdkerrors.ErrKeyNotFound
的错误。
如果找到post,该函数将创建一个新类型。QueryShowPostResponse
对象,将检索到的post对象作为字段,并返回指向该对象的指针和nil
错误。
2.5.2 Modify QueryShowPostResponse
在QueryShowPostResponse
消息的post
字段中包含选项[(gogoproto.nullable) = false]
,以生成没有指针的字段。
# proto/blog/blog/query.proto
message QueryShowPostResponse {
Post post = 1 [(gogoproto.nullable) = false];
}
运行命令从proto生成Go文件:
$ ignite generate proto-go
2.6 列出文章
在本章中,您将开发一个特性,使用户能够检索存储在区块链应用程序中的所有博客文章。该功能将允许用户执行查询并接收分页响应,这意味着输出将被划分为更小的数据块或“页”
。这将允许用户更容易地导航和浏览文章列表,因为他们将能够一次查看特定数量的帖子,而不必一次滚动一个可能很长的列表。
2.6.1 列出文章
让我们实现ListPost
keeper方法,当用户对区块链应用程序进行查询,请求存储在chain上的所有文章的分页列表时,将调用该方法。
# x/blog/keeper/query_list_post.go
package keeper
import (
"context"
"blog/x/blog/types"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/query"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (k Keeper) ListPost(goCtx context.Context, req *types.QueryListPostRequest) (*types.QueryListPostResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "invalid request")
}
var posts []types.Post
ctx := sdk.UnwrapSDKContext(goCtx)
store := ctx.KVStore(k.storeKey)
postStore := prefix.NewStore(store, types.KeyPrefix(types.PostKey))
pageRes, err := query.Paginate(postStore, req.Pagination, func(key []byte, value []byte) error {
var post types.Post
if err := k.cdc.Unmarshal(value, &post); err != nil {
return err
}
posts = append(posts, post)
return nil
})
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
return &types.QueryListPostResponse{Post: posts, Pagination: pageRes}, nil
}
ListPost有两个参数:一个上下文对象和一个QueryListPostReques
t类型的请求对象。它返回一个QueryListPostResponse
类型的响应对象和一个错误。
该函数首先检查请求对象是否为nil
,如果为nil则返回一个带有InvalidArgument
代码的错误。然后它初始化Post
对象的一个空片并展开上下文对象。
它使用keeper结构的storeKey
字段从上下文检索键值store ,并使用PostKey
的前缀创建一个新的存储。然后,它从store 和请求对象中的分页信息上调用query
包 Paginate
函数。作为参数传递给Paginate的函数遍历存储中的键-值对,并将值解组到Post
对象中,然后将这些值追加到posts
片。
如果分页期间发生错误,该函数将返回一个带有错误消息的内部错误。否则,它返回一个QueryListPostResponse
对象,其中包含文章列表和分页信息。
2.6.2 Modify QueryListPostResponse
添加一个repeated
关键字来返回一个帖子列表,并包括选项[(gogoproto.nullable) = false]
来生成不带指针的字段。
# proto/blog/blog/query.proto
message QueryListPostResponse {
repeated Post post = 1 [(gogoproto.nullable) = false];
cosmos.base.query.v1beta1.PageResponse pagination = 2;
}
运行命令从proto生成Go文件:
$ ignite generate proto-go
2.7 Play
Create a blog post by Alice
$ exampled tx example create-post hello world --from alice
[root@localhost ~]# exampled tx example create-post hello world --from alice
auth_info:
fee:
amount: []
gas_limit: "200000"
granter: ""
payer: ""
signer_infos: []
tip: null
body:
extension_options: []
memo: ""
messages:
- '@type': /example.example.MsgCreatePost
body: world
creator: cosmos19qhtq7ldt006laapts5t4v0tdhqf9turn56f2f
title: hello
non_critical_extension_options: []
timeout_height: "0"
signatures: []
confirm transaction before signing and broadcasting [y/N]: y
code: 0
codespace: ""
data: 12280A262F6578616D706C652E6578616D706C652E4D7367437265617465506F7374526573706F6E7365
events:
- attributes:
- index: true
key: ZmVl
value: ""
- index: true
key: ZmVlX3BheWVy
value: Y29zbW9zMTlxaHRxN2xkdDAwNmxhYXB0czV0NHYwdGRocWY5dHVybjU2ZjJm
type: tx
- attributes:
- index: true
key: YWNjX3NlcQ==
value: Y29zbW9zMTlxaHRxN2xkdDAwNmxhYXB0czV0NHYwdGRocWY5dHVybjU2ZjJmLzI=
type: tx
- attributes:
- index: true
key: c2lnbmF0dXJl
value: RE1NVXdlcllZaHkyQ1U0c0NxRWF1MzlkbmxrZE5mQ1hhZHVNZThFT0RMWUhrakIzQnEzVUd3ZkRYSEJOZ1ZwcStMQUdoakE4U3o0QlkxTGtuSi9VUWc9PQ==
type: tx
- attributes:
- index: true
key: YWN0aW9u
value: L2V4YW1wbGUuZXhhbXBsZS5Nc2dDcmVhdGVQb3N0
type: message
gas_used: "51338"
gas_wanted: "200000"
height: "570"
info: ""
logs:
- events:
- attributes:
- key: action
value: /example.example.MsgCreatePost
type: message
log: ""
msg_index: 0
raw_log: '[{"msg_index":0,"events":[{"type":"message","attributes":[{"key":"action","value":"/example.example.MsgCreatePost"}]}]}]'
timestamp: ""
tx: null
txhash: 402297082BED6F5C6F8488E655A254D3E20EC215ECC7F59CAA24E586918A3BA0
Show a blog post
Create a blog post by Bob
[root@localhost ~]# exampled tx example create-post foo bar --from bob
auth_info:
fee:
amount: []
gas_limit: "200000"
granter: ""
payer: ""
signer_infos: []
tip: null
body:
extension_options: []
memo: ""
messages:
- '@type': /example.example.MsgCreatePost
body: bar
creator: cosmos1ydt87tge4du7rqzjlrjvd978jfctyxxxfldqt0
title: foo
non_critical_extension_options: []
timeout_height: "0"
signatures: []
confirm transaction before signing and broadcasting [y/N]: y
code: 0
codespace: ""
data: 122C0A262F6578616D706C652E6578616D706C652E4D7367437265617465506F7374526573706F6E736512020801
events:
- attributes:
- index: true
key: ZmVl
value: ""
- index: true
key: ZmVlX3BheWVy
value: Y29zbW9zMXlkdDg3dGdlNGR1N3Jxempscmp2ZDk3OGpmY3R5eHh4ZmxkcXQw
type: tx
- attributes:
- index: true
key: YWNjX3NlcQ==
value: Y29zbW9zMXlkdDg3dGdlNGR1N3Jxempscmp2ZDk3OGpmY3R5eHh4ZmxkcXQwLzA=
type: tx
- attributes:
- index: true
key: c2lnbmF0dXJl
value: anB2UzQwZlRPQ3ZxazFQanljYkJFVVk4SW85QzRGa2Rxb1MxcGdWZnpZVjRwMDNTNUY3NWpvdERBZ041Vk4vNFZiQVFwRlZhM25oazJ1aUpZZDFkZHc9PQ==
type: tx
- attributes:
- index: true
key: YWN0aW9u
value: L2V4YW1wbGUuZXhhbXBsZS5Nc2dDcmVhdGVQb3N0
type: message
gas_used: "61440"
gas_wanted: "200000"
height: "831"
info: ""
logs:
- events:
- attributes:
- key: action
value: /example.example.MsgCreatePost
type: message
log: ""
msg_index: 0
raw_log: '[{"msg_index":0,"events":[{"type":"message","attributes":[{"key":"action","value":"/example.example.MsgCreatePost"}]}]}]'
timestamp: ""
tx: null
txhash: 16559A53BB9C9E6C6D3C01A2E08B2A0020199D7EDF8CB3BAD31EFA6C43268099
List all blog posts with pagination
Update a blog post
[root@localhost ~]# exampled tx example update-post hello cosmos 0 --from alice
auth_info:
fee:
amount: []
gas_limit: "200000"
granter: ""
payer: ""
signer_infos: []
tip: null
body:
extension_options: []
memo: ""
messages:
- '@type': /example.example.MsgUpdatePost
body: cosmos
creator: cosmos19qhtq7ldt006laapts5t4v0tdhqf9turn56f2f
id: "0"
title: hello
non_critical_extension_options: []
timeout_height: "0"
signatures: []
confirm transaction before signing and broadcasting [y/N]: y
code: 0
codespace: ""
data: 12280A262F6578616D706C652E6578616D706C652E4D7367557064617465506F7374526573706F6E7365
events:
- attributes:
- index: true
key: ZmVl
value: ""
- index: true
key: ZmVlX3BheWVy
value: Y29zbW9zMTlxaHRxN2xkdDAwNmxhYXB0czV0NHYwdGRocWY5dHVybjU2ZjJm
type: tx
- attributes:
- index: true
key: YWNjX3NlcQ==
value: Y29zbW9zMTlxaHRxN2xkdDAwNmxhYXB0czV0NHYwdGRocWY5dHVybjU2ZjJmLzM=
type: tx
- attributes:
- index: true
key: c2lnbmF0dXJl
value: OGk2RDczNzF3UHVBZkhlNTkwTFZmTnNYYVhaREFqdEtBcVRLUjJ2VlN4d2YwZlFxNmdvS1VEaisyci9rY0pXSVEwRGc4ZFBKeXcwZ0t4OFF6aTJPMWc9PQ==
type: tx
- attributes:
- index: true
key: YWN0aW9u
value: L2V4YW1wbGUuZXhhbXBsZS5Nc2dVcGRhdGVQb3N0
type: message
gas_used: "49045"
gas_wanted: "200000"
height: "997"
info: ""
logs:
- events:
- attributes:
- key: action
value: /example.example.MsgUpdatePost
type: message
log: ""
msg_index: 0
raw_log: '[{"msg_index":0,"events":[{"type":"message","attributes":[{"key":"action","value":"/example.example.MsgUpdatePost"}]}]}]'
timestamp: ""
tx: null
txhash: 52C366D4587A039BCD6D5EF7105DF0CE19980A40DA46C4384844DB4FF252608F
Delete a blog post
[root@localhost ~]# exampled tx example delete-post 0 --from alice
auth_info:
fee:
amount: []
gas_limit: "200000"
granter: ""
payer: ""
signer_infos: []
tip: null
body:
extension_options: []
memo: ""
messages:
- '@type': /example.example.MsgDeletePost
creator: cosmos19qhtq7ldt006laapts5t4v0tdhqf9turn56f2f
id: "0"
non_critical_extension_options: []
timeout_height: "0"
signatures: []
confirm transaction before signing and broadcasting [y/N]: y
code: 0
codespace: ""
data: 12280A262F6578616D706C652E6578616D706C652E4D736744656C657465506F7374526573706F6E7365
events:
- attributes:
- index: true
key: ZmVl
value: ""
- index: true
key: ZmVlX3BheWVy
value: Y29zbW9zMTlxaHRxN2xkdDAwNmxhYXB0czV0NHYwdGRocWY5dHVybjU2ZjJm
type: tx
- attributes:
- index: true
key: YWNjX3NlcQ==
value: Y29zbW9zMTlxaHRxN2xkdDAwNmxhYXB0czV0NHYwdGRocWY5dHVybjU2ZjJmLzQ=
type: tx
- attributes:
- index: true
key: c2lnbmF0dXJl
value: UUFBZkdzNDFmNmdJem9WWHJacWNFbm5UQzcwaTZ6dkpMajNUZDQzVXJDUWo4V0wwelBhUlBVT1F5d2JvQnFWVDJSS243UFZGNnVZOGtKTEhrTmdpdHc9PQ==
type: tx
- attributes:
- index: true
key: YWN0aW9u
value: L2V4YW1wbGUuZXhhbXBsZS5Nc2dEZWxldGVQb3N0
type: message
gas_used: "45498"
gas_wanted: "200000"
height: "1084"
info: ""
logs:
- events:
- attributes:
- key: action
value: /example.example.MsgDeletePost
type: message
log: ""
msg_index: 0
raw_log: '[{"msg_index":0,"events":[{"type":"message","attributes":[{"key":"action","value":"/example.example.MsgDeletePost"}]}]}]'
timestamp: ""
tx: null
txhash: DF1327893E08BC3E26EC05C3C757620E2AEA8A5BA0472D57961354C7908128FD
Delete a blog post unsuccessfully
[root@localhost ~]# exampled tx example delete-post 1 --from alice
auth_info:
fee:
amount: []
gas_limit: "200000"
granter: ""
payer: ""
signer_infos: []
tip: null
body:
extension_options: []
memo: ""
messages:
- '@type': /example.example.MsgDeletePost
creator: cosmos19qhtq7ldt006laapts5t4v0tdhqf9turn56f2f
id: "1"
non_critical_extension_options: []
timeout_height: "0"
signatures: []
confirm transaction before signing and broadcasting [y/N]: y
code: 4
codespace: sdk
data: ""
events:
- attributes:
- index: true
key: ZmVl
value: ""
- index: true
key: ZmVlX3BheWVy
value: Y29zbW9zMTlxaHRxN2xkdDAwNmxhYXB0czV0NHYwdGRocWY5dHVybjU2ZjJm
type: tx
- attributes:
- index: true
key: YWNjX3NlcQ==
value: Y29zbW9zMTlxaHRxN2xkdDAwNmxhYXB0czV0NHYwdGRocWY5dHVybjU2ZjJmLzU=
type: tx
- attributes:
- index: true
key: c2lnbmF0dXJl
value: QVJRbmlOS3NmdktMTDdwVEpMMUx2bFkwbWJmbDFmZjJJeFZDeU5EdGRsY3Y0ZS8yVFYrYTF1N09KaUlFZ29sUU5ya0hqL3BsL3VDbGRzc3FkdW05THc9PQ==
type: tx
gas_used: "44509"
gas_wanted: "200000"
height: "1167"
info: ""
logs: []
raw_log: 'failed to execute message; message index: 0: incorrect owner: unauthorized'
timestamp: ""
tx: null
txhash: DBDE030CB7BB384666BD4AA5BB00D6DA85BC4690DDF04D1DDEF13A66DEB9864C