git.delta.rocks / unique-network / refs/commits / 7c4183b4cb3a

difftreelog

feat(rmrk-rpc) nft resources and priorities

Fahrrader2022-05-26parent: #5c5d937.patch.diff
in: master

6 files changed

modifiedpallets/proxy-rmrk-core/src/lib.rsdiffbeforeafterboth
--- a/pallets/proxy-rmrk-core/src/lib.rs
+++ b/pallets/proxy-rmrk-core/src/lib.rs
@@ -486,9 +486,8 @@
         }
     }
 
-    // should this even be here, might displace it to common/nonfungible -- but they did not need it, only rmrk does
     pub fn collection_exists(collection_id: CollectionId) -> bool {
-        <pallet_common::CollectionById<T>>::contains_key(collection_id)
+        <CollectionHandle<T>>::try_get(collection_id).is_ok()
     }
 
     pub fn nft_exists(collection_id: CollectionId, nft_id: TokenId) -> bool {
modifiedpallets/proxy-rmrk-core/src/misc.rsdiffbeforeafterboth
--- a/pallets/proxy-rmrk-core/src/misc.rs
+++ b/pallets/proxy-rmrk-core/src/misc.rs
@@ -26,18 +26,6 @@
     }
 }
 
-pub trait RmrkRebind<T, S> {
-    fn rebind(&self) -> BoundedVec<u8, S>;
-}
-
-impl<T, S> RmrkRebind<T, S> for BoundedVec<u8, T> where BoundedVec<u8, S>: TryFrom<Vec<u8>> {
-    fn rebind(&self) -> BoundedVec<u8, S> {
-        BoundedVec::<u8, S>::try_from(
-            self.clone().into_inner()
-        ).unwrap_or_default()
-    }
-}
-
 #[derive(Encode, Decode, PartialEq, Eq)]
 pub enum CollectionType {
     Regular,
modifiedprimitives/data-structs/src/lib.rsdiffbeforeafterboth
--- a/primitives/data-structs/src/lib.rs
+++ b/primitives/data-structs/src/lib.rs
@@ -49,6 +49,7 @@
 	},
 	NftChild as RmrkNftChild, AccountIdOrCollectionNftTuple as RmrkAccountIdOrCollectionNftTuple,
 	FixedPart as RmrkFixedPart, SlotPart as RmrkSlotPart, EquippableList as RmrkEquippableList,
+	BasicResource as RmrkBasicResource, ComposableResource as RmrkComposableResource, SlotResource as RmrkSlotResource,
 };
 
 mod bounded;
@@ -925,17 +926,14 @@
 	}
 }
 
-pub type RmrkCollectionSymbol = BoundedVec<u8, RmrkCollectionSymbolLimit>;
 pub type RmrkCollectionInfo<AccountId> =
 	CollectionInfo<RmrkString, RmrkCollectionSymbol, AccountId>;
 pub type RmrkInstanceInfo<AccountId> = NftInfo<AccountId, Permill, RmrkString>;
 pub type RmrkResourceInfo = ResourceInfo<
-	BoundedVec<u8, RmrkResourceSymbolLimit>,
+	RmrkBoundedResource,
 	RmrkString,
-	BoundedVec<RmrkPartId, RmrkPartsLimit>,
+	RmrkBoundedParts,
 >;
-pub type RmrkKeyString = BoundedVec<u8, RmrkKeyLimit>;
-pub type RmrkValueString = BoundedVec<u8, RmrkValueLimit>;
 pub type RmrkPropertyInfo = PropertyInfo<RmrkKeyString, RmrkValueString>;
 pub type RmrkBaseInfo<AccountId> = BaseInfo<AccountId, RmrkString>;
 pub type RmrkPartType =
@@ -943,6 +941,13 @@
 pub type RmrkThemeProperty = ThemeProperty<RmrkString>;
 pub type RmrkTheme = Theme<RmrkString, Vec<RmrkThemeProperty>>;
 
+pub type RmrkCollectionSymbol = BoundedVec<u8, RmrkCollectionSymbolLimit>;
+pub type RmrkKeyString = BoundedVec<u8, RmrkKeyLimit>;
+pub type RmrkValueString = BoundedVec<u8, RmrkValueLimit>;
+
+type RmrkBoundedResource = BoundedVec<u8, RmrkResourceSymbolLimit>;
+type RmrkBoundedParts = BoundedVec<RmrkPartId, RmrkPartsLimit>;
+
 pub type RmrkRpcString = Vec<u8>;
 pub type RmrkThemeName = RmrkRpcString;
 pub type RmrkPropertyKey = RmrkRpcString;
modifiedprimitives/data-structs/src/rmrk.rsdiffbeforeafterboth
--- a/primitives/data-structs/src/rmrk.rs
+++ b/primitives/data-structs/src/rmrk.rs
@@ -154,7 +154,7 @@
 	pub value: BoundedValue,
 }
 
-#[derive(Encode, Decode, Eq, PartialEq, Clone, Debug, TypeInfo, MaxEncodedLen)]
+#[derive(Encode, Decode, Default, Eq, PartialEq, Clone, Debug, TypeInfo, MaxEncodedLen)]
 #[cfg_attr(feature = "std", derive(Serialize))]
 #[cfg_attr(
 	feature = "std",
@@ -275,7 +275,7 @@
 	pub thumb: Option<BoundedString>,
 }
 
-#[derive(Encode, Decode, Eq, PartialEq, Clone, Debug, TypeInfo, MaxEncodedLen)]
+#[derive(Encode, Decode, Derivative, Eq, PartialEq, Clone, Debug, TypeInfo, MaxEncodedLen)]
 #[cfg_attr(feature = "std", derive(Serialize))]
 #[cfg_attr(
 	feature = "std",
@@ -286,7 +286,9 @@
 		"#
 	)
 )]
