substrate輕鬆學系列6:編寫簡單的pallet

linghuyichong發表於2022-11-05

1 node-template的結構

我們下載node-template(地址:github.com/substrate-developer-hub...), 然後進入到node-template檢視目錄結構:

~/Source/learn/substrate-node-template$ tree -L 3
.
├── Cargo.lock
├── Cargo.toml
├── docker-compose.yml
├── docs
│   └── rust-setup.md
├── LICENSE
├── node
│   ├── build.rs
│   ├── Cargo.toml
│   └── src
│       ├── chain_spec.rs
│       ├── cli.rs
│       ├── command.rs
│       ├── lib.rs
│       ├── main.rs
│       ├── rpc.rs
│       └── service.rs
├── pallets
│   └── template
│       ├── Cargo.toml
│       ├── README.md
│       └── src
├── README.md
├── runtime
│   ├── build.rs
│   ├── Cargo.toml
│   └── src
│       └── lib.rs
├── rustfmt.toml
├── scripts
│   ├── docker_run.sh
│   └── init.sh
└── shell.nix

在上述的目錄結構中,node目錄中是鏈的一些基礎功能的實現(或者說比較底層的實現,如網路、rpc,搭建鏈的最基礎的code); pallet目錄中放置的就是各個pallet,也就是業務相關的模組; runtime目錄中可以簡單理解為把所有pallet組合到一起,也就是業務相關的邏輯,這部分和pallet目錄中是我們開發中經常要動到的部分,而node中則相對來說動的少一點。

如果用一張圖來展示它們之間的關係的話,可能是這樣(不太準確,但大體是這麼個意思):

當然,對於pallets來說,在runtime中使用的pallet,有些是我們自己開發的pallet,有些是substrate中已經開發好的pallet,甚至還有些是pallet是第三方開發的pallet。

2 編寫pallet

下面我們就開始寫一個簡單的pallet。

2.1 編寫pallet的一般格式

寫pallet的基本格式如下:

// 1. Imports and Dependencies
pub use pallet::*;
#[frame_support::pallet]
pub mod pallet {
    use frame_support::pallet_prelude::*;
    use frame_system::pallet_prelude::*;

    // 2. Declaration of the Pallet type
    // This is a placeholder to implement traits and methods.
    #[pallet::pallet]
    #[pallet::generate_store(pub(super) trait Store)]
    pub struct Pallet<T>(_);

    // 3. Runtime Configuration Trait
    // All types and constants go here.
    // Use #[pallet::constant] and #[pallet::extra_constants]
    // to pass in values to metadata.
    #[pallet::config]
    pub trait Config: frame_system::Config { ... }

    // 4. Runtime Storage
    // Use to declare storage items.
    #[pallet::storage]
    #[pallet::getter(fn something)]
    pub MyStorage<T: Config> = StorageValue<_, u32>;

    // 5. Runtime Events
    // Can stringify event types to metadata.
    #[pallet::event]
    #[pallet::generate_deposit(pub(super) fn deposit_event)]
    pub enum Event<T: Config> { ... }

    // 6. Hooks
    // Define some logic that should be executed
    // regularly in some context, for e.g. on_initialize.
    #[pallet::hooks]
    impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> { ... }

    // 7. Extrinsics
    // Functions that are callable from outside the runtime.
    #[pallet::call]
    impl<T:Config> Pallet<T> { ... }

}

在我們開始寫一個pallet的時候,首先先把這個模板貼到編輯器裡面,然後再針對我們具體的需求進行修改。所以從這裡可以看出,一個pallet,如果所有功能都包括的話,基本上分為這幾大部分(對應上面程式碼註釋中的1-7):

1. 依賴; 
2. pallet型別宣告;
3. config trait;
4. 定義要使用的鏈上儲存;
5. 事件;
6. 鉤子函式;
7. 交易呼叫函式;

1和2基本上是固定的寫法,而對於後面的3-7部分,則是根據實際需要寫或者不寫。關於模板中每部分的解釋,可以參考文件.

