Let's start by adding the #[macros::call]
macro to our Balances Pallet.
The purpose of the #[macros::call]
macro is to automatically generate the enum Call
from the functions of the pallet and the pallet level Dispatch
logic found in each Pallet.
We can place the #[macros::call]
attribute over our impl<T: Config> Pallet<T>
where the callable functions are implemented. From there, the macro can parse the whole object, and extract the data it needs. Not all of your functions are intended to be callable, so you can isolate the functions which should be in their own impl<T: Config> Pallet<T>
as the template does.
In order to generate the code that we want, we need to keep track of:
struct
where those functions are implemented. Normally this is Pallet
, but we can allow the developer flexibility in their naming.These things are tracked with CallDef
and CallVariantDef
.
Also, during the parsing process, we might want to check for certain consistencies in the code being parsed. In this case, we require that every callable function muse have caller
as their first parameter with type T::AccountId
. This should make sense to you since you have designed a number of different callable functions, and they all follow this pattern.
This checking logic is handled by fn check_caller_arg
.
Once we have parsed all the data we need, generating the code is pretty straight forward.
If you jump down to let dispatch_impl = quote!
you will see a bunch of code that looks like the templates we used earlier in the tutorial. We just left markers where the macro generation logic should place all the information to write the code we need.
Macros are often very "quirky" when you use them. Since all of the input going into the macro is other code, sometimes the format of that code might not match what you expect.
For example, the original Call
enum we have constructed looks like:
pub enum Call<T: Config> {Transfer { to: T::AccountId, amount: T::Balance },}
The variant is called Transfer
because the function it represents is named fn transfer
.
However, if we want to generate the Call
enum, and we only have fn transfer
, where will we get the specific string Transfer
with a capital T
?
It is possible to do string manipulation and adjust everything to make it consistent to what Rust expects, but in this case it is better for our macros to make minimal modifications to user written code.
What does this mean?
When the #[macros::call]
macro generates our enum Call
, it will actually look like this:
#[allow(non_camel_case_types)]pub enum Call<T: Config> {transfer { to: T::AccountId, amount: T::Balance },}
Here you see that transfer
is exactly the string which comes from the name of the function. Normally all enum variants should be CamelCase
, but since rust functions are snake_case
, our enum will have variants which are also snake_case
. We won't see any warnings about this because we enabled #[allow(non_camel_case_types)]
.
Ultimately, this has no significant impact on your underlying code. It is just ergonomics and expectations.
Indeed, macros can be quirky, but the amount of time they save you makes them worth it.
transfer
function into its own impl<T: Config> Pallet<T>
. We only want to apply the macro to this one function, so we need to isolate it from the other functions which are not meant to be callable.#[macros::call]
attribute over this new impl<T: Config> Pallet<T>
.enum Call
.Dispatch for Pallet
.main.rs
file, change instances of balances::Call::Transfer
to balances::Call::transfer
with a lowercase t
.At this point, everything should compile just like before! We are witnessing the power of macros to generate code for us auto-magically!