建立自定義Substrate鏈

疑難排解

Substrate是一個快速發展中的項目,即如果出現重大變更時下面的指令也有機會出現問顯。如果你遇到任何問題,請隨時與我們聯繫

這篇文章將會一步一步指引你所需要的步驟重複Gavid Wood在2018年 Web3 Submmit上的演示,展示怎樣在30分鐘內建立Substrate區塊鏈運行時函式庫。

教程將會使用Mac OS X系統,其它系統可能需要特別設定一下

必須要有的程序

在開始前確保你經已安裝最新 node 及 npm,然後需要確保你能夠運行 Substrate,這意味著你需要安裝Rust和其它相關程序。

只需要一個簡單的指令就能安裝Substrate (可能會需要一點時間,先去喝杯茶吧 :wink:

curl https://getsubstrate.io -sSf | bash

你需要在工作中的資料夾建立二個Gavin在演示中展示的倉庫(repository) :

Substrate Node Template

Substrate UI

在你的電腦下載這個腳本並且運行以下指令

注意

可能需要你重新打開终端才能執行以上腳本和Substrate指令

substrate-node-new substrate-node-template <author-name>
substrate-ui-new substrate

以上指令會建立二個資料夾分別為 substrate-node-templatesubstrate-ui 加上相關倉庫(repository)。

當然你可以利用以上指令更改項目的名稱,但是為了清晰起見,我們都會跟隨以上資料夾名稱。

第1步: 發佈區塊鏈

如果以上設定正確,現在你可以啟動 substrate (dev chain) 開發鏈。在 substrate-node-template 資料夾,執行

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

清除鏈數據庫

如果開始或運行節點時發生任何錯誤,你可能需要清除電腦上鏈的檔案。你可以執行 cargo run -- purge-chain --dev 或 手動移除整個鏈的資料夾: rm -rf ~/Library/Application\ Support/Substrate/chains/dev/

如果運作正常,你應該看到區塊開始產生。

與區塊鏈互動前,你需要啟動 Substrate UI,進入 substrate-ui 資料夾之後再執行:

npm run dev

最後打開瀏覽器前往 http://localhost:8000 你應該能夠使用這個新鏈!

第2步: 增加Alice到網絡

在substrate系統裹,Alice是預先設定好的帳戶,使你節省時間。如果你正使用最新版本 substrate-node-template ,Alice可能已經增加到你網絡,如果沒有,你可以利用以下方法增加她。

打開終端,之後利用已經安裝好的 substrate/subkey 套件,你可以檢索預設帳號的種子(Seed):

subkey restore Alice
    
> Seed
> 0x416c696365202020202020202020202020202020202020202020202020202020 is account:
>    SS58: 5GoKvZWG5ZPYL1WUovuHW3zJBWBP5eT8CbqjdRY4Q6iMaDtZ
     Hex: 0xd172a74cda4c865912c32ba0a80a57ae69abae410e5ccb59dee84e2f4432db4f

之後在Substrate UI,前往到 Wallet 部分再利用Alice的 種子seed名字name 增加 。

如果運作正常,你會見到Alice有大量 units 已經預先在她的帳戶。現在你可以前往 Send Funds 部份並從 Alice 發送資金到 Default,利用 Alice 發送一 些 unitsDefault 並等看到綠色的剔號及顯示了 Default 的餘額,說明已經成功紀錄到區塊鏈上。

第3步: 建立一個新運行時模組(new runtime module)

現在是時候建立屬於自己的模組

打開 substrate-node-template 資料夾並創建一個檔案。

./runtime/src/demo.rs

這裹就是放置運行時模組(runtime module)的位置,希望內裹註解能樣你理解代碼的運作。

首先需要在這個檔案最頂匯入幾個函式庫:

// Encoding library
use parity_codec::Encode;

// Enables access to the runtime storage
use srml_support::{StorageValue, dispatch::Result};

// Enables us to do hashing
use runtime_primitives::traits::Hash;

// Enables access to account balances and interacting with signed messages
use {balances, system::{self, ensure_signed}};

所有模組需要公開配置特徵 (trait) ,在這種情況下,由於我們將會利用從Balances模組特徵繼承了的特徵和函數。

pub trait Trait: balances::Trait {}

這個例子我們將會創造一個簡單的擲硬幣遊戲,用戶需要支付入場費進入遊戲然後再“擲硬幣”。當他們嬴了,他們會得到罐內的錢,但如果他們輸了,他們什麼也拿不到。不論結果是怎樣,他們已付的費用都會放在罐內等待下一位用戶試圖嬴取。

在建立這個遊戲前需要新增模組聲明(module declaration),這些都是我們需要編寫的功能,下面的巨集將會負責参數的集结。

這個遊戲有二個功能:

1: 玩這個遊戲

2: 設定付款

decl_module! {
  pub struct Module<T: Trait> for enum Call where origin: T::Origin {
    fn play(origin) -> Result {
      // Logic for playing the game
    }

    fn set_payment(_origin, value: T::Balance) -> Result {
      // Logic for setting the game payment
    }
  }
}

建立了模組結構後,可以正式編寫這二個功能。首先編寫玩這個遊戲的邏輯:

fn play(origin) -> Result {
  // Ensure we have a signed message, and derive the sender's account id from the signature
  let sender = ensure_signed(origin)?;
  
  // Here we grab the payment, and put it into a local variable.
  // We are able to use Self::payment() because we defined it in our decl_storage! macro below
  // If there is no payment, exit with an error message
  let payment = Self::payment().ok_or("Must have payment amount set")?;

  // First, we decrease the balance of the sender by the payment amount using the balances module
  <balances::Module<T>>::decrease_free_balance(&sender, payment)?;
  
  // Then we flip a coin by generating a random seed
  // We pass the seed with our sender's account id into a hash algorithm
  // Then we check if the first byte of the hash is less than 128
  if (<system::Module<T>>::random_seed(), &sender)
  .using_encoded(<T as system::Trait>::Hashing::hash)
  .using_encoded(|e| e[0] < 128)
  {
    // If the sender wins the coin flip, we increase the sender's balance by the pot amount
    // `::take()` will also remove the pot amount from storage, which by default will give it a value of 0
    <balances::Module<T>>::increase_free_balance_creating(&sender, <Pot<T>>::take());
  }

  // No matter the outcome, we will add the original sender's payment back into the pot
  <Pot<T>>::mutate(|pot| *pot += payment);

  Ok(())
}

下一步編寫遊戲初始化時支付的邏輯:

fn set_payment(_origin, value: T::Balance) -> Result {
  //If the payment has not been set...
  if Self::payment().is_none() {
    // ... we will set it to the value we passed in.
    <Payment<T>>::put(value);
    
    // We will also put that initial value into the pot for someone to win
    <Pot<T>>::put(value);
  }
  
  Ok(())
}

之後利用 decl_storage! 巨集新增儲存聲明(storage declaration),我們可以定義將會儲存在鏈上指定的數據模組。在這裹可以了解更多巨集。

decl_storage! {
  trait Store for Module<T: Trait> as Demo {
    Payment get(payment): Option<T::Balance>;
    Pot get(pot): T::Balance;
  }
}

那麼容易就完成建立新運行時模組(runtime modules),你可以利用這裹完整的版本來檢查你寫的。

第4步: 整合新的模組到運行時(runtime)

實際使用新模組之前我們需要告訴給運行時(runtime)知道,需要修改 ./runtime/src/lib.rs 檔案:

首先,我們需要定義將會使用新的模組時:

...
extern crate substrate_consensus_aura_primitives as consensus_aura;

mod demo;     // Add this line  

下一步,需要編寫Trait的配置,我們可以在其他特徵編寫完成後做:

...

impl sudo::Trait for Runtime {
	/// The uniquitous event type.
	type Event = Event;
	type Proposal = Call;
}

impl demo::Trait for Runtime {}      // Add this line

最後,我們放新模組到運行時結構巨集 construct_runtime!:

construct_runtime!(
	pub enum Runtime with Log(InternalLog: DigestItem<Hash, Ed25519AuthorityId>) where
		Block = Block,
		NodeBlock = opaque::Block,
		InherentData = BasicInherentData
	{
		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},    // Add this line
	}
);