2.2 編寫pallet

接下來我們將編寫一個simple-pallet.

2.2.1 simple-pallet功能介紹

simple-pallet是一個存證的pallet,簡單說就是提供一個存取一段hash到鏈上的功能,和從鏈上讀取的功能。

2.2.2 建立目錄

進去到我們前面下載的substrate-node-template中,進入到目錄pallets中,我們可以建立我們自己的simple-pallet(一般都是在template基礎上進行修改):

#先進入到substrate-node-template目錄,然後執行如下
cd pallets
cp template/ simple-pallet -rf
cd simple-pallet/src/
rm benchmarking.rs mock.rs tests.rs

接下來修改Cargo.toml,開啟substrate-node-template/pallets/simple-pallet目錄下的Cargo.toml檔案,然後進行修改,主要修改內容如下:

[package]
name = "pallet-simple-pallet"  #需要修改成自己的名字,這裡我們叫做pallet-simple-pallet
...
description = 修改成自己的
authors = 修改成自己的
...
repository = "https://github.com/substrate-developer-hub/substrate-node-template/"

對於這個檔案中的其它的依賴我們可以暫時先不修改,等程式碼寫完可以再回來刪除多餘的依賴。

2.2.3 編寫程式碼

刪除substrate-node-template/pallets/simple-pallet/src/lib.rs中的程式碼,然後將上面2.1節中pallet的一般格式的程式碼複製到這個檔案中。接下來我們開始寫程式碼,首先對於註釋中1和2的部分,我們開始不用修改,對於註釋6的部分我們需要刪除掉(這個例子中使用不到)。

那麼接下來,我們需要修改的就是裡面的3、4、5、7的部分,其實對於很多其它的pallet來說,主要也只是修改這幾部分。

首先,我們將註釋3所在部分confit修改成如下:

    #[pallet::config]
    pub trait Config: frame_system::Config {
        type Event: From<Event<Self>> + IsType<<Self as frame_system::Config>::Event>;
    }

這裡其實就是定義了一個關聯型別,這個關聯型別需要滿足後面的型別約束(From<Event> + IsType<::Event>)。至於為什麼是這樣的約束,我們其實可以從字面意思進行理解,一個是可以轉換成Event,另外一個就是它是frame_system::Config的Event型別。對於大部分pallet來說, 如果需要使用到Event,那麼都需要在這個Config中進行定義,定義的方式基本一樣.

接下來,我們修改註釋4的部分如下:

 #[pallet::storage]
    pub type Proofs<T: Config> =
        StorageMap<_, Blake2_128Concat, u32, u128>;

關於substrate中的儲存,更詳細的資料可以參考文件。這裡我們簡單解釋一下,這部分就是在鏈上定義了一個儲存,是一個key-value方式的儲存結構,用於儲存我們後面要使用的存證,key是u32格式,value也是u128格式。

再接下來,我們修改註釋5的部分如下:

    #[pallet::event]
    #[pallet::generate_deposit(pub(super) fn deposit_event)]
    pub enum Event<T: Config> {
        ClaimCreated(u32, u128),
    }

這裡的Event是用來在我們具體的函式中做完動作之後發出的,一般用來通知前端做一些處理。這裡我們在Event中定義了一個事件,就是建立存證。

最後,我們修改註釋7的部分來實現前面說的建立存證的邏輯,如下:

    #[pallet::call]
    impl<T:Config> Pallet<T> { 
        #[pallet::weight(0)]
        pub fn create_claim(origin: OriginFor<T>, id: u32, claim: u128) -> DispatchResultWithPostInfo {
            ensure_signed(origin)?;

            Proofs::<T>::insert(
                &id,
                &claim,
            );

            Self::deposit_event(Event::ClaimCreated(id, claim));

            Ok(().into())
        }
    }

至此,我們pallet部分的程式碼基本上就寫完了。

3 將pallet新增到runtime中