-pub enum ResourceTypes<BoundedString, BoundedParts> {
+#[derivative(Default(bound=""))]
+pub enum ResourceTypes<BoundedString: Default, BoundedParts> {
+	#[derivative(Default)]
 	Basic(BasicResource<BoundedString>),
 	Composable(ComposableResource<BoundedString, BoundedParts>),
 	Slot(SlotResource<BoundedString>),
@@ -305,7 +307,7 @@
 		"#
 	)
 )]
-pub struct ResourceInfo<BoundedResource, BoundedString, BoundedParts> {
+pub struct ResourceInfo<BoundedResource, BoundedString: Default, BoundedParts> {
 	/// id is a 5-character string of reasonable uniqueness.
 	/// The combination of base ID and resource id should be unique across the entire RMRK
 	/// ecosystem which
modifiedruntime/common/src/runtime_apis.rsdiffbeforeafterboth
--- a/runtime/common/src/runtime_apis.rs
+++ b/runtime/common/src/runtime_apis.rs
@@ -146,9 +146,7 @@
                 }
 
                 fn collection_by_id(collection_id: RmrkCollectionId) -> Result<Option<RmrkCollectionInfo<AccountId>>, DispatchError> {
-                    use frame_support::BoundedVec;
-                    use scale_info::prelude::string::String;
-                    use pallet_proxy_rmrk_core::{RmrkProperty, misc::{CollectionType, RmrkRebind, RmrkDecode}};
+                    use pallet_proxy_rmrk_core::{RmrkProperty, misc::{CollectionType, RmrkDecode}};
 
                     let collection_id = CollectionId(collection_id);
                     let collection = match RmrkCore::get_typed_nft_collection(collection_id, CollectionType::Regular) {
@@ -156,20 +154,18 @@
                         Err(_) => return Ok(None),
                     };
 
-                    let nfts_count = (dispatch_unique_runtime!(collection_id.total_supply()) as Result<u32, DispatchError>)?;
-                    //<Runtime as up_rpc::UniqueApi>::total_supply(collection_id); // todo can't find UniqueApi with disabled default features
+                    let nfts_count = dispatch_unique_runtime!(collection_id.total_supply())?;
 
                     Ok(Some(RmrkCollectionInfo {
                         issuer: collection.owner.clone(),
                         metadata: RmrkCore::get_collection_property(collection_id, RmrkProperty::Metadata)?.decode_or_default(),
                         max: collection.limits.token_limit,
-                        symbol: collection.token_prefix.rebind(), // change
+                        symbol: collection.token_prefix.decode_or_default(),
                         nfts_count
                     }))
                 }
 
                 fn nft_by_id(collection_id: RmrkCollectionId, nft_by_id: RmrkNftId) -> Result<Option<RmrkInstanceInfo<AccountId>>, DispatchError> {
-                    use frame_support::BoundedVec;
                     use up_data_structs::mapping::TokenAddressMapping;
                     use pallet_proxy_rmrk_core::{RmrkProperty, misc::RmrkDecode};
 
@@ -177,7 +173,7 @@
                     let nft_id = TokenId(nft_by_id);
                     if !RmrkCore::nft_exists(collection_id, nft_id) { return Ok(None); }
 
-                    let owner = match (dispatch_unique_runtime!(collection_id.token_owner(nft_id)) as Result<Option<CrossAccountId>, DispatchError>)? {
+                    let owner = match dispatch_unique_runtime!(collection_id.token_owner(nft_id))? {
                         Some(owner) => match <Runtime as pallet_common::Config>::CrossTokenAddressMapping::address_to_token(&owner) {
                             Some((col, tok)) => RmrkAccountIdOrCollectionNftTuple::CollectionAndNftTuple(col.0, tok.0),
                             None => RmrkAccountIdOrCollectionNftTuple::AccountId(owner.as_sub().clone())
@@ -197,16 +193,17 @@
                 }
 
                 fn account_tokens(account_id: AccountId, collection_id: RmrkCollectionId) -> Result<Vec<RmrkNftId>, DispatchError> {
+                    use pallet_proxy_rmrk_core::misc::CollectionType;
+
                     let cross_account_id = CrossAccountId::from_sub(account_id);
                     let collection_id = CollectionId(collection_id);
-                    if !RmrkCore::collection_exists(collection_id) { return Ok(Vec::new()); }
+                    if RmrkCore::ensure_collection_type(collection_id, CollectionType::Regular).is_err() { return Ok(Vec::new()); }
 
                     Ok(
-                        (dispatch_unique_runtime!(collection_id.account_tokens(cross_account_id)) as Result<Vec<TokenId>, DispatchError>)?
-                        //<Runtime as up_rpc::UniqueApi<Block, CrossAccountId, AccountId>>::account_tokens(collection_id, cross_account_id)?
+                        dispatch_unique_runtime!(collection_id.account_tokens(cross_account_id))?
                             .into_iter()
                             .map(|token| token.0)
-                            .collect::<Vec<_>>()
+                            .collect()
                     )
                 }
 
@@ -226,8 +223,7 @@
                             .map(|(child_id, _)| RmrkNftChild {
                                 collection_id: collection_id.0, // todo make sure they're always from this collection // spoiler: they're not
                                 nft_id: child_id.0,
-                            })
-                            .collect()
+                            }).collect()
                     )
                 }
 
