创造我们的NFT,使用Substrate 创建KItties 三
可调度项、事件和错误
在本教程的上一节中,我们奠定了旨在管理小猫的所有权的基础 - 即使它们还不存在!在这一部分中,我们将通过使用我们声明的存储项目使我们的托盘能够创建Kitty来使用这些基础。稍微分解一下,我们将写:
-
create_kitty
:一个可调度或可公开调用的函数,允许帐户铸造Kitty。 -
mint()
:一个辅助功能,用于更新托的存储项目并执行错误检查,由 调用。create_kitty
-
pallet
Events
: 使用 FRAME的#[pallet::event]
属性。
在本部分结束时,我们将检查所有内容是否编译无误,并使用外部PolkadotJS 应用程序 UI 调用我们的 create_kitty 。
公共和私人功能
在我们深入研究之前,了解我们将围绕 Kitty pallet的铸币和所有权管理功能进行编码的pallet设计决策是非常重要。
作为开发人员,我们希望确保我们编写的代码高效且优雅。 通常,针对一个进行优化会针对另一个进行优化。 我们将设置pallet以优化两者的方式是将“繁重”逻辑分解为私有辅助函数。 这也提高了代码的可读性和可重用性。 正如我们将看到的,我们可以创建可以由多个可调度函数调用的私有函数,而不会影响安全性。 事实上,以这种方式构建可以被认为是一种附加的安全功能。 查看这个关于编写和使用辅助函数的操作指南以了解更多信息。
在开始实施这种方法之前,让我们首先描绘一下组合可调度和辅助函数的样子。
create_kitty是一个可调度的功能或外在功能::
- 检查源是否已签名
- 使用签名帐户生成随机哈希
- 使用随机哈希创建新的 Kitty 对象
- 调用私有函数
mint()
mint是一个私有助手函数,它:
- 检查小猫咪是否不存在
- 使用新的 Kitty ID 更新存储(适用于所有 Kitty 和所有者的帐户)
- 更新存储和新所有者帐户的新小猫总数
- 存入一个事件,以指示已成功创建小猫
编写可调度create_kitty
FRAME 中的可调度始终遵循相同的结构。 所有的pallet dispatchable 都存在于#[pallet::call] 宏下,该宏需要使用impl<T: Config> Pallet<T> {} 声明dispatchables 部分。 阅读有关这些 FRAME 宏的文档以了解它们的工作原理。 这里我们需要知道的是,它们是 FRAME 的一个有用功能,可以最大限度地减少为将pallet正确集成到 Substrate 链的runtime所需编写的代码。
权重
根据其文档中描述的#[pallet::call] 的要求,每个可调度函数都必须具有关联的权重。 权重是使用 Substrate 开发的重要部分,因为它们提供了围绕计算量的安全防护,以在执行时适合块。
Substrate 的加权系统迫使开发人员在调用每个外部函数之前考虑其计算复杂性。 这允许节点考虑最坏情况的执行时间,避免因外部因素导致网络滞后,这些外部因素可能需要比指定的块时间更长的时间。 权重也与任何已签名外在的收费系统密切相关。
由于这只是一个教程,我们将默认所有权重为 100 以保持简单。
假设您现在已经用本节的帮助文件替换了pallets/kitties/src/lib.rs 的内容,找到ACTION #1 并使用以下行完成函数的开头:
#[pallet::weight(100)]
pub fn create_kitty(origin: OriginFor<T>) -> DispatchResult {
let sender = ensure_signed(origin)?; // <- add this line
let kitty_id = Self::mint(&sender, None, None)?; // <- add this line
// Logging to the console
log::info!("A kitty is born with ID: {:?}.", kitty_id); // <- add this line
// ACTION #4: Deposit `Created` event
Ok(())
}
我们不会进行调试,但登录到控制台是一个有用的提示,可确保您的托盘按预期运行。 为了使用 log::info,将其添加到您的托盘的 Cargo.toml 文件中。
编写函数mint()
正如我们在上一节中编写create_kitty
时所看到的,我们需要创建mint()
,以便将我们新的唯一Kitty对象写入本教程第二部分中声明的各种存储项。
让我们直接开始吧。我们的mint()
函数将采用以下参数:
-
owner
:&T::AccountId
-
dna
:Option<[u8; 16]>
-
gender
:Option<Gender>
它将返回Result<T::Hash, Error<T>>
。
粘贴以下代码片段以编写mint
函数,取代工作代码库中的ACTION #2:
// Helper to mint a Kitty.
pub fn mint(
owner: &T::AccountId,
dna: Option<[u8; 16]>,
gender: Option<Gender>,
) -> Result<T::Hash, Error<T>> {
let kitty = Kitty::<T> {
dna: dna.unwrap_or_else(Self::gen_dna),
price: None,
gender: gender.unwrap_or_else(Self::gen_gender),
owner: owner.clone(),
};
let kitty_id = T::Hashing::hash_of(&kitty);
// Performs this operation first as it may fail
let new_cnt = Self::count_for_kitties().checked_add(1)
.ok_or(<Error<T>>::CountForKittiesOverflow)?;
// Check if the kitty does not already exist in our storage map
ensure!(Self::kitties(&kitty_id) == None, <Error<T>>::KittyExists);
// Performs this operation first because as it may fail
<KittiesOwned<T>>::try_mutate(&owner, |kitty_vec| {
kitty_vec.try_push(kitty_id)
}).map_err(|_| <Error<T>>::ExceedMaxKittyOwned)?;
<Kitties<T>>::insert(kitty_id, kitty);
<CountForKitties<T>>::put(new_cnt);
Ok(kitty_id)
}
让我们来看看上面的代码在做什么。
我们正在做的第一件事就是创建一个新的Kitty对象。然后,我们使用基于kitty当前属性的哈希函数创建一个唯一的kitty_id
。
接下来,我们使用存储获取器函数Self::count_for_kitties()
增加CountForKitties
。我们还在检查check_add()
函数的溢出。
我们最后的验证是确保 kitty_id 不存在于我们的 Kitties StorageMap 中。 这是为了避免任何可能的哈希键重复插入。
一旦我们的支票通过,我们就会通过以下方式更新我们的存储项目:
- 利用
try_mutate
更新小猫的主人vector。 - 使用
insert
Substrate的StorageMap API提供的方法,用于存储实际的Kitty对象并将其与其kitty_id
相关联。 - 使用
put
由StorageValue API提供,用于存储最新的Kitty计数。
快速回顾我们的存储项目
-
<Kitties<T>>
:通过存储Kitty对象并将其与其Kitty ID相关联,存储Kitty的独特特征和价格。 -
<KittyOwned<T>>
:跟踪哪些帐户拥有哪些Kitties。 -
<CountForKitties<T>>
:所有现存小猫的计数。
实施palletEvents
我们的托盘还可以在功能结束时发出事件。这不仅报告了函数执行的成功,还告诉“链外世界”发生了一些特定的状态转换。
FRAME帮助我们使用#[pallet::event]
属性。使用FRAME宏,事件只是一个像这样声明的枚举:
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config>{
/// A function succeeded. [time, day]
Success(T::Time, T::Day),
}
正如您在上面的代码段中看到的,我们使用属性宏:
#[pallet::generate_deposit(pub(super) fn deposit_event)]
这使我们能够使用以下模式来存放特定事件:
Self::deposit_event(Event::Success(var_time, var_day));
为了在托盘中使用事件,我们需要在托盘的配置特征Config
中添加一个新的关联类型Event
。此外,就像在将任何类型添加到托盘的Config
特征时一样,我们还需要在运行时runtime/src/lib.rs
中定义它。
此模式与我们在本教程前面内容将KittyRandomness
类型添加到托盘的配置特征时相同,并且已经包含在我们代码库的初始脚手架中:
/// Configure the pallet by specifying the parameters and types it depends on.
#[pallet::config]
pub trait Config: frame_system::Config {
/// Because this pallet emits events, it depends on the runtime's definition of an event.
type Event: From<Event<Self>> + IsType<<Self as frame_system::Config>::Event>;
//--snip--//
}
通过将 ACTION #3 行替换为以下内容来声明pallet事件:
/// A new Kitty was successfully created. \[sender, kitty_id\]
Created(T::AccountId, T::Hash),
/// Kitty price was successfully set. \[sender, kitty_id, new_price\]
PriceSet(T::AccountId, T::Hash, Option<BalanceOf<T>>),
/// A Kitty was successfully transferred. \[from, to, kitty_id\]
Transferred(T::AccountId, T::AccountId, T::Hash),
/// A Kitty was successfully bought. \[buyer, seller, kitty_id, bid_price\]
Bought(T::AccountId, T::AccountId, T::Hash, BalanceOf<T>),
我们将在本教程的最后一节中使用大多数这些活动。目前,让我们使用我们的create_kitty
调度的相关事件。
将操作#4替换为:
Self::deposit_event(Event::Created(sender, kitty_id));
备注
如果您正在从上一部分构建代码库(并且尚未为此部分使用帮助文件),则需要添加Ok(())
并正确关闭create_kitty
调度。
错误处理
FRAFRAME为我们提供了一个使用[#pallet::error]
的错误处理系统,该系统允许我们为托盘指定错误,并在托盘的功能中使用它们。
使用提供的FRAME宏在#[pallet::error]
下声明所有可能的错误,将行ACTION #5替换为:
/// Handles arithmetic overflow when incrementing the Kitty counter.
CountForKittiesOverflow,
/// An account cannot own more Kitties than `MaxKittyCount`.
ExceedMaxKittyOwned,
/// Buyer cannot be the owner.
BuyerIsKittyOwner,
/// Cannot transfer a kitty to its owner.
TransferToSelf,
/// This kitty already exists
KittyExists,
/// This kitty doesn't exist
KittyNotExist,
/// Handles checking that the Kitty is owned by the account transferring, buying or setting a price for it.
NotKittyOwner,
/// Ensures the Kitty is for sale.
KittyNotForSale,
/// Ensures that the buying price is greater than the asking price.
KittyBidPriceTooLow,
/// Ensures that an account has enough funds to purchase a Kitty.
NotEnoughBalance,
在一旦我们在下一节中编写交互式函数,我们将使用这些错误。请注意,我们已经在我们的mint
函数中使用了CountForKittiesOverflow
和ExceedMaxKittyOwned
。
现在是时候看看你的链条是否可以编译了。运行以下命令,看看是否所有东西都可以构建,而不是只检查托盘是否编译:
cargo build --release
使用 Polkadot-JS Apps UI 进行测试
- 运行您的链条并使用PolkadotJS应用程序UI与它互动。在项目目录中,运行:
./target/release/node-kitties --tmp --dev
通过这样做,我们指定在开发模式下运行一个临时链,这样就不需要每次我们想启动一个新的链时都清除存储空间。您应该会在终端中看到块最终完成。
-
前往Polkadot.js Apps UI: https://polkadot.js.org/apps/#/explorer。
-
单击左上角的圆形网络图标,打开“开发”部分,然后选择“本地节点”。您的节点默认为127.0.0.1.:9944。
-
转到:“开发人员”->“外部”,并通过调用createKitty调度,使用substrateKitties提交有符号的外部。从Alice、Bob和Charlie的账户中进行3笔不同的交易。
-
通过前往“网络”->“探索者”来检查相关事件“创建”。您应该能够看到发出的事件并查询其块详细信息。
-
通过前往“开发人员”->“链状态”来检查新创建的Kitty的详细信息。选择substrateKitties托盘并查询Kitties(Hash): Kitty。
请务必取消选中“包含选项”框,您应该能够以以下格式查看新铸造的Kitty的详细信息:
substrateKitties.kitties: Option<Kitty>
[
[
[
0x15cb95604033af239640125a30c45b671a282f3ef42c6fc48a78eb18464b30a9
],
{
dna: 0xaf2f2b3f77e110a56933903a38cde1eb,
price: null,
gender: Female,
owner: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
}
]
]
- 检查其他存储项目是否正确反映了其他小猫的创建。
与你的小猫互动
到目前为止,您已经建立了一个只能创建和跟踪Kitties所有权的链条。既然已经完成,我们希望通过引入购买和销售小猫等其他功能,使我们的运行时更像游戏。为了实现这一目标,我们首先需要让用户能够标记和更新他们的小猫的价格。然后,我们可以添加功能,使用户能够转移、购买和繁殖小猫。
为每只小猫设定一个价格
在教程这一部分的助手文件,您会注意到set_price的结构已经布局。
您的工作是用下面A-D部分中将学到的操作行#1a、#1b、#2和#3替换。
A. 检查Kitty的主人
当我们创建修改存储对象的函数时,我们应该始终首先检查只有适当的用户才能在这些可调度函数中成功执行逻辑。
所有权检查的一般模式如下所示:
let owner = Self::owner_of(object_id).ok_or("No owner for this object")?;
ensure!(owner == sender, "You are not the owner");
第一行检查Self::owner_of(object_id)是否返回Some(val)如果是,它将转换为Result::Ok(val)最后从Result提取val。如果没有,它将转换为带有错误消息的Result::Err(),并尽早返回错误对象。
第二行检查是否owner == sender。 如果为真,程序执行继续到下一行。 如果不是,则立即从函数返回 Result::Err("You are not the owner") 错误对象。
粘贴在此代码片段中以替换ACTION #1a:
ensure!(Self::is_kitty_owner(&kitty_id, &sender)?, <Error<T>>::NotKittyOwner);
在行动#1b中粘贴以下内容:
pub fn is_kitty_owner(kitty_id: &T::Hash, acct: &T::AccountId) -> Result<bool, Error<T>> {
match Self::kitties(kitty_id) {
Some(kitty) => Ok(kitty.owner == *acct),
None => Err(<Error<T>>::KittyNotExist)
}
}
粘贴在ACTION #1b中的行实际上是将两个检查合并在一起。如果Self::is_kitty_owner()返回错误对象Err(<Error<T>>::KittyNotExist)则由?提前返回<Error<T>>::KittyNotExist。如果它返回Ok(bool_val)则提取theboolbool_val,如果为false,则返回<Error<T>>::NotKittyOwner错误。
B.更新我们Kitty对象的价格
每个Kitty对象都有一个价格属性,我们在本教程前面的mint函数中将其设置为None作为默认值:
pub fn is_kitty_owner(kitty_id: &T::Hash, acct: &T::AccountId) -> Result<bool, Error<T>> {
match Self::kitties(kitty_id) {
Some(kitty) => Ok(kitty.owner == *acct),
None => Err(<Error<T>>::KittyNotExist)
}
}
要更新小猫的价格,我们需要:
-
存储Kitty对象。
-
用新价格更新对象。
-
将其保存回存储中。
更改存储中现有对象中的值将以以下方式编写:
let kitty = Kitty::<T> {
dna: dna.unwrap_or_else(Self::gen_dna),
price: None, //<-- 👀 here
gender: gender.unwrap_or_else(Self::gen_gender),
owner: owner.clone(),
};
备注:Rust希望您在变量值更新时将其声明为可变(使用mut关键字)。
粘贴以下片段以替换ACTION #2行:
kitty.price = new_price.clone();
<Kitties<T>>::insert(&kitty_id, kitty);
C. 存入活动
一旦所有支票都通过,新价格写入存储,我们就可以像以前一样存入活动。将标记为“ACTION #3”的行替换为:
// Deposit a "PriceSet" event.
Self::deposit_event(Event::PriceSet(sender, kitty_id, new_price));
现在,每当成功调用set_price调度时,它都会发出一个PriceSet事件。
转移一只小猫
根据我们之前构建的create_kitty功能,您已经拥有创建传输功能所需的工具和知识。主要区别在于,实现这一目标有两个部分:
称为transfer()的可调度函数:这是托盘公开暴露的可调用调度函数。
名为transfer_kitty_to()私有助手函数:这将是transfer()调用的私有助手函数,用于在传输Kitty时处理所有存储更新。
以这种方式分离逻辑使私有transfer_kitty_to()函数可以被我们托盘的其他调度函数重用,而无需复制代码。就我们而言,我们将将其重用到我们接下来要创建的buy_kitty调度件中。
transfer
粘贴以下片段以替换模板代码中的操作#4:
#[pallet::weight(100)]
pub fn transfer(
origin: OriginFor<T>,
to: T::AccountId,
kitty_id: T::Hash
) -> DispatchResult {
let from = ensure_signed(origin)?;
// Ensure the kitty exists and is called by the kitty owner
ensure!(Self::is_kitty_owner(&kitty_id, &from)?, <Error<T>>::NotKittyOwner);
// Verify the kitty is not transferring back to its owner.
ensure!(from != to, <Error<T>>::TransferToSelf);
// Verify the recipient has the capacity to receive one more kitty
let to_owned = <KittiesOwned<T>>::get(&to);
ensure!((to_owned.len() as u32) < T::MaxKittyOwned::get(), <Error<T>>::ExceedMaxKittyOwned);
Self::transfer_kitty_to(&kitty_id, &to)?;
Self::deposit_event(Event::Transferred(from, to, kitty_id));
Ok(())
}
到目前为止,上述模式应该已经熟悉了。我们总是检查交易是否已签署;然后我们验证:
-
正在转让的Kitty归此交易的发件人所有。
-
Kitty不会转让给其所有者(冗余操作)。
-
收件人有能力再接收一只小猫。
最后,我们致电transfer_kitty_to助手,以适当更新所有存储项。
transfer_kitty_to
transfer_kitty_to函数将是Kitty传输后执行所有存储更新的助手(当Kitty被买卖时,它也会被调用)。它只需要进行安全检查并更新以下存储项:
-
KittiesOwned:更新Kitty的所有者。
-
Kitties:将Kitty对象中的价格重置为None。
复制以下内容以替换操作#5:
#[transactional]
pub fn transfer_kitty_to(
kitty_id: &T::Hash,
to: &T::AccountId,
) -> Result<(), Error<T>> {
let mut kitty = Self::kitties(&kitty_id).ok_or(<Error<T>>::KittyNotExist)?;
let prev_owner = kitty.owner.clone();
// Remove `kitty_id` from the KittiesOwned vector of `prev_owner`
<KittiesOwned<T>>::try_mutate(&prev_owner, |owned| {
if let Some(ind) = owned.iter().position(|&id| id == *kitty_id) {
owned.swap_remove(ind);
return Ok(());
}
Err(())
}).map_err(|_| <Error<T>>::KittyNotExist)?;
// Update the kitty owner
kitty.owner = to.clone();
// Reset the ask price so the kitty is not for sale until `set_price()` is called
// by the current owner.
kitty.price = None;
<Kitties<T>>::insert(kitty_id, kitty);
<KittiesOwned<T>>::try_mutate(to, |vec| {
vec.try_push(*kitty_id)
}).map_err(|_| <Error<T>>::ExceedMaxKittyOwned)?;
Ok(())
}
请注意,使用#[transactional]我们在本教程一开始就导入了。它允许我们编写可调度函数,只有当注释的函数返回Ok时,才会对存储进行更改。否则,所有更改都将被丢弃。
买一只小猫
在允许此功能的用户购买Kitty之前,我们需要确保两件事:
检查Kitty是否待售。
检查Kitty的当前价格是否在用户的预算范围内,以及用户是否有足够的自由余额。
更换行操作#6,以检查Kitty是否正在销售:
// Check the kitty is for sale and the kitty ask price <= bid_price
if let Some(ask_price) = kitty.price {
ensure!(ask_price <= bid_price, <Error<T>>::KittyBidPriceTooLow);
} else {
Err(<Error<T>>::KittyNotForSale)?;
}
// Check the buyer has enough free balance
ensure!(T::Currency::free_balance(&buyer) >= bid_price, <Error<T>>::NotEnoughBalance);
同样,我们必须验证用户是否有接收Kitty的能力。请记住,我们正在使用BoundedVec只能容纳固定数量的小猫,这在我们的托盘的MaxKittyOwned常量中定义。将行动7替换为:
// Verify the buyer has the capacity to receive one more kitty
let to_owned = <KittiesOwned<T>>::get(&buyer);
ensure!((to_owned.len() as u32) < T::MaxKittyOwned::get(), <Error<T>>::ExceedMaxKittyOwned);
let seller = kitty.owner.clone();
我们将使用FRAME的货币特征使用其transfer方法。了解为什么特别使用transfer方法很重要,以及我们将如何访问它,这是有用的:
我们将使用它的原因是为了确保我们的运行时对它交互的整个托盘中的货币有相同的理解。我们确保这一点的方式是使用frame_support赋予我们的Currency特征。
方便地,它可以处理一个Balance类型,使其与我们为kitty.price创建的BalanceOf类型兼容。看看我们将如何使用transfer功能结构化:
fn transfer(
source: &AccountId,
dest: &AccountId,
value: Self::Balance,
existence_requirement: ExistenceRequirement
) -> DispatchResult
现在,我们可以在托盘的Config特征和ExistenceRequirement中使用Currency类型,我们最初在第一部分开始。
更新此功能的调用者和接收方的余额,替换为操作#8:
// Transfer the amount from buyer to seller
T::Currency::transfer(&buyer, &seller, bid_price, ExistenceRequirement::KeepAlive)?;
// Transfer the kitty from seller to buyer
Self::transfer_kitty_to(&kitty_id, &buyer)?;
// Deposit relevant Event
Self::deposit_event(Event::Bought(buyer, seller, kitty_id, bid_price));
备注:上述两个操作,T::Currency::transfer()和Self::transfer_kitty_to()都可能失败,这就是为什么我们在每种情况下检查返回的结果。如果返回Err,我们也会立即从函数返回。为了使存储与这些潜在变化保持一致,我们还注释此功能为#[transactional]
品种小猫
繁殖两只小猫背后的逻辑是将两只小猫的每个相应的DNA片段相乘,这将产生一个新的DNA序列。然后,在铸造一只新的小猫时使用该DNA。此助手功能已在本节的模板文件中为您提供。
粘贴以下内容以完成breed_kitty函数,替换行操作#9:
let new_dna = Self::breed_dna(&parent1, &parent2)?;
别忘了添加breed_dna(&parent1, &parent2)助手函数(窥视它的定义助手文件)。
现在我们已经使用了Kitty ID的用户输入,并将其组合起来创建了一个新的唯一Kitty ID,我们可以使用mint()函数将新Kitty写入存储。替换行动作#10以完成breed_kitty外在:
Self::mint(&sender, Some(new_dna), None)?;
创世配置
在我们托盘准备使用之前,最后一步是设置我们存储物品的起源状态。我们将使用FRAME的#[pallet::genesis_config]来做到这一点。从本质上讲,这允许我们声明存储中的Kitties对象在创世纪块中包含的内容。
复制以下代码以替换操作#11:
// Our pallet's genesis configuration.
#[pallet::genesis_config]
pub struct GenesisConfig<T: Config> {
pub kitties: Vec<(T::AccountId, [u8; 16], Gender)>,
}
// Required to implement default for GenesisConfig.
#[cfg(feature = "std")]
impl<T: Config> Default for GenesisConfig<T> {
fn default() -> GenesisConfig<T> {
GenesisConfig { kitties: vec![] }
}
}
#[pallet::genesis_build]
impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {
fn build(&self) {
// When building a kitty from genesis config, we require the dna and gender to be supplied.
for (acct, dna, gender) in &self.kitties {
let _ = <Pallet<T>>::mint(acct, Some(dna.clone()), Some(gender.clone()));
}
}
}
为了让我们的链条知道托盘的起源配置,我们需要修改项目node文件夹中的chain_spec.rs文件。请务必在runtime/src/lib.rs中使用托盘实例的名称,在我们的案例中,这是SubstrateKitties。转到node/src/chain_spec.rs,在文件顶部添加use node_kitties_runtime::SubstrateKittiesConfig;,并在testnet_genesis函数中添加以下片段:
//-- snip --
substrate_kitties: SubstrateKittiesConfig {
kitties: vec![],
},
//-- snip --
构建、运行和与您的小猫互动
如果您已经完成了本教程的所有前部分和步骤,您就可以运行链条并开始与Kitties托盘的所有新功能进行交互!
使用以下命令构建和运行链:
cargo build --release
./target/release/node-kitties --dev --tmp
现在像以前一样使用Polkadot-JS应用程序检查您的工作。一旦您的链条运行并连接到PolkadotJS应用程序UI,请执行以下手动检查:
-
用代币为多个用户提供资金,这样他们都可以参与
-
让每个用户创建多个小猫
-
尝试使用正确和错误的所有者将Kitty从一个用户转移到另一个用户
-
尝试使用正确和错误的所有者来设置小猫的价格
-
使用所有者和其他用户购买一只小猫
-
使用太少的资金来购买小猫
-
超支小猫的费用,并确保适当减少余额
-
培育一只小猫,并检查新的DNA是否是新旧的混合体
在所有这些操作后,确认所有用户都有正确的Kitties数量;Kitty总数正确;以及任何其他存储变量都正确表示。
🎉恭喜你!
您已成功创建功能齐全的Substrate链的后端,该链能够创建和管理Substrate Kitties。我们的Kitties应用程序的基本功能也可以抽象到其他类似NFT的用例中。最重要的是,在教程的这个时候,您应该拥有开始创建自己的pallet逻辑和可调度功能所需的所有知识。