开发您的第一个 DApp:初学者演练

举报
Q神 发表于 2023/06/22 12:49:19 2023/06/22
【摘要】 在本文中,您将学习如何使用FRAME创建自定义 Rust 模块,该框架用于在 Polkadot 和Substrate生态系统中创建区块链。在此自定义模块或“托盘”中,您将定义在板上发布新消息的功能。完成本文中的步骤后,您将能够继续自行构建留言板的其余功能。tl;dr 您可以在GitHub上找到此 pallet 的完整工作代码。让我们开始吧!了解托盘的结构首先,看一下我们新留言板托盘的框架代码...

在本文中,您将学习如何使用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
    }
}

在我们开始为简单的留言板开发功能之前,有必要了解此托盘框架代码中每个组件的用途。以下是对每个部分的解释:

  1. #![cfg_attr(not(feature = "std"), no_std)]- 此行告诉 Rust 编译器std仅在标准库 ( ) 可用时才使用它。在 Polkadot 中,我们通常编译到 no_std 环境,这意味着标准库不可用,因为区块链运行时需要确定性环境,在给定相同输入的情况下,代码的执行始终会产生相同的输出。完整的标准库包含非确定性特征和系统依赖性,这可能会导致网络中不同节点之间的共识问题。

  2. pub use self::pallet::*;- 此行使模块托盘下的所有项目在message_board命名空间下可用。

  3. #[frame_support::pallet]- 这是应用于托盘模块的属性,使 FRAME 的宏系统能够为托盘提供包含重要运行时开发功能的能力。

  4. pub mod pallet {...}- 此行开始托盘模块的定义,它将包含您的功能的实际逻辑。

  5. pub trait Config: frame_system::Config {...}- 这是您的托盘的配置特征。它继承自frame_system::Config,您可以添加其他配置选项作为关联类型。

  6. pub struct Pallet(_);- 这是你的托盘的主要结构。它保存了托盘运行时逻辑的实现。

  7. impl<T: Config> Hooks<BlockNumberFor> for Pallet {}- 此部分用于定义运行时生命周期挂钩,您可以在其中插入要在块执行过程的不同阶段执行的自定义逻辑。

  8. impl<T: Config> Pallet {...}- 您可以在此处定义托盘的可调度调用。可调度调用代表您的托盘的公共 API,它们是用户和其他托盘与您的托盘交互的主要方式。

  9. pub enum Event<T: Config> {...}- 此部分定义您的托盘可以发出的事件。事件是您的托盘发出重要事件发生的信号的一种方式,它们通常用于向外部实体报告状态更改。

  10. pub enum Error {...}- 您可以在此处定义可分派函数可以返回的错误。错误用于向用户或可分派函数的调用者报告问题。

出于我们的目的,我们将主要使用Pallet结构体impl、托盘Event、枚举和Error枚举。在该Pallet结构中,我们将为留言板添加方法(可调度调用)。在implPallet 中,我们将包含一个允许用户发布消息的功能。在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 托盘中经常注意到这个运算符,因为它提供了一种方便的方法来处理错误并将其传播到调用堆栈,确保错误得到有效处理,并且代码保持简洁和可读。Errpost_message

然后,我们创建BoundedVec代表消息的字节。ABoundedVec是字节向量,其最大长度由运行时配置定义。如果消息超过最大长度,则返回错误。MaxMessageLength稍后我们将在托盘的配置部分中 定义。

接下来我们要做的就是向留言板添加一条新消息。我们Messages通过将新消息附加到消息向量来修改存储。上面示例中的函数mutate用于更改存储的状态,在本例中,将由发件人及其消息组成的元组添加AccountId到现有消息列表中。

下一行代码正在存放或记录一个事件,以指示消息已发布。Self::deposit_event是 FRAME 系统提供的一种方法,用于从托盘发出事件。本例中的事件是我们稍后将在枚举中定义的变体Event::MessagePosted { account: sender, message: bounded_message }的实例,携带 的帐户 ID 和发布的数据作为其数据。MessagePostedEventsenderbounded_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 StorageValuewith 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>)>,它表示用户发布的消息列表。

现在我们已经为留言板创建了存储项,我们可以继续构建EventError枚举。

发出事件

区块链上下文中的事件用于通知网络重大事件或状态变化。它们对于透明度和可追溯性至关重要,因为它们提供了所有发生的活动的可审计跟踪。在我们的消息板托盘中,每次发布消息时,都会发出一个事件,向整个网络发出此操作的信号。

对于这篇文章,我们将创建一个事件来指示已发布新消息:

#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
    Event::MessagePosted { account: sender, message: bounded_message },
}

让我们逐行分解该代码。

  1. #[pallet::event]:这是FRAME提供的属性宏,表示以下枚举(Event)将用于定义托盘可以发出的事件。

  2. #[pallet::generate_deposit(pub(super) fn deposit_event)]:此属性宏生成一个辅助函数 ,deposit_event可用于发出枚举中定义的事件Event。该函数的可见性定义为pub(super),这意味着该托盘的父模块可以公开访问它。

  3. pub enum Event<T: Config>:这是枚举本身的声明EventEvent被定义为公共枚举,并且它在Config特征上是通用的,这意味着它可以使用Config(在本例中T::AccountId)中定义的类型。

  4. Event::MessagePosted { account: sender, message: bounded_message }:这是枚举变体的定义EventMessagePosted表示新消息已发布的事件。该事件携带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)?;

在上面的代码片段中,我们试图将 转换messageBoundedVec<u8, T::MaxMessageLength>. 如果转换失败,我们返回错误MessageTooLong。如果转换成功,我们将继续进行可分派调用。

现在我们已经合并了我们的新错误,我们有一个完全可用的新框架托盘!

你想看看如何将这个新 pallet 添加到 Substrate 运行时吗?在 GitHub 上查看完整的工作代码。

下一步是什么?

现在我们已经完成了创建新 FRAME 托盘的步骤,并且您已经探索了 GitHub 上的完整工作代码,是时候添加更多功能了!

目前,我们的留言板仅允许用户在留言板上发布新消息。缺什么?回复消息怎么样?修改消息?删除消息?您将如何添加这些功能?

如果您选择接受,您面临的挑战是使用工作代码分叉存储库,并添加功能!它不需要完美,我们可以一起构建它。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。