@@ -277,28 +273,70 @@
 
                 fn nft_resources(collection_id: RmrkCollectionId, nft_id: RmrkNftId) -> Result<Vec<RmrkResourceInfo>, DispatchError> {
                     use frame_support::BoundedVec;
-                    use pallet_proxy_rmrk_core::{RmrkProperty, misc::{CollectionType, NftType}};
+                    use pallet_proxy_rmrk_core::{RmrkProperty, misc::{CollectionType, NftType, RmrkDecode}};
 
                     let collection_id = CollectionId(collection_id);
-                    if RmrkCore::ensure_collection_type(collection_id, CollectionType::Resource).is_err() { return Ok(Vec::new()); }
+                    if !RmrkCore::collection_exists(collection_id) { return Ok(Vec::new()); } // todo make sure the collection type doesn't matter
 
                     let nft_id = TokenId(nft_id);
                     if RmrkCore::ensure_nft_type(collection_id, nft_id, NftType::Resource).is_err() { return Ok(Vec::new()); }
 
-                    Ok(Vec::new(/*[RmrkResourceInfo {
+                    let resource_collection_id: CollectionId = RmrkCore::get_nft_property(collection_id, nft_id, RmrkProperty::ResourceCollection)
+                        .unwrap()
+                        .decode_or_default();
+                    if RmrkCore::ensure_collection_type(collection_id, CollectionType::Resource).is_err() { return Ok(Vec::new()); }
 
-                    }]*/))
+                    let resources = pallet_nonfungible::TokenProperties::<Runtime>::iter_prefix((resource_collection_id,))
+                        .filter_map(|(resource_id, properties)| Some(RmrkResourceInfo {
+                            id: BoundedVec::default(), // todo ResourceId property
+                            pending: RmrkCore::get_nft_property(resource_collection_id, resource_id, RmrkProperty::PendingResourceAccept).unwrap().decode_or_default(),
+                            pending_removal: RmrkCore::get_nft_property(resource_collection_id, resource_id, RmrkProperty::PendingResourceRemoval).unwrap().decode_or_default(),
+                            resource: RmrkCore::get_nft_property(resource_collection_id, resource_id, RmrkProperty::ResourceType).unwrap().decode_or_default(),/* {
+                                RmrkResourceTypes::Basic(resource) => RmrkResourceTypes::Basic(),/*(RmrkBasicResource {
+                                    src: RmrkCore::get_nft_property_inner(properties, RmrkProperty::Src).unwrap().decode_or_default(),
+                                    metadata: RmrkCore::get_nft_property_inner(properties, RmrkProperty::Metadata).unwrap().decode_or_default(),
+                                    license: RmrkCore::get_nft_property_inner(properties, RmrkProperty::License).unwrap().decode_or_default(),
+                                    thumb: RmrkCore::get_nft_property_inner(properties, RmrkProperty::Thumb).unwrap().decode_or_default(),
+                                },*///BasicResource<BoundedString>)
+                                _ => todo!(), //RmrkResourceTypes::Composable(ComposableResource<BoundedString, BoundedParts>),
+                                //RmrkResourceTypes::Slot(SlotResource<BoundedString>),
+                            },*/
+                        }))
+                        .collect();
+
+                    Ok(resources)
                 }
 
                 fn nft_resource_priorities(collection_id: RmrkCollectionId, nft_id: RmrkNftId) -> Result<Vec<RmrkResourceId>, DispatchError> {
-                    todo!()
+                    use pallet_proxy_rmrk_core::{RmrkProperty, misc::{CollectionType, NftType, RmrkDecode}};
+
+                    let collection_id = CollectionId(collection_id);
+                    if !RmrkCore::collection_exists(collection_id) { return Ok(Vec::new()); } // todo ensure the collection type doesn't matter
+
+                    let nft_id = TokenId(nft_id);
+                    if RmrkCore::ensure_nft_type(collection_id, nft_id, NftType::Resource).is_err() { return Ok(Vec::new()); }
+
+                    /*let resource_collection_id: CollectionId = RmrkCore::get_nft_property(collection_id, nft_id, RmrkProperty::ResourceCollection)
+                        .unwrap()
+                        .decode_or_default();
+                    if RmrkCore::ensure_collection_type(collection_id, CollectionType::Resource).is_err() { return Ok(Vec::new()); }
+
+                    let resources = pallet_nonfungible::TokenProperties::<Runtime>::iter_prefix((resource_collection_id,))
+                        .filter_map(|(resource_id, properties)| Some((
+                            resource_id, // ResourceId property
+                            RmrkCore::get_nft_property(resource_collection_id, resource_id, RmrkProperty::Priority).unwrap().decode_or_default(),
+                        )))
+                        .collect()
+                        .sort_by_key(|(_, index)| *index)
+                        .into_iter().map(|(resource_id, _)| resource_id)*/
+                    let priorities = RmrkCore::get_nft_property(collection_id, nft_id, RmrkProperty::ResourcePriorities)?.decode_or_default();
+
+                    Ok(priorities)
                 }
 
                 fn base(base_id: RmrkBaseId) -> Result<Option<RmrkBaseInfo<AccountId>>, DispatchError> {
-                    use frame_support::BoundedVec;
-                    use scale_info::prelude::string::String;
                     use pallet_proxy_rmrk_core::{
-                        RmrkProperty, misc::{CollectionType, RmrkRebind, RmrkDecode},
+                        RmrkProperty, misc::{CollectionType, RmrkDecode},
                     };
 
                     let collection_id = CollectionId(base_id);
@@ -310,18 +348,17 @@
                     Ok(Some(RmrkBaseInfo {
                         issuer: collection.owner.clone(),
                         base_type: RmrkCore::get_collection_property(collection_id, RmrkProperty::BaseType)?.decode_or_default(),
-                        symbol: collection.token_prefix.rebind(),
+                        symbol: collection.token_prefix.decode_or_default(),
                     }))
                 }
 
                 fn base_parts(base_id: RmrkBaseId) -> Result<Vec<RmrkPartType>, DispatchError> {
-                    use frame_support::BoundedVec;
                     use pallet_proxy_rmrk_core::{RmrkProperty, misc::{CollectionType, NftType, RmrkDecode}};
 
                     let collection_id = CollectionId(base_id);
                     if RmrkCore::ensure_collection_type(collection_id, CollectionType::Base).is_err() { return Ok(Vec::new()); }
 
-                    let parts = (dispatch_unique_runtime!(collection_id.collection_tokens()))?
+                    let parts = dispatch_unique_runtime!(collection_id.collection_tokens())?
                         .into_iter()
                         .filter_map(|token_id| {
                             let nft_type = RmrkCore::get_nft_type(collection_id, token_id).ok()?;
@@ -347,7 +384,6 @@
                 }
 
                 fn theme_names(base_id: RmrkBaseId) -> Result<Vec<RmrkThemeName>, DispatchError> {
-                    use frame_support::BoundedVec;
                     use pallet_proxy_rmrk_core::{RmrkProperty, misc::{CollectionType, RmrkDecode}};
 
                     let collection_id = CollectionId(base_id);
@@ -355,7 +391,7 @@
                         return Ok(Vec::new());
                     }
 
-                    let theme_names = (dispatch_unique_runtime!(collection_id.collection_tokens()))?
+                    let theme_names = dispatch_unique_runtime!(collection_id.collection_tokens())?
                         .iter()
                         .filter_map(|token_id| {
                             let nft_type = RmrkCore::get_nft_type(collection_id, *token_id).unwrap();
@@ -373,7 +409,6 @@
                 }
 
                 fn theme(base_id: RmrkBaseId, theme_name: RmrkThemeName, filter_keys: Option<Vec<RmrkPropertyKey>>) -> Result<Option<RmrkTheme>, DispatchError> {
-                    use frame_support::BoundedVec;
                     use pallet_proxy_rmrk_core::{
                         RmrkProperty,
                         misc::{CollectionType, NftType, RmrkDecode}
@@ -384,7 +419,7 @@
                         return Ok(None);
                     }
 
-                    let theme_info = (dispatch_unique_runtime!(collection_id.collection_tokens()))?
+                    let theme_info = dispatch_unique_runtime!(collection_id.collection_tokens())?
                         .into_iter()
                         .find_map(|token_id| {
                             RmrkCore::ensure_nft_type(collection_id, token_id, NftType::Theme).ok()?;
modifiedtests/src/nesting/properties.test.tsdiffbeforeafterboth
after · tests/src/nesting/properties.test.ts
1import {expect} from 'chai';2import privateKey from '../substrate/privateKey';3import usingApi, {executeTransaction} from '../substrate/substrate-api';4import {5  addCollectionAdminExpectSuccess,6  createCollectionExpectSuccess,7  createItemExpectSuccess,8  getCreateCollectionResult,9  transferExpectSuccess,10} from '../util/helpers';11import {IKeyringPair} from '@polkadot/types/types';1213let alice: IKeyringPair;14let bob: IKeyringPair;15let charlie: IKeyringPair;1617describe('Composite Properties Test', () => {18  before(async () => {19    await usingApi(async () => {20      alice = privateKey('//Alice');21      bob = privateKey('//Bob');22    });23  });2425  it('Makes sure collectionById supplies required fields', async () => {26    await usingApi(async api => {27      const collectionId = await createCollectionExpectSuccess();2829      const collectionOption = await api.rpc.unique.collectionById(collectionId);30      expect(collectionOption.isSome).to.be.true;31      let collection = collectionOption.unwrap();32      expect(collection.tokenPropertyPermissions.toHuman()).to.be.empty;33      expect(collection.properties.toHuman()).to.be.empty;3435      const propertyPermissions = [36        {key: 'mindgame', permission: {collectionAdmin: true, mutable: false, tokenOwner: true}},37        {key: 'skullduggery', permission: {collectionAdmin: false, mutable: true, tokenOwner: false}},38      ];39      await expect(executeTransaction(40        api, 41        alice, 42        api.tx.unique.setPropertyPermissions(collectionId, propertyPermissions), 43      )).to.not.be.rejected;4445      const collectionProperties = [46        {key: 'black_hole', value: 'LIGO'},47        {key: 'electron', value: 'come bond'}, 48      ];49      await expect(executeTransaction(50        api, 51        alice, 52        api.tx.unique.setCollectionProperties(collectionId, collectionProperties), 53      )).to.not.be.rejected;5455      collection = (await api.rpc.unique.collectionById(collectionId)).unwrap();56      expect(collection.tokenPropertyPermissions.toHuman()).to.be.deep.equal(propertyPermissions);57      expect(collection.properties.toHuman()).to.be.deep.equal(collectionProperties);58    });59  });60});6162// ---------- COLLECTION PROPERTIES6364describe('Integration Test: Collection Properties', () => {65  before(async () => {66    await usingApi(async () => {67      alice = privateKey('//Alice');68      bob = privateKey('//Bob');69    });70  });7172  it('Reads properties from a collection', async () => {73    await usingApi(async api => {74      const collection = await createCollectionExpectSuccess();75      const properties = (await api.query.common.collectionProperties(collection)).toJSON();76      expect(properties.map).to.be.empty;77      expect(properties.consumedSpace).to.equal(0);78    });79  });8081  it('Sets properties for a collection', async () => {82    await usingApi(async api => {83      const events = await executeTransaction(api, bob, api.tx.unique.createCollectionEx({mode: 'NFT'}));84      const {collectionId} = getCreateCollectionResult(events);8586      // As owner87      await expect(executeTransaction(88        api, 89        bob, 90        api.tx.unique.setCollectionProperties(collectionId, [{key: 'electron', value: 'come bond'}]), 91      )).to.not.be.rejected;9293      await addCollectionAdminExpectSuccess(bob, collectionId, alice.address);9495      // As administrator96      await expect(executeTransaction(97        api, 98        alice, 99        api.tx.unique.setCollectionProperties(collectionId, [{key: 'black_hole'}]), 100      )).to.not.be.rejected;101102      const properties = (await api.rpc.unique.collectionProperties(collectionId, ['electron', 'black_hole'])).toHuman();103      expect(properties).to.be.deep.equal([104        {key: 'electron', value: 'come bond'},105        {key: 'black_hole', value: ''},106      ]);107    });108  });109110  it('Changes properties of a collection', async () => {111    await usingApi(async api => {112      const collection = await createCollectionExpectSuccess();113114      await expect(executeTransaction(115        api, 116        alice, 117        api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black_hole'}]), 118      )).to.not.be.rejected;119120      // Mutate the properties121      await expect(executeTransaction(122        api, 123        alice, 124        api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'bonded'}, {key: 'black_hole', value: 'LIGO'}]), 125      )).to.not.be.rejected;126127      const properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black_hole'])).toHuman();128      expect(properties).to.be.deep.equal([129        {key: 'electron', value: 'bonded'},130        {key: 'black_hole', value: 'LIGO'},131      ]);132    });133  });134135  it('Deletes properties of a collection', async () => {136    await usingApi(async api => {137      const collection = await createCollectionExpectSuccess();138139      await expect(executeTransaction(140        api, 141        alice, 142        api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: 'LIGO'}]), 143      )).to.not.be.rejected;144145      await expect(executeTransaction(146        api, 147        alice, 148        api.tx.unique.deleteCollectionProperties(collection, ['electron']), 149      )).to.not.be.rejected;150151      const properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black_hole'])).toHuman();152      expect(properties).to.be.deep.equal([153        {key: 'black_hole', value: 'LIGO'},154      ]);155    });156  });157});158159describe('Negative Integration Test: Collection Properties', () => {160  before(async () => {161    await usingApi(async () => {162      alice = privateKey('//Alice');163      bob = privateKey('//Bob');164    });165  });166  167  it('Fails to set properties in a collection if not its onwer/administrator', async () => {168    await usingApi(async api => {169      const collection = await createCollectionExpectSuccess();170171      await expect(executeTransaction(172        api, 173        bob, 174        api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: 'LIGO'}]), 175      )).to.be.rejectedWith(/common\.NoPermission/);176  177      const properties = (await api.query.common.collectionProperties(collection)).toJSON();178      expect(properties.map).to.be.empty;179      expect(properties.consumedSpace).to.equal(0);180    });181  });182  183  it('Fails to set properties that exceed the limits', async () => {184    await usingApi(async api => {185      const collection = await createCollectionExpectSuccess();186      const spaceLimit = (await api.query.common.collectionProperties(collection)).toJSON().spaceLimit as number; 187188      // Mute the general tx parsing error, too many bytes to process189      {190        console.error = () => {};191        await expect(executeTransaction(192          api, 193          alice, 194          api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'low high '.repeat(Math.ceil(spaceLimit! / 9))}]), 195        )).to.be.rejected;196      }197198      let properties = (await api.rpc.unique.collectionProperties(collection, ['electron'])).toJSON();199      expect(properties).to.be.empty;200201      await expect(executeTransaction(202        api, 203        alice, 204        api.tx.unique.setCollectionProperties(collection, [205          {key: 'electron', value: 'low high '.repeat(Math.ceil(spaceLimit! / 18))}, 206          {key: 'black_hole', value: '0'.repeat(Math.ceil(spaceLimit! / 2))}, 207        ]), 208      )).to.be.rejectedWith(/common\.NoSpaceForProperty/);209210      properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black hole'])).toJSON();211      expect(properties).to.be.empty;212    });213  });214  215  it('Fails to set more properties than it is allowed', async () => {216    await usingApi(async api => {217      const collection = await createCollectionExpectSuccess();218219      const propertiesToBeSet = [];220      for (let i = 0; i < 65; i++) {221        propertiesToBeSet.push({222          key: 'electron_' + i,223          value: Math.random() > 0.5 ? 'high' : 'low',224        });225      }226227      await expect(executeTransaction(228        api, 229        alice, 230        api.tx.unique.setCollectionProperties(collection, propertiesToBeSet), 231      )).to.be.rejectedWith(/common\.PropertyLimitReached/);232233      const properties = (await api.query.common.collectionProperties(collection)).toJSON();234      expect(properties.map).to.be.empty;235      expect(properties.consumedSpace).to.equal(0);236    });237  });238239  it('Fails to set properties with invalid names', async () => {240    await usingApi(async api => {241      const collection = await createCollectionExpectSuccess();242243      const invalidProperties = [244        [{key: 'electron', value: 'negative'}, {key: 'string theory', value: 'understandable'}],245        [{key: 'Mr.Sandman', value: 'Bring me a gene'}],246        [{key: 'déjà vu', value: 'hmm...'}],247      ];248249      for (let i = 0; i < invalidProperties.length; i++) {250        await expect(executeTransaction(251          api, 252          alice, 253          api.tx.unique.setCollectionProperties(collection, invalidProperties[i]), 254        ), `on rejecting the new badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);255      }256257      await expect(executeTransaction(258        api, 259        alice, 260        api.tx.unique.setCollectionProperties(collection, [{key: '', value: 'nothing must not exist'}]), 261      ), 'on rejecting an unnamed property').to.be.rejectedWith(/common\.EmptyPropertyKey/);262263      await expect(executeTransaction(264        api, 265        alice, 266        api.tx.unique.setCollectionProperties(collection, [267          {key: 'CRISPR-Cas9', value: 'rewriting nature!'},268        ]), 269      ), 'on setting the correctly-but-still-badly-named property').to.not.be.rejected;270271      const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat('CRISPR-Cas9').concat('');272273      const properties = (await api.rpc.unique.collectionProperties(collection, keys)).toHuman();274      expect(properties).to.be.deep.equal([275        {key: 'CRISPR-Cas9', value: 'rewriting nature!'},276      ]);277278      for (let i = 0; i < invalidProperties.length; i++) {279        await expect(executeTransaction(280          api, 281          alice, 282          api.tx.unique.deleteCollectionProperties(collection, invalidProperties[i].map(propertySet => propertySet.key)), 283        ), `on trying to delete the non-existent badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);284      }285    });286  });287});288289// ---------- ACCESS RIGHTS290291describe('Integration Test: Access Rights to Token Properties', () => {292  before(async () => {293    await usingApi(async () => {294      alice = privateKey('//Alice');295      bob = privateKey('//Bob');296    });297  });298  299  it('Reads access rights to properties of a collection', async () => {300    await usingApi(async api => {301      const collection = await createCollectionExpectSuccess();302      const propertyRights = (await api.query.common.collectionPropertyPermissions(collection)).toJSON();303      expect(propertyRights).to.be.empty;304    });305  });306  307  it('Sets access rights to properties of a collection', async () => {308    await usingApi(async api => {309      const collection = await createCollectionExpectSuccess();310311      await expect(executeTransaction(312        api, 313        alice, 314        api.tx.unique.setPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true}}]), 315      )).to.not.be.rejected;316317      await addCollectionAdminExpectSuccess(alice, collection, bob.address);318319      await expect(executeTransaction(320        api, 321        alice, 322        api.tx.unique.setPropertyPermissions(collection, [{key: 'mindgame', permission: {collectionAdmin: true, tokenOwner: false}}]), 323      )).to.not.be.rejected;324325      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery', 'mindgame'])).toHuman();326      expect(propertyRights).to.be.deep.equal([327        {key: 'skullduggery', permission: {'mutable': true, 'collectionAdmin': false, 'tokenOwner': false}},328        {key: 'mindgame', permission: {'mutable': false, 'collectionAdmin': true, 'tokenOwner': false}},329      ]);330    });331  });332  333  it('Changes access rights to properties of a collection', async () => {334    await usingApi(async api => {335      const collection = await createCollectionExpectSuccess();336337      await expect(executeTransaction(338        api, 339        alice, 340        api.tx.unique.setPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true, collectionAdmin: true}}]), 341      )).to.not.be.rejected;342343      await expect(executeTransaction(344        api, 345        alice, 346        api.tx.unique.setPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]), 347      )).to.not.be.rejected;348349      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toHuman();350      expect(propertyRights).to.be.deep.equal([351        {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}},352      ]);353    });354  });355});356357describe('Negative Integration Test: Access Rights to Token Properties', () => {358  before(async () => {359    await usingApi(async () => {360      alice = privateKey('//Alice');361      bob = privateKey('//Bob');362    });363  });364365  it('Prevents from setting access rights to properties of a collection if not an onwer/admin', async () => {366    await usingApi(async api => {367      const collection = await createCollectionExpectSuccess();368369      await expect(executeTransaction(370        api, 371        bob, 372        api.tx.unique.setPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true, tokenOwner: true}}]), 373      )).to.be.rejectedWith(/common\.NoPermission/);374375      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toJSON();376      expect(propertyRights).to.be.empty;377    });378  });379380  it('Prevents from adding too many possible properties', async () => {381    await usingApi(async api => {382      const collection = await createCollectionExpectSuccess();383384      const constitution = [];385      for (let i = 0; i < 65; i++) {386        constitution.push({387          key: 'property_' + i,388          permission: Math.random() > 0.5 ? {mutable: true, collectionAdmin: true, tokenOwner: true} : {},389        });390      }391392      await expect(executeTransaction(393        api, 394        alice, 395        api.tx.unique.setPropertyPermissions(collection, constitution), 396      )).to.be.rejectedWith(/common\.PropertyLimitReached/);397398      const propertyRights = (await api.query.common.collectionPropertyPermissions(collection)).toJSON();399      expect(propertyRights).to.be.empty;400    });401  });402403  it('Prevents access rights to be modified if constant', async () => {404    await usingApi(async api => {405      const collection = await createCollectionExpectSuccess();406407      await expect(executeTransaction(408        api, 409        alice, 410        api.tx.unique.setPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]), 411      )).to.not.be.rejected;412413      await expect(executeTransaction(414        api, 415        alice, 416        api.tx.unique.setPropertyPermissions(collection, [{key: 'skullduggery', permission: {}}]), 417      )).to.be.rejectedWith(/common\.NoPermission/);418419      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toHuman();420      expect(propertyRights).to.deep.equal([421        {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}},422      ]);423    });424  });425426  it('Prevents adding properties with invalid names', async () => {427    await usingApi(async api => {428      const collection = await createCollectionExpectSuccess();429430      const invalidProperties = [431        [{key: 'skullduggery', permission: {tokenOwner: true}}, {key: 'im possible', permission: {collectionAdmin: true}}],432        [{key: 'G#4', permission: {tokenOwner: true}}],433        [{key: 'HÆMILTON', permission: {mutable: false, collectionAdmin: true, tokenOwner: true}}],434      ];435436      for (let i = 0; i < invalidProperties.length; i++) {437        await expect(executeTransaction(438          api, 439          alice, 440          api.tx.unique.setPropertyPermissions(collection, invalidProperties[i]), 441        ), `on setting the new badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);442      }443444      await expect(executeTransaction(445        api, 446        alice, 447        api.tx.unique.setPropertyPermissions(collection, [{key: '', permission: {}}]), 448      ), 'on rejecting an unnamed property').to.be.rejectedWith(/common\.EmptyPropertyKey/);449450      const correctKey = '--0x03116e387820CA05'; // PolkadotJS would parse this as an already encoded hex-string451      await expect(executeTransaction(452        api, 453        alice, 454        api.tx.unique.setPropertyPermissions(collection, [455          {key: correctKey, permission: {collectionAdmin: true}},456        ]), 457      ), 'on setting the correctly-but-still-badly-named property').to.not.be.rejected;458459      const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat(correctKey).concat('');460461      const propertyRights = (await api.rpc.unique.propertyPermissions(collection, keys)).toHuman();462      expect(propertyRights).to.be.deep.equal([463        {key: correctKey, permission: {mutable: false, collectionAdmin: true, tokenOwner: false}},464      ]);465    });466  });467});468469// ---------- TOKEN PROPERTIES470471describe('Integration Test: Token Properties', () => {472  let collection: number;473  let token: number;474  let permissions: {permission: any, signers: IKeyringPair[]}[];475476  before(async () => {477    await usingApi(async () => {478      alice = privateKey('//Alice');479      bob = privateKey('//Bob');480      charlie = privateKey('//Charlie');481482      permissions = [483        {permission: {mutable: true, collectionAdmin: true}, signers: [alice, bob]},484        {permission: {mutable: false, collectionAdmin: true}, signers: [alice, bob]},485        {permission: {mutable: true, tokenOwner: true}, signers: [charlie]},486        {permission: {mutable: false, tokenOwner: true}, signers: [charlie]},487        {permission: {mutable: true, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie]},488        {permission: {mutable: false, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie]},489      ];490    });491  });492493  beforeEach(async () => {494    await usingApi(async () => {495      collection = await createCollectionExpectSuccess();496      token = await createItemExpectSuccess(alice, collection, 'NFT');497      await addCollectionAdminExpectSuccess(alice, collection, bob.address);498      await transferExpectSuccess(collection, token, alice, charlie);499    });500  });501  502  it('Reads yet empty properties of a token', async () => {503    await usingApi(async api => {504      const collection = await createCollectionExpectSuccess();505      const token = await createItemExpectSuccess(alice, collection, 'NFT');506  507      const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();508      expect(properties.map).to.be.empty;509      expect(properties.consumedSpace).to.be.equal(0);510511      const tokenData = (await api.rpc.unique.tokenData(collection, token, ['anything'])).toJSON().properties;512      expect(tokenData).to.be.empty;513    });514  });515516  it('Assigns properties to a token according to permissions', async () => {517    await usingApi(async api => {518      const propertyKeys: string[] = [];519      let i = 0;520      for (const permission of permissions) {521        for (const signer of permission.signers) {522          const key = i + '_' + signer.address;523          propertyKeys.push(key);524525          await expect(executeTransaction(526            api, 527            alice, 528            api.tx.unique.setPropertyPermissions(collection, [{key: key, permission: permission.permission}]), 529          ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;530531          await expect(executeTransaction(532            api, 533            signer, 534            api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin increase'}]), 535          ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;536        }537538        i++;539      }540541      const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toHuman() as any[];542      const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toHuman().properties as any[];543      for (let i = 0; i < properties.length; i++) {544        expect(properties[i].value).to.be.equal('Serotonin increase');545        expect(tokensData[i].value).to.be.equal('Serotonin increase');546      }547    });548  });549550  it('Changes properties of a token according to permissions', async () => {551    await usingApi(async api => {552      const propertyKeys: string[] = [];553      let i = 0;554      for (const permission of permissions) {555        if (!permission.permission.mutable) continue;556        557        for (const signer of permission.signers) {558          const key = i + '_' + signer.address;559          propertyKeys.push(key);560561          await expect(executeTransaction(562            api, 563            alice, 564            api.tx.unique.setPropertyPermissions(collection, [{key: key, permission: permission.permission}]), 565          ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;566567          await expect(executeTransaction(568            api, 569            signer, 570            api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin increase'}]), 571          ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;572573          await expect(executeTransaction(574            api, 575            signer, 576            api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin stable'}]), 577          ), `on changing property ${i} by ${signer.address}`).to.not.be.rejected;578        }579580        i++;581      }582583      const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toHuman() as any[];584      const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toHuman().properties as any[];585      for (let i = 0; i < properties.length; i++) {586        expect(properties[i].value).to.be.equal('Serotonin stable');587        expect(tokensData[i].value).to.be.equal('Serotonin stable');588      }589    });590  });591592  it('Deletes properties of a token according to permissions', async () => {593    await usingApi(async api => {594      const propertyKeys: string[] = [];595      let i = 0;596597      for (const permission of permissions) {598        if (!permission.permission.mutable) continue;599        600        for (const signer of permission.signers) {601          const key = i + '_' + signer.address;602          propertyKeys.push(key);603604          await expect(executeTransaction(605            api, 606            alice, 607            api.tx.unique.setPropertyPermissions(collection, [{key: key, permission: permission.permission}]), 608          ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;609610          await expect(executeTransaction(611            api, 612            signer, 613            api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin increase'}]), 614          ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;615616          await expect(executeTransaction(617            api, 618            signer, 619            api.tx.unique.deleteTokenProperties(collection, token, [key]), 620          ), `on deleting property ${i} by ${signer.address}`).to.not.be.rejected;621        }622        623        i++;624      }625626      const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toJSON() as any[];627      expect(properties).to.be.empty;628      const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toJSON().properties as any[];629      expect(tokensData).to.be.empty;630      expect((await api.query.nonfungible.tokenProperties(collection, token)).toJSON().consumedSpace).to.be.equal(0);631    });632  });633});634635describe('Negative Integration Test: Token Properties', () => {636  let collection: number;637  let token: number;638  let originalSpace: number;639  let constitution: {permission: any, signers: IKeyringPair[], sinner: IKeyringPair}[];640641  before(async () => {642    await usingApi(async () => {643      alice = privateKey('//Alice');644      bob = privateKey('//Bob');645      charlie = privateKey('//Charlie');646      const dave = privateKey('//Dave');647648      constitution = [649        {permission: {mutable: true, collectionAdmin: true}, signers: [alice, bob], sinner: charlie},650        {permission: {mutable: false, collectionAdmin: true}, signers: [alice, bob], sinner: charlie},651        {permission: {mutable: true, tokenOwner: true}, signers: [charlie], sinner: alice},652        {permission: {mutable: false, tokenOwner: true}, signers: [charlie], sinner: alice},653        {permission: {mutable: true, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie], sinner: dave},654        {permission: {mutable: false, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie], sinner: dave},655      ];656    });657  });658659  beforeEach(async () => {660    collection = await createCollectionExpectSuccess();661    token = await createItemExpectSuccess(alice, collection, 'NFT');662    await addCollectionAdminExpectSuccess(alice, collection, bob.address);663    await transferExpectSuccess(collection, token, alice, charlie);664        665    await usingApi(async api => {666      let i = 0;667      for (const passage of constitution) {668        const signer = passage.signers[0];669        670        await expect(executeTransaction(671          api, 672          alice, 673          api.tx.unique.setPropertyPermissions(collection, [{key: `${i}`, permission: passage.permission}]), 674        ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;675676        await expect(executeTransaction(677          api, 678          signer, 679          api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin increase'}]), 680        ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;681682        i++;683      }684685      originalSpace = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON().consumedSpace as number;686    });687  });688689  it('Forbids changing/deleting properties of a token if the user is outside of permissions', async () => {690    await usingApi(async api => {691      let i = -1;692      for (const forbiddance of constitution) {693        i++;694        if (!forbiddance.permission.mutable) continue;695696        await expect(executeTransaction(697          api, 698          forbiddance.sinner, 699          api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin down'}]), 700        ), `on failing to change property ${i} by ${forbiddance.sinner.address}`).to.be.rejectedWith(/common\.NoPermission/);701702        await expect(executeTransaction(703          api, 704          forbiddance.sinner, 705          api.tx.unique.deleteTokenProperties(collection, token, [`${i}`]), 706        ), `on failing to delete property ${i} by ${forbiddance.sinner.address}`).to.be.rejectedWith(/common\.NoPermission/);707      }708709      const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();710      expect(properties.consumedSpace).to.be.equal(originalSpace);711    });712  });713714  it('Forbids changing/deleting properties of a token if the property is permanent (immutable)', async () => {715    await usingApi(async api => {716      let i = -1;717      for (const permission of constitution) {718        i++;719        if (permission.permission.mutable) continue;720721        await expect(executeTransaction(722          api, 723          permission.signers[0], 724          api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin down'}]), 725        ), `on failing to change property ${i} by ${permission.signers[0].address}`).to.be.rejectedWith(/common\.NoPermission/);726727        await expect(executeTransaction(728          api, 729          permission.signers[0], 730          api.tx.unique.deleteTokenProperties(collection, token, [i.toString()]), 731        ), `on failing to delete property ${i} by ${permission.signers[0].address}`).to.be.rejectedWith(/common\.NoPermission/);732      }733734      const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();735      expect(properties.consumedSpace).to.be.equal(originalSpace);736    });737  });738739  it('Forbids adding properties to a token if the property is not declared / forbidden with the \'None\' permission', async () => {740    await usingApi(async api => {741      await expect(executeTransaction(742        api, 743        alice, 744        api.tx.unique.setTokenProperties(collection, token, [{key: 'non-existent', value: 'I exist!'}]), 745      ), 'on failing to add a previously non-existent property').to.be.rejectedWith(/common\.NoPermission/);746        747      await expect(executeTransaction(748        api, 749        alice, 750        api.tx.unique.setPropertyPermissions(collection, [{key: 'now-existent', permission: {}}]), 751      ), 'on setting a new non-permitted property').to.not.be.rejected;752753      await expect(executeTransaction(754        api, 755        alice, 756        api.tx.unique.setTokenProperties(collection, token, [{key: 'now-existent', value: 'I exist!'}]), 757      ), 'on failing to add a property forbidden by the \'None\' permission').to.be.rejectedWith(/common\.NoPermission/);758759      expect((await api.rpc.unique.tokenProperties(collection, token, ['non-existent', 'now-existent'])).toJSON()).to.be.empty;760      const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();761      expect(properties.consumedSpace).to.be.equal(originalSpace);762    });763  });764765  it('Forbids adding too many properties to a token', async () => {766    await usingApi(async api => {767      await expect(executeTransaction(768        api, 769        alice, 770        api.tx.unique.setPropertyPermissions(collection, [771          {key: 'a_holy_book', permission: {collectionAdmin: true, tokenOwner: true}}, 772          {key: 'young_years', permission: {collectionAdmin: true, tokenOwner: true}},773        ]), 774      ), 'on setting a new non-permitted property').to.not.be.rejected;775776      // Mute the general tx parsing error777      {778        console.error = () => {};779        await expect(executeTransaction(780          api, 781          alice, 782          api.tx.unique.setCollectionProperties(collection, [{key: 'a_holy_book', value: 'word '.repeat(6554)}]), 783        )).to.be.rejected;784      }785786      await expect(executeTransaction(787        api, 788        alice, 789        api.tx.unique.setTokenProperties(collection, token, [790          {key: 'a_holy_book', value: 'word '.repeat(3277)}, 791          {key: 'young_years', value: 'neverending'.repeat(1490)},792        ]), 793      )).to.be.rejectedWith(/common\.NoSpaceForProperty/);794795      expect((await api.rpc.unique.tokenProperties(collection, token, ['a_holy_book', 'young years'])).toJSON()).to.be.empty;796      const propertiesMap = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();797      expect(propertiesMap.consumedSpace).to.be.equal(originalSpace);798    });799  });800});