// This file is part of Substrate. // Copyright (C) 2019-2021 Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! # Nicks Module //! //! - [`nicks::Config`](./trait.Config.html) //! - [`Call`](./enum.Call.html) //! //! ## Overview //! //! Nicks is an example module for keeping track of account names on-chain. It makes no effort to //! create a name hierarchy, be a DNS replacement or provide reverse lookups. Furthermore, the //! weights attached to this module's dispatchable functions are for demonstration purposes only and //! have not been designed to be economically secure. Do not use this pallet as-is in production. //! //! ## Interface //! //! ### Dispatchable Functions //! //! * `set_name` - Set the associated name of an account; a small deposit is reserved if not already //! taken. //! * `clear_name` - Remove an account's associated name; the deposit is returned. //! * `kill_name` - Forcibly remove the associated name; the deposit is lost. //! //! [`Call`]: ./enum.Call.html //! [`Config`]: ./trait.Config.html #![cfg_attr(not(feature = "std"), no_std)] use sp_std::prelude::*; use sp_runtime::{ traits::{StaticLookup, Zero} }; use frame_support::{ decl_module, decl_event, decl_storage, ensure, decl_error, traits::{Currency, EnsureOrigin, ReservableCurrency, OnUnbalanced, Get}, }; use frame_system::ensure_signed; type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; type NegativeImbalanceOf = <::Currency as Currency<::AccountId>>::NegativeImbalance; pub trait Config: frame_system::Config { /// The overarching event type. type Event: From> + Into<::Event>; /// The currency trait. type Currency: ReservableCurrency; /// Reservation fee. type ReservationFee: Get>; /// What to do with slashed funds. type Slashed: OnUnbalanced>; /// The origin which may forcibly set or remove a name. Root can always do this. type ForceOrigin: EnsureOrigin; /// The minimum length a name may be. type MinLength: Get; /// The maximum length a name may be. type MaxLength: Get; } decl_storage! { trait Store for Module as Nicks { /// The lookup table for names. NameOf: map hasher(twox_64_concat) T::AccountId => Option<(Vec, BalanceOf)>; } } decl_event!( pub enum Event where AccountId = ::AccountId, Balance = BalanceOf { /// A name was set. \[who\] NameSet(AccountId), /// A name was forcibly set. \[target\] NameForced(AccountId), /// A name was changed. \[who\] NameChanged(AccountId), /// A name was cleared, and the given balance returned. \[who, deposit\] NameCleared(AccountId, Balance), /// A name was removed and the given balance slashed. \[target, deposit\] NameKilled(AccountId, Balance), } ); decl_error! { /// Error for the nicks module. pub enum Error for Module { /// A name is too short. TooShort, /// A name is too long. TooLong, /// An account isn't named. Unnamed, } } decl_module! { /// Nicks module declaration. pub struct Module for enum Call where origin: T::Origin { type Error = Error; fn deposit_event() = default; /// Reservation fee. const ReservationFee: BalanceOf = T::ReservationFee::get(); /// The minimum length a name may be. const MinLength: u32 = T::MinLength::get() as u32; /// The maximum length a name may be. const MaxLength: u32 = T::MaxLength::get() as u32; /// Set an account's name. The name should be a UTF-8-encoded string by convention, though /// we don't check it. /// /// The name may not be more than `T::MaxLength` bytes, nor less than `T::MinLength` bytes. /// /// If the account doesn't already have a name, then a fee of `ReservationFee` is reserved /// in the account. /// /// The dispatch origin for this call must be _Signed_. /// /// # /// - O(1). /// - At most one balance operation. /// - One storage read/write. /// - One event. /// # #[weight = 50_000_000] fn set_name(origin, name: Vec) { let sender = ensure_signed(origin)?; ensure!(name.len() >= T::MinLength::get(), Error::::TooShort); ensure!(name.len() <= T::MaxLength::get(), Error::::TooLong); let deposit = if let Some((_, deposit)) = >::get(&sender) { Self::deposit_event(RawEvent::NameChanged(sender.clone())); deposit } else { let deposit = T::ReservationFee::get(); T::Currency::reserve(&sender, deposit.clone())?; Self::deposit_event(RawEvent::NameSet(sender.clone())); deposit }; >::insert(&sender, (name, deposit)); } /// Clear an account's name and return the deposit. Fails if the account was not named. /// /// The dispatch origin for this call must be _Signed_. /// /// # /// - O(1). /// - One balance operation. /// - One storage read/write. /// - One event. /// # #[weight = 70_000_000] fn clear_name(origin) { let sender = ensure_signed(origin)?; let deposit = >::take(&sender).ok_or(Error::::Unnamed)?.1; let _ = T::Currency::unreserve(&sender, deposit.clone()); Self::deposit_event(RawEvent::NameCleared(sender, deposit)); } /// Remove an account's name and take charge of the deposit. /// /// Fails if `who` has not been named. The deposit is dealt with through `T::Slashed` /// imbalance handler. /// /// The dispatch origin for this call must match `T::ForceOrigin`. /// /// # /// - O(1). /// - One unbalanced handler (probably a balance transfer) /// - One storage read/write. /// - One event. /// # #[weight = 70_000_000] fn kill_name(origin, target: ::Source) { T::ForceOrigin::ensure_origin(origin)?; // Figure out who we're meant to be clearing. let target = T::Lookup::lookup(target)?; // Grab their deposit (and check that they have one). let deposit = >::take(&target).ok_or(Error::::Unnamed)?.1; // Slash their deposit from them. T::Slashed::on_unbalanced(T::Currency::slash_reserved(&target, deposit.clone()).0); Self::deposit_event(RawEvent::NameKilled(target, deposit)); } /// Set a third-party account's name with no deposit. /// /// No length checking is done on the name. /// /// The dispatch origin for this call must match `T::ForceOrigin`. /// /// # /// - O(1). /// - At most one balance operation. /// - One storage read/write. /// - One event. /// # #[weight = 70_000_000] fn force_name(origin, target: ::Source, name: Vec) { T::ForceOrigin::ensure_origin(origin)?; let target = T::Lookup::lookup(target)?; let deposit = >::get(&target).map(|x| x.1).unwrap_or_else(Zero::zero); >::insert(&target, (name, deposit)); Self::deposit_event(RawEvent::NameForced(target)); } } } #[cfg(test)] mod tests { use super::*; use crate as pallet_nicks; use frame_support::{assert_ok, assert_noop, parameter_types, ord_parameter_types}; use sp_core::H256; use frame_system::EnsureSignedBy; use sp_runtime::{ testing::Header, traits::{BlakeTwo256, IdentityLookup, BadOrigin}, }; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; type Block = frame_system::mocking::MockBlock; frame_support::construct_runtime!( pub enum Test where Block = Block, NodeBlock = Block, UncheckedExtrinsic = UncheckedExtrinsic, { System: frame_system::{Module, Call, Config, Storage, Event}, Balances: pallet_balances::{Module, Call, Storage, Config, Event}, Nicks: pallet_nicks::{Module, Call, Storage, Event}, } ); parameter_types! { pub const BlockHashCount: u64 = 250; pub BlockWeights: frame_system::limits::BlockWeights = frame_system::limits::BlockWeights::simple_max(1024); } impl frame_system::Config for Test { type BaseCallFilter = (); type BlockWeights = (); type BlockLength = (); type DbWeight = (); type Origin = Origin; type Index = u64; type BlockNumber = u64; type Hash = H256; type Call = Call; type Hashing = BlakeTwo256; type AccountId = u64; type Lookup = IdentityLookup; type Header = Header; type Event = Event; type BlockHashCount = BlockHashCount; type Version = (); type PalletInfo = PalletInfo; type AccountData = pallet_balances::AccountData; type OnNewAccount = (); type OnKilledAccount = (); type SystemWeightInfo = (); type SS58Prefix = (); } parameter_types! { pub const ExistentialDeposit: u64 = 1; } impl pallet_balances::Config for Test { type MaxLocks = (); type Balance = u64; type Event = Event; type DustRemoval = (); type ExistentialDeposit = ExistentialDeposit; type AccountStore = System; type WeightInfo = (); } parameter_types! { pub const ReservationFee: u64 = 2; pub const MinLength: usize = 3; pub const MaxLength: usize = 16; } ord_parameter_types! { pub const One: u64 = 1; } impl Config for Test { type Event = Event; type Currency = Balances; type ReservationFee = ReservationFee; type Slashed = (); type ForceOrigin = EnsureSignedBy; type MinLength = MinLength; type MaxLength = MaxLength; } fn new_test_ext() -> sp_io::TestExternalities { let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); pallet_balances::GenesisConfig:: { balances: vec![ (1, 10), (2, 10), ], }.assimilate_storage(&mut t).unwrap(); t.into() } #[test] fn kill_name_should_work() { new_test_ext().execute_with(|| { assert_ok!(Nicks::set_name(Origin::signed(2), b"Dave".to_vec())); assert_eq!(Balances::total_balance(&2), 10); assert_ok!(Nicks::kill_name(Origin::signed(1), 2)); assert_eq!(Balances::total_balance(&2), 8); assert_eq!(>::get(2), None); }); } #[test] fn force_name_should_work() { new_test_ext().execute_with(|| { assert_noop!( Nicks::set_name(Origin::signed(2), b"Dr. David Brubeck, III".to_vec()), Error::::TooLong, ); assert_ok!(Nicks::set_name(Origin::signed(2), b"Dave".to_vec())); assert_eq!(Balances::reserved_balance(2), 2); assert_ok!(Nicks::force_name(Origin::signed(1), 2, b"Dr. David Brubeck, III".to_vec())); assert_eq!(Balances::reserved_balance(2), 2); assert_eq!(>::get(2).unwrap(), (b"Dr. David Brubeck, III".to_vec(), 2)); }); } #[test] fn normal_operation_should_work() { new_test_ext().execute_with(|| { assert_ok!(Nicks::set_name(Origin::signed(1), b"Gav".to_vec())); assert_eq!(Balances::reserved_balance(1), 2); assert_eq!(Balances::free_balance(1), 8); assert_eq!(>::get(1).unwrap().0, b"Gav".to_vec()); assert_ok!(Nicks::set_name(Origin::signed(1), b"Gavin".to_vec())); assert_eq!(Balances::reserved_balance(1), 2); assert_eq!(Balances::free_balance(1), 8); assert_eq!(>::get(1).unwrap().0, b"Gavin".to_vec()); assert_ok!(Nicks::clear_name(Origin::signed(1))); assert_eq!(Balances::reserved_balance(1), 0); assert_eq!(Balances::free_balance(1), 10); }); } #[test] fn error_catching_should_work() { new_test_ext().execute_with(|| { assert_noop!(Nicks::clear_name(Origin::signed(1)), Error::::Unnamed); assert_noop!( Nicks::set_name(Origin::signed(3), b"Dave".to_vec()), pallet_balances::Error::::InsufficientBalance ); assert_noop!(Nicks::set_name(Origin::signed(1), b"Ga".to_vec()), Error::::TooShort); assert_noop!( Nicks::set_name(Origin::signed(1), b"Gavin James Wood, Esquire".to_vec()), Error::::TooLong ); assert_ok!(Nicks::set_name(Origin::signed(1), b"Dave".to_vec())); assert_noop!(Nicks::kill_name(Origin::signed(2), 1), BadOrigin); assert_noop!(Nicks::force_name(Origin::signed(2), 1, b"Whatever".to_vec()), BadOrigin); }); } }