Make Balances Pallet Generic
Our goal over the next few steps will be to continually make our runtime more generic and configurable over the types we use in our Pallets.
Why Generic?
The flexibility of generic runtime means that we can write code which works for multiple different configurations and types.
For example, up until now, we have been using &'static str
to represent the accounts of users. This is obviously not the right thing to do, but is easy to implement for a basic blockchain tutorial like this.
What would you need to change in order to use more traditional cryptographic public keys?
Well, currently there are definitions of the account type in both the Balances Pallet and the System Pallet. Imagine if you had many more Pallets too! Such refactoring could be very difficult, but also totally avoided if we used generic types to begin with.
Truthfully, the advantage of generic types will not be super obvious in this tutorial, but when building a blockchain SDK like the Substrate, this kind of flexibility will allow ecosystem developers to reach their full potential.
For example, teams have used Substrate to build fully compatible Ethereum blockchains, while other teams have experimented with cutting edge cryptographic primitives. This generic framework allows both teams to be successful.
Generic Types
You have already been lightly exposed to generic types with the Result
type. Remember that this type is flexible to allow for you to configure what type is returned when there is Ok
or Err
.
If we wanted to make our Pallet
generic, it would look something like:
#![allow(unused)] fn main() { pub struct Pallet<AccountId, Balance> { balances: BTreeMap<AccountId, Balance>, } }
And implementing functions on Pallet
would look like:
#![allow(unused)] fn main() { impl<AccountId, Balance> Pallet<AccountId, Balance> { // functions which use these types } }
In this case, we have not defined what the AccountId
and Balance
type are concretely, just that we will be storing a BTreeMap
where the AccountId
type is a key and Balance
type is a value.
Trait Constraints
The Result
generic type is extremely flexible because there are no constraints on what the Ok
or Err
type has to be. Every type will work for this situation.
However, our Pallets are not that flexible. The Balance
type cannot literally be any type. Because we have functions like fn transfer
, we must require that the Balance
type at least has access to the function checked_sub
, checked_add
, and has some representation of zero
.
This is where the num
crate will come in hand. From the num
crate, you can import traits which define types which expose these functions:
#![allow(unused)] fn main() { use num::traits::{CheckedAdd, CheckedSub, Zero}; }
Then, where applicable, you need to constrain your generic types to have these traits.
That will look like:
#![allow(unused)] fn main() { impl<AccountId, Balance> Pallet<AccountId, Balance> where AccountId: Ord, Balance: Zero + CheckedSub + CheckedAdd + Copy, { // functions which use these types and have access to the traits specified } }
You will notice other types like Copy
and Ord
that have been added. These constrains come from using structures like the BTreeMap
, which requires that the key type is "orderable".
You can actually try compiling your code without these type constraints, and the compiler will tell you which traits you are missing, and what you need to include.
Instantiating a Generic Type
The final piece of the puzzle is instantiating our generic types.
Previously we could simply write:
#![allow(unused)] fn main() { let mut balances = super::Pallet::new(); }
But now that Pallet
is generic, we need to concretely define those types when we instantiate it.
That syntax looks like:
#![allow(unused)] fn main() { let mut balances = super::Pallet::<&'static str, u128>::new(); }
You will notice that now the types are defined wherever the generic struct Pallet
is being instantiated. This means that you can extract the types out of your Pallets, and move them into the Runtime.
Get Generic!
Its time to turn your balances pallet generic.
- Follow the
TODO
s in thebalances.rs
file to makePallet
generic. - Move the type definitions for
AccountId
andBalance
to yourmain.rs
. - Update your
struct Runtime
to use these types when defining thebalances::Pallet
.
To be honest, this is one of the places that developers most frequently have problems when learning Rust, which is why there is such an emphasis on teaching you and having you learn by doing these steps yourself.
Don't be afraid in this step to peek at the solution if you get stuck, but do try and learn the patterns of using generic types, and what all the syntax means in terms of what the compiler is trying to guarantee about type safety.
#![allow(unused)] fn main() { /* TODO: You might need to import some stuff for this step. */ use std::collections::BTreeMap; type AccountId = String; type Balance = u128; /* TODO: Update the `Pallet` struct to be generic over the `AccountId` and `Balance` type. You won't need the type definitions above after you are done. Types will now be defined in `main.rs`. See the TODOs there. */ /// This is the Balances Module. /// It is a simple module which keeps track of how much balance each account has in this state /// machine. #[derive(Debug)] pub struct Pallet { // A simple storage mapping from accounts to their balances. balances: BTreeMap<AccountId, Balance>, } /* TODO: The generic types need to satisfy certain traits in order to be used in the functions below. - AccountId: Ord - Balance: Zero + CheckedSub + CheckedAdd + Copy You could figure these traits out yourself by letting the compiler tell you what you're missing. NOTE: You might need to adjust some of the functions below to satisfy the borrow checker. */ impl Pallet { /// Create a new instance of the balances module. pub fn new() -> Self { Self { balances: BTreeMap::new() } } /// Set the balance of an account `who` to some `amount`. pub fn set_balance(&mut self, who: &AccountId, amount: Balance) { self.balances.insert(who.clone(), amount); } /// Get the balance of an account `who`. /// If the account has no stored balance, we return zero. pub fn balance(&self, who: &AccountId) -> Balance { *self.balances.get(who).unwrap_or(&0) } /// Transfer `amount` from one account to another. /// This function verifies that `from` has at least `amount` balance to transfer, /// and that no mathematical overflows occur. pub fn transfer( &mut self, caller: AccountId, to: AccountId, amount: Balance, ) -> Result<(), &'static str> { let caller_balance = self.balance(&caller); let to_balance = self.balance(&to); let new_caller_balance = caller_balance.checked_sub(amount).ok_or("Not enough funds.")?; let new_to_balance = to_balance.checked_add(amount).ok_or("Overflow")?; self.balances.insert(caller, new_caller_balance); self.balances.insert(to, new_to_balance); Ok(()) } } #[cfg(test)] mod tests { #[test] fn init_balances() { /* TODO: When creating an instance of `Pallet`, you should explicitly define the types you use. */ let mut balances = super::Pallet::new(); assert_eq!(balances.balance(&"alice".to_string()), 0); balances.set_balance(&"alice".to_string(), 100); assert_eq!(balances.balance(&"alice".to_string()), 100); assert_eq!(balances.balance(&"bob".to_string()), 0); } #[test] fn transfer_balance() { /* TODO: When creating an instance of `Pallet`, you should explicitly define the types you use. */ let mut balances = super::Pallet::new(); assert_eq!( balances.transfer("alice".to_string(), "bob".to_string(), 51), Err("Not enough funds.") ); balances.set_balance(&"alice".to_string(), 100); assert_eq!(balances.transfer("alice".to_string(), "bob".to_string(), 51), Ok(())); assert_eq!(balances.balance(&"alice".to_string()), 49); assert_eq!(balances.balance(&"bob".to_string()), 51); assert_eq!( balances.transfer("alice".to_string(), "bob".to_string(), 51), Err("Not enough funds.") ); } } }
mod balances; mod system; // 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 { /* TODO: Move your type definitions for `AccountId` and `Balance` here. */ } // This is our main Runtime. // It accumulates all of the different pallets we want to use. #[derive(Debug)] pub struct Runtime { system: system::Pallet, /* TODO: Use your type definitions for your new generic `balances::Pallet`. */ balances: balances::Pallet, } 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() } } } fn main() { let mut runtime = Runtime::new(); let alice = "alice".to_string(); let bob = "bob".to_string(); let charlie = "charlie".to_string(); runtime.balances.set_balance(&alice, 100); // start emulating a block runtime.system.inc_block_number(); assert_eq!(runtime.system.block_number(), 1); // first transaction runtime.system.inc_nonce(&alice); let _res = runtime .balances .transfer(alice.clone(), bob, 30) .map_err(|e| eprintln!("{}", e)); // second transaction runtime.system.inc_nonce(&alice); let _res = runtime.balances.transfer(alice, charlie, 20).map_err(|e| eprintln!("{}", e)); println!("{:#?}", runtime); }
#![allow(unused)] fn main() { use num::traits::{CheckedAdd, CheckedSub, Zero}; use std::collections::BTreeMap; /// This is the Balances Module. /// It is a simple module which keeps track of how much balance each account has in this state /// machine. #[derive(Debug)] pub struct Pallet<AccountId, Balance> { // A simple storage mapping from accounts to their balances. balances: BTreeMap<AccountId, Balance>, } impl<AccountId, Balance> Pallet<AccountId, Balance> where AccountId: Ord + Clone, Balance: Zero + CheckedSub + CheckedAdd + Copy, { /// Create a new instance of the balances module. pub fn new() -> Self { Self { balances: BTreeMap::new() } } /// Set the balance of an account `who` to some `amount`. pub fn set_balance(&mut self, who: &AccountId, amount: Balance) { self.balances.insert(who.clone(), amount); } /// Get the balance of an account `who`. /// If the account has no stored balance, we return zero. pub fn balance(&self, who: &AccountId) -> Balance { *self.balances.get(who).unwrap_or(&Balance::zero()) } /// Transfer `amount` from one account to another. /// This function verifies that `from` has at least `amount` balance to transfer, /// and that no mathematical overflows occur. pub fn transfer( &mut self, caller: AccountId, to: AccountId, amount: Balance, ) -> Result<(), &'static str> { let caller_balance = self.balance(&caller); let to_balance = self.balance(&to); let new_caller_balance = caller_balance.checked_sub(&amount).ok_or("Not enough funds.")?; let new_to_balance = to_balance.checked_add(&amount).ok_or("Overflow")?; self.balances.insert(caller, new_caller_balance); self.balances.insert(to, new_to_balance); Ok(()) } } #[cfg(test)] mod tests { #[test] fn init_balances() { let mut balances = super::Pallet::<String, u128>::new(); assert_eq!(balances.balance(&"alice".to_string()), 0); balances.set_balance(&"alice".to_string(), 100); assert_eq!(balances.balance(&"alice".to_string()), 100); assert_eq!(balances.balance(&"bob".to_string()), 0); } #[test] fn transfer_balance() { let mut balances = super::Pallet::<String, u128>::new(); assert_eq!( balances.transfer("alice".to_string(), "bob".to_string(), 51), Err("Not enough funds.") ); balances.set_balance(&"alice".to_string(), 100); assert_eq!(balances.transfer("alice".to_string(), "bob".to_string(), 51), Ok(())); assert_eq!(balances.balance(&"alice".to_string()), 49); assert_eq!(balances.balance(&"bob".to_string()), 51); assert_eq!( balances.transfer("alice".to_string(), "bob".to_string(), 51), Err("Not enough funds.") ); } } }
mod balances; mod system; // 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; } // This is our main Runtime. // It accumulates all of the different pallets we want to use. #[derive(Debug)] pub struct Runtime { system: system::Pallet, balances: balances::Pallet<types::AccountId, types::Balance>, } 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() } } } fn main() { let mut runtime = Runtime::new(); let alice = "alice".to_string(); let bob = "bob".to_string(); let charlie = "charlie".to_string(); runtime.balances.set_balance(&alice, 100); // start emulating a block runtime.system.inc_block_number(); assert_eq!(runtime.system.block_number(), 1); // first transaction runtime.system.inc_nonce(&alice); let _res = runtime .balances .transfer(alice.clone(), bob, 30) .map_err(|e| eprintln!("{}", e)); // second transaction runtime.system.inc_nonce(&alice); let _res = runtime.balances.transfer(alice, charlie, 20).map_err(|e| eprintln!("{}", e)); println!("{:#?}", runtime); }
diff --git a/src/balances.rs b/src/balances.rs
index 543111e7..1effef7a 100644
--- a/src/balances.rs
+++ b/src/balances.rs
@@ -1,8 +1,17 @@
+/* TODO: You might need to import some stuff for this step. */
use std::collections::BTreeMap;
type AccountId = String;
type Balance = u128;
+/*
+ TODO:
+ Update the `Pallet` struct to be generic over the `AccountId` and `Balance` type.
+
+ You won't need the type definitions above after you are done.
+ Types will now be defined in `main.rs`. See the TODOs there.
+*/
+
/// This is the Balances Module.
/// It is a simple module which keeps track of how much balance each account has in this state
/// machine.
@@ -12,6 +21,17 @@ pub struct Pallet {
balances: BTreeMap<AccountId, Balance>,
}
+/*
+ TODO:
+ The generic types need to satisfy certain traits in order to be used in the functions below.
+ - AccountId: Ord
+ - Balance: Zero + CheckedSub + CheckedAdd + Copy
+
+ You could figure these traits out yourself by letting the compiler tell you what you're missing.
+
+ NOTE: You might need to adjust some of the functions below to satisfy the borrow checker.
+*/
+
impl Pallet {
/// Create a new instance of the balances module.
pub fn new() -> Self {
@@ -55,6 +75,10 @@ impl Pallet {
mod tests {
#[test]
fn init_balances() {
+ /*
+ TODO:
+ When creating an instance of `Pallet`, you should explicitly define the types you use.
+ */
let mut balances = super::Pallet::new();
assert_eq!(balances.balance(&"alice".to_string()), 0);
@@ -65,6 +89,10 @@ mod tests {
#[test]
fn transfer_balance() {
+ /*
+ TODO:
+ When creating an instance of `Pallet`, you should explicitly define the types you use.
+ */
let mut balances = super::Pallet::new();
assert_eq!(
diff --git a/src/main.rs b/src/main.rs
index 1ebef924..1cfc058c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,11 +1,21 @@
mod balances;
mod system;
+// 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 {
+ /*
+ TODO: Move your type definitions for `AccountId` and `Balance` here.
+ */
+}
+
// This is our main Runtime.
// It accumulates all of the different pallets we want to use.
#[derive(Debug)]
pub struct Runtime {
system: system::Pallet,
+ /* TODO: Use your type definitions for your new generic `balances::Pallet`. */
balances: balances::Pallet,
}
diff --git a/src/balances.rs b/src/balances.rs
index 1effef7a..e333e0d9 100644
--- a/src/balances.rs
+++ b/src/balances.rs
@@ -1,38 +1,20 @@
-/* TODO: You might need to import some stuff for this step. */
+use num::traits::{CheckedAdd, CheckedSub, Zero};
use std::collections::BTreeMap;
-type AccountId = String;
-type Balance = u128;
-
-/*
- TODO:
- Update the `Pallet` struct to be generic over the `AccountId` and `Balance` type.
-
- You won't need the type definitions above after you are done.
- Types will now be defined in `main.rs`. See the TODOs there.
-*/
-
/// This is the Balances Module.
/// It is a simple module which keeps track of how much balance each account has in this state
/// machine.
#[derive(Debug)]
-pub struct Pallet {
+pub struct Pallet<AccountId, Balance> {
// A simple storage mapping from accounts to their balances.
balances: BTreeMap<AccountId, Balance>,
}
-/*
- TODO:
- The generic types need to satisfy certain traits in order to be used in the functions below.
- - AccountId: Ord
- - Balance: Zero + CheckedSub + CheckedAdd + Copy
-
- You could figure these traits out yourself by letting the compiler tell you what you're missing.
-
- NOTE: You might need to adjust some of the functions below to satisfy the borrow checker.
-*/
-
-impl Pallet {
+impl<AccountId, Balance> Pallet<AccountId, Balance>
+where
+ AccountId: Ord + Clone,
+ Balance: Zero + CheckedSub + CheckedAdd + Copy,
+{
/// Create a new instance of the balances module.
pub fn new() -> Self {
Self { balances: BTreeMap::new() }
@@ -46,7 +28,7 @@ impl Pallet {
/// Get the balance of an account `who`.
/// If the account has no stored balance, we return zero.
pub fn balance(&self, who: &AccountId) -> Balance {
- *self.balances.get(who).unwrap_or(&0)
+ *self.balances.get(who).unwrap_or(&Balance::zero())
}
/// Transfer `amount` from one account to another.
@@ -61,8 +43,8 @@ impl Pallet {
let caller_balance = self.balance(&caller);
let to_balance = self.balance(&to);
- let new_caller_balance = caller_balance.checked_sub(amount).ok_or("Not enough funds.")?;
- let new_to_balance = to_balance.checked_add(amount).ok_or("Overflow")?;
+ let new_caller_balance = caller_balance.checked_sub(&amount).ok_or("Not enough funds.")?;
+ let new_to_balance = to_balance.checked_add(&amount).ok_or("Overflow")?;
self.balances.insert(caller, new_caller_balance);
self.balances.insert(to, new_to_balance);
@@ -75,11 +57,7 @@ impl Pallet {
mod tests {
#[test]
fn init_balances() {
- /*
- TODO:
- When creating an instance of `Pallet`, you should explicitly define the types you use.
- */
- let mut balances = super::Pallet::new();
+ let mut balances = super::Pallet::<String, u128>::new();
assert_eq!(balances.balance(&"alice".to_string()), 0);
balances.set_balance(&"alice".to_string(), 100);
@@ -89,11 +67,7 @@ mod tests {
#[test]
fn transfer_balance() {
- /*
- TODO:
- When creating an instance of `Pallet`, you should explicitly define the types you use.
- */
- let mut balances = super::Pallet::new();
+ let mut balances = super::Pallet::<String, u128>::new();
assert_eq!(
balances.transfer("alice".to_string(), "bob".to_string(), 51),
diff --git a/src/main.rs b/src/main.rs
index 1cfc058c..b314856b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -5,9 +5,8 @@ mod system;
// Modules are configured for these types directly, and they satisfy all of our
// trait requirements.
mod types {
- /*
- TODO: Move your type definitions for `AccountId` and `Balance` here.
- */
+ pub type AccountId = String;
+ pub type Balance = u128;
}
// This is our main Runtime.
@@ -15,8 +14,7 @@ mod types {
#[derive(Debug)]
pub struct Runtime {
system: system::Pallet,
- /* TODO: Use your type definitions for your new generic `balances::Pallet`. */
- balances: balances::Pallet,
+ balances: balances::Pallet<types::AccountId, types::Balance>,
}
impl Runtime {