开发您的第一个 DApp:初学者演练
在本文中,您将学习如何使用FRAME创建自定义 Rust 模块,该框架用于在 Polkadot 和Substrate生态系统中创建区块链。在此自定义模块或“托盘”中,您将定义在板上发布新消息的功能。完成本文中的步骤后,您将能够继续自行构建留言板的其余功能。
tl;dr 您可以在GitHub上找到此 pallet 的完整工作代码。
让我们开始吧!
了解托盘的结构
首先,看一下我们新留言板托盘的框架代码。我们将遍历这段代码的每一部分,这样我们就可以更好地了解我们需要构建什么:
#![cfg_attr(not(feature = "std"), no_std)]
pub use self::pallet::*;
#[frame_support::pallet]
pub mod pallet {
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
#[pallet::config]
pub trait Config: frame_system::Config {
type Event: From<Event<Self>> + IsType<<Self as frame_system::Config>::Event>;
}
#[pallet::pallet]
#[pallet::generate_store(pub(super) trait Store)]
pub struct Pallet<T>(_);
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}
#[pallet::call]
impl<T: Config> Pallet<T> {
// Dispatchable functions go here
}
#[pallet::event]
#[pallet::metadata(T::AccountId = "AccountId")]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
// Events go here
}
#[pallet::error]
pub enum Error<T> {
// Errors go here
}
}
在我们开始为简单的留言板开发功能之前,有必要了解此托盘框架代码中每个组件的用途。以下是对每个部分的解释:
#![cfg_attr(not(feature = "std"), no_std)]
- 此行告诉 Rust 编译器std
仅在标准库 ( ) 可用时才使用它。在 Polkadot 中,我们通常编译到 no_std 环境,这意味着标准库不可用,因为区块链运行时需要确定性环境,在给定相同输入的情况下,代码的执行始终会产生相同的输出。完整的标准库包含非确定性特征和系统依赖性,这可能会导致网络中不同节点之间的共识问题。pub use self::pallet::*;
- 此行使模块托盘下的所有项目在message_board
命名空间下可用。#[frame_support::pallet]
- 这是应用于托盘模块的属性,使 FRAME 的宏系统能够为托盘提供包含重要运行时开发功能的能力。pub mod pallet {...}
- 此行开始托盘模块的定义,它将包含您的功能的实际逻辑。pub trait Config: frame_system::Config {...}
- 这是您的托盘的配置特征。它继承自frame_system::Config
,您可以添加其他配置选项作为关联类型。pub struct Pallet(_);
- 这是你的托盘的主要结构。它保存了托盘运行时逻辑的实现。impl<T: Config> Hooks<BlockNumberFor> for Pallet {}
- 此部分用于定义运行时生命周期挂钩,您可以在其中插入要在块执行过程的不同阶段执行的自定义逻辑。impl<T: Config> Pallet {...}
- 您可以在此处定义托盘的可调度调用。可调度调用代表您的托盘的公共 API,它们是用户和其他托盘与您的托盘交互的主要方式。pub enum Event<T: Config> {...}
- 此部分定义您的托盘可以发出的事件。事件是您的托盘发出重要事件发生的信号的一种方式,它们通常用于向外部实体报告状态更改。pub enum Error {...}
- 您可以在此处定义可分派函数可以返回的错误。错误用于向用户或可分派函数的调用者报告问题。
出于我们的目的,我们将主要使用Pallet
结构体impl
、托盘Event
、枚举和Error
枚举。在该Pallet
结构中,我们将为留言板添加方法(可调度调用)。在impl
Pallet 中,我们将包含一个允许用户发布消息的功能。在Event
枚举中,我们将创建一个在发布新消息时发出的事件。最后,我们将开发一个自定义错误并将其合并到枚举中Error
。
定义可分派呼叫
正如您从上面回忆的那样,impl
托盘的 是我们为留言板定义公共 API 的地方。在其中,我们创建了一个函数,一个可分派的调用,它允许用户将新消息发布到留言板。花点时间看看下面的代码,并在继续之前尝试自己了解发生了什么。我们将在下面解释它的每个部分。
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::weight(10_000 + T::DbWeight::get().writes(1).ref_time())]
#[pallet::call_index(0)]
pub fn post_message(origin: OriginFor<T>, message: Vec<u8>) -> DispatchResult {
let sender = ensure_signed(origin)?;
let bounded_message: BoundedVec<u8, T::MaxMessageLength> = message.try_into().map_err(|_| Error::<T>::MessageTooLong)?;
Messages::<T>::try_mutate(|messages| {
messages.try_push((sender.clone(), bounded_message.clone()))
.map_err(|_| Error::<T>::MessageBoardFull)
})?;
Self::deposit_event(Event::MessagePosted { account: sender, message: bounded_message } );
Ok(().into())
}
}
现在您已经花时间阅读了代码,让我们一起看一下。
首先,在函数体内,我们确保消息创建者是有效的签名帐户,然后AccountId
从发送者中提取并将其分配给变量sender
。您可能还注意到声明?
末尾的ensure_signed(origin)?;
。这在 Rust 中称为问号运算符,用于错误传播。如果ensure_signed(origin)
函数调用结果为Err
,则运算符立即从当前函数?
返回该值(在本例中为)。您会在 FRAME 托盘中经常注意到这个运算符,因为它提供了一种方便的方法来处理错误并将其传播到调用堆栈,确保错误得到有效处理,并且代码保持简洁和可读。Err
post_message
然后,我们创建BoundedVec
代表消息的字节。ABoundedVec
是字节向量,其最大长度由运行时配置定义。如果消息超过最大长度,则返回错误。MaxMessageLength
稍后我们将在托盘的配置部分中 定义。
接下来我们要做的就是向留言板添加一条新消息。我们Messages
通过将新消息附加到消息向量来修改存储。上面示例中的函数mutate
用于更改存储的状态,在本例中,将由发件人及其消息组成的元组添加AccountId
到现有消息列表中。
下一行代码正在存放或记录一个事件,以指示消息已发布。Self::deposit_event
是 FRAME 系统提供的一种方法,用于从托盘发出事件。本例中的事件是我们稍后将在枚举中定义的变体Event::MessagePosted { account: sender, message: bounded_message }
的实例,携带 的帐户 ID 和发布的数据作为其数据。MessagePosted
Event
sender
bounded_message
最后一行,Ok(().into())
表示函数成功完成。
该Ok(())
部分表示函数已完成且没有任何错误,用于表示成功的Ok
枚举变体也是如此。Result
然后调用.into()
将Ok(())
值转换为DispatchResult
. 该into
方法是一种在 Rust 中进行类型转换的方法,在此上下文中,它用于构造一个DispatchResult
.
您可能会注意到此函数顶部的另一件事是#[pallet::weight(...)]
和#[pallet::call_index(...)]
属性。它们分别用于定义函数的权重和函数的索引。我们不打算详细讨论这些,但简而言之,它们用于确定函数的成本以及函数在运行时的索引。可以call_index
是任意数字,但对于托盘中的每个可调度功能而言,它必须是唯一的。有点weight
复杂,但在本例中,我们说函数的基本权重为 10,000,加上一次写入存储的权重,加上执行函数所需的时间权重。
创建消息存储
Event
在我们继续讨论和枚举之前,是时候快速绕道一下了Error
。我们需要为我们的消息创建存储项。我们还没有这样做!
在 FRAME 中,它相对简单。您需要知道如何存储数据,以及需要什么类型的访问权限。在我们的例子中,我们使用 a StorageValue
with aBoundedVec<(T::AccountId, BoundedVec<u8, T::MaxMessageLength>)>
作为它的类型。这表示存储元组列表的单个值(我们的消息)。每个元组包含一个AccountId
(发布消息的用户)和一个BoundedVec<u8>
(消息本身)。此设置使我们能够轻松地将新消息附加到列表的末尾,从而提供一种简单有效的方式来管理我们的链上留言板。
#[pallet::storage]
pub type Messages<T: Config> = StorageValue<_, BoundedVec<(T::AccountId, BoundedVec<u8, T::MaxMessageLength>), T::MaxMessages>, ValueQuery>;
你可能想知道 a 到底StorageValue
是什么?FRAME Rust 文档解释得更详细一些,但本质上它是 FRAME 提供的一种类型,允许我们在区块链的存储中存储单个值。可以从托盘中的任何位置访问该值。在我们的留言板上下文中,正如我们上面提到的,我们使用 aStorageValue
来存储 a BoundedVec<(T::AccountId, BoundedVec<u8>)>
,它表示用户发布的消息列表。
现在我们已经为留言板创建了存储项,我们可以继续构建Event
和Error
枚举。
发出事件
区块链上下文中的事件用于通知网络重大事件或状态变化。它们对于透明度和可追溯性至关重要,因为它们提供了所有发生的活动的可审计跟踪。在我们的消息板托盘中,每次发布消息时,都会发出一个事件,向整个网络发出此操作的信号。
对于这篇文章,我们将创建一个事件来指示已发布新消息:
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
Event::MessagePosted { account: sender, message: bounded_message },
}
让我们逐行分解该代码。
#[pallet::event]
:这是FRAME提供的属性宏,表示以下枚举(Event
)将用于定义托盘可以发出的事件。#[pallet::generate_deposit(pub(super) fn deposit_event)]
:此属性宏生成一个辅助函数 ,deposit_event
可用于发出枚举中定义的事件Event
。该函数的可见性定义为pub(super)
,这意味着该托盘的父模块可以公开访问它。pub enum Event<T: Config>
:这是枚举本身的声明Event
。Event
被定义为公共枚举,并且它在Config
特征上是通用的,这意味着它可以使用Config
(在本例中T::AccountId
)中定义的类型。Event::MessagePosted { account: sender, message: bounded_message }
:这是枚举变体的定义Event
。MessagePosted
表示新消息已发布的事件。该事件携带AccountId
发送者和消息(有界字节向量)作为其数据。
枚举自定义错误
最后,让我们为留言板 DApp 创建一些自定义错误。当用户提交的消息太长或留言板已满时,这些自定义错误将向用户发出提示。我们将从这些错误类型开始,以强调设计区块链应用程序的另一个关键因素,即大小问题。在区块链系统中,数据存储和传输是有代价的,因此控制我们正在处理的数据的大小至关重要。对于我们的 DApp,我们将限制消息的长度以及它可以容纳的消息数量。尝试发布超过这些限制中的任何一个的消息将触发自定义错误。这不仅有助于保持我们应用程序的效率,而且还向用户发出明确的信号,告知他们需要在其中工作的约束。
值得注意的是,如果我们构建这个 DApp 用于生产用途,我们很可能不会将实际的消息数据存储在链上。相反,我们会将消息数据的哈希值存储在链上,然后将实际的消息数据存储在链下。这将使我们能够保持消息数据的完整性,同时将存储在链上的数据的大小保持在最低限度。然而,为了简单起见,我们将在这篇博文中将实际的消息数据存储在链上。
在 FRAME pallet 中,自定义错误进入Error
枚举内部,如下所示:
#[pallet::error]
pub enum Error<T> {
MessageTooLong,
MessageBoardFull,
}
然后,我们可以在 dispatchable 调用中使用它,例如,像这样:
let bounded_message: BoundedVec<u8, T::MaxMessageLength> = message.try_into().map_err(|_| Error::<T>::MessageTooLong)?;
在上面的代码片段中,我们试图将 转换message
为BoundedVec<u8, T::MaxMessageLength>
. 如果转换失败,我们返回错误MessageTooLong
。如果转换成功,我们将继续进行可分派调用。
现在我们已经合并了我们的新错误,我们有一个完全可用的新框架托盘!
你想看看如何将这个新 pallet 添加到 Substrate 运行时吗?在 GitHub 上查看完整的工作代码。
下一步是什么?
现在我们已经完成了创建新 FRAME 托盘的步骤,并且您已经探索了 GitHub 上的完整工作代码,是时候添加更多功能了!
目前,我们的留言板仅允许用户在留言板上发布新消息。缺什么?回复消息怎么样?修改消息?删除消息?您将如何添加这些功能?
如果您选择接受,您面临的挑战是使用工作代码分叉存储库,并添加功能!它不需要完美,我们可以一起构建它。
- 点赞
- 收藏
- 关注作者
评论(0)