你可以在這裹找到這個檔案的完整版本。

第5步: 升級鏈

現在我們已經創建了新運行時模組,是時候升級我們的區塊鏈。

升級之前我們首先需要編譯我們的新運行時,前往 substrate-node-template 並執行:

./build.sh

如果執行成功,它會更新接下來的檔案:

./runtime/wasm/target/wasm32-unknown-unknown/release/node_runtime.compact.wasm

你可以返回Substrate UI的 Runtime Upgrade 位置,選擇這個檔案再按下 upgrade

如果沒有問題,在Substrate UI頁面最頂部的 Runtime 你會看到更新了的名字。

第6步: 與新的模組互動

最後可以試玩一下剛剛創造的遊戲,首先需要透過打開瀏覽器。

在Substrate UI頁,按下F12打開開發者控制台,之後我們需要利用Javascript匯入函式庫。

在玩這個遊戲前,我們需要先從帳號預設 set_payment,這樣會代Alice進行信息簽名執行函數預設罐的數值。

post({sender: "5GoKvZWG5ZPYL1WUovuHW3zJBWBP5eT8CbqjdRY4Q6iMaDtZ", call: calls.demo.setPayment(1000)}).tie(console.log)

當函數執行完成,你應該會見到 {finalized: &quot;...&quot;} 顯示數據已經寫到鏈上,我們可透過讀取罐的餘額。

