#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
pub mod weights;
use tetcore_std::{fmt::Debug, prelude::*};
use tp_runtime::{RuntimeDebug, traits::{
Member, AtLeast32BitUnsigned, Zero, StaticLookup, Saturating, CheckedSub, CheckedAdd
}};
use codec::{Encode, Decode, HasCompact};
use fabric_support::{Parameter, decl_module, decl_event, decl_storage, decl_error, ensure,
traits::{Currency, ReservableCurrency, EnsureOrigin, Get, BalanceStatus::Reserved},
dispatch::{DispatchResult, DispatchError},
};
use fabric_system::ensure_signed;
pub use weights::WeightInfo;
type BalanceOf<T> = <<T as Config>::Currency as Currency<<T as fabric_system::Config>::AccountId>>::Balance;
pub trait Config: fabric_system::Config {
type Event: From<Event<Self>> + Into<<Self as fabric_system::Config>::Event>;
type Balance: Member + Parameter + AtLeast32BitUnsigned + Default + Copy;
type AssetId: Member + Parameter + Default + Copy + HasCompact;
type Currency: ReservableCurrency<Self::AccountId>;
type ForceOrigin: EnsureOrigin<Self::Origin>;
type AssetDepositBase: Get<BalanceOf<Self>>;
type AssetDepositPerZombie: Get<BalanceOf<Self>>;
type StringLimit: Get<u32>;
type MetadataDepositBase: Get<BalanceOf<Self>>;
type MetadataDepositPerByte: Get<BalanceOf<Self>>;
type WeightInfo: WeightInfo;
}
#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug)]
pub struct AssetDetails<
Balance: Encode + Decode + Clone + Debug + Eq + PartialEq,
AccountId: Encode + Decode + Clone + Debug + Eq + PartialEq,
DepositBalance: Encode + Decode + Clone + Debug + Eq + PartialEq,
> {
owner: AccountId,
issuer: AccountId,
admin: AccountId,
freezer: AccountId,
supply: Balance,
deposit: DepositBalance,
max_zombies: u32,
min_balance: Balance,
zombies: u32,
accounts: u32,
is_frozen: bool,
}
#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, Default)]
pub struct AssetBalance<
Balance: Encode + Decode + Clone + Debug + Eq + PartialEq,
> {
balance: Balance,
is_frozen: bool,
is_zombie: bool,
}
#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, Default)]
pub struct AssetMetadata<DepositBalance> {
deposit: DepositBalance,
name: Vec<u8>,
symbol: Vec<u8>,
decimals: u8,
}
decl_storage! {
trait Store for Module<T: Config> as Assets {
Asset: map hasher(blake2_128_concat) T::AssetId => Option<AssetDetails<
T::Balance,
T::AccountId,
BalanceOf<T>,
>>;
Account: double_map
hasher(blake2_128_concat) T::AssetId,
hasher(blake2_128_concat) T::AccountId
=> AssetBalance<T::Balance>;
Metadata: map hasher(blake2_128_concat) T::AssetId => AssetMetadata<BalanceOf<T>>;
}
}
decl_event! {
pub enum Event<T> where
<T as fabric_system::Config>::AccountId,
<T as Config>::Balance,
<T as Config>::AssetId,
{
Created(AssetId, AccountId, AccountId),
Issued(AssetId, AccountId, Balance),
Transferred(AssetId, AccountId, AccountId, Balance),
Burned(AssetId, AccountId, Balance),
TeamChanged(AssetId, AccountId, AccountId, AccountId),
OwnerChanged(AssetId, AccountId),
ForceTransferred(AssetId, AccountId, AccountId, Balance),
Frozen(AssetId, AccountId),
Thawed(AssetId, AccountId),
AssetFrozen(AssetId),
AssetThawed(AssetId),
Destroyed(AssetId),
ForceCreated(AssetId, AccountId),
MaxZombiesChanged(AssetId, u32),
MetadataSet(AssetId, Vec<u8>, Vec<u8>, u8),
}
}
decl_error! {
pub enum Error for Module<T: Config> {
AmountZero,
BalanceLow,
BalanceZero,
NoPermission,
Unknown,
Frozen,
InUse,
TooManyZombies,
RefsLeft,
BadWitness,
MinBalanceZero,
Overflow,
BadState,
BadMetadata,
}
}
decl_module! {
pub struct Module<T: Config> for enum Call where origin: T::Origin {
type Error = Error<T>;
fn deposit_event() = default;
#[weight = T::WeightInfo::create()]
fn create(origin,
#[compact] id: T::AssetId,
admin: <T::Lookup as StaticLookup>::Source,
max_zombies: u32,
min_balance: T::Balance,
) {
let owner = ensure_signed(origin)?;
let admin = T::Lookup::lookup(admin)?;
ensure!(!Asset::<T>::contains_key(id), Error::<T>::InUse);
ensure!(!min_balance.is_zero(), Error::<T>::MinBalanceZero);
let deposit = T::AssetDepositPerZombie::get()
.saturating_mul(max_zombies.into())
.saturating_add(T::AssetDepositBase::get());
T::Currency::reserve(&owner, deposit)?;
Asset::<T>::insert(id, AssetDetails {
owner: owner.clone(),
issuer: admin.clone(),
admin: admin.clone(),
freezer: admin.clone(),
supply: Zero::zero(),
deposit,
max_zombies,
min_balance,
zombies: Zero::zero(),
accounts: Zero::zero(),
is_frozen: false,
});
Self::deposit_event(RawEvent::Created(id, owner, admin));
}
#[weight = T::WeightInfo::force_create()]
fn force_create(origin,
#[compact] id: T::AssetId,
owner: <T::Lookup as StaticLookup>::Source,
#[compact] max_zombies: u32,
#[compact] min_balance: T::Balance,
) {
T::ForceOrigin::ensure_origin(origin)?;
let owner = T::Lookup::lookup(owner)?;
ensure!(!Asset::<T>::contains_key(id), Error::<T>::InUse);
ensure!(!min_balance.is_zero(), Error::<T>::MinBalanceZero);
Asset::<T>::insert(id, AssetDetails {
owner: owner.clone(),
issuer: owner.clone(),
admin: owner.clone(),
freezer: owner.clone(),
supply: Zero::zero(),
deposit: Zero::zero(),
max_zombies,
min_balance,
zombies: Zero::zero(),
accounts: Zero::zero(),
is_frozen: false,
});
Self::deposit_event(RawEvent::ForceCreated(id, owner));
}
#[weight = T::WeightInfo::destroy(*zombies_witness)]
fn destroy(origin,
#[compact] id: T::AssetId,
#[compact] zombies_witness: u32,
) -> DispatchResult {
let origin = ensure_signed(origin)?;
Asset::<T>::try_mutate_exists(id, |maybe_details| {
let details = maybe_details.take().ok_or(Error::<T>::Unknown)?;
ensure!(details.owner == origin, Error::<T>::NoPermission);
ensure!(details.accounts == details.zombies, Error::<T>::RefsLeft);
ensure!(details.zombies <= zombies_witness, Error::<T>::BadWitness);
let metadata = Metadata::<T>::take(&id);
T::Currency::unreserve(&details.owner, details.deposit.saturating_add(metadata.deposit));
*maybe_details = None;
Account::<T>::remove_prefix(&id);
Self::deposit_event(RawEvent::Destroyed(id));
Ok(())
})
}
#[weight = T::WeightInfo::force_destroy(*zombies_witness)]
fn force_destroy(origin,
#[compact] id: T::AssetId,
#[compact] zombies_witness: u32,
) -> DispatchResult {
T::ForceOrigin::ensure_origin(origin)?;
Asset::<T>::try_mutate_exists(id, |maybe_details| {
let details = maybe_details.take().ok_or(Error::<T>::Unknown)?;
ensure!(details.accounts == details.zombies, Error::<T>::RefsLeft);
ensure!(details.zombies <= zombies_witness, Error::<T>::BadWitness);
let metadata = Metadata::<T>::take(&id);
T::Currency::unreserve(&details.owner, details.deposit.saturating_add(metadata.deposit));
*maybe_details = None;
Account::<T>::remove_prefix(&id);
Self::deposit_event(RawEvent::Destroyed(id));
Ok(())
})
}
#[weight = T::WeightInfo::mint()]
fn mint(origin,
#[compact] id: T::AssetId,
beneficiary: <T::Lookup as StaticLookup>::Source,
#[compact] amount: T::Balance
) -> DispatchResult {
let origin = ensure_signed(origin)?;
let beneficiary = T::Lookup::lookup(beneficiary)?;
Asset::<T>::try_mutate(id, |maybe_details| {
let details = maybe_details.as_mut().ok_or(Error::<T>::Unknown)?;
ensure!(&origin == &details.issuer, Error::<T>::NoPermission);
details.supply = details.supply.checked_add(&amount).ok_or(Error::<T>::Overflow)?;
Account::<T>::try_mutate(id, &beneficiary, |t| -> DispatchResult {
let new_balance = t.balance.saturating_add(amount);
ensure!(new_balance >= details.min_balance, Error::<T>::BalanceLow);
if t.balance.is_zero() {
t.is_zombie = Self::new_account(&beneficiary, details)?;
}
t.balance = new_balance;
Ok(())
})?;
Self::deposit_event(RawEvent::Issued(id, beneficiary, amount));
Ok(())
})
}
#[weight = T::WeightInfo::burn()]
fn burn(origin,
#[compact] id: T::AssetId,
who: <T::Lookup as StaticLookup>::Source,
#[compact] amount: T::Balance
) -> DispatchResult {
let origin = ensure_signed(origin)?;
let who = T::Lookup::lookup(who)?;
Asset::<T>::try_mutate(id, |maybe_details| {
let d = maybe_details.as_mut().ok_or(Error::<T>::Unknown)?;
ensure!(&origin == &d.admin, Error::<T>::NoPermission);
let burned = Account::<T>::try_mutate_exists(
id,
&who,
|maybe_account| -> Result<T::Balance, DispatchError> {
let mut account = maybe_account.take().ok_or(Error::<T>::BalanceZero)?;
let mut burned = amount.min(account.balance);
account.balance -= burned;
*maybe_account = if account.balance < d.min_balance {
burned += account.balance;
Self::dead_account(&who, d, account.is_zombie);
None
} else {
Some(account)
};
Ok(burned)
}
)?;
d.supply = d.supply.saturating_sub(burned);
Self::deposit_event(RawEvent::Burned(id, who, burned));
Ok(())
})
}
#[weight = T::WeightInfo::transfer()]
fn transfer(origin,
#[compact] id: T::AssetId,
target: <T::Lookup as StaticLookup>::Source,
#[compact] amount: T::Balance
) -> DispatchResult {
let origin = ensure_signed(origin)?;
ensure!(!amount.is_zero(), Error::<T>::AmountZero);
let mut origin_account = Account::<T>::get(id, &origin);
ensure!(!origin_account.is_frozen, Error::<T>::Frozen);
origin_account.balance = origin_account.balance.checked_sub(&amount)
.ok_or(Error::<T>::BalanceLow)?;
let dest = T::Lookup::lookup(target)?;
Asset::<T>::try_mutate(id, |maybe_details| {
let details = maybe_details.as_mut().ok_or(Error::<T>::Unknown)?;
ensure!(!details.is_frozen, Error::<T>::Frozen);
if dest == origin {
return Ok(())
}
let mut amount = amount;
if origin_account.balance < details.min_balance {
amount += origin_account.balance;
origin_account.balance = Zero::zero();
}
Account::<T>::try_mutate(id, &dest, |a| -> DispatchResult {
let new_balance = a.balance.saturating_add(amount);
ensure!(new_balance >= details.min_balance, Error::<T>::BalanceLow);
if a.balance.is_zero() {
a.is_zombie = Self::new_account(&dest, details)?;
}
a.balance = new_balance;
Ok(())
})?;
match origin_account.balance.is_zero() {
false => {
Self::dezombify(&origin, details, &mut origin_account.is_zombie);
Account::<T>::insert(id, &origin, &origin_account)
}
true => {
Self::dead_account(&origin, details, origin_account.is_zombie);
Account::<T>::remove(id, &origin);
}
}
Self::deposit_event(RawEvent::Transferred(id, origin, dest, amount));
Ok(())
})
}
#[weight = T::WeightInfo::force_transfer()]
fn force_transfer(origin,
#[compact] id: T::AssetId,
source: <T::Lookup as StaticLookup>::Source,
dest: <T::Lookup as StaticLookup>::Source,
#[compact] amount: T::Balance,
) -> DispatchResult {
let origin = ensure_signed(origin)?;
let source = T::Lookup::lookup(source)?;
let mut source_account = Account::<T>::get(id, &source);
let mut amount = amount.min(source_account.balance);
ensure!(!amount.is_zero(), Error::<T>::AmountZero);
let dest = T::Lookup::lookup(dest)?;
if dest == source {
return Ok(())
}
Asset::<T>::try_mutate(id, |maybe_details| {
let details = maybe_details.as_mut().ok_or(Error::<T>::Unknown)?;
ensure!(&origin == &details.admin, Error::<T>::NoPermission);
source_account.balance -= amount;
if source_account.balance < details.min_balance {
amount += source_account.balance;
source_account.balance = Zero::zero();
}
Account::<T>::try_mutate(id, &dest, |a| -> DispatchResult {
let new_balance = a.balance.saturating_add(amount);
ensure!(new_balance >= details.min_balance, Error::<T>::BalanceLow);
if a.balance.is_zero() {
a.is_zombie = Self::new_account(&dest, details)?;
}
a.balance = new_balance;
Ok(())
})?;
match source_account.balance.is_zero() {
false => {
Self::dezombify(&source, details, &mut source_account.is_zombie);
Account::<T>::insert(id, &source, &source_account)
}
true => {
Self::dead_account(&source, details, source_account.is_zombie);
Account::<T>::remove(id, &source);
}
}
Self::deposit_event(RawEvent::ForceTransferred(id, source, dest, amount));
Ok(())
})
}
#[weight = T::WeightInfo::freeze()]
fn freeze(origin, #[compact] id: T::AssetId, who: <T::Lookup as StaticLookup>::Source) {
let origin = ensure_signed(origin)?;
let d = Asset::<T>::get(id).ok_or(Error::<T>::Unknown)?;
ensure!(&origin == &d.freezer, Error::<T>::NoPermission);
let who = T::Lookup::lookup(who)?;
ensure!(Account::<T>::contains_key(id, &who), Error::<T>::BalanceZero);
Account::<T>::mutate(id, &who, |a| a.is_frozen = true);
Self::deposit_event(Event::<T>::Frozen(id, who));
}
#[weight = T::WeightInfo::thaw()]
fn thaw(origin, #[compact] id: T::AssetId, who: <T::Lookup as StaticLookup>::Source) {
let origin = ensure_signed(origin)?;
let details = Asset::<T>::get(id).ok_or(Error::<T>::Unknown)?;
ensure!(&origin == &details.admin, Error::<T>::NoPermission);
let who = T::Lookup::lookup(who)?;
ensure!(Account::<T>::contains_key(id, &who), Error::<T>::BalanceZero);
Account::<T>::mutate(id, &who, |a| a.is_frozen = false);
Self::deposit_event(Event::<T>::Thawed(id, who));
}
#[weight = T::WeightInfo::freeze_asset()]
fn freeze_asset(origin, #[compact] id: T::AssetId) -> DispatchResult {
let origin = ensure_signed(origin)?;
Asset::<T>::try_mutate(id, |maybe_details| {
let d = maybe_details.as_mut().ok_or(Error::<T>::Unknown)?;
ensure!(&origin == &d.freezer, Error::<T>::NoPermission);
d.is_frozen = true;
Self::deposit_event(Event::<T>::AssetFrozen(id));
Ok(())
})
}
#[weight = T::WeightInfo::thaw_asset()]
fn thaw_asset(origin, #[compact] id: T::AssetId) -> DispatchResult {
let origin = ensure_signed(origin)?;
Asset::<T>::try_mutate(id, |maybe_details| {
let d = maybe_details.as_mut().ok_or(Error::<T>::Unknown)?;
ensure!(&origin == &d.admin, Error::<T>::NoPermission);
d.is_frozen = false;
Self::deposit_event(Event::<T>::AssetThawed(id));
Ok(())
})
}
#[weight = T::WeightInfo::transfer_ownership()]
fn transfer_ownership(origin,
#[compact] id: T::AssetId,
owner: <T::Lookup as StaticLookup>::Source,
) -> DispatchResult {
let origin = ensure_signed(origin)?;
let owner = T::Lookup::lookup(owner)?;
Asset::<T>::try_mutate(id, |maybe_details| {
let details = maybe_details.as_mut().ok_or(Error::<T>::Unknown)?;
ensure!(&origin == &details.owner, Error::<T>::NoPermission);
if details.owner == owner { return Ok(()) }
T::Currency::repatriate_reserved(&details.owner, &owner, details.deposit, Reserved)?;
details.owner = owner.clone();
Self::deposit_event(RawEvent::OwnerChanged(id, owner));
Ok(())
})
}
#[weight = T::WeightInfo::set_team()]
fn set_team(origin,
#[compact] id: T::AssetId,
issuer: <T::Lookup as StaticLookup>::Source,
admin: <T::Lookup as StaticLookup>::Source,
freezer: <T::Lookup as StaticLookup>::Source,
) -> DispatchResult {
let origin = ensure_signed(origin)?;
let issuer = T::Lookup::lookup(issuer)?;
let admin = T::Lookup::lookup(admin)?;
let freezer = T::Lookup::lookup(freezer)?;
Asset::<T>::try_mutate(id, |maybe_details| {
let details = maybe_details.as_mut().ok_or(Error::<T>::Unknown)?;
ensure!(&origin == &details.owner, Error::<T>::NoPermission);
details.issuer = issuer.clone();
details.admin = admin.clone();
details.freezer = freezer.clone();
Self::deposit_event(RawEvent::TeamChanged(id, issuer, admin, freezer));
Ok(())
})
}
#[weight = T::WeightInfo::set_max_zombies()]
fn set_max_zombies(origin,
#[compact] id: T::AssetId,
#[compact] max_zombies: u32,
) -> DispatchResult {
let origin = ensure_signed(origin)?;
Asset::<T>::try_mutate(id, |maybe_details| {
let details = maybe_details.as_mut().ok_or(Error::<T>::Unknown)?;
ensure!(&origin == &details.owner, Error::<T>::NoPermission);
ensure!(max_zombies >= details.zombies, Error::<T>::TooManyZombies);
let new_deposit = T::AssetDepositPerZombie::get()
.saturating_mul(max_zombies.into())
.saturating_add(T::AssetDepositBase::get());
if new_deposit > details.deposit {
T::Currency::reserve(&origin, new_deposit - details.deposit)?;
} else {
T::Currency::unreserve(&origin, details.deposit - new_deposit);
}
details.max_zombies = max_zombies;
Self::deposit_event(RawEvent::MaxZombiesChanged(id, max_zombies));
Ok(())
})
}
#[weight = T::WeightInfo::set_metadata(name.len() as u32, symbol.len() as u32)]
fn set_metadata(origin,
#[compact] id: T::AssetId,
name: Vec<u8>,
symbol: Vec<u8>,
decimals: u8,
) -> DispatchResult {
let origin = ensure_signed(origin)?;
ensure!(name.len() <= T::StringLimit::get() as usize, Error::<T>::BadMetadata);
ensure!(symbol.len() <= T::StringLimit::get() as usize, Error::<T>::BadMetadata);
let d = Asset::<T>::get(id).ok_or(Error::<T>::Unknown)?;
ensure!(&origin == &d.owner, Error::<T>::NoPermission);
Metadata::<T>::try_mutate_exists(id, |metadata| {
let bytes_used = name.len() + symbol.len();
let old_deposit = match metadata {
Some(m) => m.deposit,
None => Default::default()
};
if bytes_used.is_zero() && decimals.is_zero() {
T::Currency::unreserve(&origin, old_deposit);
*metadata = None;
} else {
let new_deposit = T::MetadataDepositPerByte::get()
.saturating_mul(((name.len() + symbol.len()) as u32).into())
.saturating_add(T::MetadataDepositBase::get());
if new_deposit > old_deposit {
T::Currency::reserve(&origin, new_deposit - old_deposit)?;
} else {
T::Currency::unreserve(&origin, old_deposit - new_deposit);
}
*metadata = Some(AssetMetadata {
deposit: new_deposit,
name: name.clone(),
symbol: symbol.clone(),
decimals,
})
}
Self::deposit_event(RawEvent::MetadataSet(id, name, symbol, decimals));
Ok(())
})
}
}
}
impl<T: Config> Module<T> {
pub fn balance(id: T::AssetId, who: T::AccountId) -> T::Balance {
Account::<T>::get(id, who).balance
}
pub fn total_supply(id: T::AssetId) -> T::Balance {
Asset::<T>::get(id).map(|x| x.supply).unwrap_or_else(Zero::zero)
}
pub fn zombie_allowance(id: T::AssetId) -> u32 {
Asset::<T>::get(id).map(|x| x.max_zombies - x.zombies).unwrap_or_else(Zero::zero)
}
fn new_account(
who: &T::AccountId,
d: &mut AssetDetails<T::Balance, T::AccountId, BalanceOf<T>>,
) -> Result<bool, DispatchError> {
let accounts = d.accounts.checked_add(1).ok_or(Error::<T>::Overflow)?;
let r = Ok(if fabric_system::Module::<T>::account_exists(who) {
fabric_system::Module::<T>::inc_consumers(who).map_err(|_| Error::<T>::BadState)?;
false
} else {
ensure!(d.zombies < d.max_zombies, Error::<T>::TooManyZombies);
d.zombies += 1;
true
});
d.accounts = accounts;
r
}
fn dezombify(
who: &T::AccountId,
d: &mut AssetDetails<T::Balance, T::AccountId, BalanceOf<T>>,
is_zombie: &mut bool,
) {
if *is_zombie && fabric_system::Module::<T>::account_exists(who) {
let _ = fabric_system::Module::<T>::inc_consumers(who);
*is_zombie = false;
d.zombies = d.zombies.saturating_sub(1);
}
}
fn dead_account(
who: &T::AccountId,
d: &mut AssetDetails<T::Balance, T::AccountId, BalanceOf<T>>,
is_zombie: bool,
) {
if is_zombie {
d.zombies = d.zombies.saturating_sub(1);
} else {
fabric_system::Module::<T>::dec_consumers(who);
}
d.accounts = d.accounts.saturating_sub(1);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate as noble_assets;
use fabric_support::{assert_ok, assert_noop, parameter_types};
use tet_core::H256;
use tp_runtime::{traits::{BlakeTwo256, IdentityLookup}, testing::Header};
use noble_balances::Error as BalancesError;
type UncheckedExtrinsic = fabric_system::mocking::MockUncheckedExtrinsic<Test>;
type Block = fabric_system::mocking::MockBlock<Test>;
fabric_support::construct_runtime!(
pub enum Test where
Block = Block,
NodeBlock = Block,
UncheckedExtrinsic = UncheckedExtrinsic,
{
System: fabric_system::{Module, Call, Config, Storage, Event<T>},
Balances: noble_balances::{Module, Call, Storage, Config<T>, Event<T>},
Assets: noble_assets::{Module, Call, Storage, Event<T>},
}
);
parameter_types! {
pub const BlockHashCount: u64 = 250;
}
impl fabric_system::Config for Test {
type BaseCallFilter = ();
type BlockWeights = ();
type BlockLength = ();
type DbWeight = ();
type Origin = Origin;
type Index = u64;
type Call = Call;
type BlockNumber = u64;
type Hash = H256;
type Hashing = BlakeTwo256;
type AccountId = u64;
type Lookup = IdentityLookup<Self::AccountId>;
type Header = Header;
type Event = Event;
type BlockHashCount = BlockHashCount;
type Version = ();
type NobleInfo = NobleInfo;
type AccountData = noble_balances::AccountData<u64>;
type OnNewAccount = ();
type OnKilledAccount = ();
type SystemWeightInfo = ();
type SS58Prefix = ();
}
parameter_types! {
pub const ExistentialDeposit: u64 = 1;
}
impl noble_balances::Config for Test {
type MaxLocks = ();
type Balance = u64;
type DustRemoval = ();
type Event = Event;
type ExistentialDeposit = ExistentialDeposit;
type AccountStore = System;
type WeightInfo = ();
}
parameter_types! {
pub const AssetDepositBase: u64 = 1;
pub const AssetDepositPerZombie: u64 = 1;
pub const StringLimit: u32 = 50;
pub const MetadataDepositBase: u64 = 1;
pub const MetadataDepositPerByte: u64 = 1;
}
impl Config for Test {
type Currency = Balances;
type Event = Event;
type Balance = u64;
type AssetId = u32;
type ForceOrigin = fabric_system::EnsureRoot<u64>;
type AssetDepositBase = AssetDepositBase;
type AssetDepositPerZombie = AssetDepositPerZombie;
type StringLimit = StringLimit;
type MetadataDepositBase = MetadataDepositBase;
type MetadataDepositPerByte = MetadataDepositPerByte;
type WeightInfo = ();
}
pub(crate) fn new_test_ext() -> tet_io::TestExternalities {
fabric_system::GenesisConfig::default().build_storage::<Test>().unwrap().into()
}
#[test]
fn basic_minting_should_work() {
new_test_ext().execute_with(|| {
assert_ok!(Assets::force_create(Origin::root(), 0, 1, 10, 1));
assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 100));
assert_eq!(Assets::balance(0, 1), 100);
assert_ok!(Assets::mint(Origin::signed(1), 0, 2, 100));
assert_eq!(Assets::balance(0, 2), 100);
});
}
#[test]
fn lifecycle_should_work() {
new_test_ext().execute_with(|| {
Balances::make_free_balance_be(&1, 100);
assert_ok!(Assets::create(Origin::signed(1), 0, 1, 10, 1));
assert_eq!(Balances::reserved_balance(&1), 11);
assert!(Asset::<Test>::contains_key(0));
assert_ok!(Assets::set_metadata(Origin::signed(1), 0, vec![0], vec![0], 12));
assert_eq!(Balances::reserved_balance(&1), 14);
assert!(Metadata::<Test>::contains_key(0));
assert_ok!(Assets::mint(Origin::signed(1), 0, 10, 100));
assert_ok!(Assets::mint(Origin::signed(1), 0, 20, 100));
assert_eq!(Account::<Test>::iter_prefix(0).count(), 2);
assert_ok!(Assets::destroy(Origin::signed(1), 0, 100));
assert_eq!(Balances::reserved_balance(&1), 0);
assert!(!Asset::<Test>::contains_key(0));
assert!(!Metadata::<Test>::contains_key(0));
assert_eq!(Account::<Test>::iter_prefix(0).count(), 0);
assert_ok!(Assets::create(Origin::signed(1), 0, 1, 10, 1));
assert_eq!(Balances::reserved_balance(&1), 11);
assert!(Asset::<Test>::contains_key(0));
assert_ok!(Assets::set_metadata(Origin::signed(1), 0, vec![0], vec![0], 12));
assert_eq!(Balances::reserved_balance(&1), 14);
assert!(Metadata::<Test>::contains_key(0));
assert_ok!(Assets::mint(Origin::signed(1), 0, 10, 100));
assert_ok!(Assets::mint(Origin::signed(1), 0, 20, 100));
assert_eq!(Account::<Test>::iter_prefix(0).count(), 2);
assert_ok!(Assets::force_destroy(Origin::root(), 0, 100));
assert_eq!(Balances::reserved_balance(&1), 0);
assert!(!Asset::<Test>::contains_key(0));
assert!(!Metadata::<Test>::contains_key(0));
assert_eq!(Account::<Test>::iter_prefix(0).count(), 0);
});
}
#[test]
fn destroy_with_non_zombies_should_not_work() {
new_test_ext().execute_with(|| {
Balances::make_free_balance_be(&1, 100);
assert_ok!(Assets::force_create(Origin::root(), 0, 1, 10, 1));
assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 100));
assert_noop!(Assets::destroy(Origin::signed(1), 0, 100), Error::<Test>::RefsLeft);
assert_noop!(Assets::force_destroy(Origin::root(), 0, 100), Error::<Test>::RefsLeft);
assert_ok!(Assets::burn(Origin::signed(1), 0, 1, 100));
assert_ok!(Assets::destroy(Origin::signed(1), 0, 100));
});
}
#[test]
fn destroy_with_bad_witness_should_not_work() {
new_test_ext().execute_with(|| {
Balances::make_free_balance_be(&1, 100);
assert_ok!(Assets::force_create(Origin::root(), 0, 1, 10, 1));
assert_ok!(Assets::mint(Origin::signed(1), 0, 10, 100));
assert_noop!(Assets::destroy(Origin::signed(1), 0, 0), Error::<Test>::BadWitness);
assert_noop!(Assets::force_destroy(Origin::root(), 0, 0), Error::<Test>::BadWitness);
});
}
#[test]
fn max_zombies_should_work() {
new_test_ext().execute_with(|| {
assert_ok!(Assets::force_create(Origin::root(), 0, 1, 2, 1));
assert_ok!(Assets::mint(Origin::signed(1), 0, 0, 100));
assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 100));
assert_eq!(Assets::zombie_allowance(0), 0);
assert_noop!(Assets::mint(Origin::signed(1), 0, 2, 100), Error::<Test>::TooManyZombies);
assert_noop!(Assets::transfer(Origin::signed(1), 0, 2, 50), Error::<Test>::TooManyZombies);
assert_noop!(Assets::force_transfer(Origin::signed(1), 0, 1, 2, 50), Error::<Test>::TooManyZombies);
Balances::make_free_balance_be(&3, 100);
assert_ok!(Assets::mint(Origin::signed(1), 0, 3, 100));
assert_ok!(Assets::transfer(Origin::signed(0), 0, 1, 100));
assert_eq!(Assets::zombie_allowance(0), 1);
assert_ok!(Assets::transfer(Origin::signed(1), 0, 2, 50));
});
}
#[test]
fn resetting_max_zombies_should_work() {
new_test_ext().execute_with(|| {
assert_ok!(Assets::force_create(Origin::root(), 0, 1, 2, 1));
Balances::make_free_balance_be(&1, 100);
assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 100));
assert_ok!(Assets::mint(Origin::signed(1), 0, 2, 100));
assert_ok!(Assets::mint(Origin::signed(1), 0, 3, 100));
assert_eq!(Assets::zombie_allowance(0), 0);
assert_noop!(Assets::set_max_zombies(Origin::signed(1), 0, 1), Error::<Test>::TooManyZombies);
assert_ok!(Assets::set_max_zombies(Origin::signed(1), 0, 3));
assert_eq!(Assets::zombie_allowance(0), 1);
});
}
#[test]
fn dezombifying_should_work() {
new_test_ext().execute_with(|| {
assert_ok!(Assets::force_create(Origin::root(), 0, 1, 10, 10));
assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 100));
assert_eq!(Assets::zombie_allowance(0), 9);
Balances::make_free_balance_be(&2, 100);
assert_ok!(Assets::transfer(Origin::signed(1), 0, 2, 25));
assert_eq!(Assets::zombie_allowance(0), 9);
Balances::make_free_balance_be(&1, 100);
assert_ok!(Assets::transfer(Origin::signed(1), 0, 2, 25));
assert_eq!(Assets::zombie_allowance(0), 10);
});
}
#[test]
fn min_balance_should_work() {
new_test_ext().execute_with(|| {
assert_ok!(Assets::force_create(Origin::root(), 0, 1, 10, 10));
assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 100));
assert_eq!(Asset::<Test>::get(0).unwrap().accounts, 1);
assert_noop!(Assets::mint(Origin::signed(1), 0, 2, 9), Error::<Test>::BalanceLow);
assert_noop!(Assets::transfer(Origin::signed(1), 0, 2, 9), Error::<Test>::BalanceLow);
assert_noop!(Assets::force_transfer(Origin::signed(1), 0, 1, 2, 9), Error::<Test>::BalanceLow);
assert_ok!(Assets::transfer(Origin::signed(1), 0, 2, 91));
assert!(Assets::balance(0, 1).is_zero());
assert_eq!(Assets::balance(0, 2), 100);
assert_eq!(Asset::<Test>::get(0).unwrap().accounts, 1);
assert_ok!(Assets::force_transfer(Origin::signed(1), 0, 2, 1, 91));
assert!(Assets::balance(0, 2).is_zero());
assert_eq!(Assets::balance(0, 1), 100);
assert_eq!(Asset::<Test>::get(0).unwrap().accounts, 1);
assert_ok!(Assets::burn(Origin::signed(1), 0, 1, 91));
assert!(Assets::balance(0, 1).is_zero());
assert_eq!(Asset::<Test>::get(0).unwrap().accounts, 0);
});
}
#[test]
fn querying_total_supply_should_work() {
new_test_ext().execute_with(|| {
assert_ok!(Assets::force_create(Origin::root(), 0, 1, 10, 1));
assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 100));
assert_eq!(Assets::balance(0, 1), 100);
assert_ok!(Assets::transfer(Origin::signed(1), 0, 2, 50));
assert_eq!(Assets::balance(0, 1), 50);
assert_eq!(Assets::balance(0, 2), 50);
assert_ok!(Assets::transfer(Origin::signed(2), 0, 3, 31));
assert_eq!(Assets::balance(0, 1), 50);
assert_eq!(Assets::balance(0, 2), 19);
assert_eq!(Assets::balance(0, 3), 31);
assert_ok!(Assets::burn(Origin::signed(1), 0, 3, u64::max_value()));
assert_eq!(Assets::total_supply(0), 69);
});
}
#[test]
fn transferring_amount_below_available_balance_should_work() {
new_test_ext().execute_with(|| {
assert_ok!(Assets::force_create(Origin::root(), 0, 1, 10, 1));
assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 100));
assert_eq!(Assets::balance(0, 1), 100);
assert_ok!(Assets::transfer(Origin::signed(1), 0, 2, 50));
assert_eq!(Assets::balance(0, 1), 50);
assert_eq!(Assets::balance(0, 2), 50);
});
}
#[test]
fn transferring_frozen_user_should_not_work() {
new_test_ext().execute_with(|| {
assert_ok!(Assets::force_create(Origin::root(), 0, 1, 10, 1));
assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 100));
assert_eq!(Assets::balance(0, 1), 100);
assert_ok!(Assets::freeze(Origin::signed(1), 0, 1));
assert_noop!(Assets::transfer(Origin::signed(1), 0, 2, 50), Error::<Test>::Frozen);
assert_ok!(Assets::thaw(Origin::signed(1), 0, 1));
assert_ok!(Assets::transfer(Origin::signed(1), 0, 2, 50));
});
}
#[test]
fn transferring_frozen_asset_should_not_work() {
new_test_ext().execute_with(|| {
assert_ok!(Assets::force_create(Origin::root(), 0, 1, 10, 1));
assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 100));
assert_eq!(Assets::balance(0, 1), 100);
assert_ok!(Assets::freeze_asset(Origin::signed(1), 0));
assert_noop!(Assets::transfer(Origin::signed(1), 0, 2, 50), Error::<Test>::Frozen);
assert_ok!(Assets::thaw_asset(Origin::signed(1), 0));
assert_ok!(Assets::transfer(Origin::signed(1), 0, 2, 50));
});
}
#[test]
fn origin_guards_should_work() {
new_test_ext().execute_with(|| {
assert_ok!(Assets::force_create(Origin::root(), 0, 1, 10, 1));
assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 100));
assert_noop!(Assets::transfer_ownership(Origin::signed(2), 0, 2), Error::<Test>::NoPermission);
assert_noop!(Assets::set_team(Origin::signed(2), 0, 2, 2, 2), Error::<Test>::NoPermission);
assert_noop!(Assets::freeze(Origin::signed(2), 0, 1), Error::<Test>::NoPermission);
assert_noop!(Assets::thaw(Origin::signed(2), 0, 2), Error::<Test>::NoPermission);
assert_noop!(Assets::mint(Origin::signed(2), 0, 2, 100), Error::<Test>::NoPermission);
assert_noop!(Assets::burn(Origin::signed(2), 0, 1, 100), Error::<Test>::NoPermission);
assert_noop!(Assets::force_transfer(Origin::signed(2), 0, 1, 2, 100), Error::<Test>::NoPermission);
assert_noop!(Assets::set_max_zombies(Origin::signed(2), 0, 11), Error::<Test>::NoPermission);
assert_noop!(Assets::destroy(Origin::signed(2), 0, 100), Error::<Test>::NoPermission);
});
}
#[test]
fn transfer_owner_should_work() {
new_test_ext().execute_with(|| {
Balances::make_free_balance_be(&1, 100);
Balances::make_free_balance_be(&2, 1);
assert_ok!(Assets::create(Origin::signed(1), 0, 1, 10, 1));
assert_eq!(Balances::reserved_balance(&1), 11);
assert_ok!(Assets::transfer_ownership(Origin::signed(1), 0, 2));
assert_eq!(Balances::reserved_balance(&2), 11);
assert_eq!(Balances::reserved_balance(&1), 0);
assert_noop!(Assets::transfer_ownership(Origin::signed(1), 0, 1), Error::<Test>::NoPermission);
assert_ok!(Assets::transfer_ownership(Origin::signed(2), 0, 1));
assert_eq!(Balances::reserved_balance(&1), 11);
assert_eq!(Balances::reserved_balance(&2), 0);
});
}
#[test]
fn set_team_should_work() {
new_test_ext().execute_with(|| {
assert_ok!(Assets::force_create(Origin::root(), 0, 1, 10, 1));
assert_ok!(Assets::set_team(Origin::signed(1), 0, 2, 3, 4));
assert_ok!(Assets::mint(Origin::signed(2), 0, 2, 100));
assert_ok!(Assets::freeze(Origin::signed(4), 0, 2));
assert_ok!(Assets::thaw(Origin::signed(3), 0, 2));
assert_ok!(Assets::force_transfer(Origin::signed(3), 0, 2, 3, 100));
assert_ok!(Assets::burn(Origin::signed(3), 0, 3, 100));
});
}
#[test]
fn transferring_to_frozen_account_should_work() {
new_test_ext().execute_with(|| {
assert_ok!(Assets::force_create(Origin::root(), 0, 1, 10, 1));
assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 100));
assert_ok!(Assets::mint(Origin::signed(1), 0, 2, 100));
assert_eq!(Assets::balance(0, 1), 100);
assert_eq!(Assets::balance(0, 2), 100);
assert_ok!(Assets::freeze(Origin::signed(1), 0, 2));
assert_ok!(Assets::transfer(Origin::signed(1), 0, 2, 50));
assert_eq!(Assets::balance(0, 2), 150);
});
}
#[test]
fn transferring_amount_more_than_available_balance_should_not_work() {
new_test_ext().execute_with(|| {
assert_ok!(Assets::force_create(Origin::root(), 0, 1, 10, 1));
assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 100));
assert_eq!(Assets::balance(0, 1), 100);
assert_ok!(Assets::transfer(Origin::signed(1), 0, 2, 50));
assert_eq!(Assets::balance(0, 1), 50);
assert_eq!(Assets::balance(0, 2), 50);
assert_ok!(Assets::burn(Origin::signed(1), 0, 1, u64::max_value()));
assert_eq!(Assets::balance(0, 1), 0);
assert_noop!(Assets::transfer(Origin::signed(1), 0, 1, 50), Error::<Test>::BalanceLow);
assert_noop!(Assets::transfer(Origin::signed(2), 0, 1, 51), Error::<Test>::BalanceLow);
});
}
#[test]
fn transferring_less_than_one_unit_should_not_work() {
new_test_ext().execute_with(|| {
assert_ok!(Assets::force_create(Origin::root(), 0, 1, 10, 1));
assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 100));
assert_eq!(Assets::balance(0, 1), 100);
assert_noop!(Assets::transfer(Origin::signed(1), 0, 2, 0), Error::<Test>::AmountZero);
});
}
#[test]
fn transferring_more_units_than_total_supply_should_not_work() {
new_test_ext().execute_with(|| {
assert_ok!(Assets::force_create(Origin::root(), 0, 1, 10, 1));
assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 100));
assert_eq!(Assets::balance(0, 1), 100);
assert_noop!(Assets::transfer(Origin::signed(1), 0, 2, 101), Error::<Test>::BalanceLow);
});
}
#[test]
fn burning_asset_balance_with_positive_balance_should_work() {
new_test_ext().execute_with(|| {
assert_ok!(Assets::force_create(Origin::root(), 0, 1, 10, 1));
assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 100));
assert_eq!(Assets::balance(0, 1), 100);
assert_ok!(Assets::burn(Origin::signed(1), 0, 1, u64::max_value()));
assert_eq!(Assets::balance(0, 1), 0);
});
}
#[test]
fn burning_asset_balance_with_zero_balance_should_not_work() {
new_test_ext().execute_with(|| {
assert_ok!(Assets::force_create(Origin::root(), 0, 1, 10, 1));
assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 100));
assert_eq!(Assets::balance(0, 2), 0);
assert_noop!(Assets::burn(Origin::signed(1), 0, 2, u64::max_value()), Error::<Test>::BalanceZero);
});
}
#[test]
fn set_metadata_should_work() {
new_test_ext().execute_with(|| {
assert_noop!(
Assets::set_metadata(Origin::signed(1), 0, vec![0u8; 10], vec![0u8; 10], 12),
Error::<Test>::Unknown,
);
assert_ok!(Assets::force_create(Origin::root(), 0, 1, 10, 1));
assert_noop!(
Assets::set_metadata(Origin::signed(2), 0, vec![0u8; 10], vec![0u8; 10], 12),
Error::<Test>::NoPermission,
);
assert_noop!(
Assets::set_metadata(Origin::signed(1), 0, vec![0u8; 100], vec![0u8; 10], 12),
Error::<Test>::BadMetadata,
);
assert_noop!(
Assets::set_metadata(Origin::signed(1), 0, vec![0u8; 10], vec![0u8; 100], 12),
Error::<Test>::BadMetadata,
);
Balances::make_free_balance_be(&1, 30);
assert_ok!(Assets::set_metadata(Origin::signed(1), 0, vec![0u8; 10], vec![0u8; 10], 12));
assert_eq!(Balances::free_balance(&1), 9);
assert_ok!(Assets::set_metadata(Origin::signed(1), 0, vec![0u8; 10], vec![0u8; 5], 12));
assert_eq!(Balances::free_balance(&1), 14);
assert_ok!(Assets::set_metadata(Origin::signed(1), 0, vec![0u8; 10], vec![0u8; 15], 12));
assert_eq!(Balances::free_balance(&1), 4);
assert_noop!(
Assets::set_metadata(Origin::signed(1), 0, vec![0u8; 20], vec![0u8; 20], 12),
BalancesError::<Test, _>::InsufficientBalance,
);
assert!(Metadata::<Test>::contains_key(0));
assert_ok!(Assets::set_metadata(Origin::signed(1), 0, vec![], vec![], 0));
assert!(!Metadata::<Test>::contains_key(0));
});
}
}