From f02b147e2cdace0fec478882dcde9a669b60b834 Mon Sep 17 00:00:00 2001 From: Dan Forbes Date: Sun, 2 Aug 2020 10:51:52 -0700 Subject: [PATCH] Initial commit --- .gitignore | 11 ++ Cargo.toml | 63 +++++++++ LICENSE | 24 ++++ README.md | 31 +++++ src/lib.rs | 352 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/mock.rs | 77 +++++++++++ src/nft.rs | 67 ++++++++++ src/tests.rs | 215 +++++++++++++++++++++++++++++++ 8 files changed, 840 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 src/lib.rs create mode 100644 src/mock.rs create mode 100644 src/nft.rs create mode 100644 src/tests.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ff47c2d --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d548195 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,63 @@ +[package] +authors = ['Dan Forbes '] +description = 'An update to https://github.com/shawntabrizi/substratekitties.' +edition = '2018' +homepage = 'https://github.com/danforbes/substrate-node-template/tree/nft' +license = 'Unlicense' +name = 'pallet-nft' +repository = 'https://github.com/danforbes/substrate-node-template/tree/nft' +version = '2.0.0-rc5' + +[dependencies.codec] +default-features = false +features = ['derive'] +package = 'parity-scale-codec' +version = '1.3.0' + +[dependencies.frame-support] +default-features = false +git = 'https://github.com/paritytech/substrate.git' +tag = 'v2.0.0-rc5' +version = '2.0.0-rc5' + +[dependencies.frame-system] +default-features = false +git = 'https://github.com/paritytech/substrate.git' +tag = 'v2.0.0-rc5' +version = '2.0.0-rc5' +[dev-dependencies.sp-core] +default-features = false +git = 'https://github.com/paritytech/substrate.git' +tag = 'v2.0.0-rc5' +version = '2.0.0-rc5' + +[dev-dependencies.sp-io] +default-features = false +git = 'https://github.com/paritytech/substrate.git' +tag = 'v2.0.0-rc5' +version = '2.0.0-rc5' + +[dependencies.sp-runtime] +default-features = false +git = 'https://github.com/paritytech/substrate.git' +tag = 'v2.0.0-rc5' +version = '2.0.0-rc5' + +[dependencies.sp-std] +git = 'https://github.com/paritytech/substrate.git' +default-features = false +tag = 'v2.0.0-rc5' +version = '2.0.0-rc5' + +[package.metadata.docs.rs] +targets = ['x86_64-unknown-linux-gnu'] + +[features] +default = ['std'] +std = [ + 'codec/std', + 'frame-support/std', + 'frame-system/std', + 'sp-runtime/std', + 'sp-std/std', +] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cf1ab25 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/README.md b/README.md new file mode 100644 index 0000000..1926b2f --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# Non-Fungible Token FRAME Pallet + +This is a [FRAME](https://substrate.dev/docs/en/knowledgebase/runtime/frame) pallet that defines and implements a +[non-fungible token (NFT)](https://en.wikipedia.org/wiki/Non-fungible_token) interface. + +## Tests + +Refer to the [mock runtime](src/mock.rs) and [provided tests](src/tests.rs) to see the NFT implementation in action. + +## Acknowledgements + +This project was inspired by works such as the following: + +- [The ERC-721 specification](https://eips.ethereum.org/EIPS/eip-721) +- [OpenZeppelin's ERC-721 implementation](https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/token/ERC721) +- [the original Substratekitties project](https://www.shawntabrizi.com/substrate-collectables-workshop/#/), by + [@shawntabrizi](https://github.com/shawntabrizi/) +- [Substratekitties from SubstrateCourse](https://github.com/SubstrateCourse/substrate-kitties), by [@xlc](https://github.com/xlc/) + +Thanks to the following people who helped me overcome my relatively limited understanding of Rust. + +- [@JoshOrndoff](https://github.com/JoshOrndorff/) +- [@riusricardo](https://github.com/riusricardo/) +- [@rphmeier](https://github.com/rphmeier/) +- [@thiolliere](https://github.com/thiolliere/) +- [@gnunicorn](https://github.com/gnunicorn/) + +## Upstream + +This project was forked from +[the Substrate DevHub Pallet Template](https://github.com/substrate-developer-hub/substrate-pallet-template). diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..6c7e81d --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,352 @@ +//! # Unique Assets Implementation +//! +//! This pallet exposes capabilities for managing unique assets, also known as +//! non-fungible tokens (NFTs). +//! +//! - [`nft::Trait`](./trait.Trait.html) +//! - [`Calls`](./enum.Call.html) +//! - [`Errors`](./enum.Error.html) +//! - [`Events`](./enum.RawEvent.html) +//! +//! ## Overview +//! +//! Assets that share a common metadata structure may be created and distributed +//! by an asset admin. Asset owners may burn assets or transfer their +//! ownership. Configuration parameters are used to limit the total number of a +//! type of asset that may exist as well as the number that any one account may +//! own. Assets are uniquely identified by the hash of the info that defines +//! them, as calculated by the runtime system's hashing algorithm. +//! +//! This pallet implements the [`UniqueAssets`](./nft/trait.UniqueAssets.html) +//! trait. +//! +//! ### Dispatchable Functions +//! +//! * [`mint`](./enum.Call.html#variant.mint) - Use the provided asset info to +//! create a new unique asset for the specified user. May only be called by +//! the asset admin. +//! +//! * [`burn`](./enum.Call.html#variant.burn) - Destroy an asset. May only be +//! called by asset owner. +//! +//! * [`transfer`](./enum.Call.html#variant.transfer) - Transfer ownership of +//! an asset to another account. May only be called by current asset owner. + +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::{Decode, Encode, FullCodec}; +use frame_support::{ + decl_error, decl_event, decl_module, decl_storage, dispatch, ensure, + traits::{EnsureOrigin, Get}, + Hashable, +}; +use frame_system::{self as system, ensure_signed}; +use sp_runtime::{ + traits::{Hash, Member}, + RuntimeDebug, +}; +use sp_std::{ + cmp::{Eq, Ordering}, + fmt::Debug, + vec::Vec, +}; + +pub mod nft; +pub use crate::nft::{UniqueAssets, NFT}; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + +pub trait Trait: system::Trait { + /// The dispatch origin that is able to mint new instances of this type of asset. + type AssetAdmin: EnsureOrigin; + /// The data type that is used to describe this type of asset. + type AssetInfo: Hashable + Member + Debug + Default + FullCodec; + /// The maximum number of this type of asset that may exist (minted - burned). + type AssetLimit: Get; + /// The maximum number of this type of asset that any single account may own. + type UserAssetLimit: Get; + type Event: From> + Into<::Event>; +} + +/// The runtime system's hashing algorithm is used to uniquely identify assets. +pub type AssetId = ::Hash; + +/// An alias for this pallet's NFT implementation. +pub type IdentifiedAssetFor = IdentifiedAsset, >::AssetInfo>; + +/// A generic definition of an NFT that will be used by this pallet. +#[derive(Encode, Decode, Clone, Eq, RuntimeDebug)] +pub struct IdentifiedAsset { + pub id: Hash, + pub asset: AssetInfo, +} + +impl Ord for IdentifiedAsset { + fn cmp(&self, other: &Self) -> Ordering { + self.id.cmp(&other.id) + } +} + +impl PartialOrd for IdentifiedAsset { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.id.cmp(&other.id)) + } +} + +impl PartialEq for IdentifiedAsset { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl NFT for IdentifiedAsset { + type Id = AssetId; + type Info = AssetInfo; +} + +decl_storage! { + trait Store for Module, I: Instance = DefaultInstance> as NFT { + /// The total number of this type of asset that exists (minted - burned). + Total get(fn total): u128 = 0; + /// The total number of this type of asset that has been burned (may overflow). + Burned get(fn burned): u128 = 0; + /// The total number of this type of asset owned by an account. + TotalForAccount get(fn total_for_account): map hasher(blake2_128_concat) T::AccountId => u64 = 0; + /// A mapping from an account to a list of all of the assets of this type that are owned by it. + AssetsForAccount get(fn assets_for_account): map hasher(blake2_128_concat) T::AccountId => Vec>; + /// A mapping from an asset ID to the account that owns it. + AccountForAsset get(fn account_for_asset): map hasher(identity) AssetId => T::AccountId; + } +} + +decl_event!( + pub enum Event + where + AssetId = ::Hash, + AccountId = ::AccountId, + { + /// The asset has been burned. + Burned(AssetId), + /// The asset has been minted and distributed to the account. + Minted(AssetId, AccountId), + /// Ownership of the asset has been transferred to the account. + Transferred(AssetId, AccountId), + } +); + +decl_error! { + pub enum Error for Module, I: Instance> { + // Thrown when there is an attempt to mint a duplicate asset. + AssetExists, + // Thrown when there is an attempt to burn or transfer a nonexistent asset. + NonexistentAsset, + // Thrown when someone who is not the owner of an asset attempts to transfer or burn it. + NotAssetOwner, + // Thrown when the asset admin attempts to mint an asset and the maximum number of this + // type of asset already exists. + TooManyAssets, + // Thrown when an attempt is made to mint or transfer an asset to an account that already + // owns the maximum number of this type of asset. + TooManyAssetsForAccount, + } +} + +decl_module! { + pub struct Module, I: Instance = DefaultInstance> for enum Call where origin: T::Origin { + type Error = Error; + fn deposit_event() = default; + + /// Create a new unique asset from the provided asset info and identify the specified + /// account as its owner. The ID of the new asset will be equal to the hash of the info + /// that defines it, as calculated by the runtime system's hashing algorithm. + /// + /// The dispatch origin for this call must be the asset admin. + /// + /// This function will throw an error if it is called with asset info that describes + /// an existing (duplicate) asset, if the maximum number of this type of asset already + /// exists or if the specified owner already owns the maximum number of this type of + /// asset. + /// + /// - `owner_account`: Receiver of the asset. + /// - `asset_info`: The information that defines the asset. + #[weight = 10_000] + pub fn mint(origin, owner_account: T::AccountId, asset_info: T::AssetInfo) -> dispatch::DispatchResult { + T::AssetAdmin::ensure_origin(origin)?; + + let asset_id = >::mint(&owner_account, asset_info)?; + Self::deposit_event(RawEvent::Minted(asset_id, owner_account.clone())); + Ok(()) + } + + /// Destroy the specified asset. + /// + /// The dispatch origin for this call must be the asset owner. + /// + /// - `asset_id`: The hash (calculated by the runtime system's hashing algorithm) + /// of the info that defines the asset to destroy. + #[weight = 10_000] + pub fn burn(origin, asset_id: AssetId) -> dispatch::DispatchResult { + let who = ensure_signed(origin)?; + ensure!(who == Self::account_for_asset(&asset_id), Error::::NotAssetOwner); + + >::burn(&asset_id)?; + Self::deposit_event(RawEvent::Burned(asset_id.clone())); + Ok(()) + } + + /// Transfer an asset to a new owner. + /// + /// The dispatch origin for this call must be the asset owner. + /// + /// This function will throw an error if the new owner already owns the maximum + /// number of this type of asset. + /// + /// - `dest_account`: Receiver of the asset. + /// - `asset_id`: The hash (calculated by the runtime system's hashing algorithm) + /// of the info that defines the asset to destroy. + #[weight = 10_000] + pub fn transfer(origin, dest_account: T::AccountId, asset_id: AssetId) -> dispatch::DispatchResult { + let who = ensure_signed(origin)?; + ensure!(who == Self::account_for_asset(&asset_id), Error::::NotAssetOwner); + + >::transfer(&dest_account, &asset_id)?; + Self::deposit_event(RawEvent::Transferred(asset_id.clone(), dest_account.clone())); + Ok(()) + } + } +} + +impl, I: Instance> UniqueAssets, >::AssetInfo>> + for Module +{ + type AccountId = ::AccountId; + type AssetLimit = T::AssetLimit; + type UserAssetLimit = T::UserAssetLimit; + + fn total() -> u128 { + Self::total() + } + + fn burned() -> u128 { + Self::burned() + } + + fn total_for_account(account: &T::AccountId) -> u64 { + Self::total_for_account(account) + } + + fn assets_for_account( + account: &T::AccountId, + ) -> Vec, >::AssetInfo>> { + Self::assets_for_account(account) + } + + fn owner_of(asset_id: &AssetId) -> T::AccountId { + Self::account_for_asset(asset_id) + } + + fn mint( + owner_account: &T::AccountId, + asset_info: >::AssetInfo, + ) -> dispatch::result::Result, dispatch::DispatchError> { + let asset_id = T::Hashing::hash_of(&asset_info); + + ensure!( + !AccountForAsset::::contains_key(&asset_id), + Error::::AssetExists + ); + + ensure!( + Self::total_for_account(owner_account) < T::UserAssetLimit::get(), + Error::::TooManyAssetsForAccount + ); + + ensure!( + Self::total() < T::AssetLimit::get(), + Error::::TooManyAssets + ); + + let new_asset = IdentifiedAsset { + id: asset_id, + asset: asset_info, + }; + + Total::::mutate(|total| *total += 1); + TotalForAccount::::mutate(owner_account, |total| *total += 1); + AssetsForAccount::::mutate(owner_account, |assets| { + match assets.binary_search(&new_asset) { + Ok(_pos) => {} // should never happen + Err(pos) => assets.insert(pos, new_asset), + } + }); + AccountForAsset::::insert(asset_id, &owner_account); + + Ok(asset_id) + } + + fn burn(asset_id: &AssetId) -> dispatch::DispatchResult { + let owner = Self::owner_of(asset_id); + ensure!( + owner != T::AccountId::default(), + Error::::NonexistentAsset + ); + + let burn_asset = IdentifiedAsset::, >::AssetInfo> { + id: *asset_id, + asset: >::AssetInfo::default(), + }; + + Total::::mutate(|total| *total -= 1); + Burned::::mutate(|total| *total += 1); + TotalForAccount::::mutate(&owner, |total| *total -= 1); + AssetsForAccount::::mutate(owner, |assets| { + let pos = assets + .binary_search(&burn_asset) + .expect("We already checked that we have the correct owner; qed"); + assets.remove(pos); + }); + AccountForAsset::::remove(&asset_id); + + Ok(()) + } + + fn transfer(dest_account: &T::AccountId, asset_id: &AssetId) -> dispatch::DispatchResult { + let owner = Self::owner_of(&asset_id); + ensure!( + owner != T::AccountId::default(), + Error::::NonexistentAsset + ); + + ensure!( + Self::total_for_account(dest_account) < T::UserAssetLimit::get(), + Error::::TooManyAssetsForAccount + ); + + let xfer_asset = IdentifiedAsset::, >::AssetInfo> { + id: *asset_id, + asset: >::AssetInfo::default(), + }; + + TotalForAccount::::mutate(&owner, |total| *total -= 1); + TotalForAccount::::mutate(dest_account, |total| *total += 1); + let asset = AssetsForAccount::::mutate(owner, |assets| { + let pos = assets + .binary_search(&xfer_asset) + .expect("We already checked that we have the correct owner; qed"); + assets.remove(pos) + }); + AssetsForAccount::::mutate(dest_account, |assets| { + match assets.binary_search(&asset) { + Ok(_pos) => {} // should never happen + Err(pos) => assets.insert(pos, asset), + } + }); + AccountForAsset::::insert(&asset_id, &dest_account); + + Ok(()) + } +} diff --git a/src/mock.rs b/src/mock.rs new file mode 100644 index 0000000..0548b06 --- /dev/null +++ b/src/mock.rs @@ -0,0 +1,77 @@ +// Creating mock runtime here + +use crate::{Module, Trait}; +use frame_support::{impl_outer_origin, parameter_types, weights::Weight}; +use frame_system as system; +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, + Perbill, +}; + +impl_outer_origin! { + pub enum Origin for Test where system = frame_system {} +} + +// For testing the pallet, we construct most of a mock runtime. This means +// first constructing a configuration type (`Test`) which `impl`s each of the +// configuration traits of pallets 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::from_percent(75); +} +impl system::Trait for Test { + type BaseCallFilter = (); + type Origin = Origin; + type Call = (); + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type Event = (); + type BlockHashCount = BlockHashCount; + type MaximumBlockWeight = MaximumBlockWeight; + type DbWeight = (); + type BlockExecutionWeight = (); + type ExtrinsicBaseWeight = (); + type MaximumExtrinsicWeight = MaximumBlockWeight; + type MaximumBlockLength = MaximumBlockLength; + type AvailableBlockRatio = AvailableBlockRatio; + type Version = (); + type ModuleToIndex = (); + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); +} +parameter_types! { + pub const MaxAssets: u128 = 5; + pub const MaxAssetsPerUser: u64 = 2; +} +impl Trait for Test { + type Event = (); + type AssetAdmin = frame_system::EnsureRoot; + type AssetInfo = Vec; + type AssetLimit = MaxAssets; + type UserAssetLimit = MaxAssetsPerUser; +} + +// system under test +pub type SUT = Module; + +// This function basically just builds a genesis storage key/value store according to +// our desired mockup. +pub fn new_test_ext() -> sp_io::TestExternalities { + system::GenesisConfig::default() + .build_storage::() + .unwrap() + .into() +} diff --git a/src/nft.rs b/src/nft.rs new file mode 100644 index 0000000..93db259 --- /dev/null +++ b/src/nft.rs @@ -0,0 +1,67 @@ +//! # Unique Assets Interface +//! +//! This trait describes an abstraction over a set of unique assets, also known as non-fungible +//! tokens (NFTs). +//! +//! ## Overview +//! +//! Unique assets have an owner, identified by an account ID, and are defined by a common set of +//! attributes (the asset info type). An asset ID type distinguishes unique assets from one another. +//! Assets may be created (minted), destroyed (burned) or transferred. +//! +//! This abstraction is implemented by [nft::Module](../struct.Module.html). + +use frame_support::{ + dispatch::{result::Result, DispatchError, DispatchResult}, + traits::Get, +}; +use sp_std::vec::Vec; + +/// A unique asset; assets with equivalent attributes (as defined by the Info type) **must** have an +/// equal ID and assets with different IDs **must not** have equivalent attributes. +pub trait NFT { + /// The type used to identify unique assets. + type Id; + /// The attributes that distinguish unique assets. + type Info; +} + +/// An interface over a set of unique assets. +pub trait UniqueAssets { + /// The type used to identify asset owners. + type AccountId; + /// The maximum number of this type of asset that may exist (minted - burned). + type AssetLimit: Get; + /// The maximum number of this type of asset that any single account may own. + type UserAssetLimit: Get; + + /// The total number of this type of asset that exists (minted - burned). + fn total() -> u128; + /// The total number of this type of asset that has been burned (may overflow). + fn burned() -> u128; + /// The total number of this type of asset owned by an account. + fn total_for_account(account: &Self::AccountId) -> u64; + /// The set of unique assets owned by an account. + fn assets_for_account(account: &Self::AccountId) -> Vec; + /// The ID of the account that owns an asset. + fn owner_of(asset_id: &Asset::Id) -> Self::AccountId; + + /// Use the provided asset info to create a new unique asset for the specified user. + /// This method **must** return an error in the following cases: + /// - The asset, as identified by the asset info, already exists. + /// - The specified owner account has already reached the user asset limit. + /// - The total asset limit has already been reached. + fn mint( + owner_account: &Self::AccountId, + asset_info: Asset::Info, + ) -> Result; + /// Destroy an asset. + /// This method **must** return an error in the following case: + /// - The asset with the specified ID does not exist. + fn burn(asset_id: &Asset::Id) -> DispatchResult; + /// Transfer ownership of an asset to another account. + /// This method **must** return an error in the following cases: + /// - The asset with the specified ID does not exist. + /// - The destination account has already reached the user asset limit. + fn transfer(dest_account: &Self::AccountId, asset_id: &Asset::Id) -> DispatchResult; +} diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..095635d --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,215 @@ +// Tests to be written here + +use crate::mock::*; +use crate::nft::UniqueAssets; +use crate::*; +use frame_support::{assert_err, assert_ok, Hashable}; +use sp_core::H256; + +#[test] +fn mint() { + new_test_ext().execute_with(|| { + assert_eq!(SUT::total(), 0); + assert_eq!(SUT::total_for_account(1), 0); + assert_eq!(>::total(), 0); + assert_eq!(>::total_for_account(&1), 0); + assert_eq!( + SUT::account_for_asset::(Vec::::default().blake2_256().into()), + 0 + ); + + assert_ok!(SUT::mint(Origin::root(), 1, Vec::::default())); + + assert_eq!(SUT::total(), 1); + assert_eq!(>::total(), 1); + assert_eq!(SUT::burned(), 0); + assert_eq!(>::burned(), 0); + assert_eq!(SUT::total_for_account(1), 1); + assert_eq!(>::total_for_account(&1), 1); + let assets_for_account = SUT::assets_for_account::(1); + assert_eq!(assets_for_account.len(), 1); + assert_eq!( + assets_for_account[0].id, + Vec::::default().blake2_256().into() + ); + assert_eq!(assets_for_account[0].asset, Vec::::default()); + assert_eq!( + SUT::account_for_asset::(Vec::::default().blake2_256().into()), + 1 + ); + }); +} + +#[test] +fn mint_err_non_admin() { + new_test_ext().execute_with(|| { + assert_err!( + SUT::mint(Origin::signed(1), 1, Vec::::default()), + sp_runtime::DispatchError::BadOrigin + ); + }); +} + +#[test] +fn mint_err_dupe() { + new_test_ext().execute_with(|| { + assert_ok!(SUT::mint(Origin::root(), 1, Vec::::default())); + + assert_err!( + SUT::mint(Origin::root(), 2, Vec::::default()), + Error::::AssetExists + ); + }); +} + +#[test] +fn mint_err_max_user() { + new_test_ext().execute_with(|| { + assert_ok!(SUT::mint(Origin::root(), 1, vec![])); + assert_ok!(SUT::mint(Origin::root(), 1, vec![0])); + + assert_err!( + SUT::mint(Origin::root(), 1, vec![1]), + Error::::TooManyAssetsForAccount + ); + }); +} + +#[test] +fn mint_err_max() { + new_test_ext().execute_with(|| { + assert_ok!(SUT::mint(Origin::root(), 1, vec![])); + assert_ok!(SUT::mint(Origin::root(), 2, vec![0])); + assert_ok!(SUT::mint(Origin::root(), 3, vec![1])); + assert_ok!(SUT::mint(Origin::root(), 4, vec![2])); + assert_ok!(SUT::mint(Origin::root(), 5, vec![3])); + + assert_err!( + SUT::mint(Origin::root(), 6, vec![4]), + Error::::TooManyAssets + ); + }); +} + +#[test] +fn burn() { + new_test_ext().execute_with(|| { + assert_ok!(SUT::mint(Origin::root(), 1, Vec::::default())); + assert_ok!(SUT::burn( + Origin::signed(1), + Vec::::default().blake2_256().into() + )); + + assert_eq!(SUT::total(), 0); + assert_eq!(SUT::burned(), 1); + assert_eq!(SUT::total_for_account(1), 0); + assert_eq!(SUT::assets_for_account::(1), vec![]); + assert_eq!( + SUT::account_for_asset::(Vec::::default().blake2_256().into()), + 0 + ); + }); +} + +#[test] +fn burn_err_not_owner() { + new_test_ext().execute_with(|| { + assert_ok!(SUT::mint(Origin::root(), 1, Vec::::default())); + + assert_err!( + SUT::burn(Origin::signed(2), Vec::::default().blake2_256().into()), + Error::::NotAssetOwner + ); + }); +} + +#[test] +fn burn_err_not_exist() { + new_test_ext().execute_with(|| { + assert_err!( + SUT::burn(Origin::signed(1), Vec::::default().blake2_256().into()), + Error::::NotAssetOwner + ); + }); +} + +#[test] +fn transfer() { + new_test_ext().execute_with(|| { + assert_ok!(SUT::mint(Origin::root(), 1, Vec::::default())); + assert_ok!(SUT::transfer( + Origin::signed(1), + 2, + Vec::::default().blake2_256().into() + )); + + assert_eq!(SUT::total(), 1); + assert_eq!(SUT::burned(), 0); + assert_eq!(SUT::total_for_account(1), 0); + assert_eq!(SUT::total_for_account(2), 1); + assert_eq!(SUT::assets_for_account::(1), vec![]); + let assets_for_account = SUT::assets_for_account::(2); + assert_eq!(assets_for_account.len(), 1); + assert_eq!( + assets_for_account[0].id, + Vec::::default().blake2_256().into() + ); + assert_eq!(assets_for_account[0].asset, Vec::::default()); + assert_eq!( + SUT::account_for_asset::(Vec::::default().blake2_256().into()), + 2 + ); + }); +} + +#[test] +fn transfer_err_not_owner() { + new_test_ext().execute_with(|| { + assert_ok!(SUT::mint(Origin::root(), 1, Vec::::default())); + + assert_err!( + SUT::transfer( + Origin::signed(0), + 2, + Vec::::default().blake2_256().into() + ), + Error::::NotAssetOwner + ); + }); +} + +#[test] +fn transfer_err_not_exist() { + new_test_ext().execute_with(|| { + assert_err!( + SUT::transfer( + Origin::signed(1), + 2, + Vec::::default().blake2_256().into() + ), + Error::::NotAssetOwner + ); + }); +} + +#[test] +fn transfer_err_max_user() { + new_test_ext().execute_with(|| { + assert_ok!(SUT::mint(Origin::root(), 1, vec![0])); + assert_ok!(SUT::mint(Origin::root(), 1, vec![1])); + assert_ok!(SUT::mint(Origin::root(), 2, Vec::::default())); + assert_eq!( + SUT::account_for_asset::(Vec::::default().blake2_256().into()), + 2 + ); + + assert_err!( + SUT::transfer( + Origin::signed(2), + 1, + Vec::::default().blake2_256().into() + ), + Error::::TooManyAssetsForAccount + ); + }); +}