LOADING...
LOADING...
LOADING...
当前位置: 玩币族首页 > 新闻观点 > 【Substrate开发教程】22 - 如何在基于Substrate的区块链上发行通证?

【Substrate开发教程】22 - 如何在基于Substrate的区块链上发行通证?

2020-11-06 松果 来源:区块链网络


这篇文章介绍如何在Substrate Runtime创建一个简单的功能齐全的通证(token,或代币)。

编写模块代码

修改pallets/template/src/lib.rs的代码如下:

#![cfg_attr(not(feature = "std"), no_std)] use frame_support::{decl_error, decl_event, decl_module, decl_storage, dispatch::DispatchResult, ensure}; use frame_system::{self as system, ensure_signed}; pub trait Trait: system::Trait { type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>; } decl_storage! { trait Store for Module<T: Trait> as Token { pub Balances get(fn get_balance): map hasher(blake2_128_concat) T::AccountId => u64; pub TotalSupply get(fn total_supply): u64 = 21000000; Init get(fn is_init): bool; } } decl_event!( pub enum Event<T> where AccountId = <T as system::Trait>::AccountId { Initialized(AccountId), Transfer(AccountId, AccountId, u64), } ); decl_error! { pub enum Error for Module<T: Trait> { AlreadyInitialized, InsufficientFunds, } } decl_module! { pub struct Module<T: Trait> for enum Call where origin: T::Origin { fn deposit_event() = default; #[weight = 10_000] fn init(origin) -> DispatchResult { let sender = ensure_signed(origin)?; ensure!(!Self::is_init(), <Error<T>>::AlreadyInitialized); <Balances<T>>::insert(&sender, Self::total_supply()); Init::put(true); Self::deposit_event(RawEvent::Initialized(sender)); Ok(()) } #[weight = 10_000] fn transfer(_origin, to: T::AccountId, value: u64) -> DispatchResult { let sender = ensure_signed(_origin)?; let sender_balance = Self::get_balance(&sender); let receiver_balance = Self::get_balance(&to); let updated_from_balance = sender_balance.checked_sub(value).ok_or(<Error<T>>::InsufficientFunds)?; let updated_to_balance = receiver_balance.checked_add(value).expect("Entire supply fits in u64; qed"); <Balances<T>>::insert(&sender, updated_from_balance); <Balances<T>>::insert(&to, updated_to_balance); Self::deposit_event(RawEvent::Transfer(sender, to, value)); Ok(()) } } }

下面详细分析模块代码。

创建从账户到余额的映射

映射(Mapping)在表示拥有某数据时很有用,这里使用Substrate模块内置的StorageMap创建从账户(AccountId)到余额(u64)的映射:

decl_storage! { trait Store for Module<T: Trait> as Token { pub Balances get(get_balance): map hasher(blake2_128_concat) T::AccountId => u64; pub TotalSupply get(total_supply): u64 = 21000000; Init get(is_init): bool; } }

每个持有token的账户在该映射中均表示为键(key),其值(value)就是其持有的token数量。

TotalSupply设置token的总供应量,Init跟踪token是否已初始化。

定义事件

decl_event!( pub enum Event<T> where AccountId = <T as system::Trait>::AccountId{ Initialized(AccountId), Transfer(AccountId, AccountId, u64), } );

这里定义了两个事件:

Initialized:token被用户初始化;Transfer:token在账户间转账,三个参数分别代表from、to、value;

定义错误处理机制

decl_error! { pub enum Error for Module<T: Trait> { AlreadyInitialized, InsufficientFunds, } }

这里定义了两个错误:

AlreadyInitialized:试图初始化已经初始化过的token;InsufficientFunds:转账金额超过可用余额;

初始化通证

为了使token可用,账户必须初始化它,初始化token有很多方法,如进行创世(genesis)配置、声明存储过程、lockdrop等。

这里使用一个非常简单的方法,第一个调用init函数的账户将收到所有token,类似于在EOISO区块链上发行通证时调用issue Action。

fn init(origin) -> DispatchResult { let sender = ensure_signed(origin)?; ensure!(!Self::is_init(), <Error<T>>::AlreadyInitialized); <Balances<T>>::insert(&sender, Self::total_supply()); Init::put(true); Self::deposit_event(RawEvent::Initialized(sender)); Ok(()) }

先检查执行条件,以确保token只被初始化一次,然后修改StorageMap中的相关数据并触发Initialized事件。

转账

用户要进行转账,需要持有一些token,然后调用transfer函数,并指定接收者和转账金额作为函数参数。

fn transfer(_origin, to: T::AccountId, value: u64) -> DispatchResult { let sender = ensure_signed(_origin)?; let sender_balance = Self::get_balance(&sender); let receiver_balance = Self::get_balance(&to); let updated_from_balance = sender_balance.checked_sub(value).ok_or(<Error<T>>::InsufficientFunds)?; let updated_to_balance = receiver_balance.checked_add(value).expect("Entire supply fits in u64; qed"); <Balances<T>>::insert(&sender, updated_from_balance); <Balances<T>>::insert(&to, updated_to_balance); Self::deposit_event(RawEvent::Transfer(sender, to, value)); Ok(()) }

在修改StorageMap中的相关数据之前,再次检查可能出现的错误情况,此时无需检查token是否已被初始化。

Rust数字类型自带的checked_sub、checked_add方法返回一个Option<T>,后者调用其方法检查是否存在InsufficientFunds错误,然后修改存储并触发Transfer事件。

这里用到了Rust Option<T>的方法ok_or()和expect()。

ok_or()将Option<T>类型转换成Result<T, E>,把Some(v)映射到Ok(v),把None映射到Err(err)。

expect()需要一个参数用来输出提示信息,这个消息被传递到底层的panic!,它在代码出现错误时提供一个较好的错误消息展示方式。

编译

编译命令如下:

cd substrate-node-template cargo +nightly-2020-08-23 build --release

使用了nightly-2020-08-23这个较稳定的cargo nightly版本以避免编译过程中出现bug。


测试

启动node-template

./target/release/node-template --dev

打开https://polkadot.js.org/apps,切换网络为DEVELOPMENT-Local Node:


选项卡选择开发者-交易,“提交下面的外部信息”选择templateModule,会自动获取到定义的函数:

init()transfer(to, value)


这里选择BOB账户作为init()函数的调用者,2100万个token首先会发送到BOB账户。

点击右下角“提交交易”按钮,点击“签名并提交”:


就会调用decl_module!中定义的init函数,并触发事件Initialized。

再调用transfer(to, value),指定接收账户为ALICE,金额为8888,向ALICE账户转账。

选项卡选择网络-浏览-链信息,在右侧可以看到最新触发的事件:


可以看到触发了模块代码中定义的templateModule.Initialized和templateModule.Transfer事件。

选项卡选择开发者-链状态-存储,“查询所选状态”选择templateModule,查看balances(AccountId): u64中数据:


可以看到ALICE和BOB账户目前的余额。

—-

编译者/作者:松果

玩币族申明:玩币族作为开放的资讯翻译/分享平台,所提供的所有资讯仅代表作者个人观点,与玩币族平台立场无关,且不构成任何投资理财建议。文章版权归原作者所有。

LOADING...
LOADING...