Buy Kitty Logic
Now that we have scaffolded the buy_kitty
extrinsic, its time for us to program its logic.
Sanity Checks
As always, before we execute any logic, we should check that everything is okay to actually proceed with the kitty purchase.
The first check should obviously be if the kitty the user wants to purchase actually exists.
Then we should check if the kitty is actually for sale, which is indicated by Some(real_price)
, and extract that real_price
.
Finally, we should check that there is an agreement between the buyer and the seller on the price.
The buyer will submit max_price
, and we want to ensure that this max_price
is greater than or equal to the real_price
.
Transfer Logic
To execute a purchase, we need to transfer two things:
- The token balance from the buyer to the seller.
- The kitty from the seller to the buyer.
Transfer the Native Balance
To transfer the NativeBalance
, you can use the transfer
API which is included in the fungible::Mutate
trait.
#![allow(unused)] fn main() { fn transfer( source: &AccountId, dest: &AccountId, amount: Self::Balance, preservation: Preservation ) -> Result<Self::Balance, DispatchError> }
NOTE: To access this function, you will need import the trait to bring it in scope. Otherwise you will get an error that the function does not exist. So don't forget to include:
#![allow(unused)] fn main() { use frame::traits::fungible::Mutate; }
The first 3 parameters here are easy enough to understand: source
, dest
, and amount
.
However we also have a 4th parameter which is preservation: Preservation
.
#![allow(unused)] fn main() { /// The mode by which we describe whether an operation should keep an account alive. pub enum Preservation { /// We don't care if the account gets killed by this operation. Expendable, /// The account may not be killed, but we don't care if the balance gets dusted. Protect, /// The account may not be killed and our provider reference must remain (in the context of /// tokens, this means that the account may not be dusted). Preserve, } }
To understand this, you will need to dive deep into dust accounts, existential deposit, and account deletion.
That is all beyond the scope of this tutorial, but the high level idea is that we require accounts in the polkadot-sdk
to maintain a minimum balance. Whenever we transfer funds from the user, beyond checking they have enough to transfer, we also check whether that minimum amount is still left over in their account, and adjust the result of our transfer based on our Preservation
requirements.
In this context, we don't want someone to kill their account to buy a kitty, so we want to use Preservation::Preserve
for our transfer
.
NOTE: Don't forget the TODO that imports this enum so you can use it:
#![allow(unused)] fn main() { use frame::traits::tokens::Preservation; }
So the final syntax should look like:
#![allow(unused)] fn main() { T::NativeBalance::transfer(&buyer, &kitty.owner, real_price, Preservation::Preserve)?; }
Transfer the Kitty
To transfer the kitty, we can simply reuse our Self::do_transfer
function that we wrote in the past.
Hopefully you can start to see the payoff of how we have organized our code.
As you work on more complex projects, breaking down behaviors into simple to understand and reusable pieces of internal logic will not only make your code more clean, but also easier to audit and update.
It is unlikely you will have foresight on exactly how to break down your project when you first start writing it, but it is definitely something you should keep in mind as you find yourself writing potentially duplicate code.
Propagate Up Errors
Both transfer functions need to succeed for the sale to complete successfully.
If either one of them would fail, the whole purchase should fail.
Thankfully, both of our transfer functions return a result, and to handle things correctly here, we just need to propagate up those errors. For that, we simply include ?
at the end of the function.
If at any point our extrinsic or the logic inside the extrinsic returns an error, the whole extrinsic will fail and all changes to storage will be undone. This is exactly the same behavior you would expect from a smart contract, and keeps our state transition function functioning smoothly.
Real Price
As a final change in your pallet logic, we should update the Event::<T>::Sold
to emit the real_price
instead of the buyers max_price
.
This is perhaps the first really good use case for emitting an Event.
In all of our previous events, we basically just deposit the data that we already get from the extrinsic.
However in this case, our buy_kitty
extrinsic actually uses internal logic to manipulate what actually happens. It could be that the user submits a max_price
higher than the real_price
, and thus the extrinsic and a success message would not actually tell us what happened exactly.
In this case, we can actually report by to the user and any UI they are using the executed sale price of the kitty.
So this is really where Pallet Events shine, and how they should be used. This is something you will develop a sense for over time.
Your Turn
You now have all the tools and information needed to build your do_buy_kitty
logic.
- Add the sanity checks needed to make sure
do_buy_kitty
should execute at all. - Transfer both the
NativeBalance
and theKitty
, being sure to check for success. - Finally, update the
Event::<T>::Sold
to deposit thereal_price
that was transferred.
#![allow(unused)] fn main() { use super::*; use frame::prelude::*; use frame::primitives::BlakeTwo256; /* 🚧 TODO 🚧: Import `frame::traits::tokens::Preservation`. */ use frame::traits::Hash; // Learn about internal functions. impl<T: Config> Pallet<T> { // Generates and returns DNA pub fn gen_dna() -> [u8; 32] { // Create randomness payload. Multiple kitties can be generated in the same block, // retaining uniqueness. let unique_payload = ( frame_system::Pallet::<T>::parent_hash(), frame_system::Pallet::<T>::block_number(), frame_system::Pallet::<T>::extrinsic_index(), CountForKitties::<T>::get(), ); BlakeTwo256::hash_of(&unique_payload).into() } pub fn mint(owner: T::AccountId, dna: [u8; 32]) -> DispatchResult { let kitty = Kitty { dna, owner: owner.clone(), price: None }; // Check if the kitty does not already exist in our storage map ensure!(!Kitties::<T>::contains_key(dna), Error::<T>::DuplicateKitty); let current_count: u32 = CountForKitties::<T>::get(); let new_count = current_count.checked_add(1).ok_or(Error::<T>::TooManyKitties)?; KittiesOwned::<T>::try_append(&owner, dna).map_err(|_| Error::<T>::TooManyOwned)?; Kitties::<T>::insert(dna, kitty); CountForKitties::<T>::set(new_count); Self::deposit_event(Event::<T>::Created { owner }); Ok(()) } pub fn do_transfer(from: T::AccountId, to: T::AccountId, kitty_id: [u8; 32]) -> DispatchResult { ensure!(from != to, Error::<T>::TransferToSelf); let mut kitty = Kitties::<T>::get(kitty_id).ok_or(Error::<T>::NoKitty)?; ensure!(kitty.owner == from, Error::<T>::NotOwner); kitty.owner = to.clone(); kitty.price = None; let mut to_owned = KittiesOwned::<T>::get(&to); to_owned.try_push(kitty_id).map_err(|_| Error::<T>::TooManyOwned)?; let mut from_owned = KittiesOwned::<T>::get(&from); if let Some(ind) = from_owned.iter().position(|&id| id == kitty_id) { from_owned.swap_remove(ind); } else { return Err(Error::<T>::NoKitty.into()) } Kitties::<T>::insert(kitty_id, kitty); KittiesOwned::<T>::insert(&to, to_owned); KittiesOwned::<T>::insert(&from, from_owned); Self::deposit_event(Event::<T>::Transferred { from, to, kitty_id }); Ok(()) } pub fn do_set_price( caller: T::AccountId, kitty_id: [u8; 32], new_price: Option<BalanceOf<T>>, ) -> DispatchResult { let mut kitty = Kitties::<T>::get(kitty_id).ok_or(Error::<T>::NoKitty)?; ensure!(kitty.owner == caller, Error::<T>::NotOwner); kitty.price = new_price; Kitties::<T>::insert(kitty_id, kitty); Self::deposit_event(Event::<T>::PriceSet { owner: caller, kitty_id, new_price }); Ok(()) } pub fn do_buy_kitty( buyer: T::AccountId, kitty_id: [u8; 32], price: BalanceOf<T>, ) -> DispatchResult { /* 🚧 TODO 🚧: Sanity check that the purchase is allowed: - Get `kitty` from `Kitties` using `kitty_id`, `ok_or` return `Error::<T>::NoKitty`. - Get the `real_price` from `kitty.price`, `ok_or` return `Error::<T>::NotForSale`. - `ensure!` that `price` is greater or equal to `real_price`, else `Error::<T>::MaxPriceTooLow`. */ /* 🚧 TODO 🚧: Execute the transfers: - Use `T::NativeBalance` to `transfer` from the `buyer` to the `kitty.owner`. - The amount transferred should be the `real_price`. - Use `Preservation::Preserve` to ensure the buyer account stays alive. - Use `Self::do_transfer` to transfer from the `kitty.owner` to the `buyer` with `kitty_id`. - Remember to propagate up all results from these functions with `?`. */ /* 🚧 TODO 🚧: Update the event to use the `real_price` in the `Event`. */ Self::deposit_event(Event::<T>::Sold { buyer, kitty_id, price }); Ok(()) } } }
#![allow(unused)] #![cfg_attr(not(feature = "std"), no_std)] fn main() { mod impls; mod tests; use frame::prelude::*; use frame::traits::fungible::Inspect; use frame::traits::fungible::Mutate; pub use pallet::*; #[frame::pallet(dev_mode)] pub mod pallet { use super::*; #[pallet::pallet] pub struct Pallet<T>(core::marker::PhantomData<T>); #[pallet::config] pub trait Config: frame_system::Config { type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>; /// The Fungible handler for the kitties pallet. type NativeBalance: Inspect<Self::AccountId> + Mutate<Self::AccountId>; } // Allows easy access our Pallet's `Balance` type. Comes from `Fungible` interface. pub type BalanceOf<T> = <<T as Config>::NativeBalance as Inspect<<T as frame_system::Config>::AccountId>>::Balance; #[derive(Encode, Decode, TypeInfo, MaxEncodedLen)] #[scale_info(skip_type_params(T))] pub struct Kitty<T: Config> { // Using 32 bytes to represent a kitty DNA pub dna: [u8; 32], pub owner: T::AccountId, pub price: Option<BalanceOf<T>>, } #[pallet::storage] pub(super) type CountForKitties<T: Config> = StorageValue<Value = u32, QueryKind = ValueQuery>; #[pallet::storage] pub(super) type Kitties<T: Config> = StorageMap<Key = [u8; 32], Value = Kitty<T>>; /// Track the kitties owned by each account. #[pallet::storage] pub(super) type KittiesOwned<T: Config> = StorageMap< Key = T::AccountId, Value = BoundedVec<[u8; 32], ConstU32<100>>, QueryKind = ValueQuery, >; #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event<T: Config> { Created { owner: T::AccountId }, Transferred { from: T::AccountId, to: T::AccountId, kitty_id: [u8; 32] }, PriceSet { owner: T::AccountId, kitty_id: [u8; 32], new_price: Option<BalanceOf<T>> }, Sold { buyer: T::AccountId, kitty_id: [u8; 32], price: BalanceOf<T> }, } #[pallet::error] pub enum Error<T> { TooManyKitties, DuplicateKitty, TooManyOwned, TransferToSelf, NoKitty, NotOwner, /* 🚧 TODO 🚧: Add `Errors` needed for `do_buy_kitty`: - `NotForSale`: for when the Kitty has a price set to `None`. - `MaxPriceTooLow`: for when the price offered by the buyer is too low. */ } #[pallet::call] impl<T: Config> Pallet<T> { pub fn create_kitty(origin: OriginFor<T>) -> DispatchResult { let who = ensure_signed(origin)?; let dna = Self::gen_dna(); Self::mint(who, dna)?; Ok(()) } pub fn transfer( origin: OriginFor<T>, to: T::AccountId, kitty_id: [u8; 32], ) -> DispatchResult { let who = ensure_signed(origin)?; Self::do_transfer(who, to, kitty_id)?; Ok(()) } pub fn set_price( origin: OriginFor<T>, kitty_id: [u8; 32], new_price: Option<BalanceOf<T>>, ) -> DispatchResult { let who = ensure_signed(origin)?; Self::do_set_price(who, kitty_id, new_price)?; Ok(()) } pub fn buy_kitty( origin: OriginFor<T>, kitty_id: [u8; 32], max_price: BalanceOf<T>, ) -> DispatchResult { let who = ensure_signed(origin)?; Self::do_buy_kitty(who, kitty_id, max_price)?; Ok(()) } } } }
#![allow(unused)] fn main() { use super::*; use frame::prelude::*; use frame::primitives::BlakeTwo256; use frame::traits::tokens::Preservation; use frame::traits::Hash; // Learn about internal functions. impl<T: Config> Pallet<T> { // Generates and returns DNA pub fn gen_dna() -> [u8; 32] { // Create randomness payload. Multiple kitties can be generated in the same block, // retaining uniqueness. let unique_payload = ( frame_system::Pallet::<T>::parent_hash(), frame_system::Pallet::<T>::block_number(), frame_system::Pallet::<T>::extrinsic_index(), CountForKitties::<T>::get(), ); BlakeTwo256::hash_of(&unique_payload).into() } pub fn mint(owner: T::AccountId, dna: [u8; 32]) -> DispatchResult { let kitty = Kitty { dna, owner: owner.clone(), price: None }; // Check if the kitty does not already exist in our storage map ensure!(!Kitties::<T>::contains_key(dna), Error::<T>::DuplicateKitty); let current_count: u32 = CountForKitties::<T>::get(); let new_count = current_count.checked_add(1).ok_or(Error::<T>::TooManyKitties)?; KittiesOwned::<T>::try_append(&owner, dna).map_err(|_| Error::<T>::TooManyOwned)?; Kitties::<T>::insert(dna, kitty); CountForKitties::<T>::set(new_count); Self::deposit_event(Event::<T>::Created { owner }); Ok(()) } pub fn do_transfer(from: T::AccountId, to: T::AccountId, kitty_id: [u8; 32]) -> DispatchResult { ensure!(from != to, Error::<T>::TransferToSelf); let mut kitty = Kitties::<T>::get(kitty_id).ok_or(Error::<T>::NoKitty)?; ensure!(kitty.owner == from, Error::<T>::NotOwner); kitty.owner = to.clone(); kitty.price = None; let mut to_owned = KittiesOwned::<T>::get(&to); to_owned.try_push(kitty_id).map_err(|_| Error::<T>::TooManyOwned)?; let mut from_owned = KittiesOwned::<T>::get(&from); if let Some(ind) = from_owned.iter().position(|&id| id == kitty_id) { from_owned.swap_remove(ind); } else { return Err(Error::<T>::NoKitty.into()) } Kitties::<T>::insert(kitty_id, kitty); KittiesOwned::<T>::insert(&to, to_owned); KittiesOwned::<T>::insert(&from, from_owned); Self::deposit_event(Event::<T>::Transferred { from, to, kitty_id }); Ok(()) } pub fn do_set_price( caller: T::AccountId, kitty_id: [u8; 32], new_price: Option<BalanceOf<T>>, ) -> DispatchResult { let mut kitty = Kitties::<T>::get(kitty_id).ok_or(Error::<T>::NoKitty)?; ensure!(kitty.owner == caller, Error::<T>::NotOwner); kitty.price = new_price; Kitties::<T>::insert(kitty_id, kitty); Self::deposit_event(Event::<T>::PriceSet { owner: caller, kitty_id, new_price }); Ok(()) } pub fn do_buy_kitty( buyer: T::AccountId, kitty_id: [u8; 32], price: BalanceOf<T>, ) -> DispatchResult { let kitty = Kitties::<T>::get(kitty_id).ok_or(Error::<T>::NoKitty)?; let real_price = kitty.price.ok_or(Error::<T>::NotForSale)?; ensure!(price >= real_price, Error::<T>::MaxPriceTooLow); T::NativeBalance::transfer(&buyer, &kitty.owner, real_price, Preservation::Preserve)?; Self::do_transfer(kitty.owner, buyer.clone(), kitty_id)?; Self::deposit_event(Event::<T>::Sold { buyer, kitty_id, price: real_price }); Ok(()) } } }
#![allow(unused)] #![cfg_attr(not(feature = "std"), no_std)] fn main() { mod impls; mod tests; use frame::prelude::*; use frame::traits::fungible::Inspect; use frame::traits::fungible::Mutate; pub use pallet::*; #[frame::pallet(dev_mode)] pub mod pallet { use super::*; #[pallet::pallet] pub struct Pallet<T>(core::marker::PhantomData<T>); #[pallet::config] pub trait Config: frame_system::Config { type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>; /// The Fungible handler for the kitties pallet. type NativeBalance: Inspect<Self::AccountId> + Mutate<Self::AccountId>; } // Allows easy access our Pallet's `Balance` type. Comes from `Fungible` interface. pub type BalanceOf<T> = <<T as Config>::NativeBalance as Inspect<<T as frame_system::Config>::AccountId>>::Balance; #[derive(Encode, Decode, TypeInfo, MaxEncodedLen)] #[scale_info(skip_type_params(T))] pub struct Kitty<T: Config> { // Using 32 bytes to represent a kitty DNA pub dna: [u8; 32], pub owner: T::AccountId, pub price: Option<BalanceOf<T>>, } #[pallet::storage] pub(super) type CountForKitties<T: Config> = StorageValue<Value = u32, QueryKind = ValueQuery>; #[pallet::storage] pub(super) type Kitties<T: Config> = StorageMap<Key = [u8; 32], Value = Kitty<T>>; /// Track the kitties owned by each account. #[pallet::storage] pub(super) type KittiesOwned<T: Config> = StorageMap< Key = T::AccountId, Value = BoundedVec<[u8; 32], ConstU32<100>>, QueryKind = ValueQuery, >; #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event<T: Config> { Created { owner: T::AccountId }, Transferred { from: T::AccountId, to: T::AccountId, kitty_id: [u8; 32] }, PriceSet { owner: T::AccountId, kitty_id: [u8; 32], new_price: Option<BalanceOf<T>> }, Sold { buyer: T::AccountId, kitty_id: [u8; 32], price: BalanceOf<T> }, } #[pallet::error] pub enum Error<T> { TooManyKitties, DuplicateKitty, TooManyOwned, TransferToSelf, NoKitty, NotOwner, NotForSale, MaxPriceTooLow, } #[pallet::call] impl<T: Config> Pallet<T> { pub fn create_kitty(origin: OriginFor<T>) -> DispatchResult { let who = ensure_signed(origin)?; let dna = Self::gen_dna(); Self::mint(who, dna)?; Ok(()) } pub fn transfer( origin: OriginFor<T>, to: T::AccountId, kitty_id: [u8; 32], ) -> DispatchResult { let who = ensure_signed(origin)?; Self::do_transfer(who, to, kitty_id)?; Ok(()) } pub fn set_price( origin: OriginFor<T>, kitty_id: [u8; 32], new_price: Option<BalanceOf<T>>, ) -> DispatchResult { let who = ensure_signed(origin)?; Self::do_set_price(who, kitty_id, new_price)?; Ok(()) } pub fn buy_kitty( origin: OriginFor<T>, kitty_id: [u8; 32], max_price: BalanceOf<T>, ) -> DispatchResult { let who = ensure_signed(origin)?; Self::do_buy_kitty(who, kitty_id, max_price)?; Ok(()) } } } }
#![allow(unused)] fn main() { // Tests for the Kitties Pallet. // // Normally this file would be split into two parts: `mock.rs` and `tests.rs`. // The `mock.rs` file would contain all the setup code for our `TestRuntime`. // Then `tests.rs` would only have the tests for our pallet. // However, to minimize the project, these have been combined into this single file. // // Learn more about creating tests for Pallets: // https://paritytech.github.io/polkadot-sdk/master/polkadot_sdk_docs/guides/your_first_pallet/index.html // This flag tells rust to only run this file when running `cargo test`. #![cfg(test)] use crate as pallet_kitties; use crate::*; use frame::deps::sp_io; use frame::runtime::prelude::*; use frame::testing_prelude::*; use frame::traits::fungible::*; type Balance = u64; type Block = frame_system::mocking::MockBlock<TestRuntime>; // In our "test runtime", we represent a user `AccountId` with a `u64`. // This is just a simplification so that we don't need to generate a bunch of proper cryptographic // public keys when writing tests. It is just easier to say "user 1 transfers to user 2". // We create the constants `ALICE` and `BOB` to make it clear when we are representing users below. const ALICE: u64 = 1; const BOB: u64 = 2; const DEFAULT_KITTY: Kitty<TestRuntime> = Kitty { dna: [0u8; 32], owner: 0, price: None }; // Our blockchain tests only need 3 Pallets: // 1. System: Which is included with every FRAME runtime. // 2. PalletBalances: Which is manages your blockchain's native currency. (i.e. DOT on Polkadot) // 3. PalletKitties: The pallet you are building in this tutorial! construct_runtime! { pub struct TestRuntime { System: frame_system, PalletBalances: pallet_balances, PalletKitties: pallet_kitties, } } // Normally `System` would have many more configurations, but you can see that we use some macro // magic to automatically configure most of the pallet for a "default test configuration". #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] impl frame_system::Config for TestRuntime { type Block = Block; type AccountData = pallet_balances::AccountData<Balance>; } // Normally `pallet_balances` would have many more configurations, but you can see that we use some // macro magic to automatically configure most of the pallet for a "default test configuration". #[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] impl pallet_balances::Config for TestRuntime { type AccountStore = System; type Balance = Balance; } // This is the configuration of our Pallet! If you make changes to the pallet's `trait Config`, you // will also need to update this configuration to represent that. impl pallet_kitties::Config for TestRuntime { type RuntimeEvent = RuntimeEvent; type NativeBalance = PalletBalances; } // We need to run most of our tests using this function: `new_test_ext().execute_with(|| { ... });` // It simulates the blockchain database backend for our tests. // If you forget to include this and try to access your Pallet storage, you will get an error like: // "`get_version_1` called outside of an Externalities-provided environment." pub fn new_test_ext() -> sp_io::TestExternalities { frame_system::GenesisConfig::<TestRuntime>::default() .build_storage() .unwrap() .into() } #[test] fn starting_template_is_sane() { new_test_ext().execute_with(|| { let event = Event::<TestRuntime>::Created { owner: ALICE }; let _runtime_event: RuntimeEvent = event.into(); let _call = Call::<TestRuntime>::create_kitty {}; let result = PalletKitties::create_kitty(RuntimeOrigin::signed(BOB)); assert_ok!(result); }); } #[test] fn system_and_balances_work() { // This test will just sanity check that we can access `System` and `PalletBalances`. new_test_ext().execute_with(|| { // We often need to set `System` to block 1 so that we can see events. System::set_block_number(1); // We often need to add some balance to a user to test features which needs tokens. assert_ok!(PalletBalances::mint_into(&ALICE, 100)); assert_ok!(PalletBalances::mint_into(&BOB, 100)); }); } #[test] fn create_kitty_checks_signed() { new_test_ext().execute_with(|| { // The `create_kitty` extrinsic should work when being called by a user. assert_ok!(PalletKitties::create_kitty(RuntimeOrigin::signed(ALICE))); // The `create_kitty` extrinsic should fail when being called by an unsigned message. assert_noop!(PalletKitties::create_kitty(RuntimeOrigin::none()), DispatchError::BadOrigin); }) } #[test] fn create_kitty_emits_event() { new_test_ext().execute_with(|| { // We need to set block number to 1 to view events. System::set_block_number(1); // Execute our call, and ensure it is successful. assert_ok!(PalletKitties::create_kitty(RuntimeOrigin::signed(ALICE))); // Assert the last event by our blockchain is the `Created` event with the correct owner. System::assert_last_event(Event::<TestRuntime>::Created { owner: 1 }.into()); }) } #[test] fn count_for_kitties_created_correctly() { new_test_ext().execute_with(|| { // Querying storage before anything is set will return `0`. assert_eq!(CountForKitties::<TestRuntime>::get(), 0); // You can `set` the value using an `u32`. CountForKitties::<TestRuntime>::set(1337u32); // You can `put` the value directly with a `u32`. CountForKitties::<TestRuntime>::put(1337u32); }) } #[test] fn mint_increments_count_for_kitty() { new_test_ext().execute_with(|| { // Querying storage before anything is set will return `0`. assert_eq!(CountForKitties::<TestRuntime>::get(), 0); // Call `create_kitty` which will call `mint`. assert_ok!(PalletKitties::create_kitty(RuntimeOrigin::signed(ALICE))); // Now the storage should be `1` assert_eq!(CountForKitties::<TestRuntime>::get(), 1); }) } #[test] fn mint_errors_when_overflow() { new_test_ext().execute_with(|| { // Set the count to the largest value possible. CountForKitties::<TestRuntime>::set(u32::MAX); // `create_kitty` should not succeed because of safe math. assert_noop!( PalletKitties::create_kitty(RuntimeOrigin::signed(1)), Error::<TestRuntime>::TooManyKitties ); }) } #[test] fn kitties_map_created_correctly() { new_test_ext().execute_with(|| { let zero_key = [0u8; 32]; assert!(!Kitties::<TestRuntime>::contains_key(zero_key)); Kitties::<TestRuntime>::insert(zero_key, DEFAULT_KITTY); assert!(Kitties::<TestRuntime>::contains_key(zero_key)); }) } #[test] fn create_kitty_adds_to_map() { new_test_ext().execute_with(|| { assert_ok!(PalletKitties::create_kitty(RuntimeOrigin::signed(ALICE))); assert_eq!(Kitties::<TestRuntime>::iter().count(), 1); }) } #[test] fn cannot_mint_duplicate_kitty() { new_test_ext().execute_with(|| { assert_ok!(PalletKitties::mint(ALICE, [0u8; 32])); assert_noop!(PalletKitties::mint(BOB, [0u8; 32]), Error::<TestRuntime>::DuplicateKitty); }) } #[test] fn kitty_struct_has_expected_traits() { new_test_ext().execute_with(|| { let kitty = DEFAULT_KITTY; let bytes = kitty.encode(); let _decoded_kitty = Kitty::<TestRuntime>::decode(&mut &bytes[..]).unwrap(); assert!(Kitty::<TestRuntime>::max_encoded_len() > 0); let _info = Kitty::<TestRuntime>::type_info(); }) } #[test] fn mint_stores_owner_in_kitty() { new_test_ext().execute_with(|| { assert_ok!(PalletKitties::mint(1337, [42u8; 32])); let kitty = Kitties::<TestRuntime>::get([42u8; 32]).unwrap(); assert_eq!(kitty.owner, 1337); assert_eq!(kitty.dna, [42u8; 32]); }) } #[test] fn create_kitty_makes_unique_kitties() { new_test_ext().execute_with(|| { // Two calls to `create_kitty` should work. assert_ok!(PalletKitties::create_kitty(RuntimeOrigin::signed(ALICE))); assert_ok!(PalletKitties::create_kitty(RuntimeOrigin::signed(BOB))); // And should result in two kitties in our system. assert_eq!(CountForKitties::<TestRuntime>::get(), 2); assert_eq!(Kitties::<TestRuntime>::iter().count(), 2); }) } #[test] fn kitties_owned_created_correctly() { new_test_ext().execute_with(|| { // Initially users have no kitties owned. assert_eq!(KittiesOwned::<TestRuntime>::get(1).len(), 0); // Let's create two kitties. assert_ok!(PalletKitties::create_kitty(RuntimeOrigin::signed(ALICE))); assert_ok!(PalletKitties::create_kitty(RuntimeOrigin::signed(ALICE))); // Now they should have two kitties owned. assert_eq!(KittiesOwned::<TestRuntime>::get(1).len(), 2); }); } #[test] fn cannot_own_too_many_kitties() { new_test_ext().execute_with(|| { // If your max owned is different than 100, you will need to update this. for _ in 0..100 { assert_ok!(PalletKitties::create_kitty(RuntimeOrigin::signed(ALICE))); } assert_noop!( PalletKitties::create_kitty(RuntimeOrigin::signed(1)), Error::<TestRuntime>::TooManyOwned ); }); } #[test] fn transfer_emits_event() { new_test_ext().execute_with(|| { // We need to set block number to 1 to view events. System::set_block_number(1); // Create a kitty to transfer assert_ok!(PalletKitties::create_kitty(RuntimeOrigin::signed(ALICE))); // Get the kitty id. let kitty_id = Kitties::<TestRuntime>::iter_keys().collect::<Vec<_>>()[0]; assert_ok!(PalletKitties::transfer(RuntimeOrigin::signed(ALICE), BOB, kitty_id)); System::assert_last_event( Event::<TestRuntime>::Transferred { from: ALICE, to: BOB, kitty_id }.into(), ); }); } #[test] fn transfer_logic_works() { new_test_ext().execute_with(|| { assert_ok!(PalletKitties::create_kitty(RuntimeOrigin::signed(ALICE))); // Starting state looks good. let kitty = &Kitties::<TestRuntime>::iter_values().collect::<Vec<_>>()[0]; let kitty_id = kitty.dna; assert_eq!(kitty.owner, ALICE); assert_eq!(KittiesOwned::<TestRuntime>::get(ALICE), vec![kitty_id]); assert_eq!(KittiesOwned::<TestRuntime>::get(BOB), vec![]); // Cannot transfer to yourself. assert_noop!( PalletKitties::transfer(RuntimeOrigin::signed(ALICE), ALICE, kitty_id), Error::<TestRuntime>::TransferToSelf ); // Cannot transfer a non-existent kitty. assert_noop!( PalletKitties::transfer(RuntimeOrigin::signed(ALICE), BOB, [0u8; 32]), Error::<TestRuntime>::NoKitty ); // Cannot transfer kitty you do not own. assert_noop!( PalletKitties::transfer(RuntimeOrigin::signed(BOB), ALICE, kitty_id), Error::<TestRuntime>::NotOwner ); // Transfer should work when parameters are right. assert_ok!(PalletKitties::transfer(RuntimeOrigin::signed(ALICE), BOB, kitty_id)); // Storage is updated correctly. assert_eq!(KittiesOwned::<TestRuntime>::get(ALICE), vec![]); assert_eq!(KittiesOwned::<TestRuntime>::get(BOB), vec![kitty_id]); let kitty = &Kitties::<TestRuntime>::iter_values().collect::<Vec<_>>()[0]; assert_eq!(kitty.owner, BOB); }); } #[test] fn native_balance_associated_type_works() { new_test_ext().execute_with(|| { assert_ok!(<<TestRuntime as Config>::NativeBalance as Mutate<_>>::mint_into(&ALICE, 1337)); assert_eq!( <<TestRuntime as Config>::NativeBalance as Inspect<_>>::total_balance(&ALICE), 1337 ); }); } #[test] fn balance_of_type_works() { // Inside our tests, the `BalanceOf` type has a concrete type of `u64`. let _example_balance: BalanceOf<TestRuntime> = 1337u64; } #[test] fn set_price_emits_event() { new_test_ext().execute_with(|| { // We need to set block number to 1 to view events. System::set_block_number(1); assert_ok!(PalletKitties::create_kitty(RuntimeOrigin::signed(ALICE))); let kitty_id = Kitties::<TestRuntime>::iter_keys().collect::<Vec<_>>()[0]; assert_ok!(PalletKitties::set_price(RuntimeOrigin::signed(ALICE), kitty_id, Some(1337))); // Assert the last event is `PriceSet` event with the correct information. System::assert_last_event( Event::<TestRuntime>::PriceSet { owner: ALICE, kitty_id, new_price: Some(1337) }.into(), ); }) } #[test] fn set_price_logic_works() { new_test_ext().execute_with(|| { assert_ok!(PalletKitties::create_kitty(RuntimeOrigin::signed(ALICE))); let kitty = &Kitties::<TestRuntime>::iter_values().collect::<Vec<_>>()[0]; assert_eq!(kitty.price, None); let kitty_id = kitty.dna; assert_ok!(PalletKitties::set_price(RuntimeOrigin::signed(ALICE), kitty_id, Some(1337))); let kitty = Kitties::<TestRuntime>::get(kitty_id).unwrap(); assert_eq!(kitty.price, Some(1337)); }) } #[test] fn do_buy_kitty_emits_event() { new_test_ext().execute_with(|| { // We need to set block number to 1 to view events. System::set_block_number(1); assert_ok!(PalletKitties::create_kitty(RuntimeOrigin::signed(ALICE))); let kitty_id = Kitties::<TestRuntime>::iter_keys().collect::<Vec<_>>()[0]; assert_ok!(PalletKitties::set_price(RuntimeOrigin::signed(ALICE), kitty_id, Some(1337))); assert_ok!(PalletBalances::mint_into(&BOB, 100_000)); assert_ok!(PalletKitties::buy_kitty(RuntimeOrigin::signed(BOB), kitty_id, 1337)); // Assert the last event by our blockchain is the `Created` event with the correct owner. System::assert_last_event( Event::<TestRuntime>::Sold { buyer: BOB, kitty_id, price: 1337 }.into(), ); }) } #[test] fn do_buy_kitty_logic_works() { new_test_ext().execute_with(|| { assert_ok!(PalletKitties::create_kitty(RuntimeOrigin::signed(ALICE))); let kitty = &Kitties::<TestRuntime>::iter_values().collect::<Vec<_>>()[0]; let kitty_id = kitty.dna; assert_eq!(kitty.owner, ALICE); assert_eq!(KittiesOwned::<TestRuntime>::get(ALICE), vec![kitty_id]); // Cannot buy kitty which does not exist. assert_noop!( PalletKitties::buy_kitty(RuntimeOrigin::signed(BOB), [0u8; 32], 1337), Error::<TestRuntime>::NoKitty ); // Cannot buy kitty which is not for sale. assert_noop!( PalletKitties::buy_kitty(RuntimeOrigin::signed(BOB), kitty_id, 1337), Error::<TestRuntime>::NotForSale ); assert_ok!(PalletKitties::set_price(RuntimeOrigin::signed(ALICE), kitty_id, Some(1337))); // Cannot buy kitty for a lower price. assert_noop!( PalletKitties::buy_kitty(RuntimeOrigin::signed(BOB), kitty_id, 1336), Error::<TestRuntime>::MaxPriceTooLow ); // Cannot buy kitty if you don't have the funds. assert_noop!( PalletKitties::buy_kitty(RuntimeOrigin::signed(BOB), kitty_id, 1337), frame::arithmetic::ArithmeticError::Underflow ); // Cannot buy kitty if it would kill your account (i.e. set your balance to 0). assert_ok!(PalletBalances::mint_into(&BOB, 1337)); assert!( PalletKitties::buy_kitty(RuntimeOrigin::signed(BOB), kitty_id, 1337).is_err(), // TODO: assert_noop on DispatchError::Token(TokenError::NotExpendable) ); // When everything is right, it works. assert_ok!(PalletBalances::mint_into(&BOB, 100_000)); assert_ok!(PalletKitties::buy_kitty(RuntimeOrigin::signed(BOB), kitty_id, 1337)); // State is updated correctly. assert_eq!(KittiesOwned::<TestRuntime>::get(BOB), vec![kitty_id]); let kitty = Kitties::<TestRuntime>::get(kitty_id).unwrap(); assert_eq!(kitty.owner, BOB); // Price is reset to `None`. assert_eq!(kitty.price, None); // BOB transferred funds to ALICE. assert_eq!(PalletBalances::balance(&ALICE), 1337); assert_eq!(PalletBalances::balance(&BOB), 100_000); }) } }
diff --git a/src/impls.rs b/src/impls.rs
index 03cf1f1..c7a34b2 100644
--- a/src/impls.rs
+++ b/src/impls.rs
@@ -1,6 +1,7 @@
use super::*;
use frame::prelude::*;
use frame::primitives::BlakeTwo256;
+/* 🚧 TODO 🚧: Import `frame::traits::tokens::Preservation`. */
use frame::traits::Hash;
// Learn about internal functions.
@@ -78,6 +79,21 @@ impl<T: Config> Pallet<T> {
kitty_id: [u8; 32],
price: BalanceOf<T>,
) -> DispatchResult {
+ /* 🚧 TODO 🚧: Sanity check that the purchase is allowed:
+ - Get `kitty` from `Kitties` using `kitty_id`, `ok_or` return `Error::<T>::NoKitty`.
+ - Get the `real_price` from `kitty.price`, `ok_or` return `Error::<T>::NotForSale`.
+ - `ensure!` that `price` is greater or equal to `real_price`, else `Error::<T>::MaxPriceTooLow`.
+ */
+
+ /* 🚧 TODO 🚧: Execute the transfers:
+ - Use `T::NativeBalance` to `transfer` from the `buyer` to the `kitty.owner`.
+ - The amount transferred should be the `real_price`.
+ - Use `Preservation::Preserve` to ensure the buyer account stays alive.
+ - Use `Self::do_transfer` to transfer from the `kitty.owner` to the `buyer` with `kitty_id`.
+ - Remember to propagate up all results from these functions with `?`.
+ */
+
+ /* 🚧 TODO 🚧: Update the event to use the `real_price` in the `Event`. */
Self::deposit_event(Event::<T>::Sold { buyer, kitty_id, price });
Ok(())
}
diff --git a/src/lib.rs b/src/lib.rs
index 4a8d2b1..f465b49 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -67,6 +67,10 @@ pub mod pallet {
TransferToSelf,
NoKitty,
NotOwner,
+ /* 🚧 TODO 🚧: Add `Errors` needed for `do_buy_kitty`:
+ - `NotForSale`: for when the Kitty has a price set to `None`.
+ - `MaxPriceTooLow`: for when the price offered by the buyer is too low.
+ */
}
#[pallet::call]
diff --git a/src/impls.rs b/src/impls.rs
index c7a34b2..463894e 100644
--- a/src/impls.rs
+++ b/src/impls.rs
@@ -1,7 +1,7 @@
use super::*;
use frame::prelude::*;
use frame::primitives::BlakeTwo256;
-/* 🚧 TODO 🚧: Import `frame::traits::tokens::Preservation`. */
+use frame::traits::tokens::Preservation;
use frame::traits::Hash;
// Learn about internal functions.
@@ -79,22 +79,14 @@ impl<T: Config> Pallet<T> {
kitty_id: [u8; 32],
price: BalanceOf<T>,
) -> DispatchResult {
- /* 🚧 TODO 🚧: Sanity check that the purchase is allowed:
- - Get `kitty` from `Kitties` using `kitty_id`, `ok_or` return `Error::<T>::NoKitty`.
- - Get the `real_price` from `kitty.price`, `ok_or` return `Error::<T>::NotForSale`.
- - `ensure!` that `price` is greater or equal to `real_price`, else `Error::<T>::MaxPriceTooLow`.
- */
+ let kitty = Kitties::<T>::get(kitty_id).ok_or(Error::<T>::NoKitty)?;
+ let real_price = kitty.price.ok_or(Error::<T>::NotForSale)?;
+ ensure!(price >= real_price, Error::<T>::MaxPriceTooLow);
- /* 🚧 TODO 🚧: Execute the transfers:
- - Use `T::NativeBalance` to `transfer` from the `buyer` to the `kitty.owner`.
- - The amount transferred should be the `real_price`.
- - Use `Preservation::Preserve` to ensure the buyer account stays alive.
- - Use `Self::do_transfer` to transfer from the `kitty.owner` to the `buyer` with `kitty_id`.
- - Remember to propagate up all results from these functions with `?`.
- */
+ T::NativeBalance::transfer(&buyer, &kitty.owner, real_price, Preservation::Preserve)?;
+ Self::do_transfer(kitty.owner, buyer.clone(), kitty_id)?;
- /* 🚧 TODO 🚧: Update the event to use the `real_price` in the `Event`. */
- Self::deposit_event(Event::<T>::Sold { buyer, kitty_id, price });
+ Self::deposit_event(Event::<T>::Sold { buyer, kitty_id, price: real_price });
Ok(())
}
}
diff --git a/src/lib.rs b/src/lib.rs
index f465b49..8ca2bdd 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -67,10 +67,8 @@ pub mod pallet {
TransferToSelf,
NoKitty,
NotOwner,
- /* 🚧 TODO 🚧: Add `Errors` needed for `do_buy_kitty`:
- - `NotForSale`: for when the Kitty has a price set to `None`.
- - `MaxPriceTooLow`: for when the price offered by the buyer is too low.
- */
+ NotForSale,
+ MaxPriceTooLow,
}
#[pallet::call]
diff --git a/src/tests.rs b/src/tests.rs
index 142f9d0..d25abe0 100644
--- a/src/tests.rs
+++ b/src/tests.rs
@@ -355,3 +355,53 @@ fn do_buy_kitty_emits_event() {
);
})
}
+
+#[test]
+fn do_buy_kitty_logic_works() {
+ new_test_ext().execute_with(|| {
+ assert_ok!(PalletKitties::create_kitty(RuntimeOrigin::signed(ALICE)));
+ let kitty = &Kitties::<TestRuntime>::iter_values().collect::<Vec<_>>()[0];
+ let kitty_id = kitty.dna;
+ assert_eq!(kitty.owner, ALICE);
+ assert_eq!(KittiesOwned::<TestRuntime>::get(ALICE), vec![kitty_id]);
+ // Cannot buy kitty which does not exist.
+ assert_noop!(
+ PalletKitties::buy_kitty(RuntimeOrigin::signed(BOB), [0u8; 32], 1337),
+ Error::<TestRuntime>::NoKitty
+ );
+ // Cannot buy kitty which is not for sale.
+ assert_noop!(
+ PalletKitties::buy_kitty(RuntimeOrigin::signed(BOB), kitty_id, 1337),
+ Error::<TestRuntime>::NotForSale
+ );
+ assert_ok!(PalletKitties::set_price(RuntimeOrigin::signed(ALICE), kitty_id, Some(1337)));
+ // Cannot buy kitty for a lower price.
+ assert_noop!(
+ PalletKitties::buy_kitty(RuntimeOrigin::signed(BOB), kitty_id, 1336),
+ Error::<TestRuntime>::MaxPriceTooLow
+ );
+ // Cannot buy kitty if you don't have the funds.
+ assert_noop!(
+ PalletKitties::buy_kitty(RuntimeOrigin::signed(BOB), kitty_id, 1337),
+ frame::arithmetic::ArithmeticError::Underflow
+ );
+ // Cannot buy kitty if it would kill your account (i.e. set your balance to 0).
+ assert_ok!(PalletBalances::mint_into(&BOB, 1337));
+ assert!(
+ PalletKitties::buy_kitty(RuntimeOrigin::signed(BOB), kitty_id, 1337).is_err(),
+ // TODO: assert_noop on DispatchError::Token(TokenError::NotExpendable)
+ );
+ // When everything is right, it works.
+ assert_ok!(PalletBalances::mint_into(&BOB, 100_000));
+ assert_ok!(PalletKitties::buy_kitty(RuntimeOrigin::signed(BOB), kitty_id, 1337));
+ // State is updated correctly.
+ assert_eq!(KittiesOwned::<TestRuntime>::get(BOB), vec![kitty_id]);
+ let kitty = Kitties::<TestRuntime>::get(kitty_id).unwrap();
+ assert_eq!(kitty.owner, BOB);
+ // Price is reset to `None`.
+ assert_eq!(kitty.price, None);
+ // BOB transferred funds to ALICE.
+ assert_eq!(PalletBalances::balance(&ALICE), 1337);
+ assert_eq!(PalletBalances::balance(&BOB), 100_000);
+ })
+}