├── .gitignore ├── LICENSE ├── Cargo.toml ├── src ├── mock.rs ├── nft.rs ├── tests.rs └── lib.rs └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = '2018' 3 | name = 'pallet-commodities' 4 | version = '1.0.0' 5 | authors = ['Dan Forbes '] 6 | license = 'Unlicense' 7 | description = 'A unique asset (NFT) interface and a Substrate FRAME implementation optimized for commodity assets.' 8 | homepage = 'https://github.com/danforbes/pallet-nft' 9 | repository = 'https://github.com/danforbes/pallet-nft' 10 | readme = 'README.md' 11 | keywords = ['substrate', 'frame', 'nft', 'blockchain', 'asset'] 12 | categories = ['cryptography::cryptocurrencies', 'data-structures', 'no-std'] 13 | 14 | [package.metadata.docs.rs] 15 | targets = ['x86_64-unknown-linux-gnu'] 16 | 17 | [dependencies] 18 | serde = { version = "1.0.116", optional = true } 19 | 20 | # Substrate dependencies 21 | codec = { default-features = false, features = ['derive'], package = 'parity-scale-codec', version = '1.3.5' } 22 | frame-support = { default-features = false, version = '2.0.0' } 23 | frame-system = { default-features = false, version = '2.0.0' } 24 | sp-runtime = { default-features = false, version = '2.0.0' } 25 | sp-std = { default-features = false, version = '2.0.0' } 26 | 27 | [dev-dependencies] 28 | sp-core = { default-features = false, version = '2.0.0' } 29 | sp-io = { default-features = false, version = '2.0.0' } 30 | 31 | [features] 32 | default = ['std'] 33 | std = [ 34 | 'serde', 35 | 'codec/std', 36 | 'frame-support/std', 37 | 'frame-system/std', 38 | 'sp-runtime/std', 39 | 'sp-std/std', 40 | ] 41 | -------------------------------------------------------------------------------- /src/mock.rs: -------------------------------------------------------------------------------- 1 | // Creating mock runtime here 2 | 3 | use crate::{Module, Trait}; 4 | use frame_support::{impl_outer_origin, parameter_types, weights::Weight}; 5 | use frame_system as system; 6 | use sp_core::H256; 7 | use sp_runtime::{ 8 | testing::Header, 9 | traits::{BlakeTwo256, IdentityLookup}, 10 | Perbill, 11 | }; 12 | 13 | impl_outer_origin! { 14 | pub enum Origin for Test where system = frame_system {} 15 | } 16 | 17 | parameter_types! { 18 | pub const BlockHashCount: u64 = 250; 19 | pub const MaximumBlockWeight: Weight = 1024; 20 | pub const MaximumBlockLength: u32 = 2 * 1024; 21 | pub const AvailableBlockRatio: Perbill = Perbill::from_percent(75); 22 | } 23 | 24 | impl system::Trait for Test { 25 | type BaseCallFilter = (); 26 | type Origin = Origin; 27 | type Call = (); 28 | type Index = u64; 29 | type BlockNumber = u64; 30 | type Hash = H256; 31 | type Hashing = BlakeTwo256; 32 | type AccountId = u64; 33 | type Lookup = IdentityLookup; 34 | type Header = Header; 35 | type Event = (); 36 | type BlockHashCount = BlockHashCount; 37 | type MaximumBlockWeight = MaximumBlockWeight; 38 | type DbWeight = (); 39 | type BlockExecutionWeight = (); 40 | type ExtrinsicBaseWeight = (); 41 | type MaximumExtrinsicWeight = MaximumBlockWeight; 42 | type MaximumBlockLength = MaximumBlockLength; 43 | type AvailableBlockRatio = AvailableBlockRatio; 44 | type Version = (); 45 | type PalletInfo = (); 46 | type AccountData = (); 47 | type OnNewAccount = (); 48 | type OnKilledAccount = (); 49 | type SystemWeightInfo = (); 50 | } 51 | 52 | parameter_types! { 53 | pub const MaxCommodities: u128 = 5; 54 | pub const MaxCommoditiesPerUser: u64 = 2; 55 | } 56 | 57 | // For testing the pallet, we construct most of a mock runtime. This means 58 | // first constructing a configuration type (`Test`) which `impl`s each of the 59 | // configuration traits of pallets we want to use. 60 | #[derive(Clone, Eq, PartialEq)] 61 | pub struct Test; 62 | 63 | impl Trait for Test { 64 | type Event = (); 65 | type CommodityAdmin = frame_system::EnsureRoot; 66 | type CommodityInfo = Vec; 67 | type CommodityLimit = MaxCommodities; 68 | type UserCommodityLimit = MaxCommoditiesPerUser; 69 | } 70 | 71 | // system under test 72 | pub type SUT = Module; 73 | 74 | // This function basically just builds a genesis storage key/value store according to 75 | // our desired mockup. 76 | pub fn new_test_ext() -> sp_io::TestExternalities { 77 | system::GenesisConfig::default() 78 | .build_storage::() 79 | .unwrap() 80 | .into() 81 | } 82 | -------------------------------------------------------------------------------- /src/nft.rs: -------------------------------------------------------------------------------- 1 | //! # Unique Assets Interface 2 | //! 3 | //! This trait describes an abstraction over a set of unique assets, also known as non-fungible 4 | //! tokens (NFTs). 5 | //! 6 | //! ## Overview 7 | //! 8 | //! Unique assets have an owner, identified by an account ID, and are defined by a common set of 9 | //! attributes (the asset info type). An asset ID type distinguishes unique assets from one another. 10 | //! Assets may be created (minted), destroyed (burned) or transferred. 11 | //! 12 | //! This abstraction is implemented by [pallet_commodities::Module](../struct.Module.html). 13 | 14 | use frame_support::{ 15 | dispatch::{result::Result, DispatchError, DispatchResult}, 16 | traits::Get, 17 | }; 18 | use sp_std::vec::Vec; 19 | 20 | /// An interface over a set of unique assets. 21 | /// Assets with equivalent attributes (as defined by the AssetInfo type) **must** have an equal ID 22 | /// and assets with different IDs **must not** have equivalent attributes. 23 | pub trait UniqueAssets { 24 | /// The type used to identify unique assets. 25 | type AssetId; 26 | /// The attributes that distinguish unique assets. 27 | type AssetInfo; 28 | /// The maximum number of this type of asset that may exist (minted - burned). 29 | type AssetLimit: Get; 30 | /// The maximum number of this type of asset that any single account may own. 31 | type UserAssetLimit: Get; 32 | 33 | /// The total number of this type of asset that exists (minted - burned). 34 | fn total() -> u128; 35 | /// The total number of this type of asset that has been burned (may overflow). 36 | fn burned() -> u128; 37 | /// The total number of this type of asset owned by an account. 38 | fn total_for_account(account: &AccountId) -> u64; 39 | /// The set of unique assets owned by an account. 40 | fn assets_for_account(account: &AccountId) -> Vec<(Self::AssetId, Self::AssetInfo)>; 41 | /// The ID of the account that owns an asset. 42 | fn owner_of(asset_id: &Self::AssetId) -> AccountId; 43 | 44 | /// Use the provided asset info to create a new unique asset for the specified user. 45 | /// This method **must** return an error in the following cases: 46 | /// - The asset, as identified by the asset info, already exists. 47 | /// - The specified owner account has already reached the user asset limit. 48 | /// - The total asset limit has already been reached. 49 | fn mint( 50 | owner_account: &AccountId, 51 | asset_info: Self::AssetInfo, 52 | ) -> Result; 53 | /// Destroy an asset. 54 | /// This method **must** return an error in the following case: 55 | /// - The asset with the specified ID does not exist. 56 | fn burn(asset_id: &Self::AssetId) -> DispatchResult; 57 | /// Transfer ownership of an asset to another account. 58 | /// This method **must** return an error in the following cases: 59 | /// - The asset with the specified ID does not exist. 60 | /// - The destination account has already reached the user asset limit. 61 | fn transfer(dest_account: &AccountId, asset_id: &Self::AssetId) -> DispatchResult; 62 | } 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Compatible with Substrate v2.0.0](https://img.shields.io/badge/Substrate-v2.0.0-E6007A)](https://github.com/paritytech/substrate/releases/tag/v2.0.0) 2 | 3 | # Commodities FRAME Pallet: NFTs for Substrate 4 | 5 | This is a [FRAME](https://substrate.dev/docs/en/knowledgebase/runtime/frame) pallet that defines and implements an 6 | interface for managing a set of [non-fungible tokens (NFTs)](https://en.wikipedia.org/wiki/Non-fungible_token). Assets 7 | have an owner and can be created, destroyed and transferred. 8 | 9 | ## Interface 10 | 11 | This package defines [a public trait](src/nft.rs) (Rust interface) for working with NFTs: the `UniqueAssets` trait. 12 | 13 | ## `UniqueAssets` Trait 14 | 15 | This trait is generic with respect to a type that is used to identify asset owners - the `AccountId` type. Assets with 16 | equivalent attributes (as defined by the `AssetInfo` type) **must** have equal `AssetId`s and assets with different 17 | `AssetId`s **must not** have equivalent attributes. 18 | 19 | ### Types 20 | 21 | - `AssetId`: a URI for an asset 22 | - `AssetInfo`: a set of attributes that uniquely describes an asset 23 | - `AssetLimit`: the maximum number of assets, expressed as an unsigned 128-bit integer, that may exist in this set at 24 | once 25 | - `UserAssetLimit`: the maximum number of assets, expressed as an unsigned 64-bit integer, that any single account may 26 | own from this set at once 27 | 28 | ### Functions 29 | 30 | - `total() -> u128`: returns the total number of assets in this set of assets 31 | - `burned() -> u128`: returns the total number of assets from this set that have been burned 32 | - `total_for_account(AccountId) -> u64`: returns the total number of asset from this set that are owned by a given 33 | account 34 | - `assets_for_account(AccountId) -> Vec<(AssetId, AssetInfo)>`: returns the list of assets from this set that are owned 35 | by a given account 36 | - `owner_of(AssetId) -> AccountId`: returns the ID of the account that owns the given asset from this set 37 | - `mint(AccountId, AssetInfo) -> Result`: use the given attributes to create a new unique asset 38 | that belongs to this set and assign ownership of it to the given account 39 | - Failure cases: asset duplication, asset limit reached for set, asset limit for this set reached for account 40 | - `burn(AssetId) -> DispatchResult`: destroy the given asset 41 | - Failure cases: asset doesn't exist 42 | - `transfer(AccountId, AssetId) -> DispatchResult`: transfer ownership of the given asset from this set from its current 43 | owner to a given target account 44 | - Failure cases: asset doesn't exist, asset limit for this set reached for target account 45 | 46 | ## Reference Implementation 47 | 48 | The [reference implementation](src/lib.rs) defined in this project is referred to as a "commodity" - a unique asset that 49 | is designed for frequent trading. In order to optimize for this use case, _sorted_ lists of assets are stored per owner. 50 | Although maintaining a sorted list is trivial with Rust vectors, which implement a binary search API that can be used 51 | for sorted insertion, it introduces significant overhead when an asset is _created_ because the entire list must be 52 | decoded from the backing trie in order to insert the new asset in the correct spot. Maintaining a sorted asset list is 53 | desireable for the commodity use case, however, because it allows assets to be efficiently located when destroying or 54 | transferring them. An alternative implementation, the Keepsake pallet, is in the works :rocket: 55 | 56 | ## Tests 57 | 58 | Refer to the [mock runtime](src/mock.rs) and [provided tests](src/tests.rs) to see the NFT implementation in action. 59 | 60 | ## Test Project 61 | 62 | In order to help develop this pallet, it is being consumed by 63 | [a test project](https://github.com/danforbes/substratekitties) - a work-in-progress update to 64 | [the original Substratekitties tutorial](https://github.com/shawntabrizi/substratekitties). 65 | 66 | ## Acknowledgements 67 | 68 | This project was inspired by works such as the following: 69 | 70 | - [The ERC-721 specification](https://eips.ethereum.org/EIPS/eip-721) 71 | - [OpenZeppelin's ERC-721 implementation](https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/token/ERC721) 72 | - [the original Substratekitties project](https://www.shawntabrizi.com/substrate-collectables-workshop/#/), by 73 | [@shawntabrizi](https://github.com/shawntabrizi/) 74 | - [Substratekitties from SubstrateCourse](https://github.com/SubstrateCourse/substrate-kitties), by 75 | [@xlc](https://github.com/xlc/) 76 | 77 | Thanks to the following people who helped me overcome my relatively limited understanding of Rust. 78 | 79 | - [@JoshOrndoff](https://github.com/JoshOrndorff/) 80 | - [@riusricardo](https://github.com/riusricardo/) 81 | - [@rphmeier](https://github.com/rphmeier/) 82 | - [@thiolliere](https://github.com/thiolliere/) 83 | - [@gnunicorn](https://github.com/gnunicorn/) 84 | 85 | ## Upstream 86 | 87 | This project was forked from 88 | [the Substrate DevHub Pallet Template](https://github.com/substrate-developer-hub/substrate-pallet-template). 89 | -------------------------------------------------------------------------------- /src/tests.rs: -------------------------------------------------------------------------------- 1 | // Tests to be written here 2 | 3 | use crate::mock::*; 4 | use crate::nft::UniqueAssets; 5 | use crate::*; 6 | use frame_support::{assert_err, assert_ok, Hashable}; 7 | use sp_core::H256; 8 | 9 | #[test] 10 | fn mint() { 11 | new_test_ext().execute_with(|| { 12 | assert_eq!(SUT::total(), 0); 13 | assert_eq!(SUT::total_for_account(1), 0); 14 | assert_eq!(>::total(), 0); 15 | assert_eq!(>::total_for_account(&1), 0); 16 | assert_eq!( 17 | SUT::account_for_commodity::(Vec::::default().blake2_256().into()), 18 | 0 19 | ); 20 | 21 | assert_ok!(SUT::mint(Origin::root(), 1, Vec::::default())); 22 | 23 | assert_eq!(SUT::total(), 1); 24 | assert_eq!(>::total(), 1); 25 | assert_eq!(SUT::burned(), 0); 26 | assert_eq!(>::burned(), 0); 27 | assert_eq!(SUT::total_for_account(1), 1); 28 | assert_eq!(>::total_for_account(&1), 1); 29 | let commodities_for_account = SUT::commodities_for_account::(1); 30 | assert_eq!(commodities_for_account.len(), 1); 31 | assert_eq!( 32 | commodities_for_account[0].0, 33 | Vec::::default().blake2_256().into() 34 | ); 35 | assert_eq!(commodities_for_account[0].1, Vec::::default()); 36 | assert_eq!( 37 | SUT::account_for_commodity::(Vec::::default().blake2_256().into()), 38 | 1 39 | ); 40 | }); 41 | } 42 | 43 | #[test] 44 | fn mint_err_non_admin() { 45 | new_test_ext().execute_with(|| { 46 | assert_err!( 47 | SUT::mint(Origin::signed(1), 1, Vec::::default()), 48 | sp_runtime::DispatchError::BadOrigin 49 | ); 50 | }); 51 | } 52 | 53 | #[test] 54 | fn mint_err_dupe() { 55 | new_test_ext().execute_with(|| { 56 | assert_ok!(SUT::mint(Origin::root(), 1, Vec::::default())); 57 | 58 | assert_err!( 59 | SUT::mint(Origin::root(), 2, Vec::::default()), 60 | Error::::CommodityExists 61 | ); 62 | }); 63 | } 64 | 65 | #[test] 66 | fn mint_err_max_user() { 67 | new_test_ext().execute_with(|| { 68 | assert_ok!(SUT::mint(Origin::root(), 1, vec![])); 69 | assert_ok!(SUT::mint(Origin::root(), 1, vec![0])); 70 | 71 | assert_err!( 72 | SUT::mint(Origin::root(), 1, vec![1]), 73 | Error::::TooManyCommoditiesForAccount 74 | ); 75 | }); 76 | } 77 | 78 | #[test] 79 | fn mint_err_max() { 80 | new_test_ext().execute_with(|| { 81 | assert_ok!(SUT::mint(Origin::root(), 1, vec![])); 82 | assert_ok!(SUT::mint(Origin::root(), 2, vec![0])); 83 | assert_ok!(SUT::mint(Origin::root(), 3, vec![1])); 84 | assert_ok!(SUT::mint(Origin::root(), 4, vec![2])); 85 | assert_ok!(SUT::mint(Origin::root(), 5, vec![3])); 86 | 87 | assert_err!( 88 | SUT::mint(Origin::root(), 6, vec![4]), 89 | Error::::TooManyCommodities 90 | ); 91 | }); 92 | } 93 | 94 | #[test] 95 | fn burn() { 96 | new_test_ext().execute_with(|| { 97 | assert_ok!(SUT::mint(Origin::root(), 1, Vec::::default())); 98 | assert_ok!(SUT::burn( 99 | Origin::signed(1), 100 | Vec::::default().blake2_256().into() 101 | )); 102 | 103 | assert_eq!(SUT::total(), 0); 104 | assert_eq!(SUT::burned(), 1); 105 | assert_eq!(SUT::total_for_account(1), 0); 106 | assert_eq!(SUT::commodities_for_account::(1), vec![]); 107 | assert_eq!( 108 | SUT::account_for_commodity::(Vec::::default().blake2_256().into()), 109 | 0 110 | ); 111 | }); 112 | } 113 | 114 | #[test] 115 | fn burn_err_not_owner() { 116 | new_test_ext().execute_with(|| { 117 | assert_ok!(SUT::mint(Origin::root(), 1, Vec::::default())); 118 | 119 | assert_err!( 120 | SUT::burn(Origin::signed(2), Vec::::default().blake2_256().into()), 121 | Error::::NotCommodityOwner 122 | ); 123 | }); 124 | } 125 | 126 | #[test] 127 | fn burn_err_not_exist() { 128 | new_test_ext().execute_with(|| { 129 | assert_err!( 130 | SUT::burn(Origin::signed(1), Vec::::default().blake2_256().into()), 131 | Error::::NotCommodityOwner 132 | ); 133 | }); 134 | } 135 | 136 | #[test] 137 | fn transfer() { 138 | new_test_ext().execute_with(|| { 139 | assert_ok!(SUT::mint(Origin::root(), 1, Vec::::default())); 140 | assert_ok!(SUT::transfer( 141 | Origin::signed(1), 142 | 2, 143 | Vec::::default().blake2_256().into() 144 | )); 145 | 146 | assert_eq!(SUT::total(), 1); 147 | assert_eq!(SUT::burned(), 0); 148 | assert_eq!(SUT::total_for_account(1), 0); 149 | assert_eq!(SUT::total_for_account(2), 1); 150 | assert_eq!(SUT::commodities_for_account::(1), vec![]); 151 | let commodities_for_account = SUT::commodities_for_account::(2); 152 | assert_eq!(commodities_for_account.len(), 1); 153 | assert_eq!( 154 | commodities_for_account[0].0, 155 | Vec::::default().blake2_256().into() 156 | ); 157 | assert_eq!(commodities_for_account[0].1, Vec::::default()); 158 | assert_eq!( 159 | SUT::account_for_commodity::(Vec::::default().blake2_256().into()), 160 | 2 161 | ); 162 | }); 163 | } 164 | 165 | #[test] 166 | fn transfer_err_not_owner() { 167 | new_test_ext().execute_with(|| { 168 | assert_ok!(SUT::mint(Origin::root(), 1, Vec::::default())); 169 | 170 | assert_err!( 171 | SUT::transfer( 172 | Origin::signed(0), 173 | 2, 174 | Vec::::default().blake2_256().into() 175 | ), 176 | Error::::NotCommodityOwner 177 | ); 178 | }); 179 | } 180 | 181 | #[test] 182 | fn transfer_err_not_exist() { 183 | new_test_ext().execute_with(|| { 184 | assert_err!( 185 | SUT::transfer( 186 | Origin::signed(1), 187 | 2, 188 | Vec::::default().blake2_256().into() 189 | ), 190 | Error::::NotCommodityOwner 191 | ); 192 | }); 193 | } 194 | 195 | #[test] 196 | fn transfer_err_max_user() { 197 | new_test_ext().execute_with(|| { 198 | assert_ok!(SUT::mint(Origin::root(), 1, vec![0])); 199 | assert_ok!(SUT::mint(Origin::root(), 1, vec![1])); 200 | assert_ok!(SUT::mint(Origin::root(), 2, Vec::::default())); 201 | assert_eq!( 202 | SUT::account_for_commodity::(Vec::::default().blake2_256().into()), 203 | 2 204 | ); 205 | 206 | assert_err!( 207 | SUT::transfer( 208 | Origin::signed(2), 209 | 1, 210 | Vec::::default().blake2_256().into() 211 | ), 212 | Error::::TooManyCommoditiesForAccount 213 | ); 214 | }); 215 | } 216 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Unique Assets Implementation: Commodities 2 | //! 3 | //! This pallet exposes capabilities for managing unique assets, also known as 4 | //! non-fungible tokens (NFTs). 5 | //! 6 | //! - [`pallet_commodities::Trait`](./trait.Trait.html) 7 | //! - [`Calls`](./enum.Call.html) 8 | //! - [`Errors`](./enum.Error.html) 9 | //! - [`Events`](./enum.RawEvent.html) 10 | //! 11 | //! ## Overview 12 | //! 13 | //! Assets that share a common metadata structure may be created and distributed 14 | //! by an asset admin. Asset owners may burn assets or transfer their 15 | //! ownership. Configuration parameters are used to limit the total number of a 16 | //! type of asset that may exist as well as the number that any one account may 17 | //! own. Assets are uniquely identified by the hash of the info that defines 18 | //! them, as calculated by the runtime system's hashing algorithm. 19 | //! 20 | //! This pallet implements the [`UniqueAssets`](./nft/trait.UniqueAssets.html) 21 | //! trait in a way that is optimized for assets that are expected to be traded 22 | //! frequently. 23 | //! 24 | //! ### Dispatchable Functions 25 | //! 26 | //! * [`mint`](./enum.Call.html#variant.mint) - Use the provided commodity info 27 | //! to create a new commodity for the specified user. May only be called by 28 | //! the commodity admin. 29 | //! 30 | //! * [`burn`](./enum.Call.html#variant.burn) - Destroy a commodity. May only be 31 | //! called by commodity owner. 32 | //! 33 | //! * [`transfer`](./enum.Call.html#variant.transfer) - Transfer ownership of 34 | //! a commodity to another account. May only be called by current commodity 35 | //! owner. 36 | 37 | #![cfg_attr(not(feature = "std"), no_std)] 38 | 39 | use codec::FullCodec; 40 | use frame_support::{ 41 | decl_error, decl_event, decl_module, decl_storage, dispatch, ensure, 42 | traits::{EnsureOrigin, Get}, 43 | Hashable, 44 | }; 45 | use frame_system::ensure_signed; 46 | use sp_runtime::traits::{Hash, Member}; 47 | use sp_std::{cmp::Eq, fmt::Debug, vec::Vec}; 48 | 49 | pub mod nft; 50 | pub use crate::nft::UniqueAssets; 51 | 52 | #[cfg(test)] 53 | mod mock; 54 | 55 | #[cfg(test)] 56 | mod tests; 57 | 58 | pub trait Trait: frame_system::Trait { 59 | /// The dispatch origin that is able to mint new instances of this type of commodity. 60 | type CommodityAdmin: EnsureOrigin; 61 | /// The data type that is used to describe this type of commodity. 62 | type CommodityInfo: Hashable + Member + Debug + Default + FullCodec + Ord; 63 | /// The maximum number of this type of commodity that may exist (minted - burned). 64 | type CommodityLimit: Get; 65 | /// The maximum number of this type of commodity that any single account may own. 66 | type UserCommodityLimit: Get; 67 | type Event: From> + Into<::Event>; 68 | } 69 | 70 | /// The runtime system's hashing algorithm is used to uniquely identify commodities. 71 | pub type CommodityId = ::Hash; 72 | 73 | /// Associates a commodity with its ID. 74 | pub type Commodity = (CommodityId, >::CommodityInfo); 75 | 76 | decl_storage! { 77 | trait Store for Module, I: Instance = DefaultInstance> as Commodity { 78 | /// The total number of this type of commodity that exists (minted - burned). 79 | Total get(fn total): u128 = 0; 80 | /// The total number of this type of commodity that has been burned (may overflow). 81 | Burned get(fn burned): u128 = 0; 82 | /// The total number of this type of commodity owned by an account. 83 | TotalForAccount get(fn total_for_account): map hasher(blake2_128_concat) T::AccountId => u64 = 0; 84 | /// A mapping from an account to a list of all of the commodities of this type that are owned by it. 85 | CommoditiesForAccount get(fn commodities_for_account): map hasher(blake2_128_concat) T::AccountId => Vec>; 86 | /// A mapping from a commodity ID to the account that owns it. 87 | AccountForCommodity get(fn account_for_commodity): map hasher(identity) CommodityId => T::AccountId; 88 | } 89 | 90 | add_extra_genesis { 91 | config(balances): Vec<(T::AccountId, Vec)>; 92 | build(|config: &GenesisConfig| { 93 | for (who, assets) in config.balances.iter() { 94 | for asset in assets { 95 | match as UniqueAssets::>::mint(who, asset.clone()) { 96 | Ok(_) => {} 97 | Err(err) => { panic!(err) }, 98 | } 99 | } 100 | } 101 | }); 102 | } 103 | } 104 | 105 | decl_event!( 106 | pub enum Event 107 | where 108 | CommodityId = ::Hash, 109 | AccountId = ::AccountId, 110 | { 111 | /// The commodity has been burned. 112 | Burned(CommodityId), 113 | /// The commodity has been minted and distributed to the account. 114 | Minted(CommodityId, AccountId), 115 | /// Ownership of the commodity has been transferred to the account. 116 | Transferred(CommodityId, AccountId), 117 | } 118 | ); 119 | 120 | decl_error! { 121 | pub enum Error for Module, I: Instance> { 122 | // Thrown when there is an attempt to mint a duplicate commodity. 123 | CommodityExists, 124 | // Thrown when there is an attempt to burn or transfer a nonexistent commodity. 125 | NonexistentCommodity, 126 | // Thrown when someone who is not the owner of a commodity attempts to transfer or burn it. 127 | NotCommodityOwner, 128 | // Thrown when the commodity admin attempts to mint a commodity and the maximum number of this 129 | // type of commodity already exists. 130 | TooManyCommodities, 131 | // Thrown when an attempt is made to mint or transfer a commodity to an account that already 132 | // owns the maximum number of this type of commodity. 133 | TooManyCommoditiesForAccount, 134 | } 135 | } 136 | 137 | decl_module! { 138 | pub struct Module, I: Instance = DefaultInstance> for enum Call where origin: T::Origin { 139 | type Error = Error; 140 | fn deposit_event() = default; 141 | 142 | /// Create a new commodity from the provided commodity info and identify the specified 143 | /// account as its owner. The ID of the new commodity will be equal to the hash of the info 144 | /// that defines it, as calculated by the runtime system's hashing algorithm. 145 | /// 146 | /// The dispatch origin for this call must be the commodity admin. 147 | /// 148 | /// This function will throw an error if it is called with commodity info that describes 149 | /// an existing (duplicate) commodity, if the maximum number of this type of commodity already 150 | /// exists or if the specified owner already owns the maximum number of this type of 151 | /// commodity. 152 | /// 153 | /// - `owner_account`: Receiver of the commodity. 154 | /// - `commodity_info`: The information that defines the commodity. 155 | #[weight = 10_000] 156 | pub fn mint(origin, owner_account: T::AccountId, commodity_info: T::CommodityInfo) -> dispatch::DispatchResult { 157 | T::CommodityAdmin::ensure_origin(origin)?; 158 | 159 | let commodity_id = >::mint(&owner_account, commodity_info)?; 160 | Self::deposit_event(RawEvent::Minted(commodity_id, owner_account.clone())); 161 | Ok(()) 162 | } 163 | 164 | /// Destroy the specified commodity. 165 | /// 166 | /// The dispatch origin for this call must be the commodity owner. 167 | /// 168 | /// - `commodity_id`: The hash (calculated by the runtime system's hashing algorithm) 169 | /// of the info that defines the commodity to destroy. 170 | #[weight = 10_000] 171 | pub fn burn(origin, commodity_id: CommodityId) -> dispatch::DispatchResult { 172 | let who = ensure_signed(origin)?; 173 | ensure!(who == Self::account_for_commodity(&commodity_id), Error::::NotCommodityOwner); 174 | 175 | >::burn(&commodity_id)?; 176 | Self::deposit_event(RawEvent::Burned(commodity_id.clone())); 177 | Ok(()) 178 | } 179 | 180 | /// Transfer a commodity to a new owner. 181 | /// 182 | /// The dispatch origin for this call must be the commodity owner. 183 | /// 184 | /// This function will throw an error if the new owner already owns the maximum 185 | /// number of this type of commodity. 186 | /// 187 | /// - `dest_account`: Receiver of the commodity. 188 | /// - `commodity_id`: The hash (calculated by the runtime system's hashing algorithm) 189 | /// of the info that defines the commodity to destroy. 190 | #[weight = 10_000] 191 | pub fn transfer(origin, dest_account: T::AccountId, commodity_id: CommodityId) -> dispatch::DispatchResult { 192 | let who = ensure_signed(origin)?; 193 | ensure!(who == Self::account_for_commodity(&commodity_id), Error::::NotCommodityOwner); 194 | 195 | >::transfer(&dest_account, &commodity_id)?; 196 | Self::deposit_event(RawEvent::Transferred(commodity_id.clone(), dest_account.clone())); 197 | Ok(()) 198 | } 199 | } 200 | } 201 | 202 | impl, I: Instance> UniqueAssets for Module { 203 | type AssetId = CommodityId; 204 | type AssetInfo = T::CommodityInfo; 205 | type AssetLimit = T::CommodityLimit; 206 | type UserAssetLimit = T::UserCommodityLimit; 207 | 208 | fn total() -> u128 { 209 | Self::total() 210 | } 211 | 212 | fn burned() -> u128 { 213 | Self::burned() 214 | } 215 | 216 | fn total_for_account(account: &T::AccountId) -> u64 { 217 | Self::total_for_account(account) 218 | } 219 | 220 | fn assets_for_account(account: &T::AccountId) -> Vec> { 221 | Self::commodities_for_account(account) 222 | } 223 | 224 | fn owner_of(commodity_id: &CommodityId) -> T::AccountId { 225 | Self::account_for_commodity(commodity_id) 226 | } 227 | 228 | fn mint( 229 | owner_account: &T::AccountId, 230 | commodity_info: >::CommodityInfo, 231 | ) -> dispatch::result::Result, dispatch::DispatchError> { 232 | let commodity_id = T::Hashing::hash_of(&commodity_info); 233 | 234 | ensure!( 235 | !AccountForCommodity::::contains_key(&commodity_id), 236 | Error::::CommodityExists 237 | ); 238 | 239 | ensure!( 240 | Self::total_for_account(owner_account) < T::UserCommodityLimit::get(), 241 | Error::::TooManyCommoditiesForAccount 242 | ); 243 | 244 | ensure!( 245 | Self::total() < T::CommodityLimit::get(), 246 | Error::::TooManyCommodities 247 | ); 248 | 249 | let new_commodity = (commodity_id, commodity_info); 250 | 251 | Total::::mutate(|total| *total += 1); 252 | TotalForAccount::::mutate(owner_account, |total| *total += 1); 253 | CommoditiesForAccount::::mutate(owner_account, |commodities| { 254 | match commodities.binary_search(&new_commodity) { 255 | Ok(_pos) => {} // should never happen 256 | Err(pos) => commodities.insert(pos, new_commodity), 257 | } 258 | }); 259 | AccountForCommodity::::insert(commodity_id, &owner_account); 260 | 261 | Ok(commodity_id) 262 | } 263 | 264 | fn burn(commodity_id: &CommodityId) -> dispatch::DispatchResult { 265 | let owner = Self::owner_of(commodity_id); 266 | ensure!( 267 | owner != T::AccountId::default(), 268 | Error::::NonexistentCommodity 269 | ); 270 | 271 | let burn_commodity = (*commodity_id, >::CommodityInfo::default()); 272 | 273 | Total::::mutate(|total| *total -= 1); 274 | Burned::::mutate(|total| *total += 1); 275 | TotalForAccount::::mutate(&owner, |total| *total -= 1); 276 | CommoditiesForAccount::::mutate(owner, |commodities| { 277 | let pos = commodities 278 | .binary_search(&burn_commodity) 279 | .expect("We already checked that we have the correct owner; qed"); 280 | commodities.remove(pos); 281 | }); 282 | AccountForCommodity::::remove(&commodity_id); 283 | 284 | Ok(()) 285 | } 286 | 287 | fn transfer( 288 | dest_account: &T::AccountId, 289 | commodity_id: &CommodityId, 290 | ) -> dispatch::DispatchResult { 291 | let owner = Self::owner_of(&commodity_id); 292 | ensure!( 293 | owner != T::AccountId::default(), 294 | Error::::NonexistentCommodity 295 | ); 296 | 297 | ensure!( 298 | Self::total_for_account(dest_account) < T::UserCommodityLimit::get(), 299 | Error::::TooManyCommoditiesForAccount 300 | ); 301 | 302 | let xfer_commodity = (*commodity_id, >::CommodityInfo::default()); 303 | 304 | TotalForAccount::::mutate(&owner, |total| *total -= 1); 305 | TotalForAccount::::mutate(dest_account, |total| *total += 1); 306 | let commodity = CommoditiesForAccount::::mutate(owner, |commodities| { 307 | let pos = commodities 308 | .binary_search(&xfer_commodity) 309 | .expect("We already checked that we have the correct owner; qed"); 310 | commodities.remove(pos) 311 | }); 312 | CommoditiesForAccount::::mutate(dest_account, |commodities| { 313 | match commodities.binary_search(&commodity) { 314 | Ok(_pos) => {} // should never happen 315 | Err(pos) => commodities.insert(pos, commodity), 316 | } 317 | }); 318 | AccountForCommodity::::insert(&commodity_id, &dest_account); 319 | 320 | Ok(()) 321 | } 322 | } 323 | --------------------------------------------------------------------------------