如果用開發一個程式來類別的話,上面寫完我們的pallet就類似於我們開發好了一個庫(或者說模組),但是這個庫還沒有真正的用在我們的程式中(鏈)。接下來就是要在鏈上使用,就要將pallet新增到runtime中。新增的過程也比較簡單,這裡我們分兩步進行,分別是修改Cargo.toml中和runtime/src/lib.rs中。

3.1 修改Cargo.toml

要在runtime中使用我們上面編寫的pallet,需要修改substrate-node-template/runtime/Cargo.toml,在其中新增依賴如下:

...
[dependencies]
...
pallet-simple-pallet = { version = "4.0.0-dev", default-features = false, path = "../pallets/simple-pallet" } #我們上面編寫的pallet
...    

[features]
default = ["std"]
std = [
    ...
    "pallet-template/std",
    "pallet-simple-pallet/std", #我們上面編寫的pallet
    ...
]

3.2 修改runtime/src/lib.rs

在runtime/src/lib.rs中來使用pallet。首先我們需要新增pallet的配置,其實就是指定pallet中Congfig中的關聯型別,所以在substrate-node-template/runtime/src/lib.rs中新增如下程式碼:

impl pallet_simple_pallet::Config for Runtime {
    type Event = Event;  //我們上面的定義中只有一個關聯型別Event,在此處進行指定,等好右邊的Event實際上是frame system中的Event,此處不需要深究,
                 //可以理解為在runtime中已經定義好的一種具體的型別。
}

接下來就是把simple_pallet加入到runtime中,修改如下程式碼:

construct_runtime!(
    pub enum Runtime where
        Block = Block,
        NodeBlock = opaque::Block,
        UncheckedExtrinsic = UncheckedExtrinsic
    {
        System: frame_system,
        RandomnessCollectiveFlip: pallet_randomness_collective_flip,
        Timestamp: pallet_timestamp,
        Aura: pallet_aura,
        Grandpa: pallet_grandpa,
        Balances: pallet_balances,
        TransactionPayment: pallet_transaction_payment,
        Sudo: pallet_sudo,
        // Include the custom logic from the pallet-template in the runtime.
        TemplateModule: pallet_template,
        SimplePallet: pallet_simple_pallet, //新增這一行,這裡可以看出,實際上我們前面實現的simple-pallet可以理解為一種型別,
                            //然後這裡在runtime中定義了一個變數,該變數是這個pallet_simple_pallet型別
    }
);

至此,我們就將pallet加入到我們的runtime中了。

3.3 編譯執行

接下來,我們可以進行編譯執行我們的鏈了。回到substrate-node-template目錄,執行如下命令編譯:

cargo build

執行如下命令啟動節點:

./target/debug/node-template --tmp --dev

4 除錯使用pallet中的功能

此處我們使用polkadot-js-apps和我們剛才執行的節點進行互動。步驟如下:

1、在瀏覽器中輸入https://polkadot.js.org/apps;
2、點選左上角會展開;
3、在展開的選單中點選DEVELOPMENT4、點選Local Node;
5、點選switch

接下來我們建立存證:

1、選擇Developer->Extrinsics->Submission;
2、然後使用Alice賬戶,選擇simplePallet,選擇createClaim,輸入對應的引數,然後點選右下角的提交即完成了存在的建立。

上述過程如下圖:

最後我們可以來讀取剛才建立的存證:

1、選擇Developer->Chain State;
2、選擇simplePallet,選擇proofs,然後點選提交即可。

上述過程如下圖:

5 小結

學到這裡,我們基本上就走完了整個pallet開發的流程,你已經可以開發一個簡單的pallet了。是不是並沒有想想中的那麼難?
後續我們再學習學習其它相關的知識,相信你很快就能完全掌握pallet開發了。

6 參考文件

docs.substrate.io/v3/runtime/frame...

7 完整原始碼地址

github.com/anonymousGiga/learn-sub...

本作品採用《CC 協議》,轉載必須註明作者和本文連結
令狐一衝

相關文章