Substrate是开发应用特定区块链 (Application Specific Blockchain )的快速开发框架。与基于以太坊等公链开发的DApp相比,应用特定区块链是围绕单一应用特别构建的专用区块链,所以具备最大的灵活性和最少的限制。本文将详细介绍如何使用Substrate框架快速实现一个简单的游戏应用专用区块链。php
本文将按如下顺序完成这个简单的游戏专用区块链的教程:java
若是但愿快速掌握区块链应用的开发,推荐汇智网的区块链应用开发系列教程, 内容涵盖比特币、以太坊、eos、超级帐本fabric和tendermint等多种区块链, 以及 java、c#、go、nodejs、python、php、dart等多种开发语言node
在开始本教程以前,首先在计算机中安装如下软件:python
接下来还须要克隆这两个软件仓库并进行相应的配置:git
若是安装没有问题,如今能够启动一个substrate开发链了!在substrate-node-template目录运行生成的可执行文件:github
./target/release/substrate-node-template --dev
若是在启动节点时碰到任何错误,你可能须要使用下面命令清理区块链数据文件:数据库
./target/release/substrate-node-template purge-chain --dev
一切正常的话,就能够看到它出块了!npm
要和区块链交互,你须要启动Substrate UI。进入substrate-ui目录而后运行:编程
yarn run dev
最后,在浏览器中访问http://localhost:8000
,你应该能够访问你的区块链了!c#
Alice是硬编码在你的区块链创世块配置中的帐户,这个帐户是预充值的并且是负责区块链升级的超级用户。
要访问Alice帐户,在Substrate UI中进入Wallet,而后在Seed输入栏填写://Alice
:
若是一切正常的话,你如今能够进入Send Funds功能区,从Alice向Default帐户转帐。能够看到Alice帐户已经预充值,所以向Default帐户转5000,而后等待绿色对号出现并显示转帐后Default的余额:
若是但愿快速掌握区块链应用的开发,推荐汇智网的区块链应用开发系列教程, 内容涵盖比特币、以太坊、eos、超级帐本fabric和tendermint等多种区块链, 以及 java、c#、go、nodejs、python、php、dart等多种开发语言
如今是时间建立咱们本身的运行时(Runtime)模块了。
打开substrate-node-template文件夹,建立一个新文件:
./runtime/src/demo.rs
首先须要在文件开头引入一些库:
// Encoding library use parity_codec::Encode; // Enables access to store a value in runtime storage // Imports the `Result` type that is returned from runtime functions // Imports the `decl_module!` and `decl_storage!` macros use support::{StorageValue, dispatch::Result, decl_module, decl_storage}; // Traits used for interacting with Substrate's Balances module // `Currency` gives you access to interact with the on-chain currency // `WithdrawReason` and `ExistenceRequirement` are enums for balance functions use support::traits::{Currency, WithdrawReason, ExistenceRequirement}; // These are traits which define behavior around math and hashing use runtime_primitives::traits::{Zero, Hash, Saturating}; // Enables us to verify an call to our module is signed by a user account use system::ensure_signed;
全部的模块都须要声明一个名为Trait的trait,它用来定义模块须要的独有的类型。在这个教程中,咱们的运行时模块没有本身的特殊类型,可是会继承在balances模块中定义的类型(例如Balance):
pub trait Trait: balances::Trait {}
在这个例子中,咱们将建立一个简单的抛硬币游戏。用户须要支付入门费才能够玩游戏,也就是掷一次硬币,若是赢了就能够获得罐子中的东西。不管结果如何,用户的进门费都会在开出结果后再放到罐子里,供后续用户赢取。
咱们可使用宏decl_storage
来定义模块须要跟踪的存储条目:
decl_storage! { trait Store for Module<T: Trait> as Demo { Payment get(payment): Option<T::Balance>; Pot get(pot): T::Balance; Nonce get(nonce): u64; } }
Rust中的宏用来生成其余代码,属于一种元编程。这里咱们引入了一个宏以及自定义的语法以便简化存储的定义并使其易懂。这个宏负责生成全部与Substrate存储数据库交互的实际代码。
你能够看到在咱们的存储中,有三个条目,其中两个负责跟踪Balance,另外一个跟踪Nonce。Payment的声明为可选值,所以不管是否初始化过它的值都不会有问题。
接下来咱们将须要定义分发函数:那些供用户调用咱们的区块链系统的函数。这个游戏有两个用户能够交互的函数:一个容许咱们支付进门费,另外一个让咱们开始玩游戏:
decl_module! { pub struct Module<T: Trait> for enum Call where origin: T::Origin { fn set_payment(_origin, value: T::Balance) -> Result { // Logic for setting the game payment } play(origin) -> Result { // Logic for playing the game } } }
如今咱们已经搭建好模块的结构,能够添加这些函数的实现逻辑了。首先咱们添加初始化存储条目的逻辑:
// This function initializes the `payment` storage item // It also populates the pot with an initial value fn set_payment(origin, value: T::Balance) -> Result { // Ensure that the function call is a signed message (i.e. a transaction) let _ = ensure_signed(origin)?; // If `payment` is not initialized with some value if Self::payment().is_none() { // Set the value of `payment` <Payment<T>>::put(value); // Initialize the `pot` with the same value <Pot<T>>::put(value); } // Return Ok(()) when everything happens successfully Ok(()) }
而后咱们将编写play()
函数的实现代码:
// This function is allows a user to play our coin-flip game fn play(origin) -> Result { // Ensure that the function call is a signed message (i.e. a transaction) // Additionally, derive the sender address from the signed message let sender = ensure_signed(origin)?; // Ensure that `payment` storage item has been set let payment = Self::payment().ok_or("Must have payment amount set")?; // Read our storage values, and place them in memory variables let mut nonce = Self::nonce(); let mut pot = Self::pot(); // Try to withdraw the payment from the account, making sure that it will not kill the account let _ = <balances::Module<T> as Currency<_>>::withdraw(&sender, payment, WithdrawReason::Reserve, ExistenceRequirement::KeepAlive)?; // Generate a random hash between 0-255 using a csRNG algorithm if (<system::Module<T>>::random_seed(), &sender, nonce) .using_encoded(<T as system::Trait>::Hashing::hash) .using_encoded(|e| e[0] < 128) { // If the user won the coin flip, deposit the pot winnings; cannot fail let _ = <balances::Module<T> as Currency<_>>::deposit_into_existing(&sender, pot) .expect("`sender` must exist since a transaction is being made and withdraw will keep alive; qed."); // Reduce the pot to zero pot = Zero::zero(); } // No matter the outcome, increase the pot by the payment amount pot = pot.saturating_add(payment); // Increment the nonce nonce = nonce.wrapping_add(1); // Store the updated values for our module <Pot<T>>::put(pot); <Nonce<T>>::put(nonce); // Return Ok(()) when everything happens successfully Ok(()) }
好了!你看用Substrate构建新的运行时模块多么简单,做为参照,你能够和上述代码的完整版本进行对比。
要实际应用咱们上面开发的模块,还须要告诉运行时这个模块的存在,这须要修改./runtime/src/lib.rs
文件。
首先咱们须要在项目中包含咱们建立的模块文件:
... /// Index of an account's extrinsic in the chain. pub type Nonce = u64; mod demo; // 添加这一行
接下来,咱们须要告诉运行时demo模块暴露的Trait:
... impl sudo::Trait for Runtime { /// The uniquitous event type. type Event = Event; type Proposal = Call; } impl demo::Trait for Runtime {} //添加这一行
最后,咱们须要在运行时构造函数中包含demo模块:
construct_runtime!( pub enum Runtime with Log(InternalLog: DigestItem<Hash, Ed25519AuthorityId>) where Block = Block, NodeBlock = opaque::Block, UncheckedExtrinsic = UncheckedExtrinsic { System: system::{default, Log(ChangesTrieRoot)}, Timestamp: timestamp::{Module, Call, Storage, Config<T>, Inherent}, Consensus: consensus::{Module, Call, Storage, Config<T>, Log(AuthoritiesChange), Inherent}, Aura: aura::{Module}, Indices: indices, Balances: balances, Sudo: sudo, Demo: demo::{Module, Call, Storage}, // 添加这一行 } );
为了在升级成功时更容易观察一些,咱们能够同时升级运行时规范与实现的名字:
/// This runtime version.\npub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("demo"), // 更新这个名称 impl_name: create_runtime_str!("demo-node"), // 更新这个名称 authoring_version: 3, spec_version: 3, impl_version: 0, apis: RUNTIME_API_VERSIONS, };
一样,你能够参考这个完整实现代码。
如今咱们已经建立了一个新的运行时模块,是时候升级咱们的区块链了。
为此首先咱们须要将新的运行时编译为Wasm二进制文件。进入substrate-node-template而后运行:
./scripts/build.sh
若是上述命令成功执行,它将会更新文件./runtime/wasm/target/wasm32-unknown-unknown/release/node_runtime.compact.wasm
。你能够回到Substrate UI,而后再Runtime Upgrade功能区,选择这个文件,而后点击upgrade按钮:
一切顺利的话,你能够在Substrate UI的顶部看到咱们为运行时新起的名字:
在这个教程的最后,咱们能够试玩新建立的游戏。使用浏览器的控制台开始交互。
在Susbtrate UI页面中,按F12打开开发者控制台。咱们须要借助于这个页面加载的一些JavaScript库。
在能够玩游戏以前,咱们须要使用一个帐户初始化set_payment
。咱们将以Alice的名义调用这个函数。她会使用一个签名消息慷慨的初始化钱罐:
post({ sender: runtime.indices.ss58Decode('F7Hs'), call: calls.demo.setPayment(1000), }).tie(console.log)
[外链图片转存失败(img-fxryKGXf-1568030609329)(create-first-substrate-chain/console-interact.png)]
当这个调用完成后,你应当会看到{finalized: "..."}
,表示已经添加到区块链了。能够经过查询钱罐中的余额来确认这一点:
runtime.demo.pot.then(console.log)
这应当会返回Number {1000}
。
如今咱们看到的都是在后台运行的,如今咱们更新用户界面来展现这些变化。让咱们添加一个用户界面以即可以玩游戏。为此咱们须要修改substrate-ui仓库。
打开./src/app.jsx
文件,在readyRender()
函数中,你会看到生成不一样UX组件的代码。例如,这个代码片断控制着咱们刚用过的Runtime升级用户界面:
class UpgradeSegment extends React.Component { constructor() { super() this.conditionBond = runtime.metadata.map(m => m.modules && m.modules.some(o => o.name === 'sudo') || m.modules.some(o => o.name === 'upgrade_key') ) this.runtime = new Bond } render() { return <If condition={this.conditionBond} then={ <Segment style={{ margin: '1em' }} padded> <Header as='h2'> <Icon name='search' /> <Header.Content> Runtime Upgrade <Header.Subheader>Upgrade the runtime using the UpgradeKey module</Header.Subheader> </Header.Content> </Header> <div style={{ paddingBottom: '1em' }}></div> <FileUploadBond bond={this.runtime} content='Select Runtime' /> <TransactButton content="Upgrade" icon='warning' tx={{ sender: runtime.sudo ? runtime.sudo.key : runtime.upgrade_key.key, call: calls.sudo ? calls.sudo.sudo(calls.consensus.setCode(this.runtime)) : calls.upgrade_key.upgrade(this.runtime) }} /> </Segment> } /> } }
咱们能够以此为模板实现游戏界面。在文件的结尾添加下面的代码:
class DemoSegment extends React.Component { constructor() { super() this.player = new Bond } render() { return <Segment style={{ margin: '1em' }} padded> <Header as='h2'> <Icon name='game' /> <Header.Content> Play the game <Header.Subheader>Play the game here!</Header.Subheader> </Header.Content> </Header> <div style={{ paddingBottom: '1em' }}> <div style={{ fontSize: 'small' }}>player</div> <SignerBond bond={this.player} /> <If condition={this.player.ready()} then={<span> <Label>Balance <Label.Detail> <Pretty value={runtime.balances.balance(this.player)} /> </Label.Detail> </Label> </span>} /> </div> <TransactButton content="Play" icon='game' tx={{ sender: this.player, call: calls.demo.play() }} /> <Label>Pot Balance <Label.Detail> <Pretty value={runtime.demo.pot} /> </Label.Detail> </Label> </Segment> } }
this.player表示游戏用户上下文。咱们能够利用它获取用户余额:
runtime.balances.balance(this.player)
并以该用户的身份提交交易:
tx={{ sender: this.player, call: calls.demo.play() }}
相似于在开发者控制台中的方式,咱们能够动态显示钱罐中的当前余额:
<Label>Pot Balance <Label.Detail> <Pretty value={runtime.demo.pot}/> </Label.Detail> </Label>
惟一还须要咱们作的事情,就是将这个组件加入到文件开头的App类:
readyRender() { return (<div> <Heading /> <WalletSegment /> <Divider hidden /> <AddressBookSegment /> <Divider hidden /> <FundingSegment /> <Divider hidden /> <UpgradeSegment /> <Divider hidden /> <PokeSegment /> <Divider hidden /> <TransactionsSegment /> <Divider hidden /> <DemoSegment /> // 添加这一行 </div>) }
保存上述修改而后从新载入页面,你应当能够看到新的UI了!如今可使用Default帐户来玩游戏:
这里你能够看到玩家输掉了游戏,这意味着他们的1000单位币存入了钱罐,同时从他们的帐户余额中扣除额外的1单位币交易费。
若是咱们多尝试几回,最终玩家会赢得比赛,钱罐将恢复到开始的金额等待下一个玩家:
原文连接:Substrate框架实战 —— 汇智网