Tight Coupling
You might have noticed some redundancy when making our pallets generic and configurable. Both pallets defined an AccountId
type, and technically we could define their concrete type differently!
We wouldn't want this on a real production blockchain. Instead, we would want to define common types in a single spot, and use that everywhere.
Trait Inheritance
Rust has the ability for traits to inherit from one another. That is, that for you to implement some trait, you also need to implement all traits that it inherits.
Let's look at some examples.
Trait Functions
We can extend our previous example to show what trait inheritance does with functions
#![allow(unused)] fn main() { pub trait GetName { // returns a string representing the object's name fn name() -> String; } pub trait SayName: GetName { // will print the name from `name()` to console fn say_name() { println!("{}", Self::name()); } } }
Note how in the definition of trait SayName
, we reference GetName
after a colon. This SayName
, your object must also implement GetName
. Note that we could even program a "default" implementation of get_name
by using the Self::name()
function.
So when we implement these traits, it looks like:
#![allow(unused)] fn main() { struct Shawn; impl GetName for Shawn { fn name() -> String { return "shawn".to_string(); } } impl SayName for Shawn {} }
We could choose to implement our own version of the SayName
function, for example like:
#![allow(unused)] fn main() { impl SayName for Shawn { fn say_name() { println!("My name is {}!", Self::name()); } } }
But we don't have to do this. What we do have to do is make sure that GetName
is implemented for Shawn
or you wont be able to use the SayName
trait. Again, we won't be using this in our tutorial, but it is nice to see examples of how this can be used.
Associated Types
Rather than redefining type AccountId
in each Pallet that needs it, what if we just defined it in system::Config
, and inherit that type in other Pallet configs?
Let's see what that would look like:
#![allow(unused)] fn main() { pub trait Config: crate::system::Config { type Balance: Zero + CheckedSub + CheckedAdd + Copy; } }
Here you can see our balances::Config
trait is inheriting from our crate::system::Config
trait. This means that all types defined by system::Config
, including the AccountId
, is accessible through the balances::Config
trait. Because of this, we do not need to redefine the AccountId
type in balances::Config
.
In the Polkadot SDK ecosystem, we call this "tight coupling" because a runtime which contains the Balances Pallet must also contain the System Pallet. In a sense these two pallets are tightly coupled to one another. In fact, with Substrate, all pallets are tightly coupled to the System Pallet, because the System Pallet provides all the meta-types for your blockchain system.
Tightly Couple Balances To System
Let's remove the redundant AccountId
definition from the Balances Pallet Config
.
- Inherit the
crate::system::Config
trait in thebalances::Config
trait. - Remove the
AccountId
type from yourbalances::Config
definition. - Implement
crate::system::Config
forTestConfig
. - In
main.rs
, simply removetype AccountId
frombalances::Config
.
#![allow(unused)] fn main() { use num::traits::{CheckedAdd, CheckedSub, Zero}; use std::collections::BTreeMap; /// The configuration trait for the Balances Module. /// Contains the basic types needed for handling balances. /* TODO: Tightly couple balances to the system pallet by inheriting the `system::Config` trait. After this, you won't need the `AccountId` type redefined here. */ pub trait Config { /// A type which can identify an account in our state machine. /// On a real blockchain, you would want this to be a cryptographic public key. type AccountId: Ord + Clone; /// A type which can represent the balance of an account. /// Usually this is a large unsigned integer. type Balance: Zero + CheckedSub + CheckedAdd + Copy; } /// 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<T: Config> { // A simple storage mapping from accounts to their balances. balances: BTreeMap<T::AccountId, T::Balance>, } impl<T: Config> Pallet<T> { // 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: &T::AccountId, amount: T::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: &T::AccountId) -> T::Balance { *self.balances.get(who).unwrap_or(&T::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: T::AccountId, to: T::AccountId, amount: T::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 { struct TestConfig; /* TODO: Implement `crate::system::Config` for `TestConfig` to make your tests work again. */ impl super::Config for TestConfig { type AccountId = String; type Balance = u128; } #[test] fn init_balances() { let mut balances = super::Pallet::<TestConfig>::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::<TestConfig>::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; pub type BlockNumber = u32; pub type Nonce = u32; } // 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>, } impl system::Config for Runtime { type AccountId = types::AccountId; type BlockNumber = types::BlockNumber; type Nonce = types::Nonce; } impl balances::Config for Runtime { /* TODO: After inheriting from the `system::Config` trait, you won't need `AccountId` here. */ type AccountId = types::AccountId; type Balance = 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); }
#![allow(unused)] fn main() { use num::traits::{CheckedAdd, CheckedSub, Zero}; use std::collections::BTreeMap; /// The configuration trait for the Balances Module. /// Contains the basic types needed for handling balances. pub trait Config: crate::system::Config { /// A type which can represent the balance of an account. /// Usually this is a large unsigned integer. type Balance: Zero + CheckedSub + CheckedAdd + Copy; } /// 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<T: Config> { // A simple storage mapping from accounts to their balances. balances: BTreeMap<T::AccountId, T::Balance>, } impl<T: Config> Pallet<T> { // 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: &T::AccountId, amount: T::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: &T::AccountId) -> T::Balance { *self.balances.get(who).unwrap_or(&T::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: T::AccountId, to: T::AccountId, amount: T::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 { struct TestConfig; impl crate::system::Config for TestConfig { type AccountId = String; type BlockNumber = u32; type Nonce = u32; } impl super::Config for TestConfig { type Balance = u128; } #[test] fn init_balances() { let mut balances = super::Pallet::<TestConfig>::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::<TestConfig>::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; pub type BlockNumber = u32; pub type Nonce = u32; } // 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>, } 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 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 43b49262..25256ba5 100644
--- a/src/balances.rs
+++ b/src/balances.rs
@@ -3,6 +3,11 @@ use std::collections::BTreeMap;
/// The configuration trait for the Balances Module.
/// Contains the basic types needed for handling balances.
+/*
+ TODO:
+ Tightly couple balances to the system pallet by inheriting the `system::Config` trait.
+ After this, you won't need the `AccountId` type redefined here.
+*/
pub trait Config {
/// A type which can identify an account in our state machine.
/// On a real blockchain, you would want this to be a cryptographic public key.
@@ -63,6 +68,9 @@ impl<T: Config> Pallet<T> {
#[cfg(test)]
mod tests {
struct TestConfig;
+
+ /* TODO: Implement `crate::system::Config` for `TestConfig` to make your tests work again. */
+
impl super::Config for TestConfig {
type AccountId = String;
type Balance = u128;
diff --git a/src/main.rs b/src/main.rs
index fe677ae7..c5a98c4a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -26,6 +26,7 @@ impl system::Config for Runtime {
}
impl balances::Config for Runtime {
+ /* TODO: After inheriting from the `system::Config` trait, you won't need `AccountId` here. */
type AccountId = types::AccountId;
type Balance = types::Balance;
}
diff --git a/src/balances.rs b/src/balances.rs
index 25256ba5..989fa71d 100644
--- a/src/balances.rs
+++ b/src/balances.rs
@@ -3,15 +3,7 @@ use std::collections::BTreeMap;
/// The configuration trait for the Balances Module.
/// Contains the basic types needed for handling balances.
-/*
- TODO:
- Tightly couple balances to the system pallet by inheriting the `system::Config` trait.
- After this, you won't need the `AccountId` type redefined here.
-*/
-pub trait Config {
- /// A type which can identify an account in our state machine.
- /// On a real blockchain, you would want this to be a cryptographic public key.
- type AccountId: Ord + Clone;
+pub trait Config: crate::system::Config {
/// A type which can represent the balance of an account.
/// Usually this is a large unsigned integer.
type Balance: Zero + CheckedSub + CheckedAdd + Copy;
@@ -69,10 +61,13 @@ impl<T: Config> Pallet<T> {
mod tests {
struct TestConfig;
- /* TODO: Implement `crate::system::Config` for `TestConfig` to make your tests work again. */
+ impl crate::system::Config for TestConfig {
+ type AccountId = String;
+ type BlockNumber = u32;
+ type Nonce = u32;
+ }
impl super::Config for TestConfig {
- type AccountId = String;
type Balance = u128;
}
diff --git a/src/main.rs b/src/main.rs
index c5a98c4a..dd1d1f69 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -26,8 +26,6 @@ impl system::Config for Runtime {
}
impl balances::Config for Runtime {
- /* TODO: After inheriting from the `system::Config` trait, you won't need `AccountId` here. */
- type AccountId = types::AccountId;
type Balance = types::Balance;
}