Transfer Logic
Now that we scaffolded the transfer
extrinsic, we can actually populate the appropriate logic to actually do a transfer.
Sanity Checks
Before we should write any logic which actually transfers a kitty, we should sanity check that all the conditions are met for us to be able to actually execute the transfer.
Don't Transfer to Yourself
The do_transfer
logic has a from
and to
account. If they are the same, well we wouldn't really be doing a transfer.
As the developer, you can treat this as a noop
, and return early, or as an error and return an error. In our pallet, we are choosing to emit an error.
This should be the first check you do because it takes no additional logic to make this check. You already have access to from
and to
, so checking equality is about the lightest amount of logic you could do.
Subsequent sanity checks will require us to read storage, and this is much more expensive to execute. If we can error early and without doing that storage read, that is way better for the blockchain.
So remember, error early, error fast.
Does the Kitty Exist?
The input to the transfer
extrinsic allows the sender to submit any [u8; 32]
identifier to transfer, but we shouldn't assume that kitty actually exists in storage.
If the sender is trying to send a kitty which doesn't exist, we should emit an error.
That should be easy to write with something like:
#![allow(unused)] fn main() { let kitty = Kitties::<T>::get(kitty_id).ok_or(Error::<T>::NoKitty)?; }
Correct Owner?
This is an important one.
It could be super simple to really mess up your blockchain if you do not check that the right owner is the one who is actually initiating the transfer.
Now that we have the kitty
object, we want to make sure the from
account matches the kitty.owner
account, else someone is trying to transfer the kitty who is not allowed to!
Updating the Owner
At this point, we have done all of the sanity checks, and we can actually update the owner of the kitty. Based on our storage, we need to do this in three places:
- Update the
Kitty
object so thatkitty.owner
is set toto
. - Add the
kitty_id
from the vector ofKittiesOwned
forto
. - Remove the
kitty_id
from the vector ofKittiesOwned
forfrom
.
This is really an exercise in writing Rust logic and using vector APIs. Unfortunately, we don't have any low level storage optimizations for such tasks, so you really just have to read both vectors and mutate them both.
There are still optimizations you can make by following the principle of fail early and fail fast. For example, it is probably better to try and add a new item to the to_owned
bounded vector first to check that their vector isn't full. If it is full, we wouldn't be able to do the transfer anyway, so we could fail early and fast.
On the other hand, removing an item from a bounded vector can never fail, and since we already checked that kitty.owner == from
, removing the kitty_id
from from_owned
should be infallible, unless there is really a big issue in the pallet logic. So the chances of an error here is much lower.
Update Storage
Now that we have mutated the appropriate storage values, all that is left is to write those updated values back into storage.
No magic tricks here, just call the insert
API.
The most important thing is to not forget to update everything!
Your Turn
Follow the TODO
s included in the template to flesh out the logic required to complete the transfer
extrinsic.
#![allow(unused)] fn main() { use super::*; use frame::prelude::*; use frame::primitives::BlakeTwo256; 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() }; // 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 { /* 🚧 TODO 🚧: Sanity check the transfer is allowed: - First `ensure!` that `from` and `to` are not equal, else return `Error::<T>::TransferToSelf`. - Get the `kitty` from `Kitties` using `kitty_id`, else return `Error::<T>::NoKitty`. - Check the `kitty.owner` is equal to `from`, else return `NotOwner`. */ /* 🚧 TODO 🚧: Update the owner of the kitty: - Update `kitty.owner` to `to`. - Update the `KittiesOwned` of `from` and `to: - Create a mutable `to_owned` by querying `KittiesOwned` for `to`. - `try_push` the `kitty_id` to the `to_owned` vector. - If the vector is full, `map_err` and return `Error::<T>::TooManyOwned`. - Create a mutable `from_owned` by querying `KittiesOwned` for `from`. - Write logic to `swap_remove` the item from the `from_owned` vector. - If you cannot find the kitty in the vector, return `Error::<T>::NoKitty`. */ /* 🚧 TODO 🚧: Update the final storage. - Insert into `Kitties` under `kitty_id` the modified `kitty` struct. - Insert into `KittiesOwned` under `to` the modified `to_owned` vector. - Insert into `KittiesOwned` under `from` the modified `from_owned` vector. */ Self::deposit_event(Event::<T>::Transferred { from, to, kitty_id }); Ok(()) } } }
#![allow(unused)] #![cfg_attr(not(feature = "std"), no_std)] fn main() { mod impls; mod tests; use frame::prelude::*; 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>; } #[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, } #[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] }, } #[pallet::error] pub enum Error<T> { TooManyKitties, DuplicateKitty, TooManyOwned, /* 🚧 TODO 🚧: Add new `Error` variants needed for `do_transfer`: - `TransferToSelf`: for when the `from` and `to` of the transfer is the same. - `NoKitty`: for when a transfer involves a kitty that does not exist. - `NotOwner`: for when a transfer is initiated by someone who is not the current owner. */ } #[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(()) } } } }
#![allow(unused)] fn main() { use super::*; use frame::prelude::*; use frame::primitives::BlakeTwo256; 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() }; // 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(); 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(()) } } }
#![allow(unused)] #![cfg_attr(not(feature = "std"), no_std)] fn main() { mod impls; mod tests; use frame::prelude::*; 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>; } #[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, } #[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] }, } #[pallet::error] pub enum Error<T> { TooManyKitties, DuplicateKitty, TooManyOwned, TransferToSelf, NoKitty, NotOwner, } #[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(()) } } } }
#![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 }; // 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; } // 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); }); } }
diff --git a/src/impls.rs b/src/impls.rs
index 9d435f5..d44508b 100644
--- a/src/impls.rs
+++ b/src/impls.rs
@@ -36,6 +36,29 @@ impl<T: Config> Pallet<T> {
}
pub fn do_transfer(from: T::AccountId, to: T::AccountId, kitty_id: [u8; 32]) -> DispatchResult {
+ /* 🚧 TODO 🚧: Sanity check the transfer is allowed:
+ - First `ensure!` that `from` and `to` are not equal, else return `Error::<T>::TransferToSelf`.
+ - Get the `kitty` from `Kitties` using `kitty_id`, else return `Error::<T>::NoKitty`.
+ - Check the `kitty.owner` is equal to `from`, else return `NotOwner`.
+ */
+
+ /* 🚧 TODO 🚧: Update the owner of the kitty:
+ - Update `kitty.owner` to `to`.
+ - Update the `KittiesOwned` of `from` and `to:
+ - Create a mutable `to_owned` by querying `KittiesOwned` for `to`.
+ - `try_push` the `kitty_id` to the `to_owned` vector.
+ - If the vector is full, `map_err` and return `Error::<T>::TooManyOwned`.
+ - Create a mutable `from_owned` by querying `KittiesOwned` for `from`.
+ - Write logic to `swap_remove` the item from the `from_owned` vector.
+ - If you cannot find the kitty in the vector, return `Error::<T>::NoKitty`.
+ */
+
+ /* 🚧 TODO 🚧: Update the final storage.
+ - Insert into `Kitties` under `kitty_id` the modified `kitty` struct.
+ - Insert into `KittiesOwned` under `to` the modified `to_owned` vector.
+ - Insert into `KittiesOwned` under `from` the modified `from_owned` vector.
+ */
+
Self::deposit_event(Event::<T>::Transferred { from, to, kitty_id });
Ok(())
}
diff --git a/src/lib.rs b/src/lib.rs
index 971af78..7256cb5 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -52,6 +52,11 @@ pub mod pallet {
TooManyKitties,
DuplicateKitty,
TooManyOwned,
+ /* 🚧 TODO 🚧: Add new `Error` variants needed for `do_transfer`:
+ - `TransferToSelf`: for when the `from` and `to` of the transfer is the same.
+ - `NoKitty`: for when a transfer involves a kitty that does not exist.
+ - `NotOwner`: for when a transfer is initiated by someone who is not the current owner.
+ */
}
#[pallet::call]
diff --git a/src/impls.rs b/src/impls.rs
index d44508b..5574211 100644
--- a/src/impls.rs
+++ b/src/impls.rs
@@ -36,28 +36,23 @@ impl<T: Config> Pallet<T> {
}
pub fn do_transfer(from: T::AccountId, to: T::AccountId, kitty_id: [u8; 32]) -> DispatchResult {
- /* 🚧 TODO 🚧: Sanity check the transfer is allowed:
- - First `ensure!` that `from` and `to` are not equal, else return `Error::<T>::TransferToSelf`.
- - Get the `kitty` from `Kitties` using `kitty_id`, else return `Error::<T>::NoKitty`.
- - Check the `kitty.owner` is equal to `from`, else return `NotOwner`.
- */
-
- /* 🚧 TODO 🚧: Update the owner of the kitty:
- - Update `kitty.owner` to `to`.
- - Update the `KittiesOwned` of `from` and `to:
- - Create a mutable `to_owned` by querying `KittiesOwned` for `to`.
- - `try_push` the `kitty_id` to the `to_owned` vector.
- - If the vector is full, `map_err` and return `Error::<T>::TooManyOwned`.
- - Create a mutable `from_owned` by querying `KittiesOwned` for `from`.
- - Write logic to `swap_remove` the item from the `from_owned` vector.
- - If you cannot find the kitty in the vector, return `Error::<T>::NoKitty`.
- */
-
- /* 🚧 TODO 🚧: Update the final storage.
- - Insert into `Kitties` under `kitty_id` the modified `kitty` struct.
- - Insert into `KittiesOwned` under `to` the modified `to_owned` vector.
- - Insert into `KittiesOwned` under `from` the modified `from_owned` vector.
- */
+ 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();
+
+ 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(())
diff --git a/src/lib.rs b/src/lib.rs
index 7256cb5..d44d77d 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -52,11 +52,9 @@ pub mod pallet {
TooManyKitties,
DuplicateKitty,
TooManyOwned,
- /* 🚧 TODO 🚧: Add new `Error` variants needed for `do_transfer`:
- - `TransferToSelf`: for when the `from` and `to` of the transfer is the same.
- - `NoKitty`: for when a transfer involves a kitty that does not exist.
- - `NotOwner`: for when a transfer is initiated by someone who is not the current owner.
- */
+ TransferToSelf,
+ NoKitty,
+ NotOwner,
}
#[pallet::call]
diff --git a/src/tests.rs b/src/tests.rs
index 9a1878d..bc81a1d 100644
--- a/src/tests.rs
+++ b/src/tests.rs
@@ -257,3 +257,38 @@ fn transfer_emits_event() {
);
});
}
+
+#[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);
+ });
+}