// Copyright 2019-2020 Parity Technologies (UK) Ltd. // This file is part of Substrate. // Substrate is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // Substrate is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with Substrate. If not, see . //! # Nicks Module //! //! - [`nicks::Trait`](./trait.Trait.html) //! - [`Call`](./enum.Call.html) //! //! ## Overview //! //! Nicks is a trivial 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. //! //! ## 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 //! [`Trait`]: ./trait.Trait.html #![cfg_attr(not(feature = "std"), no_std)] use sp_std::prelude::*; use sp_runtime::{ traits::{StaticLookup, EnsureOrigin, Zero} }; use frame_support::{ decl_module, decl_event, decl_storage, ensure, decl_error, traits::{Currency, ReservableCurrency, OnUnbalanced, Get}, weights::SimpleDispatchInfo, }; use frame_system::{self as system, ensure_signed, ensure_root}; type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; type NegativeImbalanceOf = <::Currency as Currency<::AccountId>>::NegativeImbalance; pub trait Trait: frame_system::Trait { /// 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 Sudo { /// The lookup table for names. NameOf: map hasher(blake2_256) T::AccountId => Option<(Vec, BalanceOf)>; } } decl_event!( pub enum Event where AccountId = ::AccountId, Balance = BalanceOf { /// A name was set. NameSet(AccountId), /// A name was forcibly set. NameForced(AccountId), /// A name was changed. NameChanged(AccountId), /// A name was cleared, and the given balance returned. NameCleared(AccountId, Balance), /// A name was removed and the given balance slashed. 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 in't named. Unnamed, } } decl_module! { // Simple declaration of the `Module` type. Lets the macro know what it's working on. 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 = SimpleDispatchInfo::FixedNormal(50_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::NameSet(sender.clone())); deposit } else { let deposit = T::ReservationFee::get(); T::Currency::reserve(&sender, deposit.clone())?; Self::deposit_event(RawEvent::NameChanged(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. /// # 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 be _Root_ or match `T::ForceOrigin`. /// /// # /// - O(1). /// - One unbalanced handler (probably a balance transfer) /// - One storage read/write. /// - One event. /// # #[weight = SimpleDispatchInfo::FixedNormal(70_000)] fn kill_name(origin, target: ::Source) { T::ForceOrigin::try_origin(origin) .map(|_| ()) .or_else(ensure_root)?; // 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 be _Root_ or match `T::ForceOrigin`. /// /// # /// - O(1). /// - At most one balance operation. /// - One storage read/write. /// - One event. /// # #[weight = SimpleDispatchInfo::FixedNormal(70_000)] fn force_name(origin, target: ::Source, name: Vec) { T::ForceOrigin::try_origin(origin) .map(|_| ()) .or_else(ensure_root)?; 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 frame_support::{ assert_ok, assert_noop, impl_outer_origin, parameter_types, weights::Weight, ord_parameter_types }; use sp_core::H256; use frame_system::EnsureSignedBy; // The testing primitives are very useful for avoiding having to work with signatures // or public keys. `u64` is used as the `AccountId` and no `Signature`s are required. use sp_runtime::{ Perbill, testing::Header, traits::{BlakeTwo256, IdentityLookup, BadOrigin}, }; impl_outer_origin! { pub enum Origin for Test where system = frame_system {} } // For testing the module, we construct most of a mock runtime. This means // first constructing a configuration type (`Test`) which `impl`s each of the // configuration traits of modules we want to use. #[derive(Clone, Eq, PartialEq)] pub struct Test; parameter_types! { pub const BlockHashCount: u64 = 250; pub const MaximumBlockWeight: Weight = 1024; pub const MaximumBlockLength: u32 = 2 * 1024; pub const AvailableBlockRatio: Perbill = Perbill::one(); } impl frame_system::Trait for Test { type Origin = Origin; type Index = u64; type BlockNumber = u64; type Hash = H256; type Call = (); type Hashing = BlakeTwo256; type AccountId = u64; type Lookup = IdentityLookup; type Header = Header; type Event = (); type BlockHashCount = BlockHashCount; type MaximumBlockWeight = MaximumBlockWeight; type MaximumBlockLength = MaximumBlockLength; type AvailableBlockRatio = AvailableBlockRatio; type Version = (); type ModuleToIndex = (); type AccountData = pallet_balances::AccountData; type OnNewAccount = (); type OnReapAccount = Balances; } parameter_types! { pub const ExistentialDeposit: u64 = 1; } impl pallet_balances::Trait for Test { type Balance = u64; type Event = (); type DustRemoval = (); type ExistentialDeposit = ExistentialDeposit; type AccountStore = System; } 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 Trait for Test { type Event = (); type Currency = Balances; type ReservationFee = ReservationFee; type Slashed = (); type ForceOrigin = EnsureSignedBy; type MinLength = MinLength; type MaxLength = MaxLength; } type System = frame_system::Module; type Balances = pallet_balances::Module; type Nicks = Module; // This function basically just builds a genesis storage key/value store according to // our desired mockup. fn new_test_ext() -> sp_io::TestExternalities { let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); // We use default for brevity, but you can configure as desired if needed. 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); }); } }