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、在展開的選單中點選DEVELOPMENT;
4、點選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 協議》,轉載必須註明作者和本文連結