diff --git a/Cargo.lock b/Cargo.lock index 3414de0b..48f7b34c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 4 +version = 3 [[package]] name = "Inflector" diff --git a/pallets/api/src/nonfungibles/mod.rs b/pallets/api/src/nonfungibles/mod.rs index 6da7de5f..4bdd99ce 100644 --- a/pallets/api/src/nonfungibles/mod.rs +++ b/pallets/api/src/nonfungibles/mod.rs @@ -11,37 +11,17 @@ pub use pallet_nfts::{ CollectionSetting, CollectionSettings, DestroyWitness, ItemDeposit, ItemDetails, ItemMetadata, ItemSetting, MintSettings, MintType, MintWitness, }; -use sp_runtime::traits::StaticLookup; +use sp_runtime::{traits::StaticLookup, BoundedVec}; +use types::*; use weights::WeightInfo; #[cfg(feature = "runtime-benchmarks")] mod benchmarking; #[cfg(test)] mod tests; +pub mod types; pub mod weights; -type AccountIdOf = ::AccountId; -type NftsOf = pallet_nfts::Pallet>; -type NftsErrorOf = pallet_nfts::Error>; -type NftsWeightInfoOf = >>::WeightInfo; -type NftsInstanceOf = ::NftsInstance; -type BalanceOf = <>>::Currency as Currency< - ::AccountId, ->>::Balance; -type CollectionIdOf = - as Inspect<::AccountId>>::CollectionId; -type ItemIdOf = as Inspect<::AccountId>>::ItemId; -type ItemPriceOf = BalanceOf; -type CollectionDetailsFor = CollectionDetails, BalanceOf>; -type AttributeNamespaceOf = AttributeNamespace>; -type CollectionConfigFor = - CollectionConfig, BlockNumberFor, CollectionIdOf>; -// Type aliases for pallet-nfts storage items. -pub(super) type AccountBalanceOf = pallet_nfts::AccountBalance>; -pub(super) type AttributeOf = pallet_nfts::Attribute>; -pub(super) type NextCollectionIdOf = pallet_nfts::NextCollectionId>; -pub(super) type CollectionOf = pallet_nfts::Collection>; - #[frame_support::pallet] pub mod pallet { use frame_support::{ @@ -50,8 +30,6 @@ pub mod pallet { traits::Incrementable, }; use frame_system::pallet_prelude::*; - use pallet_nfts::{CancelAttributesApprovalWitness, DestroyWitness, MintWitness}; - use sp_runtime::BoundedVec; use sp_std::vec::Vec; use super::*; @@ -104,7 +82,7 @@ pub mod pallet { /// The namespace of the attribute. namespace: AttributeNamespaceOf, /// The key of the attribute. - key: BoundedVec, + key: AttributeKey, }, /// Details of a specified collection. #[codec(index = 9)] @@ -354,8 +332,8 @@ pub mod pallet { collection: CollectionIdOf, item: Option>, namespace: AttributeNamespaceOf, - key: BoundedVec, - value: BoundedVec, + key: AttributeKey, + value: AttributeValue, ) -> DispatchResult { NftsOf::::set_attribute(origin, collection, item, namespace, key, value) } @@ -374,7 +352,7 @@ pub mod pallet { collection: CollectionIdOf, item: Option>, namespace: AttributeNamespaceOf, - key: BoundedVec, + key: AttributeKey, ) -> DispatchResult { NftsOf::::clear_attribute(origin, collection, item, namespace, key) } @@ -391,7 +369,7 @@ pub mod pallet { origin: OriginFor, collection: CollectionIdOf, item: ItemIdOf, - data: BoundedVec, + data: MetadataData, ) -> DispatchResult { NftsOf::::set_metadata(origin, collection, item, data) } diff --git a/pallets/api/src/nonfungibles/types.rs b/pallets/api/src/nonfungibles/types.rs new file mode 100644 index 00000000..e15d430b --- /dev/null +++ b/pallets/api/src/nonfungibles/types.rs @@ -0,0 +1,31 @@ +use super::*; + +pub(super) type AccountIdOf = ::AccountId; +pub(super) type NftsOf = pallet_nfts::Pallet>; +pub(super) type NftsErrorOf = pallet_nfts::Error>; +pub(super) type NftsWeightInfoOf = >>::WeightInfo; +pub(super) type NftsInstanceOf = ::NftsInstance; +pub(super) type BalanceOf = + <>>::Currency as Currency< + ::AccountId, + >>::Balance; +pub(super) type CollectionIdOf = + as Inspect<::AccountId>>::CollectionId; +pub(super) type ItemIdOf = + as Inspect<::AccountId>>::ItemId; +pub(super) type ItemPriceOf = BalanceOf; +pub(super) type CollectionDetailsFor = CollectionDetails, BalanceOf>; +pub(super) type AttributeNamespaceOf = AttributeNamespace>; +pub(super) type CollectionConfigFor = + CollectionConfig, BlockNumberFor, CollectionIdOf>; +// Public due to pop-api integration tests crate. +pub type AccountBalanceOf = pallet_nfts::AccountBalance>; +pub type AttributeOf = pallet_nfts::Attribute>; +pub type AttributeKey = BoundedVec>>::KeyLimit>; +pub type AttributeValue = + BoundedVec>>::ValueLimit>; +pub type CollectionOf = pallet_nfts::Collection>; +pub type CollectionConfigOf = pallet_nfts::CollectionConfigOf>; +pub type NextCollectionIdOf = pallet_nfts::NextCollectionId>; +pub type MetadataData = + BoundedVec>>::StringLimit>; diff --git a/pop-api/Cargo.toml b/pop-api/Cargo.toml index 4caf8eaa..c9d0c4f5 100644 --- a/pop-api/Cargo.toml +++ b/pop-api/Cargo.toml @@ -6,6 +6,8 @@ name = "pop-api" version = "0.0.0" [dependencies] +bitflags = { version = "1.3.2" } +enumflags2 = "0.7.9" ink = { version = "5.0.0", default-features = false } pop-primitives = { path = "../primitives", default-features = false } sp-io = { version = "37.0.0", default-features = false, features = [ @@ -22,4 +24,5 @@ path = "src/lib.rs" [features] default = [ "std" ] fungibles = [ ] +nonfungibles = [ ] std = [ "ink/std", "pop-primitives/std", "sp-io/std" ] diff --git a/pop-api/integration-tests/Cargo.toml b/pop-api/integration-tests/Cargo.toml index 482a214f..edc7017f 100644 --- a/pop-api/integration-tests/Cargo.toml +++ b/pop-api/integration-tests/Cargo.toml @@ -13,11 +13,14 @@ frame-support = { version = "36.0.0", default-features = false } frame-support-procedural = { version = "=30.0.1", default-features = false } frame-system = { version = "36.1.0", default-features = false } log = "0.4.22" +pallet-api = { path = "../../pallets/api", default-features = false } pallet-assets = { version = "37.0.0", default-features = false } pallet-balances = { version = "37.0.0", default-features = false } pallet-contracts = { version = "35.0.0", default-features = false } +pallet-nfts = { path = "../../pallets/nfts", default-features = false } pop-api = { path = "../../pop-api", default-features = false, features = [ "fungibles", + "nonfungibles", ] } pop-primitives = { path = "../../primitives", default-features = false } pop-runtime-devnet = { path = "../../runtime/devnet", default-features = false } @@ -36,9 +39,11 @@ devnet = [ ] std = [ "frame-support/std", "frame-system/std", + "pallet-api/std", "pallet-assets/std", "pallet-balances/std", "pallet-contracts/std", + "pallet-nfts/std", "pop-api/std", "pop-primitives/std", "pop-runtime-devnet/std", diff --git a/pop-api/integration-tests/contracts/nonfungibles/Cargo.toml b/pop-api/integration-tests/contracts/nonfungibles/Cargo.toml new file mode 100644 index 00000000..9c8bda79 --- /dev/null +++ b/pop-api/integration-tests/contracts/nonfungibles/Cargo.toml @@ -0,0 +1,21 @@ +[package] +authors = [ "R0GUE " ] +edition = "2021" +name = "nonfungibles" +version = "0.1.0" + +[dependencies] +ink = { version = "5.0.0", default-features = false } +pop-api = { path = "../../../../pop-api", default-features = false, features = [ "nonfungibles" ] } + +[lib] +path = "lib.rs" + +[features] +default = [ "std" ] +e2e-tests = [ ] +ink-as-dependency = [ ] +std = [ + "ink/std", + "pop-api/std", +] diff --git a/pop-api/integration-tests/contracts/nonfungibles/lib.rs b/pop-api/integration-tests/contracts/nonfungibles/lib.rs new file mode 100644 index 00000000..717498a1 --- /dev/null +++ b/pop-api/integration-tests/contracts/nonfungibles/lib.rs @@ -0,0 +1,244 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +/// 1. PSP-34 +/// 2. PSP-34 Metadata +/// 3. Management +/// 4. PSP-34 Mintable & Burnable +use ink::prelude::vec::Vec; +use pop_api::{ + nonfungibles::{ + self as api, + events::{Approval, AttributeSet, Transfer}, + AttributeNamespace, CancelAttributesApprovalWitness, CollectionConfig, CollectionDetails, + CollectionId, DestroyWitness, ItemId, MintWitness, + }, + StatusCode, +}; + +pub type Result = core::result::Result; + +#[ink::contract] +mod nonfungibles { + use super::*; + + #[ink(storage)] + #[derive(Default)] + pub struct NonFungibles; + + impl NonFungibles { + #[ink(constructor, payable)] + pub fn new() -> Self { + ink::env::debug_println!("PopApiNonFungiblesExample::new"); + Default::default() + } + + /// 1. PSP-34 Interface: + /// - total_supply + /// - balance_of + /// - allowance + /// - transfer + /// - approve + /// - owner_of + + #[ink(message)] + pub fn total_supply(&self, collection: CollectionId) -> Result { + api::total_supply(collection) + } + + #[ink(message)] + pub fn balance_of(&self, collection: CollectionId, owner: AccountId) -> Result { + api::balance_of(collection, owner) + } + + #[ink(message)] + pub fn allowance( + &self, + collection: CollectionId, + item: Option, + owner: AccountId, + operator: AccountId, + ) -> Result { + api::allowance(collection, item, owner, operator) + } + + #[ink(message)] + pub fn transfer( + &mut self, + collection: CollectionId, + item: ItemId, + to: AccountId, + ) -> Result<()> { + api::transfer(collection, item, to)?; + self.env().emit_event(Transfer { + from: Some(self.env().account_id()), + to: Some(to), + item, + }); + Ok(()) + } + + #[ink(message)] + pub fn approve( + &mut self, + collection: CollectionId, + item: Option, + operator: AccountId, + approved: bool, + ) -> Result<()> { + api::approve(collection, item, operator, approved)?; + self.env().emit_event(Approval { + owner: self.env().account_id(), + operator, + item, + approved, + }); + Ok(()) + } + + #[ink(message)] + pub fn owner_of( + &self, + collection: CollectionId, + item: ItemId, + ) -> Result> { + api::owner_of(collection, item) + } + + /// 2. PSP-34 Metadata Interface: + /// - get_attribute + + #[ink(message)] + pub fn get_attribute( + &self, + collection: CollectionId, + item: ItemId, + namespace: AttributeNamespace, + key: Vec, + ) -> Result>> { + api::get_attribute(collection, item, namespace, key) + } + + /// 3. Asset Management: + /// - create + /// - destroy + /// - collection + /// - set_attribute + /// - clear_attribute + /// - set_metadata + /// - clear_metadata + /// - approve_item_attributes + /// - cancel_item_attributes_approval + /// - set_max_supply + /// - item_metadata + + #[ink(message)] + pub fn create(&mut self, admin: AccountId, config: CollectionConfig) -> Result<()> { + api::create(admin, config) + } + + #[ink(message)] + pub fn destroy(&mut self, collection: CollectionId, witness: DestroyWitness) -> Result<()> { + api::destroy(collection, witness) + } + + #[ink(message)] + pub fn collection(&self, collection: CollectionId) -> Result> { + api::collection(collection) + } + + #[ink(message)] + pub fn set_attribute( + &mut self, + collection: CollectionId, + item: ItemId, + namespace: AttributeNamespace, + key: Vec, + value: Vec, + ) -> Result<()> { + api::set_attribute(collection, item, namespace, key.clone(), value.clone())?; + self.env().emit_event(AttributeSet { item, key, data: value }); + Ok(()) + } + + #[ink(message)] + pub fn clear_attribute( + &mut self, + collection: CollectionId, + item: ItemId, + namespace: AttributeNamespace, + key: Vec, + ) -> Result<()> { + api::clear_attribute(collection, item, namespace, key) + } + + #[ink(message)] + pub fn set_metadata( + &mut self, + collection: CollectionId, + item: ItemId, + data: Vec, + ) -> Result<()> { + api::set_metadata(collection, item, data) + } + + #[ink(message)] + pub fn clear_metadata(&mut self, collection: CollectionId, item: ItemId) -> Result<()> { + api::clear_metadata(collection, item) + } + + #[ink(message)] + pub fn approve_item_attributes( + &mut self, + collection: CollectionId, + item: ItemId, + delegate: AccountId, + ) -> Result<()> { + api::approve_item_attributes(collection, item, delegate) + } + + #[ink(message)] + pub fn cancel_item_attributes_approval( + &mut self, + collection: CollectionId, + item: ItemId, + delegate: AccountId, + witness: CancelAttributesApprovalWitness, + ) -> Result<()> { + api::cancel_item_attributes_approval(collection, item, delegate, witness) + } + + #[ink(message)] + pub fn set_max_supply(&mut self, collection: CollectionId, max_supply: u32) -> Result<()> { + api::set_max_supply(collection, max_supply) + } + + #[ink(message)] + pub fn item_metadata( + &mut self, + collection: CollectionId, + item: ItemId, + ) -> Result>> { + api::item_metadata(collection, item) + } + + /// 4. PSP-22 Mintable & Burnable Interface: + /// - mint + /// - burn + + #[ink(message)] + pub fn mint( + &mut self, + to: AccountId, + collection: CollectionId, + item: ItemId, + witness: MintWitness, + ) -> Result<()> { + api::mint(to, collection, item, witness) + } + + #[ink(message)] + pub fn burn(&mut self, collection: CollectionId, item: ItemId) -> Result<()> { + api::burn(collection, item) + } + } +} diff --git a/pop-api/integration-tests/src/fungibles/utils.rs b/pop-api/integration-tests/src/fungibles/utils.rs index 07be866e..018c6cfc 100644 --- a/pop-api/integration-tests/src/fungibles/utils.rs +++ b/pop-api/integration-tests/src/fungibles/utils.rs @@ -1,16 +1,5 @@ use super::*; -fn do_bare_call(function: &str, addr: &AccountId32, params: Vec) -> ExecReturnValue { - let function = function_selector(function); - let params = [function, params].concat(); - bare_call(addr.clone(), params, 0).expect("should work") -} - -// TODO - issue #263 - why result.data[1..] -pub(super) fn decoded(result: ExecReturnValue) -> Result { - ::decode(&mut &result.data[1..]).map_err(|_| result) -} - pub(super) fn total_supply(addr: &AccountId32, token_id: TokenId) -> Result { let result = do_bare_call("total_supply", addr, token_id.encode()); decoded::>(result.clone()) @@ -340,25 +329,3 @@ pub(super) fn instantiate_and_create_fungible( .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) .map(|_| address) } - -/// Get the last event from pallet contracts. -pub(super) fn last_contract_event() -> Vec { - let events = System::read_events_for_pallet::>(); - let contract_events = events - .iter() - .filter_map(|event| match event { - pallet_contracts::Event::::ContractEmitted { data, .. } => - Some(data.as_slice()), - _ => None, - }) - .collect::>(); - contract_events.last().unwrap().to_vec() -} - -/// Decodes a byte slice into an `AccountId` as defined in `primitives`. -/// -/// This is used to resolve type mismatches between the `AccountId` in the integration tests and the -/// contract environment. -pub fn account_id_from_slice(s: &[u8; 32]) -> pop_api::primitives::AccountId { - pop_api::primitives::AccountId::decode(&mut &s[..]).expect("Should be decoded to AccountId") -} diff --git a/pop-api/integration-tests/src/lib.rs b/pop-api/integration-tests/src/lib.rs index 58641889..a396c3a5 100644 --- a/pop-api/integration-tests/src/lib.rs +++ b/pop-api/integration-tests/src/lib.rs @@ -9,13 +9,18 @@ use frame_support::{ }; use pallet_contracts::{Code, CollectEvents, Determinism, ExecReturnValue}; #[cfg(feature = "devnet")] -use pop_runtime_devnet::{Assets, Contracts, Runtime, RuntimeOrigin, System, UNIT}; +use pop_runtime_devnet::{Assets, Contracts, Nfts, Runtime, RuntimeOrigin, System, UNIT}; #[cfg(feature = "testnet")] -use pop_runtime_testnet::{Assets, Contracts, Runtime, RuntimeOrigin, System, UNIT}; +use pop_runtime_testnet::{Assets, Contracts, Nfts, Runtime, RuntimeOrigin, System, UNIT}; use scale::{Decode, Encode}; use sp_runtime::{AccountId32, BuildStorage, DispatchError}; +use utils::*; +#[cfg(any(feature = "devnet", feature = "testnet"))] mod fungibles; +#[cfg(any(feature = "devnet"))] +mod nonfungibles; +mod utils; type Balance = u128; @@ -23,7 +28,7 @@ const ALICE: AccountId32 = AccountId32::new([1_u8; 32]); const BOB: AccountId32 = AccountId32::new([2_u8; 32]); const DEBUG_OUTPUT: pallet_contracts::DebugInfo = pallet_contracts::DebugInfo::UnsafeDebug; const FERDIE: AccountId32 = AccountId32::new([3_u8; 32]); -const GAS_LIMIT: Weight = Weight::from_parts(100_000_000_000, 3 * 1024 * 1024); +const GAS_LIMIT: Weight = Weight::from_parts(500_000_000_000, 3 * 1024 * 1024); const INIT_AMOUNT: Balance = 100_000_000 * UNIT; const INIT_VALUE: Balance = 100 * UNIT; diff --git a/pop-api/integration-tests/src/nonfungibles/mod.rs b/pop-api/integration-tests/src/nonfungibles/mod.rs new file mode 100644 index 00000000..5c236f9f --- /dev/null +++ b/pop-api/integration-tests/src/nonfungibles/mod.rs @@ -0,0 +1,558 @@ +use frame_support::BoundedVec; +use pallet_api::nonfungibles::types::*; +use pop_api::{ + nonfungibles::{ + events::{Approval, AttributeSet, Transfer}, + AttributeNamespace, CancelAttributesApprovalWitness, CollectionConfig, CollectionDetails, + CollectionId, CollectionSettings, DestroyWitness, ItemId, MintSettings, MintWitness, + }, + primitives::BlockNumber, +}; +use pop_primitives::{ArithmeticError::*, Error, Error::*, TokenError::*}; +use utils::*; + +use super::*; + +mod utils; + +const COLLECTION: CollectionId = 0; +const ITEM: ItemId = 0; +const CONTRACT: &str = "contracts/nonfungibles/target/ink/nonfungibles.wasm"; + +#[test] +fn total_supply_works() { + new_test_ext().execute_with(|| { + let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + + // No collection item is created. + assert_eq!( + total_supply(&addr, COLLECTION), + Ok(Nfts::collection_items(COLLECTION).unwrap_or_default() as u128) + ); + assert_eq!(total_supply(&addr, COLLECTION), Ok(0)); + + // Collection item is created. + nfts::create_collection_and_mint_to(&addr, &addr, &ALICE, ITEM); + assert_eq!( + total_supply(&addr, COLLECTION), + Ok(Nfts::collection_items(COLLECTION).unwrap_or_default() as u128) + ); + assert_eq!(total_supply(&addr, COLLECTION), Ok(1)); + }); +} + +#[test] +fn balance_of_works() { + new_test_ext().execute_with(|| { + let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + + // No collection item is created. + assert_eq!(balance_of(&addr, COLLECTION, ALICE), Ok(nfts::balance_of(COLLECTION, ALICE)),); + assert_eq!(total_supply(&addr, COLLECTION), Ok(0)); + + // Collection item is created. + nfts::create_collection_and_mint_to(&addr, &addr, &ALICE, ITEM); + assert_eq!(balance_of(&addr, COLLECTION, ALICE), Ok(nfts::balance_of(COLLECTION, ALICE)),); + assert_eq!(total_supply(&addr, COLLECTION), Ok(1)); + }); +} + +#[test] +fn allowance_works() { + new_test_ext().execute_with(|| { + let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + // No collection item is created. + assert_eq!( + allowance(&addr.clone(), COLLECTION, None, addr.clone(), ALICE), + Ok(!Nfts::check_allowance(&COLLECTION, &None, &addr, &ALICE).is_err()), + ); + assert_eq!(allowance(&addr.clone(), COLLECTION, None, addr.clone(), ALICE), Ok(false)); + + // Collection item is created. + let (_, item) = nfts::create_collection_mint_and_approve(&addr, &addr, ITEM, &addr, &ALICE); + assert_eq!( + allowance(&addr.clone(), COLLECTION, Some(item), addr.clone(), ALICE), + Ok(Nfts::check_allowance(&COLLECTION, &Some(item), &addr.clone(), &ALICE).is_ok()), + ); + assert_eq!(allowance(&addr.clone(), COLLECTION, Some(item), addr.clone(), ALICE), Ok(true)); + }); +} + +#[test] +fn transfer_works() { + new_test_ext().execute_with(|| { + let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + + // Collection item does not exist. + assert_eq!( + transfer(&addr, COLLECTION, ITEM, ALICE), + Err(Module { index: 50, error: [1, 0] }) + ); + // Create a collection and mint to a contract address. + let (collection, item) = nfts::create_collection_and_mint_to(&addr, &addr, &addr, ITEM); + // Privilege to transfer a collection item is locked. + nfts::lock_item_transfer(&addr, COLLECTION, ITEM); + assert_eq!( + transfer(&addr, COLLECTION, ITEM, ALICE), + Err(Module { index: 50, error: [12, 0] }) + ); + nfts::unlock_item_transfer(&addr, COLLECTION, ITEM); + // Successful transfer. + let before_transfer_balance = nfts::balance_of(COLLECTION, ALICE); + assert_ok!(transfer(&addr, collection, item, ALICE)); + let after_transfer_balance = nfts::balance_of(COLLECTION, ALICE); + assert_eq!(after_transfer_balance - before_transfer_balance, 1); + // Successfully emit event. + let from = account_id_from_slice(addr.as_ref()); + let to = account_id_from_slice(ALICE.as_ref()); + let expected = Transfer { from: Some(from), to: Some(to), item: ITEM }.encode(); + assert_eq!(last_contract_event(), expected.as_slice()); + // Collection item does not exist, i.e. burnt. + nfts::burn(COLLECTION, ITEM, &ALICE); + assert_eq!( + transfer(&addr, COLLECTION, ITEM, ALICE), + Err(Module { index: 50, error: [20, 0] }) + ); + }); +} + +#[test] +fn approve_item_transfer_works() { + new_test_ext().execute_with(|| { + let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + + // Collection item does not exist. + assert_eq!( + approve(&addr, COLLECTION, Some(ITEM), ALICE, true), + Err(Module { index: 50, error: [20, 0] }) + ); + // Successful approvals. + let (collection, item) = nfts::create_collection_and_mint_to(&addr, &addr, &addr, ITEM); + assert_ok!(approve(&addr, collection, Some(item), ALICE, true)); + assert!(Nfts::check_allowance(&collection, &Some(item), &addr.clone(), &ALICE).is_ok()); + // Successfully emit event. + let owner = account_id_from_slice(addr.as_ref()); + let operator = account_id_from_slice(ALICE.as_ref()); + let expected = Approval { owner, operator, item: Some(item), approved: true }.encode(); + assert_eq!(last_contract_event(), expected.as_slice()); + // New value overrides old value. + assert_ok!(approve(&addr, collection, Some(item), ALICE, false)); + assert!(Nfts::check_allowance(&collection, &Some(item), &addr.clone(), &ALICE).is_err()); + }); +} + +#[test] +fn approve_collection_transfer_works() { + new_test_ext().execute_with(|| { + let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + + // Collection does not exist. + assert_eq!( + approve(&addr, COLLECTION, None, ALICE, true), + Err(Module { index: 50, error: [1, 0] }) + ); + // Successful approvals. + let (collection, item) = nfts::create_collection_and_mint_to(&addr, &addr, &addr, ITEM); + assert_ok!(approve(&addr, collection, None, ALICE, true)); + assert!(Nfts::check_allowance(&collection, &None, &addr.clone(), &ALICE).is_ok()); + // Successfully emit event. + let owner = account_id_from_slice(addr.as_ref()); + let operator = account_id_from_slice(ALICE.as_ref()); + let expected = Approval { owner, operator, item: None, approved: true }.encode(); + assert_eq!(last_contract_event(), expected.as_slice()); + // New value overrides old value. + assert_ok!(approve(&addr, collection, None, ALICE, false)); + assert!(Nfts::check_allowance(&collection, &None, &addr.clone(), &ALICE).is_err()); + }); +} + +#[test] +fn owner_of_works() { + new_test_ext().execute_with(|| { + let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + let (collection, item) = nfts::create_collection_and_mint_to(&addr, &addr, &ALICE, ITEM); + assert_eq!(owner_of(&addr, collection, item), Ok(ALICE)); + }); +} + +#[test] +fn get_attribute_works() { + new_test_ext().execute_with(|| { + let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + + let (collection, item) = nfts::create_collection_and_mint_to(&addr, &addr, &addr, ITEM); + + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(addr.clone()), + collection, + Some(item), + pallet_nfts::AttributeNamespace::CollectionOwner, + BoundedVec::truncate_from("some attribute".as_bytes().to_vec()), + BoundedVec::truncate_from("some value".as_bytes().to_vec()), + )); + assert_eq!( + get_attribute( + &addr.clone(), + collection, + item, + AttributeNamespace::CollectionOwner, + "some attribute".as_bytes().to_vec(), + ), + Ok(Some("some value".as_bytes().to_vec())) + ); + }); +} + +#[test] +fn set_attribute_works() { + new_test_ext().execute_with(|| { + let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + + let (collection, item) = nfts::create_collection_and_mint_to(&addr, &addr, &addr, ITEM); + + assert_ok!(set_attribute( + &addr.clone(), + collection, + item, + AttributeNamespace::CollectionOwner, + "some attribute".as_bytes().to_vec(), + "some value".as_bytes().to_vec(), + )); + + assert_eq!( + AttributeOf::::get(( + collection, + Some(item), + pallet_nfts::AttributeNamespace::CollectionOwner, + AttributeKey::::truncate_from("some attribute".as_bytes().to_vec()), + )) + .map(|attribute| attribute.0), + Some(AttributeValue::::truncate_from("some value".as_bytes().to_vec())) + ); + }); +} + +#[test] +fn clear_attribute_works() { + new_test_ext().execute_with(|| { + let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + + let (collection, item) = nfts::create_collection_and_mint_to(&addr, &addr, &addr, ITEM); + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(addr.clone()), + collection, + Some(item), + pallet_nfts::AttributeNamespace::CollectionOwner, + BoundedVec::truncate_from("some attribute".as_bytes().to_vec()), + BoundedVec::truncate_from("some value".as_bytes().to_vec()), + )); + assert_ok!(clear_attribute( + &addr.clone(), + collection, + item, + AttributeNamespace::CollectionOwner, + "some attribute".as_bytes().to_vec() + )); + assert_eq!( + AttributeOf::::get(( + collection, + Some(item), + pallet_nfts::AttributeNamespace::CollectionOwner, + AttributeKey::::truncate_from("some attribute".as_bytes().to_vec()), + )) + .map(|attribute| attribute.0), + None + ); + }); +} + +#[test] +fn approve_item_attributes_works() { + new_test_ext().execute_with(|| { + let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + + let (collection, item) = nfts::create_collection_and_mint_to(&addr, &addr, &addr, ITEM); + assert_ok!(approve_item_attributes(&addr.clone(), collection, item, ALICE)); + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(ALICE), + collection, + Some(item), + pallet_nfts::AttributeNamespace::Account(ALICE), + BoundedVec::truncate_from("some attribute".as_bytes().to_vec()), + BoundedVec::truncate_from("some value".as_bytes().to_vec()), + )); + assert_eq!( + AttributeOf::::get(( + collection, + Some(item), + pallet_nfts::AttributeNamespace::Account(ALICE), + AttributeKey::::truncate_from("some attribute".as_bytes().to_vec()), + )) + .map(|attribute| attribute.0), + Some(AttributeValue::::truncate_from("some value".as_bytes().to_vec())) + ); + }); +} + +#[test] +fn cancel_item_attributes_approval_works() { + new_test_ext().execute_with(|| { + let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + + let (collection, item) = nfts::create_collection_and_mint_to(&addr, &addr, &addr, ITEM); + assert_ok!(Nfts::approve_item_attributes( + RuntimeOrigin::signed(addr.clone()), + collection, + item, + ALICE.into() + )); + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(ALICE), + collection, + Some(item), + pallet_nfts::AttributeNamespace::Account(ALICE), + BoundedVec::truncate_from("some attribute".as_bytes().to_vec()), + BoundedVec::truncate_from("some value".as_bytes().to_vec()), + )); + assert_ok!(cancel_item_attributes_approval( + &addr.clone(), + collection, + item, + ALICE, + CancelAttributesApprovalWitness { account_attributes: 1 } + )); + assert!(Nfts::set_attribute( + RuntimeOrigin::signed(ALICE), + collection, + Some(item), + pallet_nfts::AttributeNamespace::Account(ALICE), + BoundedVec::truncate_from("some attribute".as_bytes().to_vec()), + BoundedVec::truncate_from("some value".as_bytes().to_vec()), + ) + .is_err()); + }); +} + +#[test] +fn set_metadata_works() { + new_test_ext().execute_with(|| { + let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + + // Collection does not exist. + assert_eq!( + set_metadata(&addr.clone(), COLLECTION, ITEM, vec![]), + Err(Module { index: 50, error: [0, 0] }) + ); + // No Permission. + let (collection, item) = nfts::create_collection_and_mint_to(&ALICE, &ALICE, &ALICE, ITEM); + assert_eq!( + set_metadata(&addr.clone(), collection, item, vec![]), + Err(Module { index: 50, error: [0, 0] }), + ); + // Successful set metadata. + let (collection, item) = nfts::create_collection_and_mint_to(&addr, &addr, &addr, ITEM); + assert_ok!(set_metadata(&addr.clone(), collection, item, vec![])); + assert_eq!(Nfts::item_metadata(collection, item), Some(MetadataData::::default())); + }); +} + +#[test] +fn clear_metadata_works() { + new_test_ext().execute_with(|| { + let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + + // Collection does not exist. + assert_eq!( + clear_metadata(&addr.clone(), COLLECTION, ITEM), + Err(Module { index: 50, error: [0, 0] }) + ); + // Successful clear metadata. + let (collection, item) = nfts::create_collection_and_mint_to(&addr, &addr, &addr, ITEM); + assert_ok!(Nfts::set_metadata( + RuntimeOrigin::signed(addr.clone()), + collection, + item, + MetadataData::::default() + )); + assert_ok!(clear_metadata(&addr.clone(), collection, item)); + assert_eq!(Nfts::item_metadata(collection, item), None); + }); +} + +#[test] +fn create_works() { + new_test_ext().execute_with(|| { + let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + + let collection = nfts::next_collection_id(); + assert_ok!(create( + &addr.clone(), + addr.clone(), + CollectionConfig { + max_supply: Some(100), + mint_settings: MintSettings::default(), + settings: CollectionSettings::all_enabled(), + } + )); + assert_eq!( + CollectionOf::::get(collection), + Some(pallet_nfts::CollectionDetails { + owner: addr.clone(), + owner_deposit: 100000000000, + items: 0, + item_metadatas: 0, + item_configs: 0, + attributes: 0, + item_holders: 0, + allowances: 0 + }) + ); + }); +} + +#[test] +fn destroy_works() { + new_test_ext().execute_with(|| { + let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + + // Collection does not exist. + assert_eq!( + destroy( + &addr.clone(), + COLLECTION, + DestroyWitness { + item_metadatas: 0, + item_configs: 0, + attributes: 0, + item_holders: 0, + allowances: 0 + } + ), + Err(Module { index: 50, error: [1, 0] }) + ); + // Destroying can only be done by the collection owner. + let collection = nfts::create_collection(&ALICE, &ALICE); + assert_eq!( + destroy( + &addr.clone(), + collection, + DestroyWitness { + item_metadatas: 0, + item_configs: 0, + attributes: 0, + item_holders: 0, + allowances: 0 + } + ), + Err(Module { index: 50, error: [0, 0] }) + ); + // Successful destroy. + let collection = nfts::create_collection(&addr, &addr); + assert_ok!(destroy( + &addr.clone(), + collection, + DestroyWitness { + item_metadatas: 0, + item_configs: 0, + attributes: 0, + item_holders: 0, + allowances: 0 + } + )); + assert_eq!(CollectionOf::::get(collection), None); + }); +} + +#[test] +fn set_max_supply_works() { + new_test_ext().execute_with(|| { + let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + let value = 10; + + // Collection does not exist. + assert_eq!( + set_max_supply(&addr.clone(), COLLECTION, value), + Err(Module { index: 50, error: [32, 0] }) + ); + // Sucessfully set max supply. + let collection = nfts::create_collection(&addr, &addr); + assert_ok!(set_max_supply(&addr.clone(), collection, value)); + assert_eq!(nfts::max_supply(collection), Some(value)); + // Non-additive, sets new value. + assert_ok!(set_max_supply(&addr.clone(), collection, value + 1)); + assert_eq!(nfts::max_supply(collection), Some(value + 1)); + }); +} + +#[test] +fn mint_works() { + new_test_ext().execute_with(|| { + let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + let value = 10; + + // Collection does not exist. + assert_eq!( + mint( + &addr.clone(), + ALICE, + COLLECTION, + ITEM, + MintWitness { mint_price: None, owned_item: None } + ), + Err(Module { index: 50, error: [32, 0] }) + ); + // Mitning can only be done by the collection owner. + let collection = nfts::create_collection(&ALICE, &ALICE); + assert_eq!( + mint( + &addr.clone(), + ALICE, + collection, + ITEM, + MintWitness { mint_price: None, owned_item: None } + ), + Err(Module { index: 50, error: [0, 0] }) + ); + // Successful mint. + let collection = nfts::create_collection(&addr, &addr); + assert_ok!(mint( + &addr.clone(), + ALICE, + collection, + ITEM, + MintWitness { owned_item: None, mint_price: None } + )); + assert_eq!(nfts::balance_of(collection, ALICE), 1); + // Minting an existing item ID. + assert_eq!( + mint( + &addr.clone(), + ALICE, + collection, + ITEM, + MintWitness { owned_item: None, mint_price: None } + ), + Err(Module { index: 50, error: [2, 0] }) + ); + }); +} + +#[test] +fn burn_works() { + new_test_ext().execute_with(|| { + let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + + // Collection item does not exist. + assert_eq!( + burn(&addr.clone(), COLLECTION, ITEM), + Err(Module { index: 50, error: [20, 0] }) + ); + // Burning can only be done by the collection item owner. + let (collection, item) = nfts::create_collection_and_mint_to(&ALICE, &ALICE, &BOB, ITEM); + assert_eq!(burn(&addr.clone(), collection, item), Err(Module { index: 50, error: [0, 0] })); + // Successful burn. + let (collection, item) = nfts::create_collection_and_mint_to(&addr, &addr, &addr, ITEM); + assert_ok!(burn(&addr.clone(), collection, item)); + assert_eq!(nfts::balance_of(COLLECTION, addr.clone()), 0); + }); +} diff --git a/pop-api/integration-tests/src/nonfungibles/utils.rs b/pop-api/integration-tests/src/nonfungibles/utils.rs new file mode 100644 index 00000000..48d4dbaa --- /dev/null +++ b/pop-api/integration-tests/src/nonfungibles/utils.rs @@ -0,0 +1,354 @@ +use super::*; + +pub(super) fn total_supply(addr: &AccountId32, collection: CollectionId) -> Result { + let result = do_bare_call("total_supply", addr, collection.encode()); + decoded::>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) +} + +pub(super) fn balance_of( + addr: &AccountId32, + collection: CollectionId, + owner: AccountId32, +) -> Result { + let params = [collection.encode(), owner.encode()].concat(); + let result = do_bare_call("balance_of", &addr, params); + decoded::>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) +} + +pub(super) fn allowance( + addr: &AccountId32, + collection: CollectionId, + item: Option, + owner: AccountId32, + operator: AccountId32, +) -> Result { + let params = [collection.encode(), item.encode(), owner.encode(), operator.encode()].concat(); + let result = do_bare_call("allowance", &addr, params); + decoded::>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) +} + +pub(super) fn transfer( + addr: &AccountId32, + collection: CollectionId, + item: ItemId, + to: AccountId32, +) -> Result<(), Error> { + let params = [collection.encode(), item.encode(), to.encode()].concat(); + let result = do_bare_call("transfer", &addr, params); + decoded::>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) +} + +pub(super) fn approve( + addr: &AccountId32, + collection: CollectionId, + item: Option, + operator: AccountId32, + approved: bool, +) -> Result<(), Error> { + let params = + [collection.encode(), item.encode(), operator.encode(), approved.encode()].concat(); + let result = do_bare_call("approve", &addr, params); + decoded::>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) +} + +pub(super) fn owner_of( + addr: &AccountId32, + collection: CollectionId, + item: ItemId, +) -> Result { + let params = [collection.encode(), item.encode()].concat(); + let result = do_bare_call("owner_of", &addr, params); + decoded::>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) +} + +pub(super) fn get_attribute( + addr: &AccountId32, + collection: CollectionId, + item: ItemId, + namespace: AttributeNamespace, + key: Vec, +) -> Result>, Error> { + let params = [ + collection.encode(), + item.encode(), + namespace.encode(), + AttributeKey::::truncate_from(key).encode(), + ] + .concat(); + let result = do_bare_call("get_attribute", &addr, params); + decoded::>, Error>>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) + .map(|value| value.map(|v| v.to_vec())) +} + +pub(super) fn create( + addr: &AccountId32, + admin: AccountId32, + config: CollectionConfig, +) -> Result<(), Error> { + let params = [admin.encode(), config.encode()].concat(); + let result = do_bare_call("create", &addr, params); + decoded::>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) +} + +pub(super) fn destroy( + addr: &AccountId32, + collection: CollectionId, + witness: DestroyWitness, +) -> Result<(), Error> { + let params = [collection.encode(), witness.encode()].concat(); + let result = do_bare_call("destroy", &addr, params); + decoded::>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) +} + +pub(super) fn collection( + addr: &AccountId32, + collection: CollectionId, +) -> Result, Error> { + let result = do_bare_call("collection", &addr, collection.encode()); + decoded::, Error>>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) +} + +pub(super) fn set_attribute( + addr: &AccountId32, + collection: CollectionId, + item: ItemId, + namespace: AttributeNamespace, + key: Vec, + value: Vec, +) -> Result<(), Error> { + let params = [ + collection.encode(), + item.encode(), + namespace.encode(), + AttributeKey::::truncate_from(key).encode(), + AttributeValue::::truncate_from(value).encode(), + ] + .concat(); + let result = do_bare_call("set_attribute", &addr, params); + decoded::>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) +} + +pub(super) fn clear_attribute( + addr: &AccountId32, + collection: CollectionId, + item: ItemId, + namespace: AttributeNamespace, + key: Vec, +) -> Result<(), Error> { + let params = [collection.encode(), item.encode(), namespace.encode(), key.encode()].concat(); + let result = do_bare_call("clear_attribute", &addr, params); + decoded::>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) +} + +pub(super) fn set_metadata( + addr: &AccountId32, + collection: CollectionId, + item: ItemId, + data: Vec, +) -> Result<(), Error> { + let params = [collection.encode(), item.encode(), data.encode()].concat(); + let result = do_bare_call("set_metadata", &addr, params); + decoded::>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) +} + +pub(super) fn clear_metadata( + addr: &AccountId32, + collection: CollectionId, + item: ItemId, +) -> Result<(), Error> { + let params = [collection.encode(), item.encode()].concat(); + let result = do_bare_call("clear_metadata", &addr, params); + decoded::>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) +} + +pub(super) fn approve_item_attributes( + addr: &AccountId32, + collection: CollectionId, + item: ItemId, + delegate: AccountId32, +) -> Result<(), Error> { + let params = [collection.encode(), item.encode(), delegate.encode()].concat(); + let result = do_bare_call("approve_item_attributes", &addr, params); + decoded::>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) +} + +pub(super) fn cancel_item_attributes_approval( + addr: &AccountId32, + collection: CollectionId, + item: ItemId, + delegate: AccountId32, + witness: CancelAttributesApprovalWitness, +) -> Result<(), Error> { + let params = [collection.encode(), item.encode(), delegate.encode(), witness.encode()].concat(); + let result = do_bare_call("cancel_item_attributes_approval", &addr, params); + decoded::>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) +} + +pub(super) fn set_max_supply( + addr: &AccountId32, + collection: CollectionId, + max_supply: u32, +) -> Result<(), Error> { + let params = [collection.encode(), max_supply.encode()].concat(); + let result = do_bare_call("set_max_supply", &addr, params); + decoded::>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) +} + +pub(super) fn item_metadata( + addr: &AccountId32, + collection: CollectionId, + item: ItemId, +) -> Result>, Error> { + let params = [collection.encode(), item.encode()].concat(); + let result = do_bare_call("item_metadata", &addr, params); + decoded::>, Error>>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) + .map(|value| value.map(|v| v.to_vec())) +} + +pub(super) fn mint( + addr: &AccountId32, + to: AccountId32, + collection: CollectionId, + item: ItemId, + witness: MintWitness, +) -> Result<(), Error> { + let params = [to.encode(), collection.encode(), item.encode(), witness.encode()].concat(); + let result = do_bare_call("mint", &addr, params); + decoded::>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) +} + +pub(super) fn burn( + addr: &AccountId32, + collection: CollectionId, + item: ItemId, +) -> Result<(), Error> { + let params = [collection.encode(), item.encode()].concat(); + let result = do_bare_call("burn", &addr, params); + decoded::>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) +} + +pub(super) mod nfts { + use super::*; + + pub(crate) fn create_collection_and_mint_to( + owner: &AccountId32, + admin: &AccountId32, + to: &AccountId32, + item: ItemId, + ) -> (CollectionId, ItemId) { + let collection = create_collection(owner, admin); + mint(collection, item, owner, to); + (collection, item) + } + + pub(crate) fn create_collection_mint_and_approve( + owner: &AccountId32, + admin: &AccountId32, + item: ItemId, + to: &AccountId32, + operator: &AccountId32, + ) -> (u32, u32) { + let (collection, item) = create_collection_and_mint_to(&owner.clone(), admin, to, item); + assert_ok!(Nfts::approve_transfer( + RuntimeOrigin::signed(to.clone()), + collection, + Some(item), + operator.clone().into(), + None + )); + (collection, item) + } + + pub(crate) fn create_collection(owner: &AccountId32, admin: &AccountId32) -> CollectionId { + let next_id = next_collection_id(); + assert_ok!(Nfts::create( + RuntimeOrigin::signed(owner.clone()), + admin.clone().into(), + collection_config_with_all_settings_enabled() + )); + next_id + } + + pub(crate) fn mint( + collection: CollectionId, + item: ItemId, + owner: &AccountId32, + to: &AccountId32, + ) -> ItemId { + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(owner.clone()), + collection, + item, + to.clone().into(), + None + )); + item + } + + pub(crate) fn burn(collection: CollectionId, item: ItemId, owner: &AccountId32) { + assert_ok!(Nfts::burn(RuntimeOrigin::signed(owner.clone()), collection, item)); + } + + pub(crate) fn lock_item_transfer(owner: &AccountId32, collection: CollectionId, item: ItemId) { + assert_ok!(Nfts::lock_item_transfer( + RuntimeOrigin::signed(owner.clone()), + collection, + item + )); + } + + pub(crate) fn unlock_item_transfer( + owner: &AccountId32, + collection: CollectionId, + item: ItemId, + ) { + assert_ok!(Nfts::unlock_item_transfer( + RuntimeOrigin::signed(owner.clone()), + collection, + item + )); + } + + pub(crate) fn balance_of(collection: CollectionId, owner: AccountId32) -> u32 { + AccountBalanceOf::::get(collection, owner) + } + + pub(crate) fn max_supply(collection: CollectionId) -> Option { + CollectionConfigOf::::get(collection) + .map(|config| config.max_supply) + .unwrap_or_default() + } + + pub(super) fn collection_config_with_all_settings_enabled( + ) -> pallet_nfts::CollectionConfig { + pallet_nfts::CollectionConfig { + settings: pallet_nfts::CollectionSettings::all_enabled(), + max_supply: None, + mint_settings: pallet_nfts::MintSettings::default(), + } + } + + pub(crate) fn next_collection_id() -> u32 { + NextCollectionIdOf::::get().unwrap_or_default() + } +} diff --git a/pop-api/integration-tests/src/utils.rs b/pop-api/integration-tests/src/utils.rs new file mode 100644 index 00000000..9b2131fd --- /dev/null +++ b/pop-api/integration-tests/src/utils.rs @@ -0,0 +1,34 @@ +use super::*; + +/// Get the last event from pallet contracts. +pub(super) fn last_contract_event() -> Vec { + let events = System::read_events_for_pallet::>(); + let contract_events = events + .iter() + .filter_map(|event| match event { + pallet_contracts::Event::::ContractEmitted { data, .. } => + Some(data.as_slice()), + _ => None, + }) + .collect::>(); + contract_events.last().unwrap().to_vec() +} + +/// Decodes a byte slice into an `AccountId` as defined in `primitives`. +/// +/// This is used to resolve type mismatches between the `AccountId` in the integration tests and the +/// contract environment. +pub(super) fn account_id_from_slice(s: &[u8; 32]) -> pop_api::primitives::AccountId { + pop_api::primitives::AccountId::decode(&mut &s[..]).expect("Should be decoded to AccountId") +} + +pub(super) fn do_bare_call(function: &str, addr: &AccountId32, params: Vec) -> ExecReturnValue { + let function = function_selector(function); + let params = [function, params].concat(); + bare_call(addr.clone(), params, 0).expect("should work") +} + +// TODO - issue #263 - why result.data[1..] +pub(super) fn decoded(result: ExecReturnValue) -> Result { + ::decode(&mut &result.data[1..]).map_err(|_| result) +} diff --git a/pop-api/src/lib.rs b/pop-api/src/lib.rs index b9dbf634..9a4172f8 100644 --- a/pop-api/src/lib.rs +++ b/pop-api/src/lib.rs @@ -12,6 +12,8 @@ use constants::DECODING_FAILED; use ink::env::chain_extension::{ChainExtensionMethod, FromStatusCode}; pub use v0::*; +/// Module providing macros. +pub mod macros; /// Module providing primitives types. pub mod primitives; /// The first version of the API. @@ -73,6 +75,7 @@ mod constants { pub(crate) const ASSETS: u8 = 52; pub(crate) const BALANCES: u8 = 10; pub(crate) const FUNGIBLES: u8 = 150; + pub(crate) const NONFUNGIBLES: u8 = 151; } // Helper method to build a dispatch call or a call to read state. diff --git a/pop-api/src/macros.rs b/pop-api/src/macros.rs new file mode 100644 index 00000000..7a9fd34d --- /dev/null +++ b/pop-api/src/macros.rs @@ -0,0 +1,53 @@ +/// Implements encoding and decoding traits for a wrapper type that represents +/// bitflags. The wrapper type should contain a field of type `$size`, where +/// `$size` is an integer type (e.g., u8, u16, u32) that can represent the bitflags. +/// The `$bitflag_enum` type is the enumeration type that defines the individual bitflags. +/// +/// This macro provides implementations for the following traits: +/// - `MaxEncodedLen`: Calculates the maximum encoded length for the wrapper type. +/// - `Encode`: Encodes the wrapper type using the provided encoding function. +/// - `EncodeLike`: Trait indicating the type can be encoded as is. +/// - `Decode`: Decodes the wrapper type from the input. +/// - `TypeInfo`: Provides type information for the wrapper type. +macro_rules! impl_codec_bitflags { + ($wrapper:ty, $size:ty, $bitflag_enum:ty) => { + impl ink::scale::MaxEncodedLen for $wrapper { + fn max_encoded_len() -> usize { + <$size>::max_encoded_len() + } + } + impl ink::scale::Encode for $wrapper { + fn using_encoded R>(&self, f: F) -> R { + self.0.bits().using_encoded(f) + } + } + impl ink::scale::EncodeLike for $wrapper {} + impl ink::scale::Decode for $wrapper { + fn decode( + input: &mut I, + ) -> ::core::result::Result { + let field = <$size>::decode(input)?; + Ok(Self(BitFlags::from_bits(field as $size).map_err(|_| "invalid value")?)) + } + } + + #[cfg(feature = "std")] + impl ink::scale_info::TypeInfo for $wrapper { + type Identity = Self; + + fn type_info() -> ink::scale_info::Type { + ink::scale_info::Type::builder() + .path(ink::scale_info::Path::new("BitFlags", module_path!())) + .type_params(vec![ink::scale_info::TypeParameter::new( + "T", + Some(ink::scale_info::meta_type::<$bitflag_enum>()), + )]) + .composite( + ink::scale_info::build::Fields::unnamed() + .field(|f| f.ty::<$size>().type_name(stringify!($bitflag_enum))), + ) + } + } + }; +} +pub(crate) use impl_codec_bitflags; diff --git a/pop-api/src/primitives.rs b/pop-api/src/primitives.rs index 2fcb8a95..ce396fc8 100644 --- a/pop-api/src/primitives.rs +++ b/pop-api/src/primitives.rs @@ -3,4 +3,5 @@ pub use pop_primitives::*; // Public due to integration tests crate. pub type AccountId = ::AccountId; -pub(crate) type Balance = ::Balance; +pub type Balance = ::Balance; +pub type BlockNumber = ::BlockNumber; diff --git a/pop-api/src/v0/mod.rs b/pop-api/src/v0/mod.rs index c6153892..2f12f5f4 100644 --- a/pop-api/src/v0/mod.rs +++ b/pop-api/src/v0/mod.rs @@ -8,6 +8,9 @@ use crate::{ /// APIs for fungible tokens. #[cfg(feature = "fungibles")] pub mod fungibles; +/// APIs for nonfungible tokens. +#[cfg(feature = "nonfungibles")] +pub mod nonfungibles; pub(crate) const V0: u8 = 0; diff --git a/pop-api/src/v0/nonfungibles/errors.rs b/pop-api/src/v0/nonfungibles/errors.rs new file mode 100644 index 00000000..c955bf60 --- /dev/null +++ b/pop-api/src/v0/nonfungibles/errors.rs @@ -0,0 +1,35 @@ +//! A set of errors for use in smart contracts that interact with the nonfungibles api. This +//! includes errors compliant to standards. + +use ink::prelude::string::{String, ToString}; + +use super::*; + +/// The PSP34 error. +#[derive(Debug, PartialEq, Eq)] +#[ink::scale_derive(Encode, Decode, TypeInfo)] +pub enum Psp34Error { + /// Custom error type for cases if writer of traits added own restrictions + Custom(String), + /// Returned if owner approves self + SelfApprove, + /// Returned if the caller doesn't have allowance for transferring. + NotApproved, + /// Returned if the owner already own the token. + TokenExists, + /// Returned if the token doesn't exist + TokenNotExists, + /// Returned if safe transfer check fails + SafeTransferCheckFailed(String), +} + +impl From for Psp34Error { + /// Converts a `StatusCode` to a `PSP22Error`. + fn from(value: StatusCode) -> Self { + let encoded = value.0.to_le_bytes(); + match encoded { + // TODO: Handle conversion. + _ => Psp34Error::Custom(value.0.to_string()), + } + } +} diff --git a/pop-api/src/v0/nonfungibles/events.rs b/pop-api/src/v0/nonfungibles/events.rs new file mode 100644 index 00000000..3c409868 --- /dev/null +++ b/pop-api/src/v0/nonfungibles/events.rs @@ -0,0 +1,50 @@ +//! A set of events for use in smart contracts interacting with the nonfungibles API. +//! +//! The `Transfer` and `Approval` events conform to the PSP-34 standard. +//! +//! These events are not emitted by the API itself but can be used in your contracts to +//! track token operations. Be mindful of the costs associated with emitting events. +//! +//! For more details, refer to [ink! events](https://use.ink/basics/events). + +use super::*; + +/// Event emitted when a token transfer occurs. +#[ink::event] +pub struct Transfer { + /// The source of the transfer. `None` when minting. + #[ink(topic)] + pub from: Option, + /// The recipient of the transfer. `None` when burning. + #[ink(topic)] + pub to: Option, + /// The item transferred (or minted/burned). + pub item: ItemId, +} + +/// Event emitted when a token approve occurs. +#[ink::event] +pub struct Approval { + /// The owner providing the allowance. + #[ink(topic)] + pub owner: AccountId, + /// The beneficiary of the allowance. + #[ink(topic)] + pub operator: AccountId, + /// The item which is (dis)approved. `None` for all owner's items. + pub item: Option, + /// Whether allowance is set or removed. + pub approved: bool, +} + +/// Event emitted when an attribute is set for a token. +#[ink::event] +pub struct AttributeSet { + /// The item which attribute is set. + #[ink(topic)] + pub item: ItemId, + /// The key for the attribute. + pub key: Vec, + /// The data for the attribute. + pub data: Vec, +} diff --git a/pop-api/src/v0/nonfungibles/mod.rs b/pop-api/src/v0/nonfungibles/mod.rs new file mode 100644 index 00000000..438b7fa5 --- /dev/null +++ b/pop-api/src/v0/nonfungibles/mod.rs @@ -0,0 +1,300 @@ +//! The `nonfungibles` module provides an API for interacting and managing nonfungible tokens. +//! +//! The API includes the following interfaces: +//! 1. PSP-34 +//! 2. PSP-34 Metadata +//! 3. PSP-22 Mintable & Burnable + +use constants::*; +pub use errors::*; +pub use events::*; +use ink::prelude::vec::Vec; +pub use traits::*; +pub use types::*; + +use crate::{ + constants::NONFUNGIBLES, + primitives::{AccountId, BlockNumber}, + ChainExtensionMethodApi, Result, StatusCode, +}; + +pub mod errors; +pub mod events; +pub mod traits; +pub mod types; + +#[inline] +pub fn total_supply(collection: CollectionId) -> Result { + build_read_state(TOTAL_SUPPLY) + .input::() + .output::, true>() + .handle_error_code::() + .call(&(collection)) +} + +#[inline] +pub fn balance_of(collection: CollectionId, owner: AccountId) -> Result { + build_read_state(BALANCE_OF) + .input::<(CollectionId, AccountId)>() + .output::, true>() + .handle_error_code::() + .call(&(collection, owner)) +} + +#[inline] +pub fn allowance( + collection: CollectionId, + item: Option, + owner: AccountId, + operator: AccountId, +) -> Result { + build_read_state(ALLOWANCE) + .input::<(CollectionId, Option, AccountId, AccountId)>() + .output::, true>() + .handle_error_code::() + .call(&(collection, item, owner, operator)) +} + +#[inline] +pub fn transfer(collection: CollectionId, item: ItemId, to: AccountId) -> Result<()> { + build_dispatch(TRANSFER) + .input::<(CollectionId, ItemId, AccountId)>() + .output::, true>() + .handle_error_code::() + .call(&(collection, item, to)) +} + +#[inline] +pub fn approve( + collection: CollectionId, + item: Option, + operator: AccountId, + approved: bool, +) -> Result<()> { + build_dispatch(APPROVE) + .input::<(CollectionId, Option, AccountId, bool)>() + .output::, true>() + .handle_error_code::() + .call(&(collection, item, operator, approved)) +} + +#[inline] +pub fn owner_of(collection: CollectionId, item: ItemId) -> Result> { + build_read_state(OWNER_OF) + .input::<(CollectionId, ItemId)>() + .output::>, true>() + .handle_error_code::() + .call(&(collection, item)) +} + +#[inline] +pub fn get_attribute( + collection: CollectionId, + item: ItemId, + namespace: AttributeNamespace, + key: Vec, +) -> Result>> { + build_read_state(GET_ATTRIBUTE) + .input::<(CollectionId, ItemId, AttributeNamespace, Vec)>() + .output::>>, true>() + .handle_error_code::() + .call(&(collection, item, namespace, key)) +} + +#[inline] +pub fn create(admin: AccountId, config: CollectionConfig) -> Result<()> { + build_dispatch(CREATE) + .input::<(AccountId, CollectionConfig)>() + .output::, true>() + .handle_error_code::() + .call(&(admin, config))?; + Ok(()) +} + +#[inline] +pub fn destroy(collection: CollectionId, witness: DestroyWitness) -> Result<()> { + build_dispatch(DESTROY) + .input::<(CollectionId, DestroyWitness)>() + .output::, true>() + .handle_error_code::() + .call(&(collection, witness)) +} + +#[inline] +pub fn collection(collection: CollectionId) -> Result> { + build_read_state(COLLECTION) + .input::() + .output::>, true>() + .handle_error_code::() + .call(&(collection)) +} + +#[inline] +pub fn item_metadata(collection: CollectionId, item: ItemId) -> Result>> { + build_read_state(ITEM_METADATA) + .input::<(CollectionId, ItemId)>() + .output::>>, true>() + .handle_error_code::() + .call(&(collection, item)) +} + +#[inline] +pub fn set_attribute( + collection: CollectionId, + item: ItemId, + namespace: AttributeNamespace, + key: Vec, + value: Vec, +) -> Result<()> { + build_dispatch(SET_ATTRIBUTE) + .input::<(CollectionId, Option, AttributeNamespace, Vec, Vec)>() + .output::, true>() + .handle_error_code::() + .call(&(collection, Some(item), namespace, key, value)) +} + +#[inline] +pub fn clear_attribute( + collection: CollectionId, + item: ItemId, + namespace: AttributeNamespace, + key: Vec, +) -> Result<()> { + build_dispatch(CLEAR_ATTRIBUTE) + .input::<(CollectionId, Option, AttributeNamespace, Vec)>() + .output::, true>() + .handle_error_code::() + .call(&(collection, Some(item), namespace, key)) +} + +#[inline] +pub fn set_metadata(collection: CollectionId, item: ItemId, data: Vec) -> Result<()> { + build_dispatch(SET_METADATA) + .input::<(CollectionId, ItemId, Vec)>() + .output::, true>() + .handle_error_code::() + .call(&(collection, item, data)) +} + +#[inline] +pub fn clear_metadata(collection: CollectionId, item: ItemId) -> Result<()> { + build_dispatch(CLEAR_METADATA) + .input::<(CollectionId, ItemId)>() + .output::, true>() + .handle_error_code::() + .call(&(collection, item)) +} + +#[inline] +pub fn approve_item_attributes( + collection: CollectionId, + item: ItemId, + delegate: AccountId, +) -> Result<()> { + build_dispatch(APPROVE_ITEM_ATTRIBUTES) + .input::<(CollectionId, ItemId, AccountId)>() + .output::, true>() + .handle_error_code::() + .call(&(collection, item, delegate)) +} + +#[inline] +pub fn cancel_item_attributes_approval( + collection: CollectionId, + item: ItemId, + delegate: AccountId, + witness: CancelAttributesApprovalWitness, +) -> Result<()> { + build_dispatch(CANCEL_ITEM_ATTRIBUTES_APPROVAL) + .input::<(CollectionId, ItemId, AccountId, CancelAttributesApprovalWitness)>() + .output::, true>() + .handle_error_code::() + .call(&(collection, item, delegate, witness)) +} + +#[inline] +pub fn set_max_supply(collection: CollectionId, max_supply: u32) -> Result<()> { + build_dispatch(SET_MAX_SUPPLY) + .input::<(CollectionId, u32)>() + .output::, true>() + .handle_error_code::() + .call(&(collection, max_supply)) +} + +#[inline] +pub fn mint( + to: AccountId, + collection: CollectionId, + item: ItemId, + witness: MintWitness, +) -> Result<()> { + build_dispatch(MINT) + .input::<(AccountId, CollectionId, ItemId, MintWitness)>() + .output::, true>() + .handle_error_code::() + .call(&(to, collection, item, witness)) +} + +#[inline] +pub fn burn(collection: CollectionId, item: ItemId) -> Result<()> { + build_dispatch(BURN) + .input::<(CollectionId, ItemId)>() + .output::, true>() + .handle_error_code::() + .call(&(collection, item)) +} + +#[inline] +pub fn next_collection_id() -> Result { + build_read_state(NEXT_COLLECTION_ID) + .output::, true>() + .handle_error_code::() + .call(&()) +} + +mod constants { + /// 1. PSP-34 + pub(super) const TOTAL_SUPPLY: u8 = 0; + pub(super) const BALANCE_OF: u8 = 1; + pub(super) const ALLOWANCE: u8 = 2; + pub(super) const TRANSFER: u8 = 3; + pub(super) const APPROVE: u8 = 4; + pub(super) const OWNER_OF: u8 = 5; + + /// 2. PSP-34 Metadata + pub(super) const GET_ATTRIBUTE: u8 = 6; + + /// 3. Management + pub(super) const CREATE: u8 = 7; + pub(super) const DESTROY: u8 = 8; + pub(super) const COLLECTION: u8 = 9; + pub(super) const NEXT_COLLECTION_ID: u8 = 10; + pub(super) const ITEM_METADATA: u8 = 11; + pub(super) const SET_ATTRIBUTE: u8 = 12; + pub(super) const CLEAR_ATTRIBUTE: u8 = 13; + pub(super) const SET_METADATA: u8 = 14; + pub(super) const CLEAR_METADATA: u8 = 15; + pub(super) const APPROVE_ITEM_ATTRIBUTES: u8 = 16; + pub(super) const CANCEL_ITEM_ATTRIBUTES_APPROVAL: u8 = 17; + pub(super) const SET_MAX_SUPPLY: u8 = 18; + + /// 4. PSP-34 Mintable & Burnable + pub(super) const MINT: u8 = 19; + pub(super) const BURN: u8 = 20; +} + +// Helper method to build a dispatch call. +// +// Parameters: +// - 'dispatchable': The index of the dispatchable function within the module. +fn build_dispatch(dispatchable: u8) -> ChainExtensionMethodApi { + crate::v0::build_dispatch(NONFUNGIBLES, dispatchable) +} + +// Helper method to build a call to read state. +// +// Parameters: +// - 'state_query': The index of the runtime state query. +fn build_read_state(state_query: u8) -> ChainExtensionMethodApi { + crate::v0::build_read_state(NONFUNGIBLES, state_query) +} diff --git a/pop-api/src/v0/nonfungibles/traits.rs b/pop-api/src/v0/nonfungibles/traits.rs new file mode 100644 index 00000000..c9e3ad04 --- /dev/null +++ b/pop-api/src/v0/nonfungibles/traits.rs @@ -0,0 +1,85 @@ +//! Traits that can be used by contracts. Including standard compliant traits. + +use core::result::Result; + +use super::*; + +#[ink::trait_definition] +pub trait Psp34 { + /// Returns the collection `Id`. + #[ink(message, selector = 0xffa27a5f)] + fn collection_id(&self) -> ItemId; + + // Returns the current total supply of the NFT. + #[ink(message, selector = 0x628413fe)] + fn total_supply(&self) -> u128; + + /// Returns the amount of items the owner has within a collection. + /// + /// # Parameters + /// - `owner` - The account whose balance is being queried. + #[ink(message, selector = 0xcde7e55f)] + fn balance_of(&self, owner: AccountId) -> u32; + + /// Returns whether the operator is approved by the owner to withdraw `item`. If `item` is + /// `None`, it returns whether the operator is approved to withdraw all owner's items for the + /// given collection. + /// + /// # Parameters + /// * `owner` - The account that owns the item(s). + /// * `operator` - the account that is allowed to withdraw the item(s). + /// * `item` - The item. If `None`, it is regarding all owner's items in collection. + #[ink(message, selector = 0x4790f55a)] + fn allowance(&self, owner: AccountId, operator: AccountId, id: Option) -> bool; + + /// Transfers an owned or approved item to the specified recipient. + /// + /// # Parameters + /// * `to` - The recipient account. + /// * `item` - The item. + /// - `data` - Additional data in unspecified format. + #[ink(message, selector = 0x3128d61b)] + fn transfer(&mut self, to: AccountId, id: ItemId, data: Vec) -> Result<(), Psp34Error>; + + /// Approves operator to withdraw item(s) from the contract's account. + /// + /// # Parameters + /// * `operator` - The account that is allowed to withdraw the item. + /// * `item` - Optional item. `None` means all items owned in the specified collection. + /// * `approved` - Whether the operator is given or removed the right to withdraw the item(s). + #[ink(message, selector = 0x1932a8b0)] + fn approve( + &mut self, + operator: AccountId, + id: Option, + approved: bool, + ) -> Result<(), Psp34Error>; + + /// Returns the owner of an item within a specified collection, if any. + /// + /// # Parameters + /// * `item` - The item. + #[ink(message, selector = 0x1168624d)] + fn owner_of(&self, id: ItemId) -> Option; +} + +#[ink::trait_definition] +pub trait Psp34Metadata { + /// Returns the attribute of `item` for the given `key`. + /// + /// # Parameters + /// * `item` - The item. If `None` the attributes for the collection are queried. + /// * `namespace` - The attribute's namespace. + /// * `key` - The key of the attribute. + #[ink(message, selector = 0xf19d48d1)] + fn get_attribute(&self, id: ItemId, key: Vec) -> Option>; +} + +#[ink::trait_definition] +pub trait Psp34Enumerable { + #[ink(message, selector = 0x3bcfb511)] + fn owners_token_by_index(&self, owner: AccountId, index: u128) -> Result; + + #[ink(message, selector = 0xcd0340d0)] + fn token_by_index(&self, index: u128) -> Result; +} diff --git a/pop-api/src/v0/nonfungibles/types.rs b/pop-api/src/v0/nonfungibles/types.rs new file mode 100644 index 00000000..5cd725ae --- /dev/null +++ b/pop-api/src/v0/nonfungibles/types.rs @@ -0,0 +1,202 @@ +use enumflags2::{bitflags, BitFlags}; + +use super::*; +use crate::{macros::impl_codec_bitflags, primitives::AccountId}; + +pub type ItemId = u32; +pub type CollectionId = u32; +pub(super) type Balance = u32; + +/// Information about a collection. +#[derive(Debug, PartialEq, Eq)] +#[ink::scale_derive(Encode, Decode, TypeInfo)] +pub struct CollectionDetails { + /// Collection's owner. + pub owner: AccountId, + /// The total balance deposited by the owner for all the storage data associated with this + /// collection. Used by `destroy`. + pub owner_deposit: Balance, + /// The total number of outstanding items of this collection. + pub items: u32, + /// The total number of outstanding item metadata of this collection. + pub item_metadatas: u32, + /// The total number of outstanding item configs of this collection. + pub item_configs: u32, + /// The total number of accounts that hold items of the collection. + pub item_holders: u32, + /// The total number of attributes for this collection. + pub attributes: u32, + /// The total number of allowances to spend all items within collections. + pub allowances: u32, +} + +/// Attribute namespaces for non-fungible tokens. +#[derive(Debug, PartialEq, Eq)] +#[ink::scale_derive(Encode, Decode, TypeInfo)] +pub enum AttributeNamespace { + /// An attribute set by collection's owner. + #[codec(index = 1)] + CollectionOwner, + /// An attribute set by item's owner. + #[codec(index = 2)] + ItemOwner, + /// An attribute set by a pre-approved account. + #[codec(index = 3)] + Account(AccountId), +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[ink::scale_derive(Encode, Decode, TypeInfo)] +pub enum MintType { + /// Only an `Issuer` could mint items. + Issuer, + /// Anyone could mint items. + Public, + /// Only holders of items in specified collection could mint new items. + HolderOf(CollectionId), +} + +#[derive(Debug, PartialEq, Eq)] +#[ink::scale_derive(Encode, Decode, TypeInfo)] +pub struct CancelAttributesApprovalWitness { + /// An amount of attributes previously created by account. + pub account_attributes: u32, +} + +/// Witness data for the destroy transactions. +#[derive(Debug, PartialEq, Eq)] +#[ink::scale_derive(Encode, Decode, TypeInfo)] +pub struct DestroyWitness { + /// The total number of items in this collection that have outstanding item metadata. + #[codec(compact)] + pub item_metadatas: u32, + /// The total number of outstanding item configs of this collection. + #[codec(compact)] + pub item_configs: u32, + /// The total number of accounts that hold items of the collection. + #[codec(compact)] + pub item_holders: u32, + /// The total number of attributes for this collection. + #[codec(compact)] + pub attributes: u32, + /// The total number of allowances to spend all items within collections. + #[codec(compact)] + pub allowances: u32, +} + +/// Witness data for items mint transactions. +#[derive(Debug, PartialEq, Eq)] +#[ink::scale_derive(Encode, Decode, TypeInfo)] +pub struct MintWitness { + /// Provide the id of the item in a required collection. + pub owned_item: Option, + /// The price specified in mint settings. + pub mint_price: Option, +} + +/// Collection's configuration. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[ink::scale_derive(Encode, Decode, TypeInfo)] +pub struct CollectionConfig { + /// Collection's settings. + pub settings: CollectionSettings, + /// Collection's max supply. + pub max_supply: Option, + /// Default settings each item will get during the mint. + pub mint_settings: MintSettings, +} + +/// Support for up to 64 user-enabled features on a collection. +#[bitflags] +#[repr(u64)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[ink::scale_derive(Encode, Decode, TypeInfo)] +pub enum CollectionSetting { + /// Items in this collection are transferable. + TransferableItems, + /// The metadata of this collection can be modified. + UnlockedMetadata, + /// Attributes of this collection can be modified. + UnlockedAttributes, + /// The supply of this collection can be modified. + UnlockedMaxSupply, + /// When this isn't set then the deposit is required to hold the items of this collection. + DepositRequired, +} + +/// Wrapper type for `BitFlags` that implements `Codec`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct CollectionSettings(pub BitFlags); + +impl CollectionSettings { + pub fn from_disabled(settings: BitFlags) -> Self { + Self(settings) + } + + #[cfg(feature = "std")] + pub fn all_enabled() -> Self { + Self(BitFlags::EMPTY) + } +} + +impl_codec_bitflags!(CollectionSettings, u64, CollectionSetting); + +/// Support for up to 64 user-enabled features on an item. +#[bitflags] +#[repr(u64)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[ink::scale_derive(Encode, Decode, TypeInfo)] +pub enum ItemSetting { + /// This item is transferable. + Transferable, + /// The metadata of this item can be modified. + UnlockedMetadata, + /// Attributes of this item can be modified. + UnlockedAttributes, +} + +/// Wrapper type for `BitFlags` that implements `Codec`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ItemSettings(pub BitFlags); + +impl ItemSettings { + pub fn all_enabled() -> Self { + Self(BitFlags::EMPTY) + } + + #[cfg(feature = "std")] + pub fn from_disabled(settings: BitFlags) -> Self { + Self(settings) + } +} + +impl_codec_bitflags!(ItemSettings, u64, ItemSetting); + +/// Holds the information about minting. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[ink::scale_derive(Encode, Decode, TypeInfo)] +pub struct MintSettings { + /// Whether anyone can mint or if minters are restricted to some subset. + pub mint_type: MintType, + /// An optional price per mint. + pub price: Option, + /// When the mint starts. + pub start_block: Option, + /// When the mint ends. + pub end_block: Option, + /// Default settings each item will get during the mint. + pub default_item_settings: ItemSettings, +} + +#[cfg(feature = "std")] +impl Default for MintSettings { + fn default() -> Self { + Self { + mint_type: MintType::Issuer, + price: None, + start_block: None, + end_block: None, + default_item_settings: ItemSettings::all_enabled(), + } + } +}