runtime.demo.pot.then(console.log)

應該會返回 Number {1000}

第7步: 更新Substrate UI

現在我們看到運作正常, 是時候加點其它功能,建立一個樣用戶玩遊戲的界面,開始之前需要修改 substrate-ui 庫。

打開 ./src/app.jsx 檔案,搜尋 readyRender() 功能你會看到所有生成界面的組件。

例如: 此段代碼控制我們剛剛用到的升級運行時的界面:

<Divider hidden />
<Segment style={{margin: '1em'}} padded>
  <Header as='h2'>
    <Icon name='search' />
    <Header.Content>
      Runtime Upgrade
      <Header.Subheader>Upgrade the runtime using the Sudo 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.key,
      call: calls.sudo.sudo(calls.consensus.setCode(this.runtime))
    }}
  />
</Segment>

我們可以利用這個模板作為參考應該如何添加遊戲界面。

</Segment> 之後,貼上下面的代碼:

...

</Segment>
<Divider hidden />
<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 bond,表示那個用戶正在玩遊戲。

利用以下代碼,可以獲得用戶餘額等詳細資料:

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>

餘下一件需要做的事就是在同一個檔案最頂上的 constructor() 函數裹新增 player Bond :

...

this.runtime = new Bond;
this.player = new Bond;         // Add this line

當你儲存並且重新整理這一頁,你應該會見到新界面! 現在你可以利用 Default 用戶試玩這個遊戲:

現在你看到玩家輸了,他1,000 units 被加到罐及1 unit 交易費用將會從他的結餘扣除。

當我們再玩幾次之後,用戶終於嬴了,而罐將會重設到一開始的數量給下一位玩家:

最後筆記

雖然玩這個遊戲賺不了錢,但是希望你知道利用Substrate開發區塊鏈是有多容易。

總結一下你學會了:

  • 透過一個指令在你電腦下載及安裝 substrate

  • 建立新的 substrate-node-templatesubstrate-ui 使你可以馬上開始開發

  • 編寫屬於你的區塊鏈運行(runtime)

  • 透過 substrate-ui 實時升級你的運行時(runtime)不用分叉

  • 更新 substrae-ui 反映新運行時(runtime)特點及功能

Substrate是一項快速發展的技術,我們期待收到你的意見、回答你的問題並了解你更多的新開發想法。

歡迎聯絡我們詳情在這裹

最後謝謝中國Polkadot社區 @0xThreeBody@yuelipeng 幫助核對!

2 Likes

New telegram group: https://t.me/polkadotChi

WeChat:
image

谢谢,这些中文资料让我更加了解polkadot。:kissing_smiling_eyes: