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框架實戰 —— 匯智網