Adding Call Macro to PoE
We have already seen the #[macros::call]
macro help clean up the Balances Pallet.
Let's also add it to the Proof of Existence Pallet, where there is even more code that can be eliminated.
Add Your Call Macro
We basically need to repeat the steps that we did for the Balances Pallet here:
- Move your
create_claim
andrevoke_claim
functions into its ownimpl<T: Config> Pallet<T>
. - Add the
#[macros::call]
attribute over this newimpl<T: Config> Pallet<T>
. - Delete your existing
enum Call
. - Delete your existing implementation of
Dispatch for Pallet
. - Then, in your
main.rs
file, change instances of:proof_of_existence::Call::CreateClaim
toproof_of_existence::Call::create_claim
usingsnake_case
.proof_of_existence::Call::RevokeClaim
toproof_of_existence::Call::revoke_claim
usingsnake_case
.
Check that everything is compiling and running just as before.
Expand your Rust Code
Let's take the opportunity to show you how you can peek deeper into what the macros are doing.
Rust provides the command cargo expand
which allows you to output the generated rust code after all macros have been applied to your project.
To install cargo expand
:
cargo install cargo-expand
Then, run the following command:
cargo expand > out.rs
This will output your project's generated code into a file out.rs
.
Then take a look at that file.
Here are some things you should notice:
-
All of your different
mod
files have been combined together into a single file with yourmain.rs
. -
You will see that our final Pallet code has all of the
Call
andDispatch
logic generated! -
You might notice that the very first
#[derive(Debug)]
macro has generated code#![allow(unused)] fn main() { #[automatically_derived] impl<T: ::core::fmt::Debug + Config> ::core::fmt::Debug for Pallet<T> where T::Content: ::core::fmt::Debug, T::AccountId: ::core::fmt::Debug, { fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { ::core::fmt::Formatter::debug_struct_field1_finish(f, "Pallet", "claims", &&self.claims) } } }
-
You might even notice that other smaller macros like
vec![]
have changed:#![allow(unused)] fn main() { extrinsics: <[_]>::into_vec( #[rustc_box] ::alloc::boxed::Box::new([ // stuff ]) ) }
-
And
println!()
:#![allow(unused)] fn main() { { ::std::io::_print(format_args!("{0:#?}\n", runtime)); }; }
-
etc...
There are two main takeaways for you:
- Macros ultimately follow all the same rules as regular Rust code, because it does generate regular Rust code. They feel magical, but there is really nothing magic about them.
- Macros are an important part of the Rust ecosystem, and heavily used to improve developer experience and code quality.
If you ever use externally developed macros, and you want to look closer at what is going on, cargo expand
can be a useful tool for you to better understand some of the hidden architectural details of a project. As you jump into the Polkadot SDK, I recommend you continue to use this tool to enhance your learning and understanding.
mod balances; mod proof_of_existence; mod support; mod system; use crate::support::Dispatch; // These are the concrete types we will use in our simple state machine. // Modules are configured for these types directly, and they satisfy all of our // trait requirements. mod types { pub type AccountId = String; pub type Balance = u128; pub type BlockNumber = u32; pub type Nonce = u32; pub type Extrinsic = crate::support::Extrinsic<AccountId, crate::RuntimeCall>; pub type Header = crate::support::Header<BlockNumber>; pub type Block = crate::support::Block<Header, Extrinsic>; pub type Content = &'static str; } // These are all the calls which are exposed to the world. // Note that it is just an accumulation of the calls exposed by each module. pub enum RuntimeCall { Balances(balances::Call<Runtime>), ProofOfExistence(proof_of_existence::Call<Runtime>), } // This is our main Runtime. // It accumulates all of the different pallets we want to use. #[derive(Debug)] pub struct Runtime { system: system::Pallet<Self>, balances: balances::Pallet<Self>, proof_of_existence: proof_of_existence::Pallet<Self>, } impl system::Config for Runtime { type AccountId = types::AccountId; type BlockNumber = types::BlockNumber; type Nonce = types::Nonce; } impl balances::Config for Runtime { type Balance = types::Balance; } impl proof_of_existence::Config for Runtime { type Content = types::Content; } impl Runtime { // Create a new instance of the main Runtime, by creating a new instance of each pallet. fn new() -> Self { Self { system: system::Pallet::new(), balances: balances::Pallet::new(), proof_of_existence: proof_of_existence::Pallet::new(), } } // Execute a block of extrinsics. Increments the block number. fn execute_block(&mut self, block: types::Block) -> support::DispatchResult { self.system.inc_block_number(); if block.header.block_number != self.system.block_number() { return Err("block number does not match what is expected"); } // An extrinsic error is not enough to trigger the block to be invalid. We capture the // result, and emit an error message if one is emitted. for (i, support::Extrinsic { caller, call }) in block.extrinsics.into_iter().enumerate() { self.system.inc_nonce(&caller); let _res = self.dispatch(caller, call).map_err(|e| { eprintln!( "Extrinsic Error\n\tBlock Number: {}\n\tExtrinsic Number: {}\n\tError: {}", block.header.block_number, i, e ) }); } Ok(()) } } impl crate::support::Dispatch for Runtime { type Caller = <Runtime as system::Config>::AccountId; type Call = RuntimeCall; // Dispatch a call on behalf of a caller. Increments the caller's nonce. // // Dispatch allows us to identify which underlying module call we want to execute. // Note that we extract the `caller` from the extrinsic, and use that information // to determine who we are executing the call on behalf of. fn dispatch( &mut self, caller: Self::Caller, runtime_call: Self::Call, ) -> support::DispatchResult { // This match statement will allow us to correctly route `RuntimeCall`s // to the appropriate pallet level function. match runtime_call { RuntimeCall::Balances(call) => { self.balances.dispatch(caller, call)?; }, RuntimeCall::ProofOfExistence(call) => { self.proof_of_existence.dispatch(caller, call)?; }, } Ok(()) } } fn main() { // Create a new instance of the Runtime. // It will instantiate with it all the modules it uses. let mut runtime = Runtime::new(); let alice = "alice".to_string(); let bob = "bob".to_string(); let charlie = "charlie".to_string(); // Initialize the system with some initial balance. runtime.balances.set_balance(&alice, 100); // Here are the extrinsics in our block. // You can add or remove these based on the modules and calls you have set up. let block_1 = types::Block { header: support::Header { block_number: 1 }, extrinsics: vec![ support::Extrinsic { caller: alice.clone(), call: RuntimeCall::Balances(balances::Call::transfer { to: bob.clone(), amount: 30, }), }, support::Extrinsic { caller: alice.clone(), call: RuntimeCall::Balances(balances::Call::transfer { to: charlie, amount: 20 }), }, ], }; /* TODO: Update the extrinsics below for the updated format after the macros. */ let block_2 = types::Block { header: support::Header { block_number: 2 }, extrinsics: vec![ support::Extrinsic { caller: alice.clone(), call: RuntimeCall::ProofOfExistence(proof_of_existence::Call::CreateClaim { claim: "Hello, world!", }), }, support::Extrinsic { caller: bob.clone(), call: RuntimeCall::ProofOfExistence(proof_of_existence::Call::CreateClaim { claim: "Hello, world!", }), }, ], }; let block_3 = types::Block { header: support::Header { block_number: 3 }, extrinsics: vec![ support::Extrinsic { caller: alice, call: RuntimeCall::ProofOfExistence(proof_of_existence::Call::RevokeClaim { claim: "Hello, world!", }), }, support::Extrinsic { caller: bob, call: RuntimeCall::ProofOfExistence(proof_of_existence::Call::CreateClaim { claim: "Hello, world!", }), }, ], }; // Execute the extrinsics which make up our blocks. // If there are any errors, our system panics, since we should not execute invalid blocks. runtime.execute_block(block_1).expect("invalid block"); runtime.execute_block(block_2).expect("invalid block"); runtime.execute_block(block_3).expect("invalid block"); // Simply print the debug format of our runtime state. println!("{:#?}", runtime); }
#![allow(unused)] fn main() { use crate::support::DispatchResult; use core::fmt::Debug; use std::collections::BTreeMap; pub trait Config: crate::system::Config { /// The type which represents the content that can be claimed using this pallet. /// Could be the content directly as bytes, or better yet the hash of that content. /// We leave that decision to the runtime developer. type Content: Debug + Ord; } /// This is the Proof of Existence Module. /// It is a simple module that allows accounts to claim existence of some data. #[derive(Debug)] pub struct Pallet<T: Config> { /// A simple storage map from content to the owner of that content. /// Accounts can make multiple different claims, but each claim can only have one owner. claims: BTreeMap<T::Content, T::AccountId>, } /* TODO: Add the `#[macros::call]` attribute here too. Make the changes needed to this pallet. */ impl<T: Config> Pallet<T> { /// Create a new instance of the Proof of Existence Module. pub fn new() -> Self { Self { claims: BTreeMap::new() } } /// Get the owner (if any) of a claim. pub fn get_claim(&self, claim: &T::Content) -> Option<&T::AccountId> { self.claims.get(claim) } /// Create a new claim on behalf of the `caller`. /// This function will return an error if someone already has claimed that content. pub fn create_claim(&mut self, caller: T::AccountId, claim: T::Content) -> DispatchResult { if self.claims.contains_key(&claim) { return Err("this content is already claimed"); } self.claims.insert(claim, caller); Ok(()) } /// Revoke an existing claim on some content. /// This function should only succeed if the caller is the owner of an existing claim. /// It will return an error if the claim does not exist, or if the caller is not the owner. pub fn revoke_claim(&mut self, caller: T::AccountId, claim: T::Content) -> DispatchResult { let owner = self.get_claim(&claim).ok_or("claim does not exist")?; if caller != *owner { return Err("this content is owned by someone else"); } self.claims.remove(&claim); Ok(()) } } // A public enum which describes the calls we want to expose to the dispatcher. // We should expect that the caller of each call will be provided by the dispatcher, // and not included as a parameter of the call. pub enum Call<T: Config> { CreateClaim { claim: T::Content }, RevokeClaim { claim: T::Content }, } /// Implementation of the dispatch logic, mapping from `POECall` to the appropriate underlying /// function we want to execute. impl<T: Config> crate::support::Dispatch for Pallet<T> { type Caller = T::AccountId; type Call = Call<T>; fn dispatch( &mut self, caller: Self::Caller, call: Self::Call, ) -> crate::support::DispatchResult { match call { Call::CreateClaim { claim } => { self.create_claim(caller, claim)?; }, Call::RevokeClaim { claim } => { self.revoke_claim(caller, claim)?; }, } Ok(()) } } #[cfg(test)] mod test { struct TestConfig; impl super::Config for TestConfig { type Content = &'static str; } impl crate::system::Config for TestConfig { type AccountId = &'static str; type BlockNumber = u32; type Nonce = u32; } #[test] fn basic_proof_of_existence() { let mut poe = super::Pallet::<TestConfig>::new(); assert_eq!(poe.get_claim(&"Hello, world!"), None); assert_eq!(poe.create_claim("alice", "Hello, world!"), Ok(())); assert_eq!(poe.get_claim(&"Hello, world!"), Some(&"alice")); assert_eq!( poe.create_claim("bob", "Hello, world!"), Err("this content is already claimed") ); assert_eq!(poe.revoke_claim("alice", "Hello, world!"), Ok(())); assert_eq!(poe.create_claim("bob", "Hello, world!"), Ok(())); } } }
mod balances; mod proof_of_existence; mod support; mod system; use crate::support::Dispatch; // These are the concrete types we will use in our simple state machine. // Modules are configured for these types directly, and they satisfy all of our // trait requirements. mod types { pub type AccountId = String; pub type Balance = u128; pub type BlockNumber = u32; pub type Nonce = u32; pub type Extrinsic = crate::support::Extrinsic<AccountId, crate::RuntimeCall>; pub type Header = crate::support::Header<BlockNumber>; pub type Block = crate::support::Block<Header, Extrinsic>; pub type Content = &'static str; } // These are all the calls which are exposed to the world. // Note that it is just an accumulation of the calls exposed by each module. pub enum RuntimeCall { Balances(balances::Call<Runtime>), ProofOfExistence(proof_of_existence::Call<Runtime>), } // This is our main Runtime. // It accumulates all of the different pallets we want to use. #[derive(Debug)] pub struct Runtime { system: system::Pallet<Self>, balances: balances::Pallet<Self>, proof_of_existence: proof_of_existence::Pallet<Self>, } impl system::Config for Runtime { type AccountId = types::AccountId; type BlockNumber = types::BlockNumber; type Nonce = types::Nonce; } impl balances::Config for Runtime { type Balance = types::Balance; } impl proof_of_existence::Config for Runtime { type Content = types::Content; } impl Runtime { // Create a new instance of the main Runtime, by creating a new instance of each pallet. fn new() -> Self { Self { system: system::Pallet::new(), balances: balances::Pallet::new(), proof_of_existence: proof_of_existence::Pallet::new(), } } // Execute a block of extrinsics. Increments the block number. fn execute_block(&mut self, block: types::Block) -> support::DispatchResult { self.system.inc_block_number(); if block.header.block_number != self.system.block_number() { return Err("block number does not match what is expected"); } // An extrinsic error is not enough to trigger the block to be invalid. We capture the // result, and emit an error message if one is emitted. for (i, support::Extrinsic { caller, call }) in block.extrinsics.into_iter().enumerate() { self.system.inc_nonce(&caller); let _res = self.dispatch(caller, call).map_err(|e| { eprintln!( "Extrinsic Error\n\tBlock Number: {}\n\tExtrinsic Number: {}\n\tError: {}", block.header.block_number, i, e ) }); } Ok(()) } } impl crate::support::Dispatch for Runtime { type Caller = <Runtime as system::Config>::AccountId; type Call = RuntimeCall; // Dispatch a call on behalf of a caller. Increments the caller's nonce. // // Dispatch allows us to identify which underlying module call we want to execute. // Note that we extract the `caller` from the extrinsic, and use that information // to determine who we are executing the call on behalf of. fn dispatch( &mut self, caller: Self::Caller, runtime_call: Self::Call, ) -> support::DispatchResult { // This match statement will allow us to correctly route `RuntimeCall`s // to the appropriate pallet level function. match runtime_call { RuntimeCall::Balances(call) => { self.balances.dispatch(caller, call)?; }, RuntimeCall::ProofOfExistence(call) => { self.proof_of_existence.dispatch(caller, call)?; }, } Ok(()) } } fn main() { // Create a new instance of the Runtime. // It will instantiate with it all the modules it uses. let mut runtime = Runtime::new(); let alice = "alice".to_string(); let bob = "bob".to_string(); let charlie = "charlie".to_string(); // Initialize the system with some initial balance. runtime.balances.set_balance(&alice, 100); // Here are the extrinsics in our block. // You can add or remove these based on the modules and calls you have set up. let block_1 = types::Block { header: support::Header { block_number: 1 }, extrinsics: vec![ support::Extrinsic { caller: alice.clone(), call: RuntimeCall::Balances(balances::Call::transfer { to: bob.clone(), amount: 30, }), }, support::Extrinsic { caller: alice.clone(), call: RuntimeCall::Balances(balances::Call::transfer { to: charlie, amount: 20 }), }, ], }; let block_2 = types::Block { header: support::Header { block_number: 2 }, extrinsics: vec![ support::Extrinsic { caller: alice.clone(), call: RuntimeCall::ProofOfExistence(proof_of_existence::Call::create_claim { claim: "Hello, world!", }), }, support::Extrinsic { caller: bob.clone(), call: RuntimeCall::ProofOfExistence(proof_of_existence::Call::create_claim { claim: "Hello, world!", }), }, ], }; let block_3 = types::Block { header: support::Header { block_number: 3 }, extrinsics: vec![ support::Extrinsic { caller: alice, call: RuntimeCall::ProofOfExistence(proof_of_existence::Call::revoke_claim { claim: "Hello, world!", }), }, support::Extrinsic { caller: bob, call: RuntimeCall::ProofOfExistence(proof_of_existence::Call::create_claim { claim: "Hello, world!", }), }, ], }; // Execute the extrinsics which make up our blocks. // If there are any errors, our system panics, since we should not execute invalid blocks. runtime.execute_block(block_1).expect("invalid block"); runtime.execute_block(block_2).expect("invalid block"); runtime.execute_block(block_3).expect("invalid block"); // Simply print the debug format of our runtime state. println!("{:#?}", runtime); }
#![allow(unused)] fn main() { use crate::support::DispatchResult; use core::fmt::Debug; use std::collections::BTreeMap; pub trait Config: crate::system::Config { /// The type which represents the content that can be claimed using this pallet. /// Could be the content directly as bytes, or better yet the hash of that content. /// We leave that decision to the runtime developer. type Content: Debug + Ord; } /// This is the Proof of Existence Module. /// It is a simple module that allows accounts to claim existence of some data. #[derive(Debug)] pub struct Pallet<T: Config> { /// A simple storage map from content to the owner of that content. /// Accounts can make multiple different claims, but each claim can only have one owner. claims: BTreeMap<T::Content, T::AccountId>, } impl<T: Config> Pallet<T> { /// Create a new instance of the Proof of Existence Module. pub fn new() -> Self { Self { claims: BTreeMap::new() } } /// Get the owner (if any) of a claim. pub fn get_claim(&self, claim: &T::Content) -> Option<&T::AccountId> { self.claims.get(claim) } } #[macros::call] impl<T: Config> Pallet<T> { /// Create a new claim on behalf of the `caller`. /// This function will return an error if someone already has claimed that content. pub fn create_claim(&mut self, caller: T::AccountId, claim: T::Content) -> DispatchResult { if self.claims.contains_key(&claim) { return Err("this content is already claimed"); } self.claims.insert(claim, caller); Ok(()) } /// Revoke an existing claim on some content. /// This function should only succeed if the caller is the owner of an existing claim. /// It will return an error if the claim does not exist, or if the caller is not the owner. pub fn revoke_claim(&mut self, caller: T::AccountId, claim: T::Content) -> DispatchResult { let owner = self.get_claim(&claim).ok_or("claim does not exist")?; if caller != *owner { return Err("this content is owned by someone else"); } self.claims.remove(&claim); Ok(()) } } #[cfg(test)] mod test { struct TestConfig; impl super::Config for TestConfig { type Content = &'static str; } impl crate::system::Config for TestConfig { type AccountId = &'static str; type BlockNumber = u32; type Nonce = u32; } #[test] fn basic_proof_of_existence() { let mut poe = super::Pallet::<TestConfig>::new(); assert_eq!(poe.get_claim(&"Hello, world!"), None); assert_eq!(poe.create_claim("alice", "Hello, world!"), Ok(())); assert_eq!(poe.get_claim(&"Hello, world!"), Some(&"alice")); assert_eq!( poe.create_claim("bob", "Hello, world!"), Err("this content is already claimed") ); assert_eq!(poe.revoke_claim("alice", "Hello, world!"), Ok(())); assert_eq!(poe.create_claim("bob", "Hello, world!"), Ok(())); } } }
diff --git a/src/main.rs b/src/main.rs
index eb1e404d..902eb9ac 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -137,6 +137,7 @@ fn main() {
],
};
+ /* TODO: Update the extrinsics below for the updated format after the macros. */
let block_2 = types::Block {
header: support::Header { block_number: 2 },
extrinsics: vec![
diff --git a/src/proof_of_existence.rs b/src/proof_of_existence.rs
index 4e2e0ac7..f88d4398 100644
--- a/src/proof_of_existence.rs
+++ b/src/proof_of_existence.rs
@@ -18,6 +18,7 @@ pub struct Pallet<T: Config> {
claims: BTreeMap<T::Content, T::AccountId>,
}
+/* TODO: Add the `#[macros::call]` attribute here too. Make the changes needed to this pallet. */
impl<T: Config> Pallet<T> {
/// Create a new instance of the Proof of Existence Module.
pub fn new() -> Self {
diff --git a/src/main.rs b/src/main.rs
index 902eb9ac..9e1570c6 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -137,19 +137,18 @@ fn main() {
],
};
- /* TODO: Update the extrinsics below for the updated format after the macros. */
let block_2 = types::Block {
header: support::Header { block_number: 2 },
extrinsics: vec![
support::Extrinsic {
caller: alice.clone(),
- call: RuntimeCall::ProofOfExistence(proof_of_existence::Call::CreateClaim {
+ call: RuntimeCall::ProofOfExistence(proof_of_existence::Call::create_claim {
claim: "Hello, world!",
}),
},
support::Extrinsic {
caller: bob.clone(),
- call: RuntimeCall::ProofOfExistence(proof_of_existence::Call::CreateClaim {
+ call: RuntimeCall::ProofOfExistence(proof_of_existence::Call::create_claim {
claim: "Hello, world!",
}),
},
@@ -161,13 +160,13 @@ fn main() {
extrinsics: vec![
support::Extrinsic {
caller: alice,
- call: RuntimeCall::ProofOfExistence(proof_of_existence::Call::RevokeClaim {
+ call: RuntimeCall::ProofOfExistence(proof_of_existence::Call::revoke_claim {
claim: "Hello, world!",
}),
},
support::Extrinsic {
caller: bob,
- call: RuntimeCall::ProofOfExistence(proof_of_existence::Call::CreateClaim {
+ call: RuntimeCall::ProofOfExistence(proof_of_existence::Call::create_claim {
claim: "Hello, world!",
}),
},
diff --git a/src/proof_of_existence.rs b/src/proof_of_existence.rs
index f88d4398..10aa7af7 100644
--- a/src/proof_of_existence.rs
+++ b/src/proof_of_existence.rs
@@ -18,7 +18,6 @@ pub struct Pallet<T: Config> {
claims: BTreeMap<T::Content, T::AccountId>,
}
-/* TODO: Add the `#[macros::call]` attribute here too. Make the changes needed to this pallet. */
impl<T: Config> Pallet<T> {
/// Create a new instance of the Proof of Existence Module.
pub fn new() -> Self {
@@ -29,7 +28,10 @@ impl<T: Config> Pallet<T> {
pub fn get_claim(&self, claim: &T::Content) -> Option<&T::AccountId> {
self.claims.get(claim)
}
+}
+#[macros::call]
+impl<T: Config> Pallet<T> {
/// Create a new claim on behalf of the `caller`.
/// This function will return an error if someone already has claimed that content.
pub fn create_claim(&mut self, caller: T::AccountId, claim: T::Content) -> DispatchResult {
@@ -53,37 +55,6 @@ impl<T: Config> Pallet<T> {
}
}
-// A public enum which describes the calls we want to expose to the dispatcher.
-// We should expect that the caller of each call will be provided by the dispatcher,
-// and not included as a parameter of the call.
-pub enum Call<T: Config> {
- CreateClaim { claim: T::Content },
- RevokeClaim { claim: T::Content },
-}
-
-/// Implementation of the dispatch logic, mapping from `POECall` to the appropriate underlying
-/// function we want to execute.
-impl<T: Config> crate::support::Dispatch for Pallet<T> {
- type Caller = T::AccountId;
- type Call = Call<T>;
-
- fn dispatch(
- &mut self,
- caller: Self::Caller,
- call: Self::Call,
- ) -> crate::support::DispatchResult {
- match call {
- Call::CreateClaim { claim } => {
- self.create_claim(caller, claim)?;
- },
- Call::RevokeClaim { claim } => {
- self.revoke_claim(caller, claim)?;
- },
- }
- Ok(())
- }
-}
-
#[cfg(test)]
mod test {
struct TestConfig;