difftreelog
feat add conditional supportInterface for ERC721Metadata
in: master
29 files changed
pallets/common/src/erc.rsdiffbeforeafterboth--- a/pallets/common/src/erc.rs
+++ b/pallets/common/src/erc.rs
@@ -684,6 +684,11 @@
pub fn parent_nft() -> up_data_structs::PropertyKey {
property_key_from_bytes(b"parentNft").expect(EXPECT_CONVERT_ERROR)
}
+
+ /// Key "parentNft".
+ pub fn erc721_metadata() -> up_data_structs::PropertyKey {
+ property_key_from_bytes(b"ERC721Metadata").expect(EXPECT_CONVERT_ERROR)
+ }
}
/// Values.
@@ -693,10 +698,21 @@
/// Value "ERC721Metadata".
pub const ERC721_METADATA: &[u8] = b"ERC721Metadata";
+ /// Value "1" ERC721 metadata supported.
+ pub const ERC721_METADATA_SUPPORTED: &[u8] = b"1";
+
+ /// Value "0" ERC721 metadata supported.
+ pub const ERC721_METADATA_UNSUPPORTED: &[u8] = b"0";
+
/// Value for [`ERC721_METADATA`].
pub fn erc721() -> up_data_structs::PropertyValue {
property_value_from_bytes(ERC721_METADATA).expect(EXPECT_CONVERT_ERROR)
}
+
+ /// Value for [`ERC721_METADATA`].
+ pub fn erc721_metadata_supported() -> up_data_structs::PropertyValue {
+ property_value_from_bytes(ERC721_METADATA_SUPPORTED).expect(EXPECT_CONVERT_ERROR)
+ }
}
/// Convert `byte` to [`PropertyKey`].
pallets/nonfungible/src/erc.rsdiffbeforeafterboth--- a/pallets/nonfungible/src/erc.rs
+++ b/pallets/nonfungible/src/erc.rs
@@ -232,7 +232,7 @@
if !url.is_empty() {
return Ok(url);
}
- } else if !is_erc721_metadata_compatible::<T>(self.id) {
+ } else if !self.supports_metadata() {
return Err("tokenURI not set".into());
}
@@ -548,17 +548,6 @@
}
Err("Property tokenURI not found".into())
-}
-
-fn is_erc721_metadata_compatible<T: Config>(collection_id: CollectionId) -> bool {
- if let Some(shema_name) =
- pallet_common::Pallet::<T>::get_collection_property(collection_id, &key::schema_name())
- {
- let shema_name = shema_name.into_inner();
- shema_name == property_value::ERC721_METADATA
- } else {
- false
- }
}
fn get_token_permission<T: Config>(
@@ -577,16 +566,6 @@
Ok(a)
}
-fn has_token_permission<T: Config>(collection_id: CollectionId, key: &PropertyKey) -> bool {
- if let Ok(token_property_permissions) =
- CollectionPropertyPermissions::<T>::try_get(collection_id)
- {
- return token_property_permissions.contains_key(key);
- }
-
- false
-}
-
/// @title Unique extensions for ERC721.
#[solidity_interface(name = ERC721UniqueExtensions)]
impl<T: Config> NonfungibleHandle<T> {
@@ -731,7 +710,7 @@
name = UniqueNFT,
is(
ERC721,
- ERC721Metadata,
+ ERC721Metadata(if(this.supports_metadata())),
ERC721Enumerable,
ERC721UniqueExtensions,
ERC721Mintable,
pallets/nonfungible/src/lib.rsdiffbeforeafterboth1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617//! # Nonfungible Pallet18//!19//! The Nonfungible pallet provides functionality for handling nonfungible collections and tokens.20//!21//! - [`Config`]22//! - [`NonfungibleHandle`]23//! - [`Pallet`]24//! - [`CommonWeights`](common::CommonWeights)25//!26//! ## Overview27//!28//! The Nonfungible pallet provides functions for:29//!30//! - NFT collection creation and removal31//! - Minting and burning of NFT tokens32//! - Retrieving account balances33//! - Transfering NFT tokens34//! - Setting and checking allowance for NFT tokens35//! - Setting properties and permissions for NFT collections and tokens36//! - Nesting and unnesting tokens37//!38//! ### Terminology39//!40//! - **NFT token:** Non fungible token.41//!42//! - **NFT Collection:** A collection of NFT tokens. All NFT tokens are part of a collection.43//! Each collection can define it's own properties, properties for it's tokens and set of permissions.44//!45//! - **Balance:** Number of NFT tokens owned by an account46//!47//! - **Allowance:** NFT tokens owned by one account that another account is allowed to make operations on48//!49//! - **Burning:** The process of “deleting” a token from a collection and from50//! an account balance of the owner.51//!52//! - **Nesting:** Setting up parent-child relationship between tokens. Nested tokens are inhereting53//! owner from their parent. There could be multiple levels of nesting. Token couldn't be nested in54//! it's child token i.e. parent-child relationship graph shouldn't have cycles.55//!56//! - **Properties:** Key-Values pairs. Token properties are attached to a token. Collection properties are57//! attached to a collection. Set of permissions could be defined for each property.58//!59//! ### Implementations60//!61//! The Nonfungible pallet provides implementations for the following traits. If these traits provide62//! the functionality that you need, then you can avoid coupling with the Nonfungible pallet.63//!64//! - [`CommonWeightInfo`](pallet_common::CommonWeightInfo): Functions for retrieval of transaction weight65//! - [`CommonCollectionOperations`](pallet_common::CommonCollectionOperations): Functions for dealing66//! with collections67//!68//! ## Interface69//!70//! ### Dispatchable Functions71//!72//! - `init_collection` - Create NFT collection. NFT collection can be configured to allow or deny access for73//! some accounts.74//! - `destroy_collection` - Destroy exising NFT collection. There should be no tokens in the collection.75//! - `burn` - Burn NFT token owned by account.76//! - `transfer` - Transfer NFT token. Transfers should be enabled for NFT collection.77//! Nests the NFT token if it is sent to another token.78//! - `create_item` - Mint NFT token in collection. Sender should have permission to mint tokens.79//! - `set_allowance` - Set allowance for another account.80//! - `set_token_property` - Set token property value.81//! - `delete_token_property` - Remove property from the token.82//! - `set_collection_properties` - Set collection properties.83//! - `delete_collection_properties` - Remove properties from the collection.84//! - `set_property_permission` - Set collection property permission.85//! - `set_token_property_permissions` - Set token property permissions.86//!87//! ## Assumptions88//!89//! * To perform operations on tokens sender should be in collection's allow list if collection access mode is `AllowList`.9091#![cfg_attr(not(feature = "std"), no_std)]9293use erc::ERC721Events;94use evm_coder::ToLog;95use frame_support::{96 BoundedVec, ensure, fail, transactional,97 storage::with_transaction,98 pallet_prelude::DispatchResultWithPostInfo,99 pallet_prelude::Weight,100 weights::{PostDispatchInfo, Pays},101};102use up_data_structs::{103 AccessMode, CollectionId, CollectionFlags, CustomDataLimit, TokenId, CreateCollectionData,104 CreateNftExData, mapping::TokenAddressMapping, budget::Budget, Property, PropertyPermission,105 PropertyKey, PropertyValue, PropertyKeyPermission, Properties, PropertyScope, TrySetProperty,106 TokenChild, AuxPropertyValue,107};108use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};109use pallet_common::{110 Error as CommonError, Pallet as PalletCommon, Event as CommonEvent, CollectionHandle,111 eth::collection_id_to_address,112};113use pallet_structure::{Pallet as PalletStructure, Error as StructureError};114use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};115use sp_core::H160;116use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, TransactionOutcome};117use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap};118use core::ops::Deref;119use codec::{Encode, Decode, MaxEncodedLen};120use scale_info::TypeInfo;121122pub use pallet::*;123use weights::WeightInfo;124#[cfg(feature = "runtime-benchmarks")]125pub mod benchmarking;126pub mod common;127pub mod erc;128pub mod weights;129130pub type CreateItemData<T> = CreateNftExData<<T as pallet_evm::account::Config>::CrossAccountId>;131pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;132133/// Token data, stored independently from other data used to describe it134/// for the convenience of database access. Notably contains the owner account address.135#[struct_versioning::versioned(version = 2, upper)]136#[derive(Encode, Decode, TypeInfo, MaxEncodedLen)]137pub struct ItemData<CrossAccountId> {138 #[version(..2)]139 pub const_data: BoundedVec<u8, CustomDataLimit>,140141 #[version(..2)]142 pub variable_data: BoundedVec<u8, CustomDataLimit>,143144 pub owner: CrossAccountId,145}146147#[frame_support::pallet]148pub mod pallet {149 use super::*;150 use frame_support::{151 Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key, traits::StorageVersion,152 };153 use frame_system::pallet_prelude::*;154 use up_data_structs::{CollectionId, TokenId};155 use super::weights::WeightInfo;156157 #[pallet::error]158 pub enum Error<T> {159 /// Not Nonfungible item data used to mint in Nonfungible collection.160 NotNonfungibleDataUsedToMintFungibleCollectionToken,161 /// Used amount > 1 with NFT162 NonfungibleItemsHaveNoAmount,163 /// Unable to burn NFT with children164 CantBurnNftWithChildren,165 }166167 #[pallet::config]168 pub trait Config:169 frame_system::Config + pallet_common::Config + pallet_structure::Config + pallet_evm::Config170 {171 type WeightInfo: WeightInfo;172 }173174 const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);175176 #[pallet::pallet]177 #[pallet::storage_version(STORAGE_VERSION)]178 #[pallet::generate_store(pub(super) trait Store)]179 pub struct Pallet<T>(_);180181 /// Total amount of minted tokens in a collection.182 #[pallet::storage]183 pub type TokensMinted<T: Config> =184 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;185186 /// Amount of burnt tokens in a collection.187 #[pallet::storage]188 pub type TokensBurnt<T: Config> =189 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;190191 /// Token data, used to partially describe a token.192 #[pallet::storage]193 pub type TokenData<T: Config> = StorageNMap<194 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),195 Value = ItemData<T::CrossAccountId>,196 QueryKind = OptionQuery,197 >;198199 /// Map of key-value pairs, describing the metadata of a token.200 #[pallet::storage]201 #[pallet::getter(fn token_properties)]202 pub type TokenProperties<T: Config> = StorageNMap<203 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),204 Value = Properties,205 QueryKind = ValueQuery,206 OnEmpty = up_data_structs::TokenProperties,207 >;208209 /// Custom data of a token that is serialized to bytes,210 /// primarily reserved for on-chain operations,211 /// normally obscured from the external users.212 ///213 /// Auxiliary properties are slightly different from214 /// usual [`TokenProperties`] due to an unlimited number215 /// and separately stored and written-to key-value pairs.216 ///217 /// Currently used to store RMRK data.218 #[pallet::storage]219 #[pallet::getter(fn token_aux_property)]220 pub type TokenAuxProperties<T: Config> = StorageNMap<221 Key = (222 Key<Twox64Concat, CollectionId>,223 Key<Twox64Concat, TokenId>,224 Key<Twox64Concat, PropertyScope>,225 Key<Twox64Concat, PropertyKey>,226 ),227 Value = AuxPropertyValue,228 QueryKind = OptionQuery,229 >;230231 /// Used to enumerate tokens owned by account.232 #[pallet::storage]233 pub type Owned<T: Config> = StorageNMap<234 Key = (235 Key<Twox64Concat, CollectionId>,236 Key<Blake2_128Concat, T::CrossAccountId>,237 Key<Twox64Concat, TokenId>,238 ),239 Value = bool,240 QueryKind = ValueQuery,241 >;242243 /// Used to enumerate token's children.244 #[pallet::storage]245 #[pallet::getter(fn token_children)]246 pub type TokenChildren<T: Config> = StorageNMap<247 Key = (248 Key<Twox64Concat, CollectionId>,249 Key<Twox64Concat, TokenId>,250 Key<Twox64Concat, (CollectionId, TokenId)>,251 ),252 Value = bool,253 QueryKind = ValueQuery,254 >;255256 /// Amount of tokens owned by an account in a collection.257 #[pallet::storage]258 pub type AccountBalance<T: Config> = StorageNMap<259 Key = (260 Key<Twox64Concat, CollectionId>,261 Key<Blake2_128Concat, T::CrossAccountId>,262 ),263 Value = u32,264 QueryKind = ValueQuery,265 >;266267 /// Allowance set by a token owner for another user to perform one of certain transactions on a token.268 #[pallet::storage]269 pub type Allowance<T: Config> = StorageNMap<270 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),271 Value = T::CrossAccountId,272 QueryKind = OptionQuery,273 >;274275 /// Upgrade from the old schema to properties.276 #[pallet::hooks]277 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {278 fn on_runtime_upgrade() -> Weight {279 StorageVersion::new(1).put::<Pallet<T>>();280281 Weight::zero()282 }283 }284}285286pub struct NonfungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);287impl<T: Config> NonfungibleHandle<T> {288 pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {289 Self(inner)290 }291 pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {292 self.0293 }294 pub fn common_mut(&mut self) -> &mut pallet_common::CollectionHandle<T> {295 &mut self.0296 }297}298impl<T: Config> WithRecorder<T> for NonfungibleHandle<T> {299 fn recorder(&self) -> &SubstrateRecorder<T> {300 self.0.recorder()301 }302 fn into_recorder(self) -> SubstrateRecorder<T> {303 self.0.into_recorder()304 }305}306impl<T: Config> Deref for NonfungibleHandle<T> {307 type Target = pallet_common::CollectionHandle<T>;308309 fn deref(&self) -> &Self::Target {310 &self.0311 }312}313314impl<T: Config> Pallet<T> {315 /// Get number of NFT tokens in collection.316 pub fn total_supply(collection: &NonfungibleHandle<T>) -> u32 {317 <TokensMinted<T>>::get(collection.id) - <TokensBurnt<T>>::get(collection.id)318 }319320 /// Check that NFT token exists.321 ///322 /// - `token`: Token ID.323 pub fn token_exists(collection: &NonfungibleHandle<T>, token: TokenId) -> bool {324 <TokenData<T>>::contains_key((collection.id, token))325 }326327 /// Set the token property with the scope.328 ///329 /// - `property`: Contains key-value pair.330 pub fn set_scoped_token_property(331 collection_id: CollectionId,332 token_id: TokenId,333 scope: PropertyScope,334 property: Property,335 ) -> DispatchResult {336 TokenProperties::<T>::try_mutate((collection_id, token_id), |properties| {337 properties.try_scoped_set(scope, property.key, property.value)338 })339 .map_err(<CommonError<T>>::from)?;340341 Ok(())342 }343344 /// Batch operation to set multiple properties with the same scope.345 pub fn set_scoped_token_properties(346 collection_id: CollectionId,347 token_id: TokenId,348 scope: PropertyScope,349 properties: impl Iterator<Item = Property>,350 ) -> DispatchResult {351 TokenProperties::<T>::try_mutate((collection_id, token_id), |stored_properties| {352 stored_properties.try_scoped_set_from_iter(scope, properties)353 })354 .map_err(<CommonError<T>>::from)?;355356 Ok(())357 }358359 /// Add or edit auxiliary data for the property.360 ///361 /// - `f`: function that adds or edits auxiliary data.362 pub fn try_mutate_token_aux_property<R, E>(363 collection_id: CollectionId,364 token_id: TokenId,365 scope: PropertyScope,366 key: PropertyKey,367 f: impl FnOnce(&mut Option<AuxPropertyValue>) -> Result<R, E>,368 ) -> Result<R, E> {369 <TokenAuxProperties<T>>::try_mutate((collection_id, token_id, scope, key), f)370 }371372 /// Remove auxiliary data for the property.373 pub fn remove_token_aux_property(374 collection_id: CollectionId,375 token_id: TokenId,376 scope: PropertyScope,377 key: PropertyKey,378 ) {379 <TokenAuxProperties<T>>::remove((collection_id, token_id, scope, key));380 }381382 /// Get all auxiliary data in a given scope.383 ///384 /// Returns iterator over Property Key - Data pairs.385 pub fn iterate_token_aux_properties(386 collection_id: CollectionId,387 token_id: TokenId,388 scope: PropertyScope,389 ) -> impl Iterator<Item = (PropertyKey, AuxPropertyValue)> {390 <TokenAuxProperties<T>>::iter_prefix((collection_id, token_id, scope))391 }392393 /// Get ID of the last minted token394 pub fn current_token_id(collection_id: CollectionId) -> TokenId {395 TokenId(<TokensMinted<T>>::get(collection_id))396 }397}398399// unchecked calls skips any permission checks400impl<T: Config> Pallet<T> {401 /// Create NFT collection402 ///403 /// `init_collection` will take non-refundable deposit for collection creation.404 ///405 /// - `data`: Contains settings for collection limits and permissions.406 pub fn init_collection(407 owner: T::CrossAccountId,408 payer: T::CrossAccountId,409 data: CreateCollectionData<T::AccountId>,410 is_external: bool,411 ) -> Result<CollectionId, DispatchError> {412 <PalletCommon<T>>::init_collection(413 owner,414 payer,415 data,416 CollectionFlags {417 external: is_external,418 ..Default::default()419 },420 )421 }422423 /// Destroy NFT collection424 ///425 /// `destroy_collection` will throw error if collection contains any tokens.426 /// Only owner can destroy collection.427 pub fn destroy_collection(428 collection: NonfungibleHandle<T>,429 sender: &T::CrossAccountId,430 ) -> DispatchResult {431 let id = collection.id;432433 if Self::collection_has_tokens(id) {434 return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());435 }436437 // =========438439 PalletCommon::destroy_collection(collection.0, sender)?;440441 let _ = <TokenData<T>>::clear_prefix((id,), u32::MAX, None);442 let _ = <TokenChildren<T>>::clear_prefix((id,), u32::MAX, None);443 let _ = <Owned<T>>::clear_prefix((id,), u32::MAX, None);444 <TokensMinted<T>>::remove(id);445 <TokensBurnt<T>>::remove(id);446 let _ = <Allowance<T>>::clear_prefix((id,), u32::MAX, None);447 let _ = <AccountBalance<T>>::clear_prefix((id,), u32::MAX, None);448 Ok(())449 }450451 /// Burn NFT token452 ///453 /// `burn` removes `token` from the `collection`, from it's owner and from the parent token454 /// if the token is nested.455 /// Only the owner can `burn` the token. The `token` shouldn't have any nested tokens.456 /// Also removes all corresponding properties and auxiliary properties.457 ///458 /// - `token`: Token that should be burned459 /// - `collection`: Collection that contains the token460 pub fn burn(461 collection: &NonfungibleHandle<T>,462 sender: &T::CrossAccountId,463 token: TokenId,464 ) -> DispatchResult {465 let token_data =466 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;467 ensure!(&token_data.owner == sender, <CommonError<T>>::NoPermission);468469 if collection.permissions.access() == AccessMode::AllowList {470 collection.check_allowlist(sender)?;471 }472473 if Self::token_has_children(collection.id, token) {474 return Err(<Error<T>>::CantBurnNftWithChildren.into());475 }476477 let burnt = <TokensBurnt<T>>::get(collection.id)478 .checked_add(1)479 .ok_or(ArithmeticError::Overflow)?;480481 let balance = <AccountBalance<T>>::get((collection.id, token_data.owner.clone()))482 .checked_sub(1)483 .ok_or(ArithmeticError::Overflow)?;484485 // =========486487 if balance == 0 {488 <AccountBalance<T>>::remove((collection.id, token_data.owner.clone()));489 } else {490 <AccountBalance<T>>::insert((collection.id, token_data.owner.clone()), balance);491 }492493 <PalletStructure<T>>::unnest_if_nested(&token_data.owner, collection.id, token);494495 <Owned<T>>::remove((collection.id, &token_data.owner, token));496 <TokensBurnt<T>>::insert(collection.id, burnt);497 <TokenData<T>>::remove((collection.id, token));498 <TokenProperties<T>>::remove((collection.id, token));499 let _ = <TokenAuxProperties<T>>::clear_prefix((collection.id, token), u32::MAX, None);500 let old_spender = <Allowance<T>>::take((collection.id, token));501502 if let Some(old_spender) = old_spender {503 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(504 collection.id,505 token,506 token_data.owner.clone(),507 old_spender,508 0,509 ));510 }511512 <PalletEvm<T>>::deposit_log(513 ERC721Events::Transfer {514 from: *token_data.owner.as_eth(),515 to: H160::default(),516 token_id: token.into(),517 }518 .to_log(collection_id_to_address(collection.id)),519 );520 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(521 collection.id,522 token,523 token_data.owner,524 1,525 ));526 Ok(())527 }528529 /// Same as [`burn`] but burns all the tokens that are nested in the token first530 ///531 /// - `self_budget`: Limit for searching children in depth.532 /// - `breadth_budget`: Limit of breadth of searching children.533 ///534 /// [`burn`]: struct.Pallet.html#method.burn535 #[transactional]536 pub fn burn_recursively(537 collection: &NonfungibleHandle<T>,538 sender: &T::CrossAccountId,539 token: TokenId,540 self_budget: &dyn Budget,541 breadth_budget: &dyn Budget,542 ) -> DispatchResultWithPostInfo {543 ensure!(self_budget.consume(), <StructureError<T>>::DepthLimit,);544545 let current_token_account =546 T::CrossTokenAddressMapping::token_to_address(collection.id, token);547548 let mut weight = Weight::zero();549550 // This method is transactional, if user in fact doesn't have permissions to remove token -551 // tokens removed here will be restored after rejected transaction552 for ((collection, token), _) in <TokenChildren<T>>::iter_prefix((collection.id, token)) {553 ensure!(breadth_budget.consume(), <StructureError<T>>::BreadthLimit,);554 let PostDispatchInfo { actual_weight, .. } =555 <PalletStructure<T>>::burn_item_recursively(556 current_token_account.clone(),557 collection,558 token,559 self_budget,560 breadth_budget,561 )?;562 if let Some(actual_weight) = actual_weight {563 weight = weight.saturating_add(actual_weight);564 }565 }566567 Self::burn(collection, sender, token)?;568 DispatchResultWithPostInfo::Ok(PostDispatchInfo {569 actual_weight: Some(weight + <SelfWeightOf<T>>::burn_item()),570 pays_fee: Pays::Yes,571 })572 }573574 /// Batch operation to add, edit or remove properties for the token575 ///576 /// All affected properties should have mutable permission and sender should have577 /// permission to edit those properties.578 ///579 /// - `nesting_budget`: Limit for searching parents in depth to check ownership.580 /// - `is_token_create`: Indicates that method is called during token initialization.581 /// Allows to bypass ownership check.582 #[transactional]583 fn modify_token_properties(584 collection: &NonfungibleHandle<T>,585 sender: &T::CrossAccountId,586 token_id: TokenId,587 properties: impl Iterator<Item = (PropertyKey, Option<PropertyValue>)>,588 is_token_create: bool,589 nesting_budget: &dyn Budget,590 ) -> DispatchResult {591 let mut collection_admin_status = None;592 let mut token_owner_result = None;593594 let mut is_collection_admin =595 || *collection_admin_status.get_or_insert_with(|| collection.is_owner_or_admin(sender));596597 let mut is_token_owner = || {598 *token_owner_result.get_or_insert_with(|| -> Result<bool, DispatchError> {599 let is_owned = <PalletStructure<T>>::check_indirectly_owned(600 sender.clone(),601 collection.id,602 token_id,603 None,604 nesting_budget,605 )?;606607 Ok(is_owned)608 })609 };610611 for (key, value) in properties {612 let permission = <PalletCommon<T>>::property_permissions(collection.id)613 .get(&key)614 .cloned()615 .unwrap_or_else(PropertyPermission::none);616617 let is_property_exists = TokenProperties::<T>::get((collection.id, token_id))618 .get(&key)619 .is_some();620621 match permission {622 PropertyPermission { mutable: false, .. } if is_property_exists => {623 return Err(<CommonError<T>>::NoPermission.into());624 }625626 PropertyPermission {627 collection_admin,628 token_owner,629 ..630 } => {631 //TODO: investigate threats during public minting.632 if is_token_create && (collection_admin || token_owner) && value.is_some() {633 // Pass634 } else if collection_admin && is_collection_admin() {635 // Pass636 } else if token_owner && is_token_owner()? {637 // Pass638 } else {639 fail!(<CommonError<T>>::NoPermission);640 }641 }642 }643644 match value {645 Some(value) => {646 <TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {647 properties.try_set(key.clone(), value)648 })649 .map_err(<CommonError<T>>::from)?;650651 <PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertySet(652 collection.id,653 token_id,654 key,655 ));656 }657 None => {658 <TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {659 properties.remove(&key)660 })661 .map_err(<CommonError<T>>::from)?;662663 <PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertyDeleted(664 collection.id,665 token_id,666 key,667 ));668 }669 }670 }671672 Ok(())673 }674675 /// Batch operation to add or edit properties for the token676 ///677 /// Same as [`modify_token_properties`] but doesn't allow to remove properties678 ///679 /// [`modify_token_properties`]: struct.Pallet.html#method.modify_token_properties680 pub fn set_token_properties(681 collection: &NonfungibleHandle<T>,682 sender: &T::CrossAccountId,683 token_id: TokenId,684 properties: impl Iterator<Item = Property>,685 is_token_create: bool,686 nesting_budget: &dyn Budget,687 ) -> DispatchResult {688 Self::modify_token_properties(689 collection,690 sender,691 token_id,692 properties.map(|p| (p.key, Some(p.value))),693 is_token_create,694 nesting_budget,695 )696 }697698 /// Add or edit single property for the token699 ///700 /// Calls [`set_token_properties`] internally701 ///702 /// [`set_token_properties`]: struct.Pallet.html#method.set_token_properties703 pub fn set_token_property(704 collection: &NonfungibleHandle<T>,705 sender: &T::CrossAccountId,706 token_id: TokenId,707 property: Property,708 nesting_budget: &dyn Budget,709 ) -> DispatchResult {710 let is_token_create = false;711712 Self::set_token_properties(713 collection,714 sender,715 token_id,716 [property].into_iter(),717 is_token_create,718 nesting_budget,719 )720 }721722 /// Batch operation to remove properties from the token723 ///724 /// Same as [`modify_token_properties`] but doesn't allow to add or edit properties725 ///726 /// [`modify_token_properties`]: struct.Pallet.html#method.modify_token_properties727 pub fn delete_token_properties(728 collection: &NonfungibleHandle<T>,729 sender: &T::CrossAccountId,730 token_id: TokenId,731 property_keys: impl Iterator<Item = PropertyKey>,732 nesting_budget: &dyn Budget,733 ) -> DispatchResult {734 let is_token_create = false;735736 Self::modify_token_properties(737 collection,738 sender,739 token_id,740 property_keys.into_iter().map(|key| (key, None)),741 is_token_create,742 nesting_budget,743 )744 }745746 /// Remove single property from the token747 ///748 /// Calls [`delete_token_properties`] internally749 ///750 /// [`delete_token_properties`]: struct.Pallet.html#method.delete_token_properties751 pub fn delete_token_property(752 collection: &NonfungibleHandle<T>,753 sender: &T::CrossAccountId,754 token_id: TokenId,755 property_key: PropertyKey,756 nesting_budget: &dyn Budget,757 ) -> DispatchResult {758 Self::delete_token_properties(759 collection,760 sender,761 token_id,762 [property_key].into_iter(),763 nesting_budget,764 )765 }766767 /// Add or edit properties for the collection768 pub fn set_collection_properties(769 collection: &NonfungibleHandle<T>,770 sender: &T::CrossAccountId,771 properties: Vec<Property>,772 ) -> DispatchResult {773 <PalletCommon<T>>::set_collection_properties(collection, sender, properties)774 }775776 /// Remove properties from the collection777 pub fn delete_collection_properties(778 collection: &CollectionHandle<T>,779 sender: &T::CrossAccountId,780 property_keys: Vec<PropertyKey>,781 ) -> DispatchResult {782 <PalletCommon<T>>::delete_collection_properties(collection, sender, property_keys)783 }784785 /// Set property permissions for the token.786 ///787 /// Sender should be the owner or admin of token's collection.788 pub fn set_token_property_permissions(789 collection: &CollectionHandle<T>,790 sender: &T::CrossAccountId,791 property_permissions: Vec<PropertyKeyPermission>,792 ) -> DispatchResult {793 <PalletCommon<T>>::set_token_property_permissions(collection, sender, property_permissions)794 }795796 /// Set property permissions for the token with scope.797 ///798 /// Sender should be the owner or admin of token's collection.799 pub fn set_scoped_token_property_permissions(800 collection: &CollectionHandle<T>,801 sender: &T::CrossAccountId,802 scope: PropertyScope,803 property_permissions: Vec<PropertyKeyPermission>,804 ) -> DispatchResult {805 <PalletCommon<T>>::set_scoped_token_property_permissions(806 collection,807 sender,808 scope,809 property_permissions,810 )811 }812813 /// Set property permissions for the collection.814 ///815 /// Sender should be the owner or admin of the collection.816 pub fn set_property_permission(817 collection: &CollectionHandle<T>,818 sender: &T::CrossAccountId,819 permission: PropertyKeyPermission,820 ) -> DispatchResult {821 <PalletCommon<T>>::set_property_permission(collection, sender, permission)822 }823824 /// Transfer NFT token from one account to another.825 ///826 /// `from` account stops being the owner and `to` account becomes the owner of the token.827 /// If `to` is token than `to` becomes owner of the token and the token become nested.828 /// Unnests token from previous parent if it was nested before.829 /// Removes allowance for the token if there was any.830 /// Throws if transfers aren't allowed for collection or if receiver reached token ownership limit.831 ///832 /// - `nesting_budget`: Limit for token nesting depth833 pub fn transfer(834 collection: &NonfungibleHandle<T>,835 from: &T::CrossAccountId,836 to: &T::CrossAccountId,837 token: TokenId,838 nesting_budget: &dyn Budget,839 ) -> DispatchResult {840 ensure!(841 collection.limits.transfers_enabled(),842 <CommonError<T>>::TransferNotAllowed843 );844845 let token_data =846 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;847 ensure!(&token_data.owner == from, <CommonError<T>>::NoPermission);848849 if collection.permissions.access() == AccessMode::AllowList {850 collection.check_allowlist(from)?;851 collection.check_allowlist(to)?;852 }853 <PalletCommon<T>>::ensure_correct_receiver(to)?;854855 let balance_from = <AccountBalance<T>>::get((collection.id, from))856 .checked_sub(1)857 .ok_or(<CommonError<T>>::TokenValueTooLow)?;858 let balance_to = if from != to {859 let balance_to = <AccountBalance<T>>::get((collection.id, to))860 .checked_add(1)861 .ok_or(ArithmeticError::Overflow)?;862863 ensure!(864 balance_to < collection.limits.account_token_ownership_limit(),865 <CommonError<T>>::AccountTokenLimitExceeded,866 );867868 Some(balance_to)869 } else {870 None871 };872873 <PalletStructure<T>>::nest_if_sent_to_token(874 from.clone(),875 to,876 collection.id,877 token,878 nesting_budget,879 )?;880881 // =========882883 <PalletStructure<T>>::unnest_if_nested(&token_data.owner, collection.id, token);884885 <TokenData<T>>::insert(886 (collection.id, token),887 ItemData {888 owner: to.clone(),889 ..token_data890 },891 );892893 if let Some(balance_to) = balance_to {894 // from != to895 if balance_from == 0 {896 <AccountBalance<T>>::remove((collection.id, from));897 } else {898 <AccountBalance<T>>::insert((collection.id, from), balance_from);899 }900 <AccountBalance<T>>::insert((collection.id, to), balance_to);901 <Owned<T>>::remove((collection.id, from, token));902 <Owned<T>>::insert((collection.id, to, token), true);903 }904 Self::set_allowance_unchecked(collection, from, token, None, true);905906 <PalletEvm<T>>::deposit_log(907 ERC721Events::Transfer {908 from: *from.as_eth(),909 to: *to.as_eth(),910 token_id: token.into(),911 }912 .to_log(collection_id_to_address(collection.id)),913 );914 <PalletCommon<T>>::deposit_event(CommonEvent::Transfer(915 collection.id,916 token,917 from.clone(),918 to.clone(),919 1,920 ));921 Ok(())922 }923924 /// Batch operation to mint multiple NFT tokens.925 ///926 /// The sender should be the owner/admin of the collection or collection should be configured927 /// to allow public minting.928 /// Throws if amount of tokens reached it's limit for the collection or if caller reached929 /// token ownership limit.930 ///931 /// - `data`: Contains list of token properties and users who will become the owners of the932 /// corresponging tokens.933 /// - `nesting_budget`: Limit for token nesting depth934 pub fn create_multiple_items(935 collection: &NonfungibleHandle<T>,936 sender: &T::CrossAccountId,937 data: Vec<CreateItemData<T>>,938 nesting_budget: &dyn Budget,939 ) -> DispatchResult {940 if !collection.is_owner_or_admin(sender) {941 ensure!(942 collection.permissions.mint_mode(),943 <CommonError<T>>::PublicMintingNotAllowed944 );945 collection.check_allowlist(sender)?;946947 for item in data.iter() {948 collection.check_allowlist(&item.owner)?;949 }950 }951952 for data in data.iter() {953 <PalletCommon<T>>::ensure_correct_receiver(&data.owner)?;954 }955956 let first_token = <TokensMinted<T>>::get(collection.id);957 let tokens_minted = first_token958 .checked_add(data.len() as u32)959 .ok_or(ArithmeticError::Overflow)?;960 ensure!(961 tokens_minted <= collection.limits.token_limit(),962 <CommonError<T>>::CollectionTokenLimitExceeded963 );964965 let mut balances = BTreeMap::new();966 for data in &data {967 let balance = balances968 .entry(&data.owner)969 .or_insert_with(|| <AccountBalance<T>>::get((collection.id, &data.owner)));970 *balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?;971972 ensure!(973 *balance <= collection.limits.account_token_ownership_limit(),974 <CommonError<T>>::AccountTokenLimitExceeded,975 );976 }977978 for (i, data) in data.iter().enumerate() {979 let token = TokenId(first_token + i as u32 + 1);980981 <PalletStructure<T>>::check_nesting(982 sender.clone(),983 &data.owner,984 collection.id,985 token,986 nesting_budget,987 )?;988 }989990 // =========991992 with_transaction(|| {993 for (i, data) in data.iter().enumerate() {994 let token = first_token + i as u32 + 1;995996 <TokenData<T>>::insert(997 (collection.id, token),998 ItemData {999 // const_data: data.const_data.clone(),1000 owner: data.owner.clone(),1001 },1002 );10031004 <PalletStructure<T>>::nest_if_sent_to_token_unchecked(1005 &data.owner,1006 collection.id,1007 TokenId(token),1008 );10091010 if let Err(e) = Self::set_token_properties(1011 collection,1012 sender,1013 TokenId(token),1014 data.properties.clone().into_iter(),1015 true,1016 nesting_budget,1017 ) {1018 return TransactionOutcome::Rollback(Err(e));1019 }1020 }1021 TransactionOutcome::Commit(Ok(()))1022 })?;10231024 <TokensMinted<T>>::insert(collection.id, tokens_minted);1025 for (account, balance) in balances {1026 <AccountBalance<T>>::insert((collection.id, account), balance);1027 }1028 for (i, data) in data.into_iter().enumerate() {1029 let token = first_token + i as u32 + 1;1030 <Owned<T>>::insert((collection.id, &data.owner, token), true);10311032 <PalletEvm<T>>::deposit_log(1033 ERC721Events::Transfer {1034 from: H160::default(),1035 to: *data.owner.as_eth(),1036 token_id: token.into(),1037 }1038 .to_log(collection_id_to_address(collection.id)),1039 );1040 <PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(1041 collection.id,1042 TokenId(token),1043 data.owner.clone(),1044 1,1045 ));1046 }1047 Ok(())1048 }10491050 pub fn set_allowance_unchecked(1051 collection: &NonfungibleHandle<T>,1052 sender: &T::CrossAccountId,1053 token: TokenId,1054 spender: Option<&T::CrossAccountId>,1055 assume_implicit_eth: bool,1056 ) {1057 if let Some(spender) = spender {1058 let old_spender = <Allowance<T>>::get((collection.id, token));1059 <Allowance<T>>::insert((collection.id, token), spender);1060 // In ERC721 there is only one possible approved user of token, so we set1061 // approved user to spender1062 <PalletEvm<T>>::deposit_log(1063 ERC721Events::Approval {1064 owner: *sender.as_eth(),1065 approved: *spender.as_eth(),1066 token_id: token.into(),1067 }1068 .to_log(collection_id_to_address(collection.id)),1069 );1070 // In Unique chain, any token can have any amount of approved users, so we need to1071 // set allowance of old owner to 0, and allowance of new owner to 11072 if old_spender.as_ref() != Some(spender) {1073 if let Some(old_owner) = old_spender {1074 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(1075 collection.id,1076 token,1077 sender.clone(),1078 old_owner,1079 0,1080 ));1081 }1082 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(1083 collection.id,1084 token,1085 sender.clone(),1086 spender.clone(),1087 1,1088 ));1089 }1090 } else {1091 let old_spender = <Allowance<T>>::take((collection.id, token));1092 if !assume_implicit_eth {1093 // In ERC721 there is only one possible approved user of token, so we set1094 // approved user to zero address1095 <PalletEvm<T>>::deposit_log(1096 ERC721Events::Approval {1097 owner: *sender.as_eth(),1098 approved: H160::default(),1099 token_id: token.into(),1100 }1101 .to_log(collection_id_to_address(collection.id)),1102 );1103 }1104 // In Unique chain, any token can have any amount of approved users, so we need to1105 // set allowance of old owner to 01106 if let Some(old_spender) = old_spender {1107 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(1108 collection.id,1109 token,1110 sender.clone(),1111 old_spender,1112 0,1113 ));1114 }1115 }1116 }11171118 /// Set allowance for the spender to `transfer` or `burn` sender's token.1119 ///1120 /// - `token`: Token the spender is allowed to `transfer` or `burn`.1121 pub fn set_allowance(1122 collection: &NonfungibleHandle<T>,1123 sender: &T::CrossAccountId,1124 token: TokenId,1125 spender: Option<&T::CrossAccountId>,1126 ) -> DispatchResult {1127 if collection.permissions.access() == AccessMode::AllowList {1128 collection.check_allowlist(sender)?;1129 if let Some(spender) = spender {1130 collection.check_allowlist(spender)?;1131 }1132 }11331134 if let Some(spender) = spender {1135 <PalletCommon<T>>::ensure_correct_receiver(spender)?;1136 }11371138 let token_data =1139 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;1140 if &token_data.owner != sender {1141 ensure!(1142 collection.ignores_owned_amount(sender),1143 <CommonError<T>>::CantApproveMoreThanOwned1144 );1145 }11461147 // =========11481149 Self::set_allowance_unchecked(collection, sender, token, spender, false);1150 Ok(())1151 }11521153 /// Checks allowance for the spender to use the token.1154 fn check_allowed(1155 collection: &NonfungibleHandle<T>,1156 spender: &T::CrossAccountId,1157 from: &T::CrossAccountId,1158 token: TokenId,1159 nesting_budget: &dyn Budget,1160 ) -> DispatchResult {1161 if spender.conv_eq(from) {1162 return Ok(());1163 }1164 if collection.permissions.access() == AccessMode::AllowList {1165 // `from`, `to` checked in [`transfer`]1166 collection.check_allowlist(spender)?;1167 }11681169 if collection.limits.owner_can_transfer() && collection.is_owner_or_admin(spender) {1170 return Ok(());1171 }11721173 if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {1174 ensure!(1175 <PalletStructure<T>>::check_indirectly_owned(1176 spender.clone(),1177 source.0,1178 source.1,1179 None,1180 nesting_budget1181 )?,1182 <CommonError<T>>::ApprovedValueTooLow,1183 );1184 return Ok(());1185 }1186 if <Allowance<T>>::get((collection.id, token)).as_ref() == Some(spender) {1187 return Ok(());1188 }1189 ensure!(1190 collection.ignores_allowance(spender),1191 <CommonError<T>>::ApprovedValueTooLow1192 );1193 Ok(())1194 }11951196 /// Transfer NFT token from one account to another.1197 ///1198 /// Same as the [`transfer`] but spender doesn't needs to be the owner of the token.1199 /// The owner should set allowance for the spender to transfer token.1200 ///1201 /// [`transfer`]: struct.Pallet.html#method.transfer1202 pub fn transfer_from(1203 collection: &NonfungibleHandle<T>,1204 spender: &T::CrossAccountId,1205 from: &T::CrossAccountId,1206 to: &T::CrossAccountId,1207 token: TokenId,1208 nesting_budget: &dyn Budget,1209 ) -> DispatchResult {1210 Self::check_allowed(collection, spender, from, token, nesting_budget)?;12111212 // =========12131214 // Allowance is reset in [`transfer`]1215 Self::transfer(collection, from, to, token, nesting_budget)1216 }12171218 /// Burn NFT token for `from` account.1219 ///1220 /// Same as the [`burn`] but spender doesn't need to be an owner of the token. The owner should1221 /// set allowance for the spender to burn token.1222 ///1223 /// [`burn`]: struct.Pallet.html#method.burn1224 pub fn burn_from(1225 collection: &NonfungibleHandle<T>,1226 spender: &T::CrossAccountId,1227 from: &T::CrossAccountId,1228 token: TokenId,1229 nesting_budget: &dyn Budget,1230 ) -> DispatchResult {1231 Self::check_allowed(collection, spender, from, token, nesting_budget)?;12321233 // =========12341235 Self::burn(collection, from, token)1236 }12371238 /// Check that `from` token could be nested in `under` token.1239 ///1240 pub fn check_nesting(1241 handle: &NonfungibleHandle<T>,1242 sender: T::CrossAccountId,1243 from: (CollectionId, TokenId),1244 under: TokenId,1245 nesting_budget: &dyn Budget,1246 ) -> DispatchResult {1247 let nesting = handle.permissions.nesting();12481249 #[cfg(not(feature = "runtime-benchmarks"))]1250 let permissive = false;1251 #[cfg(feature = "runtime-benchmarks")]1252 let permissive = nesting.permissive;12531254 if permissive {1255 // Pass1256 } else if nesting.token_owner1257 && <PalletStructure<T>>::check_indirectly_owned(1258 sender.clone(),1259 handle.id,1260 under,1261 Some(from),1262 nesting_budget,1263 )? {1264 // Pass1265 } else if nesting.collection_admin && handle.is_owner_or_admin(&sender) {1266 // Pass1267 } else {1268 fail!(<CommonError<T>>::UserIsNotAllowedToNest);1269 }12701271 if let Some(whitelist) = &nesting.restricted {1272 ensure!(1273 whitelist.contains(&from.0),1274 <CommonError<T>>::SourceCollectionIsNotAllowedToNest1275 );1276 }1277 Ok(())1278 }12791280 fn nest(under: (CollectionId, TokenId), to_nest: (CollectionId, TokenId)) {1281 <TokenChildren<T>>::insert((under.0, under.1, (to_nest.0, to_nest.1)), true);1282 }12831284 fn unnest(under: (CollectionId, TokenId), to_unnest: (CollectionId, TokenId)) {1285 <TokenChildren<T>>::remove((under.0, under.1, to_unnest));1286 }12871288 fn collection_has_tokens(collection_id: CollectionId) -> bool {1289 <TokenData<T>>::iter_prefix((collection_id,))1290 .next()1291 .is_some()1292 }12931294 fn token_has_children(collection_id: CollectionId, token_id: TokenId) -> bool {1295 <TokenChildren<T>>::iter_prefix((collection_id, token_id))1296 .next()1297 .is_some()1298 }12991300 pub fn token_children_ids(collection_id: CollectionId, token_id: TokenId) -> Vec<TokenChild> {1301 <TokenChildren<T>>::iter_prefix((collection_id, token_id))1302 .map(|((child_collection_id, child_id), _)| TokenChild {1303 collection: child_collection_id,1304 token: child_id,1305 })1306 .collect()1307 }13081309 /// Mint single NFT token.1310 ///1311 /// Delegated to [`create_multiple_items`]1312 ///1313 /// [`create_multiple_items`]: struct.Pallet.html#method.create_multiple_items1314 pub fn create_item(1315 collection: &NonfungibleHandle<T>,1316 sender: &T::CrossAccountId,1317 data: CreateItemData<T>,1318 nesting_budget: &dyn Budget,1319 ) -> DispatchResult {1320 Self::create_multiple_items(collection, sender, vec![data], nesting_budget)1321 }1322}1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617//! # Nonfungible Pallet18//!19//! The Nonfungible pallet provides functionality for handling nonfungible collections and tokens.20//!21//! - [`Config`]22//! - [`NonfungibleHandle`]23//! - [`Pallet`]24//! - [`CommonWeights`](common::CommonWeights)25//!26//! ## Overview27//!28//! The Nonfungible pallet provides functions for:29//!30//! - NFT collection creation and removal31//! - Minting and burning of NFT tokens32//! - Retrieving account balances33//! - Transfering NFT tokens34//! - Setting and checking allowance for NFT tokens35//! - Setting properties and permissions for NFT collections and tokens36//! - Nesting and unnesting tokens37//!38//! ### Terminology39//!40//! - **NFT token:** Non fungible token.41//!42//! - **NFT Collection:** A collection of NFT tokens. All NFT tokens are part of a collection.43//! Each collection can define it's own properties, properties for it's tokens and set of permissions.44//!45//! - **Balance:** Number of NFT tokens owned by an account46//!47//! - **Allowance:** NFT tokens owned by one account that another account is allowed to make operations on48//!49//! - **Burning:** The process of “deleting” a token from a collection and from50//! an account balance of the owner.51//!52//! - **Nesting:** Setting up parent-child relationship between tokens. Nested tokens are inhereting53//! owner from their parent. There could be multiple levels of nesting. Token couldn't be nested in54//! it's child token i.e. parent-child relationship graph shouldn't have cycles.55//!56//! - **Properties:** Key-Values pairs. Token properties are attached to a token. Collection properties are57//! attached to a collection. Set of permissions could be defined for each property.58//!59//! ### Implementations60//!61//! The Nonfungible pallet provides implementations for the following traits. If these traits provide62//! the functionality that you need, then you can avoid coupling with the Nonfungible pallet.63//!64//! - [`CommonWeightInfo`](pallet_common::CommonWeightInfo): Functions for retrieval of transaction weight65//! - [`CommonCollectionOperations`](pallet_common::CommonCollectionOperations): Functions for dealing66//! with collections67//!68//! ## Interface69//!70//! ### Dispatchable Functions71//!72//! - `init_collection` - Create NFT collection. NFT collection can be configured to allow or deny access for73//! some accounts.74//! - `destroy_collection` - Destroy exising NFT collection. There should be no tokens in the collection.75//! - `burn` - Burn NFT token owned by account.76//! - `transfer` - Transfer NFT token. Transfers should be enabled for NFT collection.77//! Nests the NFT token if it is sent to another token.78//! - `create_item` - Mint NFT token in collection. Sender should have permission to mint tokens.79//! - `set_allowance` - Set allowance for another account.80//! - `set_token_property` - Set token property value.81//! - `delete_token_property` - Remove property from the token.82//! - `set_collection_properties` - Set collection properties.83//! - `delete_collection_properties` - Remove properties from the collection.84//! - `set_property_permission` - Set collection property permission.85//! - `set_token_property_permissions` - Set token property permissions.86//!87//! ## Assumptions88//!89//! * To perform operations on tokens sender should be in collection's allow list if collection access mode is `AllowList`.9091#![cfg_attr(not(feature = "std"), no_std)]9293use erc::ERC721Events;94use evm_coder::ToLog;95use frame_support::{96 BoundedVec, ensure, fail, transactional,97 storage::with_transaction,98 pallet_prelude::DispatchResultWithPostInfo,99 pallet_prelude::Weight,100 weights::{PostDispatchInfo, Pays},101};102use up_data_structs::{103 AccessMode, CollectionId, CollectionFlags, CustomDataLimit, TokenId, CreateCollectionData,104 CreateNftExData, mapping::TokenAddressMapping, budget::Budget, Property, PropertyPermission,105 PropertyKey, PropertyValue, PropertyKeyPermission, Properties, PropertyScope, TrySetProperty,106 TokenChild, AuxPropertyValue,107};108use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};109use pallet_common::{110 Error as CommonError, Pallet as PalletCommon, Event as CommonEvent, CollectionHandle,111 erc::static_property::{key, value},112 eth::collection_id_to_address,113};114use pallet_structure::{Pallet as PalletStructure, Error as StructureError};115use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};116use sp_core::H160;117use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, TransactionOutcome};118use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap};119use core::ops::Deref;120use codec::{Encode, Decode, MaxEncodedLen};121use scale_info::TypeInfo;122123pub use pallet::*;124use weights::WeightInfo;125#[cfg(feature = "runtime-benchmarks")]126pub mod benchmarking;127pub mod common;128pub mod erc;129pub mod weights;130131pub type CreateItemData<T> = CreateNftExData<<T as pallet_evm::account::Config>::CrossAccountId>;132pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;133134/// Token data, stored independently from other data used to describe it135/// for the convenience of database access. Notably contains the owner account address.136#[struct_versioning::versioned(version = 2, upper)]137#[derive(Encode, Decode, TypeInfo, MaxEncodedLen)]138pub struct ItemData<CrossAccountId> {139 #[version(..2)]140 pub const_data: BoundedVec<u8, CustomDataLimit>,141142 #[version(..2)]143 pub variable_data: BoundedVec<u8, CustomDataLimit>,144145 pub owner: CrossAccountId,146}147148#[frame_support::pallet]149pub mod pallet {150 use super::*;151 use frame_support::{152 Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key, traits::StorageVersion,153 };154 use frame_system::pallet_prelude::*;155 use up_data_structs::{CollectionId, TokenId};156 use super::weights::WeightInfo;157158 #[pallet::error]159 pub enum Error<T> {160 /// Not Nonfungible item data used to mint in Nonfungible collection.161 NotNonfungibleDataUsedToMintFungibleCollectionToken,162 /// Used amount > 1 with NFT163 NonfungibleItemsHaveNoAmount,164 /// Unable to burn NFT with children165 CantBurnNftWithChildren,166 }167168 #[pallet::config]169 pub trait Config:170 frame_system::Config + pallet_common::Config + pallet_structure::Config + pallet_evm::Config171 {172 type WeightInfo: WeightInfo;173 }174175 const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);176177 #[pallet::pallet]178 #[pallet::storage_version(STORAGE_VERSION)]179 #[pallet::generate_store(pub(super) trait Store)]180 pub struct Pallet<T>(_);181182 /// Total amount of minted tokens in a collection.183 #[pallet::storage]184 pub type TokensMinted<T: Config> =185 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;186187 /// Amount of burnt tokens in a collection.188 #[pallet::storage]189 pub type TokensBurnt<T: Config> =190 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;191192 /// Token data, used to partially describe a token.193 #[pallet::storage]194 pub type TokenData<T: Config> = StorageNMap<195 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),196 Value = ItemData<T::CrossAccountId>,197 QueryKind = OptionQuery,198 >;199200 /// Map of key-value pairs, describing the metadata of a token.201 #[pallet::storage]202 #[pallet::getter(fn token_properties)]203 pub type TokenProperties<T: Config> = StorageNMap<204 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),205 Value = Properties,206 QueryKind = ValueQuery,207 OnEmpty = up_data_structs::TokenProperties,208 >;209210 /// Custom data of a token that is serialized to bytes,211 /// primarily reserved for on-chain operations,212 /// normally obscured from the external users.213 ///214 /// Auxiliary properties are slightly different from215 /// usual [`TokenProperties`] due to an unlimited number216 /// and separately stored and written-to key-value pairs.217 ///218 /// Currently used to store RMRK data.219 #[pallet::storage]220 #[pallet::getter(fn token_aux_property)]221 pub type TokenAuxProperties<T: Config> = StorageNMap<222 Key = (223 Key<Twox64Concat, CollectionId>,224 Key<Twox64Concat, TokenId>,225 Key<Twox64Concat, PropertyScope>,226 Key<Twox64Concat, PropertyKey>,227 ),228 Value = AuxPropertyValue,229 QueryKind = OptionQuery,230 >;231232 /// Used to enumerate tokens owned by account.233 #[pallet::storage]234 pub type Owned<T: Config> = StorageNMap<235 Key = (236 Key<Twox64Concat, CollectionId>,237 Key<Blake2_128Concat, T::CrossAccountId>,238 Key<Twox64Concat, TokenId>,239 ),240 Value = bool,241 QueryKind = ValueQuery,242 >;243244 /// Used to enumerate token's children.245 #[pallet::storage]246 #[pallet::getter(fn token_children)]247 pub type TokenChildren<T: Config> = StorageNMap<248 Key = (249 Key<Twox64Concat, CollectionId>,250 Key<Twox64Concat, TokenId>,251 Key<Twox64Concat, (CollectionId, TokenId)>,252 ),253 Value = bool,254 QueryKind = ValueQuery,255 >;256257 /// Amount of tokens owned by an account in a collection.258 #[pallet::storage]259 pub type AccountBalance<T: Config> = StorageNMap<260 Key = (261 Key<Twox64Concat, CollectionId>,262 Key<Blake2_128Concat, T::CrossAccountId>,263 ),264 Value = u32,265 QueryKind = ValueQuery,266 >;267268 /// Allowance set by a token owner for another user to perform one of certain transactions on a token.269 #[pallet::storage]270 pub type Allowance<T: Config> = StorageNMap<271 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),272 Value = T::CrossAccountId,273 QueryKind = OptionQuery,274 >;275276 /// Upgrade from the old schema to properties.277 #[pallet::hooks]278 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {279 fn on_runtime_upgrade() -> Weight {280 StorageVersion::new(1).put::<Pallet<T>>();281282 Weight::zero()283 }284 }285}286287pub struct NonfungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);288impl<T: Config> NonfungibleHandle<T> {289 pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {290 Self(inner)291 }292 pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {293 self.0294 }295 pub fn common_mut(&mut self) -> &mut pallet_common::CollectionHandle<T> {296 &mut self.0297 }298}299300impl<T: Config> NonfungibleHandle<T> {301 pub fn supports_metadata(&self) -> bool {302 if let Some(erc721_metadata) =303 pallet_common::Pallet::<T>::get_collection_property(self.id, &key::erc721_metadata())304 {305 *erc721_metadata.into_inner() == *value::ERC721_METADATA_SUPPORTED306 } else {307 false308 }309 }310}311312impl<T: Config> WithRecorder<T> for NonfungibleHandle<T> {313 fn recorder(&self) -> &SubstrateRecorder<T> {314 self.0.recorder()315 }316 fn into_recorder(self) -> SubstrateRecorder<T> {317 self.0.into_recorder()318 }319}320impl<T: Config> Deref for NonfungibleHandle<T> {321 type Target = pallet_common::CollectionHandle<T>;322323 fn deref(&self) -> &Self::Target {324 &self.0325 }326}327328impl<T: Config> Pallet<T> {329 /// Get number of NFT tokens in collection.330 pub fn total_supply(collection: &NonfungibleHandle<T>) -> u32 {331 <TokensMinted<T>>::get(collection.id) - <TokensBurnt<T>>::get(collection.id)332 }333334 /// Check that NFT token exists.335 ///336 /// - `token`: Token ID.337 pub fn token_exists(collection: &NonfungibleHandle<T>, token: TokenId) -> bool {338 <TokenData<T>>::contains_key((collection.id, token))339 }340341 /// Set the token property with the scope.342 ///343 /// - `property`: Contains key-value pair.344 pub fn set_scoped_token_property(345 collection_id: CollectionId,346 token_id: TokenId,347 scope: PropertyScope,348 property: Property,349 ) -> DispatchResult {350 TokenProperties::<T>::try_mutate((collection_id, token_id), |properties| {351 properties.try_scoped_set(scope, property.key, property.value)352 })353 .map_err(<CommonError<T>>::from)?;354355 Ok(())356 }357358 /// Batch operation to set multiple properties with the same scope.359 pub fn set_scoped_token_properties(360 collection_id: CollectionId,361 token_id: TokenId,362 scope: PropertyScope,363 properties: impl Iterator<Item = Property>,364 ) -> DispatchResult {365 TokenProperties::<T>::try_mutate((collection_id, token_id), |stored_properties| {366 stored_properties.try_scoped_set_from_iter(scope, properties)367 })368 .map_err(<CommonError<T>>::from)?;369370 Ok(())371 }372373 /// Add or edit auxiliary data for the property.374 ///375 /// - `f`: function that adds or edits auxiliary data.376 pub fn try_mutate_token_aux_property<R, E>(377 collection_id: CollectionId,378 token_id: TokenId,379 scope: PropertyScope,380 key: PropertyKey,381 f: impl FnOnce(&mut Option<AuxPropertyValue>) -> Result<R, E>,382 ) -> Result<R, E> {383 <TokenAuxProperties<T>>::try_mutate((collection_id, token_id, scope, key), f)384 }385386 /// Remove auxiliary data for the property.387 pub fn remove_token_aux_property(388 collection_id: CollectionId,389 token_id: TokenId,390 scope: PropertyScope,391 key: PropertyKey,392 ) {393 <TokenAuxProperties<T>>::remove((collection_id, token_id, scope, key));394 }395396 /// Get all auxiliary data in a given scope.397 ///398 /// Returns iterator over Property Key - Data pairs.399 pub fn iterate_token_aux_properties(400 collection_id: CollectionId,401 token_id: TokenId,402 scope: PropertyScope,403 ) -> impl Iterator<Item = (PropertyKey, AuxPropertyValue)> {404 <TokenAuxProperties<T>>::iter_prefix((collection_id, token_id, scope))405 }406407 /// Get ID of the last minted token408 pub fn current_token_id(collection_id: CollectionId) -> TokenId {409 TokenId(<TokensMinted<T>>::get(collection_id))410 }411}412413// unchecked calls skips any permission checks414impl<T: Config> Pallet<T> {415 /// Create NFT collection416 ///417 /// `init_collection` will take non-refundable deposit for collection creation.418 ///419 /// - `data`: Contains settings for collection limits and permissions.420 pub fn init_collection(421 owner: T::CrossAccountId,422 payer: T::CrossAccountId,423 data: CreateCollectionData<T::AccountId>,424 is_external: bool,425 ) -> Result<CollectionId, DispatchError> {426 <PalletCommon<T>>::init_collection(427 owner,428 payer,429 data,430 CollectionFlags {431 external: is_external,432 ..Default::default()433 },434 )435 }436437 /// Destroy NFT collection438 ///439 /// `destroy_collection` will throw error if collection contains any tokens.440 /// Only owner can destroy collection.441 pub fn destroy_collection(442 collection: NonfungibleHandle<T>,443 sender: &T::CrossAccountId,444 ) -> DispatchResult {445 let id = collection.id;446447 if Self::collection_has_tokens(id) {448 return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());449 }450451 // =========452453 PalletCommon::destroy_collection(collection.0, sender)?;454455 let _ = <TokenData<T>>::clear_prefix((id,), u32::MAX, None);456 let _ = <TokenChildren<T>>::clear_prefix((id,), u32::MAX, None);457 let _ = <Owned<T>>::clear_prefix((id,), u32::MAX, None);458 <TokensMinted<T>>::remove(id);459 <TokensBurnt<T>>::remove(id);460 let _ = <Allowance<T>>::clear_prefix((id,), u32::MAX, None);461 let _ = <AccountBalance<T>>::clear_prefix((id,), u32::MAX, None);462 Ok(())463 }464465 /// Burn NFT token466 ///467 /// `burn` removes `token` from the `collection`, from it's owner and from the parent token468 /// if the token is nested.469 /// Only the owner can `burn` the token. The `token` shouldn't have any nested tokens.470 /// Also removes all corresponding properties and auxiliary properties.471 ///472 /// - `token`: Token that should be burned473 /// - `collection`: Collection that contains the token474 pub fn burn(475 collection: &NonfungibleHandle<T>,476 sender: &T::CrossAccountId,477 token: TokenId,478 ) -> DispatchResult {479 let token_data =480 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;481 ensure!(&token_data.owner == sender, <CommonError<T>>::NoPermission);482483 if collection.permissions.access() == AccessMode::AllowList {484 collection.check_allowlist(sender)?;485 }486487 if Self::token_has_children(collection.id, token) {488 return Err(<Error<T>>::CantBurnNftWithChildren.into());489 }490491 let burnt = <TokensBurnt<T>>::get(collection.id)492 .checked_add(1)493 .ok_or(ArithmeticError::Overflow)?;494495 let balance = <AccountBalance<T>>::get((collection.id, token_data.owner.clone()))496 .checked_sub(1)497 .ok_or(ArithmeticError::Overflow)?;498499 // =========500501 if balance == 0 {502 <AccountBalance<T>>::remove((collection.id, token_data.owner.clone()));503 } else {504 <AccountBalance<T>>::insert((collection.id, token_data.owner.clone()), balance);505 }506507 <PalletStructure<T>>::unnest_if_nested(&token_data.owner, collection.id, token);508509 <Owned<T>>::remove((collection.id, &token_data.owner, token));510 <TokensBurnt<T>>::insert(collection.id, burnt);511 <TokenData<T>>::remove((collection.id, token));512 <TokenProperties<T>>::remove((collection.id, token));513 let _ = <TokenAuxProperties<T>>::clear_prefix((collection.id, token), u32::MAX, None);514 let old_spender = <Allowance<T>>::take((collection.id, token));515516 if let Some(old_spender) = old_spender {517 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(518 collection.id,519 token,520 token_data.owner.clone(),521 old_spender,522 0,523 ));524 }525526 <PalletEvm<T>>::deposit_log(527 ERC721Events::Transfer {528 from: *token_data.owner.as_eth(),529 to: H160::default(),530 token_id: token.into(),531 }532 .to_log(collection_id_to_address(collection.id)),533 );534 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(535 collection.id,536 token,537 token_data.owner,538 1,539 ));540 Ok(())541 }542543 /// Same as [`burn`] but burns all the tokens that are nested in the token first544 ///545 /// - `self_budget`: Limit for searching children in depth.546 /// - `breadth_budget`: Limit of breadth of searching children.547 ///548 /// [`burn`]: struct.Pallet.html#method.burn549 #[transactional]550 pub fn burn_recursively(551 collection: &NonfungibleHandle<T>,552 sender: &T::CrossAccountId,553 token: TokenId,554 self_budget: &dyn Budget,555 breadth_budget: &dyn Budget,556 ) -> DispatchResultWithPostInfo {557 ensure!(self_budget.consume(), <StructureError<T>>::DepthLimit,);558559 let current_token_account =560 T::CrossTokenAddressMapping::token_to_address(collection.id, token);561562 let mut weight = Weight::zero();563564 // This method is transactional, if user in fact doesn't have permissions to remove token -565 // tokens removed here will be restored after rejected transaction566 for ((collection, token), _) in <TokenChildren<T>>::iter_prefix((collection.id, token)) {567 ensure!(breadth_budget.consume(), <StructureError<T>>::BreadthLimit,);568 let PostDispatchInfo { actual_weight, .. } =569 <PalletStructure<T>>::burn_item_recursively(570 current_token_account.clone(),571 collection,572 token,573 self_budget,574 breadth_budget,575 )?;576 if let Some(actual_weight) = actual_weight {577 weight = weight.saturating_add(actual_weight);578 }579 }580581 Self::burn(collection, sender, token)?;582 DispatchResultWithPostInfo::Ok(PostDispatchInfo {583 actual_weight: Some(weight + <SelfWeightOf<T>>::burn_item()),584 pays_fee: Pays::Yes,585 })586 }587588 /// Batch operation to add, edit or remove properties for the token589 ///590 /// All affected properties should have mutable permission and sender should have591 /// permission to edit those properties.592 ///593 /// - `nesting_budget`: Limit for searching parents in depth to check ownership.594 /// - `is_token_create`: Indicates that method is called during token initialization.595 /// Allows to bypass ownership check.596 #[transactional]597 fn modify_token_properties(598 collection: &NonfungibleHandle<T>,599 sender: &T::CrossAccountId,600 token_id: TokenId,601 properties: impl Iterator<Item = (PropertyKey, Option<PropertyValue>)>,602 is_token_create: bool,603 nesting_budget: &dyn Budget,604 ) -> DispatchResult {605 let mut collection_admin_status = None;606 let mut token_owner_result = None;607608 let mut is_collection_admin =609 || *collection_admin_status.get_or_insert_with(|| collection.is_owner_or_admin(sender));610611 let mut is_token_owner = || {612 *token_owner_result.get_or_insert_with(|| -> Result<bool, DispatchError> {613 let is_owned = <PalletStructure<T>>::check_indirectly_owned(614 sender.clone(),615 collection.id,616 token_id,617 None,618 nesting_budget,619 )?;620621 Ok(is_owned)622 })623 };624625 for (key, value) in properties {626 let permission = <PalletCommon<T>>::property_permissions(collection.id)627 .get(&key)628 .cloned()629 .unwrap_or_else(PropertyPermission::none);630631 let is_property_exists = TokenProperties::<T>::get((collection.id, token_id))632 .get(&key)633 .is_some();634635 match permission {636 PropertyPermission { mutable: false, .. } if is_property_exists => {637 return Err(<CommonError<T>>::NoPermission.into());638 }639640 PropertyPermission {641 collection_admin,642 token_owner,643 ..644 } => {645 //TODO: investigate threats during public minting.646 if is_token_create && (collection_admin || token_owner) && value.is_some() {647 // Pass648 } else if collection_admin && is_collection_admin() {649 // Pass650 } else if token_owner && is_token_owner()? {651 // Pass652 } else {653 fail!(<CommonError<T>>::NoPermission);654 }655 }656 }657658 match value {659 Some(value) => {660 <TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {661 properties.try_set(key.clone(), value)662 })663 .map_err(<CommonError<T>>::from)?;664665 <PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertySet(666 collection.id,667 token_id,668 key,669 ));670 }671 None => {672 <TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {673 properties.remove(&key)674 })675 .map_err(<CommonError<T>>::from)?;676677 <PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertyDeleted(678 collection.id,679 token_id,680 key,681 ));682 }683 }684 }685686 Ok(())687 }688689 /// Batch operation to add or edit properties for the token690 ///691 /// Same as [`modify_token_properties`] but doesn't allow to remove properties692 ///693 /// [`modify_token_properties`]: struct.Pallet.html#method.modify_token_properties694 pub fn set_token_properties(695 collection: &NonfungibleHandle<T>,696 sender: &T::CrossAccountId,697 token_id: TokenId,698 properties: impl Iterator<Item = Property>,699 is_token_create: bool,700 nesting_budget: &dyn Budget,701 ) -> DispatchResult {702 Self::modify_token_properties(703 collection,704 sender,705 token_id,706 properties.map(|p| (p.key, Some(p.value))),707 is_token_create,708 nesting_budget,709 )710 }711712 /// Add or edit single property for the token713 ///714 /// Calls [`set_token_properties`] internally715 ///716 /// [`set_token_properties`]: struct.Pallet.html#method.set_token_properties717 pub fn set_token_property(718 collection: &NonfungibleHandle<T>,719 sender: &T::CrossAccountId,720 token_id: TokenId,721 property: Property,722 nesting_budget: &dyn Budget,723 ) -> DispatchResult {724 let is_token_create = false;725726 Self::set_token_properties(727 collection,728 sender,729 token_id,730 [property].into_iter(),731 is_token_create,732 nesting_budget,733 )734 }735736 /// Batch operation to remove properties from the token737 ///738 /// Same as [`modify_token_properties`] but doesn't allow to add or edit properties739 ///740 /// [`modify_token_properties`]: struct.Pallet.html#method.modify_token_properties741 pub fn delete_token_properties(742 collection: &NonfungibleHandle<T>,743 sender: &T::CrossAccountId,744 token_id: TokenId,745 property_keys: impl Iterator<Item = PropertyKey>,746 nesting_budget: &dyn Budget,747 ) -> DispatchResult {748 let is_token_create = false;749750 Self::modify_token_properties(751 collection,752 sender,753 token_id,754 property_keys.into_iter().map(|key| (key, None)),755 is_token_create,756 nesting_budget,757 )758 }759760 /// Remove single property from the token761 ///762 /// Calls [`delete_token_properties`] internally763 ///764 /// [`delete_token_properties`]: struct.Pallet.html#method.delete_token_properties765 pub fn delete_token_property(766 collection: &NonfungibleHandle<T>,767 sender: &T::CrossAccountId,768 token_id: TokenId,769 property_key: PropertyKey,770 nesting_budget: &dyn Budget,771 ) -> DispatchResult {772 Self::delete_token_properties(773 collection,774 sender,775 token_id,776 [property_key].into_iter(),777 nesting_budget,778 )779 }780781 /// Add or edit properties for the collection782 pub fn set_collection_properties(783 collection: &NonfungibleHandle<T>,784 sender: &T::CrossAccountId,785 properties: Vec<Property>,786 ) -> DispatchResult {787 <PalletCommon<T>>::set_collection_properties(collection, sender, properties)788 }789790 /// Remove properties from the collection791 pub fn delete_collection_properties(792 collection: &CollectionHandle<T>,793 sender: &T::CrossAccountId,794 property_keys: Vec<PropertyKey>,795 ) -> DispatchResult {796 <PalletCommon<T>>::delete_collection_properties(collection, sender, property_keys)797 }798799 /// Set property permissions for the token.800 ///801 /// Sender should be the owner or admin of token's collection.802 pub fn set_token_property_permissions(803 collection: &CollectionHandle<T>,804 sender: &T::CrossAccountId,805 property_permissions: Vec<PropertyKeyPermission>,806 ) -> DispatchResult {807 <PalletCommon<T>>::set_token_property_permissions(collection, sender, property_permissions)808 }809810 /// Set property permissions for the token with scope.811 ///812 /// Sender should be the owner or admin of token's collection.813 pub fn set_scoped_token_property_permissions(814 collection: &CollectionHandle<T>,815 sender: &T::CrossAccountId,816 scope: PropertyScope,817 property_permissions: Vec<PropertyKeyPermission>,818 ) -> DispatchResult {819 <PalletCommon<T>>::set_scoped_token_property_permissions(820 collection,821 sender,822 scope,823 property_permissions,824 )825 }826827 /// Set property permissions for the collection.828 ///829 /// Sender should be the owner or admin of the collection.830 pub fn set_property_permission(831 collection: &CollectionHandle<T>,832 sender: &T::CrossAccountId,833 permission: PropertyKeyPermission,834 ) -> DispatchResult {835 <PalletCommon<T>>::set_property_permission(collection, sender, permission)836 }837838 /// Transfer NFT token from one account to another.839 ///840 /// `from` account stops being the owner and `to` account becomes the owner of the token.841 /// If `to` is token than `to` becomes owner of the token and the token become nested.842 /// Unnests token from previous parent if it was nested before.843 /// Removes allowance for the token if there was any.844 /// Throws if transfers aren't allowed for collection or if receiver reached token ownership limit.845 ///846 /// - `nesting_budget`: Limit for token nesting depth847 pub fn transfer(848 collection: &NonfungibleHandle<T>,849 from: &T::CrossAccountId,850 to: &T::CrossAccountId,851 token: TokenId,852 nesting_budget: &dyn Budget,853 ) -> DispatchResult {854 ensure!(855 collection.limits.transfers_enabled(),856 <CommonError<T>>::TransferNotAllowed857 );858859 let token_data =860 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;861 ensure!(&token_data.owner == from, <CommonError<T>>::NoPermission);862863 if collection.permissions.access() == AccessMode::AllowList {864 collection.check_allowlist(from)?;865 collection.check_allowlist(to)?;866 }867 <PalletCommon<T>>::ensure_correct_receiver(to)?;868869 let balance_from = <AccountBalance<T>>::get((collection.id, from))870 .checked_sub(1)871 .ok_or(<CommonError<T>>::TokenValueTooLow)?;872 let balance_to = if from != to {873 let balance_to = <AccountBalance<T>>::get((collection.id, to))874 .checked_add(1)875 .ok_or(ArithmeticError::Overflow)?;876877 ensure!(878 balance_to < collection.limits.account_token_ownership_limit(),879 <CommonError<T>>::AccountTokenLimitExceeded,880 );881882 Some(balance_to)883 } else {884 None885 };886887 <PalletStructure<T>>::nest_if_sent_to_token(888 from.clone(),889 to,890 collection.id,891 token,892 nesting_budget,893 )?;894895 // =========896897 <PalletStructure<T>>::unnest_if_nested(&token_data.owner, collection.id, token);898899 <TokenData<T>>::insert(900 (collection.id, token),901 ItemData {902 owner: to.clone(),903 ..token_data904 },905 );906907 if let Some(balance_to) = balance_to {908 // from != to909 if balance_from == 0 {910 <AccountBalance<T>>::remove((collection.id, from));911 } else {912 <AccountBalance<T>>::insert((collection.id, from), balance_from);913 }914 <AccountBalance<T>>::insert((collection.id, to), balance_to);915 <Owned<T>>::remove((collection.id, from, token));916 <Owned<T>>::insert((collection.id, to, token), true);917 }918 Self::set_allowance_unchecked(collection, from, token, None, true);919920 <PalletEvm<T>>::deposit_log(921 ERC721Events::Transfer {922 from: *from.as_eth(),923 to: *to.as_eth(),924 token_id: token.into(),925 }926 .to_log(collection_id_to_address(collection.id)),927 );928 <PalletCommon<T>>::deposit_event(CommonEvent::Transfer(929 collection.id,930 token,931 from.clone(),932 to.clone(),933 1,934 ));935 Ok(())936 }937938 /// Batch operation to mint multiple NFT tokens.939 ///940 /// The sender should be the owner/admin of the collection or collection should be configured941 /// to allow public minting.942 /// Throws if amount of tokens reached it's limit for the collection or if caller reached943 /// token ownership limit.944 ///945 /// - `data`: Contains list of token properties and users who will become the owners of the946 /// corresponging tokens.947 /// - `nesting_budget`: Limit for token nesting depth948 pub fn create_multiple_items(949 collection: &NonfungibleHandle<T>,950 sender: &T::CrossAccountId,951 data: Vec<CreateItemData<T>>,952 nesting_budget: &dyn Budget,953 ) -> DispatchResult {954 if !collection.is_owner_or_admin(sender) {955 ensure!(956 collection.permissions.mint_mode(),957 <CommonError<T>>::PublicMintingNotAllowed958 );959 collection.check_allowlist(sender)?;960961 for item in data.iter() {962 collection.check_allowlist(&item.owner)?;963 }964 }965966 for data in data.iter() {967 <PalletCommon<T>>::ensure_correct_receiver(&data.owner)?;968 }969970 let first_token = <TokensMinted<T>>::get(collection.id);971 let tokens_minted = first_token972 .checked_add(data.len() as u32)973 .ok_or(ArithmeticError::Overflow)?;974 ensure!(975 tokens_minted <= collection.limits.token_limit(),976 <CommonError<T>>::CollectionTokenLimitExceeded977 );978979 let mut balances = BTreeMap::new();980 for data in &data {981 let balance = balances982 .entry(&data.owner)983 .or_insert_with(|| <AccountBalance<T>>::get((collection.id, &data.owner)));984 *balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?;985986 ensure!(987 *balance <= collection.limits.account_token_ownership_limit(),988 <CommonError<T>>::AccountTokenLimitExceeded,989 );990 }991992 for (i, data) in data.iter().enumerate() {993 let token = TokenId(first_token + i as u32 + 1);994995 <PalletStructure<T>>::check_nesting(996 sender.clone(),997 &data.owner,998 collection.id,999 token,1000 nesting_budget,1001 )?;1002 }10031004 // =========10051006 with_transaction(|| {1007 for (i, data) in data.iter().enumerate() {1008 let token = first_token + i as u32 + 1;10091010 <TokenData<T>>::insert(1011 (collection.id, token),1012 ItemData {1013 // const_data: data.const_data.clone(),1014 owner: data.owner.clone(),1015 },1016 );10171018 <PalletStructure<T>>::nest_if_sent_to_token_unchecked(1019 &data.owner,1020 collection.id,1021 TokenId(token),1022 );10231024 if let Err(e) = Self::set_token_properties(1025 collection,1026 sender,1027 TokenId(token),1028 data.properties.clone().into_iter(),1029 true,1030 nesting_budget,1031 ) {1032 return TransactionOutcome::Rollback(Err(e));1033 }1034 }1035 TransactionOutcome::Commit(Ok(()))1036 })?;10371038 <TokensMinted<T>>::insert(collection.id, tokens_minted);1039 for (account, balance) in balances {1040 <AccountBalance<T>>::insert((collection.id, account), balance);1041 }1042 for (i, data) in data.into_iter().enumerate() {1043 let token = first_token + i as u32 + 1;1044 <Owned<T>>::insert((collection.id, &data.owner, token), true);10451046 <PalletEvm<T>>::deposit_log(1047 ERC721Events::Transfer {1048 from: H160::default(),1049 to: *data.owner.as_eth(),1050 token_id: token.into(),1051 }1052 .to_log(collection_id_to_address(collection.id)),1053 );1054 <PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(1055 collection.id,1056 TokenId(token),1057 data.owner.clone(),1058 1,1059 ));1060 }1061 Ok(())1062 }10631064 pub fn set_allowance_unchecked(1065 collection: &NonfungibleHandle<T>,1066 sender: &T::CrossAccountId,1067 token: TokenId,1068 spender: Option<&T::CrossAccountId>,1069 assume_implicit_eth: bool,1070 ) {1071 if let Some(spender) = spender {1072 let old_spender = <Allowance<T>>::get((collection.id, token));1073 <Allowance<T>>::insert((collection.id, token), spender);1074 // In ERC721 there is only one possible approved user of token, so we set1075 // approved user to spender1076 <PalletEvm<T>>::deposit_log(1077 ERC721Events::Approval {1078 owner: *sender.as_eth(),1079 approved: *spender.as_eth(),1080 token_id: token.into(),1081 }1082 .to_log(collection_id_to_address(collection.id)),1083 );1084 // In Unique chain, any token can have any amount of approved users, so we need to1085 // set allowance of old owner to 0, and allowance of new owner to 11086 if old_spender.as_ref() != Some(spender) {1087 if let Some(old_owner) = old_spender {1088 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(1089 collection.id,1090 token,1091 sender.clone(),1092 old_owner,1093 0,1094 ));1095 }1096 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(1097 collection.id,1098 token,1099 sender.clone(),1100 spender.clone(),1101 1,1102 ));1103 }1104 } else {1105 let old_spender = <Allowance<T>>::take((collection.id, token));1106 if !assume_implicit_eth {1107 // In ERC721 there is only one possible approved user of token, so we set1108 // approved user to zero address1109 <PalletEvm<T>>::deposit_log(1110 ERC721Events::Approval {1111 owner: *sender.as_eth(),1112 approved: H160::default(),1113 token_id: token.into(),1114 }1115 .to_log(collection_id_to_address(collection.id)),1116 );1117 }1118 // In Unique chain, any token can have any amount of approved users, so we need to1119 // set allowance of old owner to 01120 if let Some(old_spender) = old_spender {1121 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(1122 collection.id,1123 token,1124 sender.clone(),1125 old_spender,1126 0,1127 ));1128 }1129 }1130 }11311132 /// Set allowance for the spender to `transfer` or `burn` sender's token.1133 ///1134 /// - `token`: Token the spender is allowed to `transfer` or `burn`.1135 pub fn set_allowance(1136 collection: &NonfungibleHandle<T>,1137 sender: &T::CrossAccountId,1138 token: TokenId,1139 spender: Option<&T::CrossAccountId>,1140 ) -> DispatchResult {1141 if collection.permissions.access() == AccessMode::AllowList {1142 collection.check_allowlist(sender)?;1143 if let Some(spender) = spender {1144 collection.check_allowlist(spender)?;1145 }1146 }11471148 if let Some(spender) = spender {1149 <PalletCommon<T>>::ensure_correct_receiver(spender)?;1150 }11511152 let token_data =1153 <TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;1154 if &token_data.owner != sender {1155 ensure!(1156 collection.ignores_owned_amount(sender),1157 <CommonError<T>>::CantApproveMoreThanOwned1158 );1159 }11601161 // =========11621163 Self::set_allowance_unchecked(collection, sender, token, spender, false);1164 Ok(())1165 }11661167 /// Checks allowance for the spender to use the token.1168 fn check_allowed(1169 collection: &NonfungibleHandle<T>,1170 spender: &T::CrossAccountId,1171 from: &T::CrossAccountId,1172 token: TokenId,1173 nesting_budget: &dyn Budget,1174 ) -> DispatchResult {1175 if spender.conv_eq(from) {1176 return Ok(());1177 }1178 if collection.permissions.access() == AccessMode::AllowList {1179 // `from`, `to` checked in [`transfer`]1180 collection.check_allowlist(spender)?;1181 }11821183 if collection.limits.owner_can_transfer() && collection.is_owner_or_admin(spender) {1184 return Ok(());1185 }11861187 if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {1188 ensure!(1189 <PalletStructure<T>>::check_indirectly_owned(1190 spender.clone(),1191 source.0,1192 source.1,1193 None,1194 nesting_budget1195 )?,1196 <CommonError<T>>::ApprovedValueTooLow,1197 );1198 return Ok(());1199 }1200 if <Allowance<T>>::get((collection.id, token)).as_ref() == Some(spender) {1201 return Ok(());1202 }1203 ensure!(1204 collection.ignores_allowance(spender),1205 <CommonError<T>>::ApprovedValueTooLow1206 );1207 Ok(())1208 }12091210 /// Transfer NFT token from one account to another.1211 ///1212 /// Same as the [`transfer`] but spender doesn't needs to be the owner of the token.1213 /// The owner should set allowance for the spender to transfer token.1214 ///1215 /// [`transfer`]: struct.Pallet.html#method.transfer1216 pub fn transfer_from(1217 collection: &NonfungibleHandle<T>,1218 spender: &T::CrossAccountId,1219 from: &T::CrossAccountId,1220 to: &T::CrossAccountId,1221 token: TokenId,1222 nesting_budget: &dyn Budget,1223 ) -> DispatchResult {1224 Self::check_allowed(collection, spender, from, token, nesting_budget)?;12251226 // =========12271228 // Allowance is reset in [`transfer`]1229 Self::transfer(collection, from, to, token, nesting_budget)1230 }12311232 /// Burn NFT token for `from` account.1233 ///1234 /// Same as the [`burn`] but spender doesn't need to be an owner of the token. The owner should1235 /// set allowance for the spender to burn token.1236 ///1237 /// [`burn`]: struct.Pallet.html#method.burn1238 pub fn burn_from(1239 collection: &NonfungibleHandle<T>,1240 spender: &T::CrossAccountId,1241 from: &T::CrossAccountId,1242 token: TokenId,1243 nesting_budget: &dyn Budget,1244 ) -> DispatchResult {1245 Self::check_allowed(collection, spender, from, token, nesting_budget)?;12461247 // =========12481249 Self::burn(collection, from, token)1250 }12511252 /// Check that `from` token could be nested in `under` token.1253 ///1254 pub fn check_nesting(1255 handle: &NonfungibleHandle<T>,1256 sender: T::CrossAccountId,1257 from: (CollectionId, TokenId),1258 under: TokenId,1259 nesting_budget: &dyn Budget,1260 ) -> DispatchResult {1261 let nesting = handle.permissions.nesting();12621263 #[cfg(not(feature = "runtime-benchmarks"))]1264 let permissive = false;1265 #[cfg(feature = "runtime-benchmarks")]1266 let permissive = nesting.permissive;12671268 if permissive {1269 // Pass1270 } else if nesting.token_owner1271 && <PalletStructure<T>>::check_indirectly_owned(1272 sender.clone(),1273 handle.id,1274 under,1275 Some(from),1276 nesting_budget,1277 )? {1278 // Pass1279 } else if nesting.collection_admin && handle.is_owner_or_admin(&sender) {1280 // Pass1281 } else {1282 fail!(<CommonError<T>>::UserIsNotAllowedToNest);1283 }12841285 if let Some(whitelist) = &nesting.restricted {1286 ensure!(1287 whitelist.contains(&from.0),1288 <CommonError<T>>::SourceCollectionIsNotAllowedToNest1289 );1290 }1291 Ok(())1292 }12931294 fn nest(under: (CollectionId, TokenId), to_nest: (CollectionId, TokenId)) {1295 <TokenChildren<T>>::insert((under.0, under.1, (to_nest.0, to_nest.1)), true);1296 }12971298 fn unnest(under: (CollectionId, TokenId), to_unnest: (CollectionId, TokenId)) {1299 <TokenChildren<T>>::remove((under.0, under.1, to_unnest));1300 }13011302 fn collection_has_tokens(collection_id: CollectionId) -> bool {1303 <TokenData<T>>::iter_prefix((collection_id,))1304 .next()1305 .is_some()1306 }13071308 fn token_has_children(collection_id: CollectionId, token_id: TokenId) -> bool {1309 <TokenChildren<T>>::iter_prefix((collection_id, token_id))1310 .next()1311 .is_some()1312 }13131314 pub fn token_children_ids(collection_id: CollectionId, token_id: TokenId) -> Vec<TokenChild> {1315 <TokenChildren<T>>::iter_prefix((collection_id, token_id))1316 .map(|((child_collection_id, child_id), _)| TokenChild {1317 collection: child_collection_id,1318 token: child_id,1319 })1320 .collect()1321 }13221323 /// Mint single NFT token.1324 ///1325 /// Delegated to [`create_multiple_items`]1326 ///1327 /// [`create_multiple_items`]: struct.Pallet.html#method.create_multiple_items1328 pub fn create_item(1329 collection: &NonfungibleHandle<T>,1330 sender: &T::CrossAccountId,1331 data: CreateItemData<T>,1332 nesting_budget: &dyn Budget,1333 ) -> DispatchResult {1334 Self::create_multiple_items(collection, sender, vec![data], nesting_budget)1335 }1336}pallets/refungible/src/erc.rsdiffbeforeafterboth--- a/pallets/refungible/src/erc.rs
+++ b/pallets/refungible/src/erc.rs
@@ -228,7 +228,7 @@
if !url.is_empty() {
return Ok(url);
}
- } else if !is_erc721_metadata_compatible::<T>(self.id) {
+ } else if !self.supports_metadata() {
return Err("tokenURI not set".into());
}
@@ -578,17 +578,6 @@
Err("Property tokenURI not found".into())
}
-fn is_erc721_metadata_compatible<T: Config>(collection_id: CollectionId) -> bool {
- if let Some(shema_name) =
- pallet_common::Pallet::<T>::get_collection_property(collection_id, &key::schema_name())
- {
- let shema_name = shema_name.into_inner();
- shema_name == property_value::ERC721_METADATA
- } else {
- false
- }
-}
-
fn get_token_permission<T: Config>(
collection_id: CollectionId,
key: &PropertyKey,
@@ -780,7 +769,7 @@
name = UniqueRefungible,
is(
ERC721,
- ERC721Metadata,
+ ERC721Metadata(if(this.supports_metadata())),
ERC721Enumerable,
ERC721UniqueExtensions,
ERC721Mintable,
pallets/refungible/src/lib.rsdiffbeforeafterboth--- a/pallets/refungible/src/lib.rs
+++ b/pallets/refungible/src/lib.rs
@@ -92,14 +92,19 @@
use codec::{Encode, Decode, MaxEncodedLen};
use core::ops::Deref;
+use derivative::Derivative;
use evm_coder::ToLog;
use frame_support::{
- BoundedVec, ensure, fail, storage::with_transaction, transactional, pallet_prelude::ConstU32,
+ BoundedBTreeMap, BoundedVec, ensure, fail, storage::with_transaction, transactional,
+ pallet_prelude::ConstU32,
};
use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};
use pallet_evm_coder_substrate::WithRecorder;
use pallet_common::{
- CommonCollectionOperations, Error as CommonError, eth::collection_id_to_address,
+ CommonCollectionOperations,
+ erc::static_property::{key, value},
+ Error as CommonError,
+ eth::collection_id_to_address,
Event as CommonEvent, Pallet as PalletCommon,
};
use pallet_structure::Pallet as PalletStructure;
@@ -113,8 +118,6 @@
MAX_REFUNGIBLE_PIECES, Property, PropertyKey, PropertyKeyPermission, PropertyPermission,
PropertyScope, PropertyValue, TokenId, TrySetProperty,
};
-use frame_support::BoundedBTreeMap;
-use derivative::Derivative;
pub use pallet::*;
#[cfg(feature = "runtime-benchmarks")]
@@ -301,6 +304,18 @@
}
}
+impl<T: Config> RefungibleHandle<T> {
+ pub fn supports_metadata(&self) -> bool {
+ if let Some(erc721_metadata) =
+ pallet_common::Pallet::<T>::get_collection_property(self.id, &key::erc721_metadata())
+ {
+ *erc721_metadata.into_inner() == *value::ERC721_METADATA_SUPPORTED
+ } else {
+ false
+ }
+ }
+}
+
impl<T: Config> Deref for RefungibleHandle<T> {
type Target = pallet_common::CollectionHandle<T>;
pallets/unique/src/eth/mod.rsdiffbeforeafterboth--- a/pallets/unique/src/eth/mod.rs
+++ b/pallets/unique/src/eth/mod.rs
@@ -130,6 +130,13 @@
})
.map_err(|e| Error::Revert(format!("{:?}", e)))?;
+ properties
+ .try_push(up_data_structs::Property {
+ key: key::erc721_metadata(),
+ value: property_value::erc721_metadata_supported(),
+ })
+ .map_err(|e| Error::Revert(format!("{:?}", e)))?;
+
if !base_uri_value.is_empty() {
properties
.try_push(up_data_structs::Property {
@@ -212,7 +219,8 @@
/// @param tokenPrefix Token prefix to represent the collection tokens in UI and user applications
/// @return address Address of the newly created collection
#[weight(<SelfWeightOf<T>>::create_collection())]
- fn create_nonfungible_collection(
+ #[solidity(rename_selector = "createNFTCollection")]
+ fn create_nft_collection(
&mut self,
caller: caller,
value: value,
@@ -239,9 +247,26 @@
let address = pallet_common::eth::collection_id_to_address(collection_id);
Ok(address)
}
+ /// Create an NFT collection
+ /// @param name Name of the collection
+ /// @param description Informative description of the collection
+ /// @param tokenPrefix Token prefix to represent the collection tokens in UI and user applications
+ /// @return address Address of the newly created collection
+ #[weight(<SelfWeightOf<T>>::create_collection())]
+ #[deprecated(note = "mathod was renamed to `create_nft_collection`, prefer it instead")]
+ fn create_nonfungible_collection(
+ &mut self,
+ caller: caller,
+ value: value,
+ name: string,
+ description: string,
+ token_prefix: string,
+ ) -> Result<address> {
+ self.create_nft_collection(caller, value, name, description, token_prefix)
+ }
#[weight(<SelfWeightOf<T>>::create_collection())]
- #[solidity(rename_selector = "createERC721MetadataCompatibleCollection")]
+ #[solidity(rename_selector = "createERC721MetadataNFTCollection")]
fn create_nonfungible_collection_with_properties(
&mut self,
caller: caller,
@@ -273,6 +298,27 @@
#[weight(<SelfWeightOf<T>>::create_collection())]
#[solidity(rename_selector = "createRFTCollection")]
+ fn create_rft_collection(
+ &mut self,
+ caller: caller,
+ value: value,
+ name: string,
+ description: string,
+ token_prefix: string,
+ ) -> Result<address> {
+ create_refungible_collection_internal::<T>(
+ caller,
+ value,
+ name,
+ description,
+ token_prefix,
+ Default::default(),
+ false,
+ )
+ }
+
+ #[weight(<SelfWeightOf<T>>::create_collection())]
+ #[deprecated(note = "mathod was renamed to `create_rft_collection`, prefer it instead")]
fn create_refungible_collection(
&mut self,
caller: caller,
@@ -293,7 +339,7 @@
}
#[weight(<SelfWeightOf<T>>::create_collection())]
- #[solidity(rename_selector = "createERC721MetadataCompatibleRFTCollection")]
+ #[solidity(rename_selector = "createERC721MetadataRFTCollection")]
fn create_refungible_collection_with_properties(
&mut self,
caller: caller,
pallets/unique/src/eth/stubs/CollectionHelpers.rawdiffbeforeafterbothbinary blob — no preview
pallets/unique/src/eth/stubs/CollectionHelpers.soldiffbeforeafterboth--- a/pallets/unique/src/eth/stubs/CollectionHelpers.sol
+++ b/pallets/unique/src/eth/stubs/CollectionHelpers.sol
@@ -23,13 +23,33 @@
}
/// @title Contract, which allows users to operate with collections
-/// @dev the ERC-165 identifier for this interface is 0x5ad4f440
+/// @dev the ERC-165 identifier for this interface is 0xf62c7aa9
contract CollectionHelpers is Dummy, ERC165, CollectionHelpersEvents {
/// Create an NFT collection
/// @param name Name of the collection
/// @param description Informative description of the collection
/// @param tokenPrefix Token prefix to represent the collection tokens in UI and user applications
/// @return address Address of the newly created collection
+ /// @dev EVM selector for this function is: 0x844af658,
+ /// or in textual repr: createNFTCollection(string,string,string)
+ function createNFTCollection(
+ string memory name,
+ string memory description,
+ string memory tokenPrefix
+ ) public payable returns (address) {
+ require(false, stub_error);
+ name;
+ description;
+ tokenPrefix;
+ dummy = 0;
+ return 0x0000000000000000000000000000000000000000;
+ }
+
+ /// Create an NFT collection
+ /// @param name Name of the collection
+ /// @param description Informative description of the collection
+ /// @param tokenPrefix Token prefix to represent the collection tokens in UI and user applications
+ /// @return address Address of the newly created collection
/// @dev EVM selector for this function is: 0xe34a6844,
/// or in textual repr: createNonfungibleCollection(string,string,string)
function createNonfungibleCollection(
@@ -45,9 +65,9 @@
return 0x0000000000000000000000000000000000000000;
}
- /// @dev EVM selector for this function is: 0xa634a5f9,
- /// or in textual repr: createERC721MetadataCompatibleCollection(string,string,string,string)
- function createERC721MetadataCompatibleCollection(
+ /// @dev EVM selector for this function is: 0xd1df968c,
+ /// or in textual repr: createERC721MetadataNFTCollection(string,string,string,string)
+ function createERC721MetadataNFTCollection(
string memory name,
string memory description,
string memory tokenPrefix,
@@ -77,9 +97,24 @@
return 0x0000000000000000000000000000000000000000;
}
- /// @dev EVM selector for this function is: 0xa5596388,
- /// or in textual repr: createERC721MetadataCompatibleRFTCollection(string,string,string,string)
- function createERC721MetadataCompatibleRFTCollection(
+ /// @dev EVM selector for this function is: 0x44a68ad5,
+ /// or in textual repr: createRefungibleCollection(string,string,string)
+ function createRefungibleCollection(
+ string memory name,
+ string memory description,
+ string memory tokenPrefix
+ ) public payable returns (address) {
+ require(false, stub_error);
+ name;
+ description;
+ tokenPrefix;
+ dummy = 0;
+ return 0x0000000000000000000000000000000000000000;
+ }
+
+ /// @dev EVM selector for this function is: 0xbea6a299,
+ /// or in textual repr: createERC721MetadataRFTCollection(string,string,string,string)
+ function createERC721MetadataRFTCollection(
string memory name,
string memory description,
string memory tokenPrefix,
tests/src/deprecated-helpers/eth/helpers.tsdiffbeforeafterboth--- a/tests/src/deprecated-helpers/eth/helpers.ts
+++ b/tests/src/deprecated-helpers/eth/helpers.ts
@@ -150,10 +150,10 @@
}
-export async function createNonfungibleCollection(api: ApiPromise, web3: Web3, owner: string) {
+export async function createNFTCollection(api: ApiPromise, web3: Web3, owner: string) {
const collectionHelper = evmCollectionHelpers(web3, owner);
const result = await collectionHelper.methods
- .createNonfungibleCollection('A', 'B', 'C')
+ .createNFTCollection('A', 'B', 'C')
.send({value: Number(2n * UNIQUE)});
return await getCollectionAddressFromResult(api, result);
}
tests/src/deprecated-helpers/helpers.tsdiffbeforeafterboth--- a/tests/src/deprecated-helpers/helpers.ts
+++ b/tests/src/deprecated-helpers/helpers.ts
@@ -433,6 +433,7 @@
mode: {type: 'NFT'},
name: 'name',
tokenPrefix: 'prefix',
+ properties: [{key: 'ERC721Metadata', value: '1'}],
};
export async function
@@ -441,7 +442,7 @@
sender: IKeyringPair,
params: Partial<CreateCollectionParams> = {},
): Promise<CreateCollectionResult> {
- const {name, description, mode, tokenPrefix} = {...defaultCreateCollectionParams, ...params};
+ const {name, description, mode, tokenPrefix, properties} = {...defaultCreateCollectionParams, ...params};
let modeprm = {};
if (mode.type === 'NFT') {
@@ -457,6 +458,7 @@
description: strToUTF16(description),
tokenPrefix: strToUTF16(tokenPrefix),
mode: modeprm as any,
+ properties,
});
const events = await executeTransaction(api, sender, tx);
return getCreateCollectionResult(events);
tests/src/eth/allowlist.test.tsdiffbeforeafterboth--- a/tests/src/eth/allowlist.test.ts
+++ b/tests/src/eth/allowlist.test.ts
@@ -78,7 +78,7 @@
const owner = await helper.eth.createAccountWithBalance(donor);
const user = helper.eth.createAccount();
- const {collectionAddress} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C');
+ const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C');
const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
expect(await collectionEvm.methods.allowed(user).call({from: owner})).to.be.false;
@@ -94,7 +94,7 @@
// const owner = await helper.eth.createAccountWithBalance(donor);
// const user = donor;
- // const {collectionAddress, collectionId} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C');
+ // const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C');
// const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
// expect(await helper.collection.allowed(collectionId, {Substrate: user.address})).to.be.false;
@@ -110,7 +110,7 @@
const notOwner = await helper.eth.createAccountWithBalance(donor);
const user = helper.eth.createAccount();
- const {collectionAddress} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C');
+ const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C');
const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
expect(await collectionEvm.methods.allowed(user).call({from: owner})).to.be.false;
@@ -129,7 +129,7 @@
// const notOwner = await helper.eth.createAccountWithBalance(donor);
// const user = donor;
- // const {collectionAddress, collectionId} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C');
+ // const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C');
// const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
// expect(await helper.collection.allowed(collectionId, {Substrate: user.address})).to.be.false;
tests/src/eth/api/CollectionHelpers.soldiffbeforeafterboth--- a/tests/src/eth/api/CollectionHelpers.sol
+++ b/tests/src/eth/api/CollectionHelpers.sol
@@ -18,13 +18,26 @@
}
/// @title Contract, which allows users to operate with collections
-/// @dev the ERC-165 identifier for this interface is 0x5ad4f440
+/// @dev the ERC-165 identifier for this interface is 0xf62c7aa9
interface CollectionHelpers is Dummy, ERC165, CollectionHelpersEvents {
/// Create an NFT collection
/// @param name Name of the collection
/// @param description Informative description of the collection
/// @param tokenPrefix Token prefix to represent the collection tokens in UI and user applications
/// @return address Address of the newly created collection
+ /// @dev EVM selector for this function is: 0x844af658,
+ /// or in textual repr: createNFTCollection(string,string,string)
+ function createNFTCollection(
+ string memory name,
+ string memory description,
+ string memory tokenPrefix
+ ) external payable returns (address);
+
+ /// Create an NFT collection
+ /// @param name Name of the collection
+ /// @param description Informative description of the collection
+ /// @param tokenPrefix Token prefix to represent the collection tokens in UI and user applications
+ /// @return address Address of the newly created collection
/// @dev EVM selector for this function is: 0xe34a6844,
/// or in textual repr: createNonfungibleCollection(string,string,string)
function createNonfungibleCollection(
@@ -33,9 +46,9 @@
string memory tokenPrefix
) external payable returns (address);
- /// @dev EVM selector for this function is: 0xa634a5f9,
- /// or in textual repr: createERC721MetadataCompatibleCollection(string,string,string,string)
- function createERC721MetadataCompatibleCollection(
+ /// @dev EVM selector for this function is: 0xd1df968c,
+ /// or in textual repr: createERC721MetadataNFTCollection(string,string,string,string)
+ function createERC721MetadataNFTCollection(
string memory name,
string memory description,
string memory tokenPrefix,
@@ -50,9 +63,17 @@
string memory tokenPrefix
) external payable returns (address);
- /// @dev EVM selector for this function is: 0xa5596388,
- /// or in textual repr: createERC721MetadataCompatibleRFTCollection(string,string,string,string)
- function createERC721MetadataCompatibleRFTCollection(
+ /// @dev EVM selector for this function is: 0x44a68ad5,
+ /// or in textual repr: createRefungibleCollection(string,string,string)
+ function createRefungibleCollection(
+ string memory name,
+ string memory description,
+ string memory tokenPrefix
+ ) external payable returns (address);
+
+ /// @dev EVM selector for this function is: 0xbea6a299,
+ /// or in textual repr: createERC721MetadataRFTCollection(string,string,string,string)
+ function createERC721MetadataRFTCollection(
string memory name,
string memory description,
string memory tokenPrefix,
tests/src/eth/collectionAdmin.test.tsdiffbeforeafterboth--- a/tests/src/eth/collectionAdmin.test.ts
+++ b/tests/src/eth/collectionAdmin.test.ts
@@ -38,7 +38,7 @@
itEth('Add admin by owner', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor);
- const {collectionAddress, collectionId} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C');
+ const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C');
const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
const newAdmin = helper.eth.createAccount();
@@ -51,7 +51,7 @@
itEth.skip('Add substrate admin by owner', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor);
- const {collectionAddress, collectionId} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C');
+ const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C');
const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
const [newAdmin] = await helper.arrange.createAccounts([10n], donor);
@@ -64,7 +64,7 @@
itEth('Verify owner or admin', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor);
- const {collectionAddress} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C');
+ const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C');
const newAdmin = helper.eth.createAccount();
const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
@@ -75,7 +75,7 @@
itEth('(!negative tests!) Add admin by ADMIN is not allowed', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor);
- const {collectionAddress, collectionId} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C');
+ const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C');
const admin = await helper.eth.createAccountWithBalance(donor);
const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
@@ -93,7 +93,7 @@
itEth('(!negative tests!) Add admin by USER is not allowed', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor);
- const {collectionAddress, collectionId} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C');
+ const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C');
const notAdmin = await helper.eth.createAccountWithBalance(donor);
const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
@@ -108,7 +108,7 @@
itEth.skip('(!negative tests!) Add substrate admin by ADMIN is not allowed', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor);
- const {collectionAddress, collectionId} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C');
+ const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C');
const admin = await helper.eth.createAccountWithBalance(donor);
const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
@@ -126,7 +126,7 @@
itEth.skip('(!negative tests!) Add substrate admin by USER is not allowed', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor);
- const {collectionAddress, collectionId} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C');
+ const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C');
const notAdmin0 = await helper.eth.createAccountWithBalance(donor);
const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
@@ -150,7 +150,7 @@
itEth('Remove admin by owner', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor);
- const {collectionAddress, collectionId} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C');
+ const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C');
const newAdmin = helper.eth.createAccount();
const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
@@ -170,7 +170,7 @@
itEth.skip('Remove substrate admin by owner', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor);
- const {collectionAddress, collectionId} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C');
+ const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C');
const [newAdmin] = await helper.arrange.createAccounts([10n], donor);
const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
@@ -188,7 +188,7 @@
itEth('(!negative tests!) Remove admin by ADMIN is not allowed', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor);
- const {collectionAddress, collectionId} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C');
+ const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C');
const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
@@ -210,7 +210,7 @@
itEth('(!negative tests!) Remove admin by USER is not allowed', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor);
- const {collectionAddress, collectionId} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C');
+ const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C');
const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
@@ -230,7 +230,7 @@
itEth.skip('(!negative tests!) Remove substrate admin by ADMIN is not allowed', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor);
- const {collectionAddress, collectionId} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C');
+ const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C');
const [adminSub] = await helper.arrange.createAccounts([10n], donor);
const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
@@ -250,7 +250,7 @@
itEth.skip('(!negative tests!) Remove substrate admin by USER is not allowed', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor);
- const {collectionAddress, collectionId} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C');
+ const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C');
const [adminSub] = await helper.arrange.createAccounts([10n], donor);
const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
@@ -279,7 +279,7 @@
itEth('Change owner', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor);
const newOwner = await helper.eth.createAccountWithBalance(donor);
- const {collectionAddress} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C');
+ const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C');
const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
await collectionEvm.methods.setOwner(newOwner).send();
@@ -291,7 +291,7 @@
itEth('change owner call fee', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor);
const newOwner = await helper.eth.createAccountWithBalance(donor);
- const {collectionAddress} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C');
+ const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C');
const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
const cost = await recordEthFee(helper, owner, () => collectionEvm.methods.setOwner(newOwner).send());
expect(cost < BigInt(0.2 * Number(helper.balance.getOneTokenNominal())));
@@ -301,7 +301,7 @@
itEth('(!negative tests!) call setOwner by non owner', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor);
const newOwner = await helper.eth.createAccountWithBalance(donor);
- const {collectionAddress} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C');
+ const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C');
const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
await expect(collectionEvm.methods.setOwner(newOwner).send({from: newOwner})).to.be.rejected;
@@ -321,7 +321,7 @@
itEth.skip('Change owner', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor);
const [newOwner] = await helper.arrange.createAccounts([10n], donor);
- const {collectionAddress} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C');
+ const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C');
const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
expect(await collectionEvm.methods.isOwnerOrAdmin(owner).call()).to.be.true;
@@ -336,7 +336,7 @@
itEth.skip('change owner call fee', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor);
const [newOwner] = await helper.arrange.createAccounts([10n], donor);
- const {collectionAddress} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C');
+ const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C');
const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
const cost = await recordEthFee(helper, owner, () => collectionEvm.methods.setOwnerSubstrate(newOwner.addressRaw).send());
@@ -348,7 +348,7 @@
const owner = await helper.eth.createAccountWithBalance(donor);
const otherReceiver = await helper.eth.createAccountWithBalance(donor);
const [newOwner] = await helper.arrange.createAccounts([10n], donor);
- const {collectionAddress} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C');
+ const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C');
const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
await expect(collectionEvm.methods.setOwnerSubstrate(newOwner.addressRaw).send({from: otherReceiver})).to.be.rejected;
tests/src/eth/collectionHelpersAbi.jsondiffbeforeafterboth--- a/tests/src/eth/collectionHelpersAbi.json
+++ b/tests/src/eth/collectionHelpersAbi.json
@@ -32,7 +32,7 @@
{ "internalType": "string", "name": "tokenPrefix", "type": "string" },
{ "internalType": "string", "name": "baseUri", "type": "string" }
],
- "name": "createERC721MetadataCompatibleCollection",
+ "name": "createERC721MetadataNFTCollection",
"outputs": [{ "internalType": "address", "name": "", "type": "address" }],
"stateMutability": "payable",
"type": "function"
@@ -44,7 +44,18 @@
{ "internalType": "string", "name": "tokenPrefix", "type": "string" },
{ "internalType": "string", "name": "baseUri", "type": "string" }
],
- "name": "createERC721MetadataCompatibleRFTCollection",
+ "name": "createERC721MetadataRFTCollection",
+ "outputs": [{ "internalType": "address", "name": "", "type": "address" }],
+ "stateMutability": "payable",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ { "internalType": "string", "name": "name", "type": "string" },
+ { "internalType": "string", "name": "description", "type": "string" },
+ { "internalType": "string", "name": "tokenPrefix", "type": "string" }
+ ],
+ "name": "createNFTCollection",
"outputs": [{ "internalType": "address", "name": "", "type": "address" }],
"stateMutability": "payable",
"type": "function"
@@ -73,6 +84,17 @@
},
{
"inputs": [
+ { "internalType": "string", "name": "name", "type": "string" },
+ { "internalType": "string", "name": "description", "type": "string" },
+ { "internalType": "string", "name": "tokenPrefix", "type": "string" }
+ ],
+ "name": "createRefungibleCollection",
+ "outputs": [{ "internalType": "address", "name": "", "type": "address" }],
+ "stateMutability": "payable",
+ "type": "function"
+ },
+ {
+ "inputs": [
{
"internalType": "address",
"name": "collectionAddress",
tests/src/eth/collectionProperties.test.tsdiffbeforeafterboth--- a/tests/src/eth/collectionProperties.test.ts
+++ b/tests/src/eth/collectionProperties.test.ts
@@ -24,7 +24,7 @@
const raw = (await collection.getData())?.raw;
- expect(raw.properties[0].value).to.equal('testValue');
+ expect(raw.properties[1].value).to.equal('testValue');
});
itEth('Can be deleted', async({helper}) => {
@@ -54,3 +54,46 @@
expect(value).to.equal(helper.getWeb3().utils.toHex('testValue'));
});
});
+
+describe('Supports ERC721Metadata', () => {
+ let donor: IKeyringPair;
+
+ before(async function() {
+ await usingEthPlaygrounds(async (_helper, privateKey) => {
+ donor = privateKey('//Alice');
+ });
+ });
+
+ itEth('ERC721Metadata property can be set for NFT collection', async({helper}) => {
+ const caller = await helper.eth.createAccountWithBalance(donor);
+ const collection = await helper.nft.mintCollection(donor, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+
+ await collection.addAdmin(donor, {Ethereum: caller});
+ const contract = helper.ethNativeContract.collectionById(collection.collectionId, 'nft', caller);
+
+ await contract.methods.setCollectionProperty('ERC721Metadata', Buffer.from('1')).send({from: caller});
+
+ expect(await contract.methods.supportsInterface('0x5b5e139f').call()).to.be.true;
+
+ await contract.methods.setCollectionProperty('ERC721Metadata', Buffer.from('0')).send({from: caller});
+
+ expect(await contract.methods.supportsInterface('0x5b5e139f').call()).to.be.false;
+ });
+
+ itEth('ERC721Metadata property can be set for RFT collection', async({helper}) => {
+ const caller = await helper.eth.createAccountWithBalance(donor);
+ const collection = await helper.rft.mintCollection(donor, {name: 'col', description: 'descr', tokenPrefix: 'COL'});
+
+ await collection.addAdmin(donor, {Ethereum: caller});
+
+ const contract = helper.ethNativeContract.collectionById(collection.collectionId, 'nft', caller);
+
+ await contract.methods.setCollectionProperty('ERC721Metadata', Buffer.from('1')).send({from: caller});
+
+ expect(await contract.methods.supportsInterface('0x5b5e139f').call()).to.be.true;
+
+ await contract.methods.setCollectionProperty('ERC721Metadata', Buffer.from('0')).send({from: caller});
+
+ expect(await contract.methods.supportsInterface('0x5b5e139f').call()).to.be.false;
+ });
+});
tests/src/eth/collectionSponsoring.test.tsdiffbeforeafterboth--- a/tests/src/eth/collectionSponsoring.test.ts
+++ b/tests/src/eth/collectionSponsoring.test.ts
@@ -54,7 +54,7 @@
// itWeb3('Set substrate sponsor', async ({api, web3, privateKeyWrapper}) => {
// const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
// const collectionHelpers = evmCollectionHelpers(web3, owner);
- // let result = await collectionHelpers.methods.createNonfungibleCollection('Sponsor collection', '1', '1').send();
+ // let result = await collectionHelpers.methods.createNFTCollection('Sponsor collection', '1', '1').send();
// const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result);
// const sponsor = privateKeyWrapper('//Alice');
// const collectionEvm = evmCollection(web3, owner, collectionIdAddress);
@@ -75,7 +75,7 @@
const owner = await helper.eth.createAccountWithBalance(donor);
const collectionHelpers = helper.ethNativeContract.collectionHelpers(owner);
- let result = await collectionHelpers.methods.createNonfungibleCollection('Sponsor collection', '1', '1').send({value: Number(2n * nominal)});
+ let result = await collectionHelpers.methods.createNFTCollection('Sponsor collection', '1', '1').send({value: Number(2n * nominal)});
const collectionIdAddress = helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId);
const sponsor = await helper.eth.createAccountWithBalance(donor);
const collectionEvm = helper.ethNativeContract.collection(collectionIdAddress, 'nft', owner);
@@ -97,7 +97,7 @@
const owner = await helper.eth.createAccountWithBalance(donor);
const collectionHelpers = helper.ethNativeContract.collectionHelpers(owner);
- let result = await collectionHelpers.methods.createNonfungibleCollection('Sponsor collection', '1', '1').send({value: Number(2n * nominal)});
+ let result = await collectionHelpers.methods.createERC721MetadataNFTCollection('Sponsor collection', '1', '1', '').send({value: Number(2n * nominal)});
const collectionIdAddress = helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId);
const collectionId = helper.ethAddress.extractCollectionId(collectionIdAddress);
const collection = helper.nft.getCollectionObject(collectionId);
@@ -167,7 +167,7 @@
// itWeb3('Sponsoring collection from substrate address via access list', async ({api, web3, privateKeyWrapper}) => {
// const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
// const collectionHelpers = evmCollectionHelpers(web3, owner);
- // const result = await collectionHelpers.methods.createNonfungibleCollection('Sponsor collection', '1', '1').send();
+ // const result = await collectionHelpers.methods.createERC721MetadataNFTCollection('Sponsor collection', '1', '1', '').send();
// const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result);
// const sponsor = privateKeyWrapper('//Alice');
// const collectionEvm = evmCollection(web3, owner, collectionIdAddress);
@@ -223,7 +223,7 @@
const owner = await helper.eth.createAccountWithBalance(donor);
const collectionHelpers = helper.ethNativeContract.collectionHelpers(owner);
- let result = await collectionHelpers.methods.createNonfungibleCollection('Sponsor collection', '1', '1').send({value: Number(2n * nominal)});
+ let result = await collectionHelpers.methods.createERC721MetadataNFTCollection('Sponsor collection', '1', '1', '').send({value: Number(2n * nominal)});
const collectionIdAddress = helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId);
const collectionId = helper.ethAddress.extractCollectionId(collectionIdAddress);
const collection = helper.nft.getCollectionObject(collectionId);
tests/src/eth/createNFTCollection.test.tsdiffbeforeafterboth--- a/tests/src/eth/createNFTCollection.test.ts
+++ b/tests/src/eth/createNFTCollection.test.ts
@@ -37,7 +37,7 @@
// todo:playgrounds this might fail when in async environment.
const collectionCountBefore = +(await helper.callRpc('api.rpc.unique.collectionStats')).created;
- const {collectionId} = await helper.eth.createNonfungibleCollection(owner, name, description, prefix);
+ const {collectionId} = await helper.eth.createNFTCollection(owner, name, description, prefix);
const collectionCountAfter = +(await helper.callRpc('api.rpc.unique.collectionStats')).created;
const collection = helper.nft.getCollectionObject(collectionId);
@@ -64,7 +64,7 @@
.call()).to.be.false;
await collectionHelpers.methods
- .createNonfungibleCollection('A', 'A', 'A')
+ .createNFTCollection('A', 'A', 'A')
.send({value: Number(2n * helper.balance.getOneTokenNominal())});
expect(await collectionHelpers.methods
@@ -76,7 +76,7 @@
const owner = await helper.eth.createAccountWithBalance(donor);
const sponsor = await helper.eth.createAccountWithBalance(donor);
const ss58Format = helper.chain.getChainProperties().ss58Format;
- const {collectionId, collectionAddress} = await helper.eth.createNonfungibleCollection(owner, 'Sponsor', 'absolutely anything', 'ROC');
+ const {collectionId, collectionAddress} = await helper.eth.createNFTCollection(owner, 'Sponsor', 'absolutely anything', 'ROC');
const collection = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
await collection.methods.setCollectionSponsor(sponsor).send();
@@ -95,7 +95,7 @@
itEth('Set limits', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor);
- const {collectionId, collectionAddress} = await helper.eth.createNonfungibleCollection(owner, 'Limits', 'absolutely anything', 'FLO');
+ const {collectionId, collectionAddress} = await helper.eth.createNFTCollection(owner, 'Limits', 'absolutely anything', 'FLO');
const limits = {
accountTokenOwnershipLimit: 1000,
sponsoredDataSize: 1024,
@@ -138,7 +138,7 @@
.methods.isCollectionExist(collectionAddressForNonexistentCollection).call())
.to.be.false;
- const {collectionAddress} = await helper.eth.createNonfungibleCollection(owner, 'Exister', 'absolutely anything', 'EVC');
+ const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'Exister', 'absolutely anything', 'EVC');
expect(await helper.ethNativeContract.collectionHelpers(collectionAddress)
.methods.isCollectionExist(collectionAddress).call())
.to.be.true;
@@ -166,7 +166,7 @@
const tokenPrefix = 'A';
await expect(collectionHelper.methods
- .createNonfungibleCollection(collectionName, description, tokenPrefix)
+ .createNFTCollection(collectionName, description, tokenPrefix)
.call({value: Number(2n * nominal)})).to.be.rejectedWith('name is too long. Max length is ' + MAX_NAME_LENGTH);
}
@@ -176,7 +176,7 @@
const description = 'A'.repeat(MAX_DESCRIPTION_LENGTH + 1);
const tokenPrefix = 'A';
await expect(collectionHelper.methods
- .createNonfungibleCollection(collectionName, description, tokenPrefix)
+ .createNFTCollection(collectionName, description, tokenPrefix)
.call({value: Number(2n * nominal)})).to.be.rejectedWith('description is too long. Max length is ' + MAX_DESCRIPTION_LENGTH);
}
{
@@ -185,7 +185,7 @@
const description = 'A';
const tokenPrefix = 'A'.repeat(MAX_TOKEN_PREFIX_LENGTH + 1);
await expect(collectionHelper.methods
- .createNonfungibleCollection(collectionName, description, tokenPrefix)
+ .createNFTCollection(collectionName, description, tokenPrefix)
.call({value: Number(2n * nominal)})).to.be.rejectedWith('token_prefix is too long. Max length is ' + MAX_TOKEN_PREFIX_LENGTH);
}
});
@@ -194,14 +194,14 @@
const owner = await helper.eth.createAccountWithBalance(donor);
const collectionHelper = helper.ethNativeContract.collectionHelpers(owner);
await expect(collectionHelper.methods
- .createNonfungibleCollection('Peasantry', 'absolutely anything', 'CVE')
+ .createNFTCollection('Peasantry', 'absolutely anything', 'CVE')
.call({value: Number(1n * nominal)})).to.be.rejectedWith('Sent amount not equals to collection creation price (2000000000000000000)');
});
itEth('(!negative test!) Check owner', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor);
const malfeasant = helper.eth.createAccount();
- const {collectionAddress} = await helper.eth.createNonfungibleCollection(owner, 'Transgressed', 'absolutely anything', 'COR');
+ const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'Transgressed', 'absolutely anything', 'COR');
const malfeasantCollection = helper.ethNativeContract.collection(collectionAddress, 'nft', malfeasant);
const EXPECTED_ERROR = 'NoPermission';
{
@@ -224,7 +224,7 @@
itEth('(!negative test!) Set limits', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor);
- const {collectionAddress} = await helper.eth.createNonfungibleCollection(owner, 'Limits', 'absolutely anything', 'OLF');
+ const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'Limits', 'absolutely anything', 'OLF');
const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
await expect(collectionEvm.methods
.setCollectionLimit('badLimit', 'true')
tests/src/eth/createRFTCollection.test.tsdiffbeforeafterboth--- a/tests/src/eth/createRFTCollection.test.ts
+++ b/tests/src/eth/createRFTCollection.test.ts
@@ -39,7 +39,7 @@
// todo:playgrounds this might fail when in async environment.
const collectionCountBefore = +(await helper.callRpc('api.rpc.unique.collectionStats')).created;
- const {collectionId} = await helper.eth.createRefungibleCollection(owner, name, description, prefix);
+ const {collectionId} = await helper.eth.createRFTCollection(owner, name, description, prefix);
const collectionCountAfter = +(await helper.callRpc('api.rpc.unique.collectionStats')).created;
const data = (await helper.rft.getData(collectionId))!;
@@ -77,7 +77,7 @@
const owner = await helper.eth.createAccountWithBalance(donor);
const sponsor = await helper.eth.createAccountWithBalance(donor);
const ss58Format = helper.chain.getChainProperties().ss58Format;
- const {collectionId, collectionAddress} = await helper.eth.createRefungibleCollection(owner, 'Sponsor', 'absolutely anything', 'ENVY');
+ const {collectionId, collectionAddress} = await helper.eth.createRFTCollection(owner, 'Sponsor', 'absolutely anything', 'ENVY');
const collection = helper.ethNativeContract.collection(collectionAddress, 'rft', owner);
await collection.methods.setCollectionSponsor(sponsor).send();
@@ -96,7 +96,7 @@
itEth('Set limits', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor);
- const {collectionId, collectionAddress} = await helper.eth.createRefungibleCollection(owner, 'Limits', 'absolutely anything', 'INSI');
+ const {collectionId, collectionAddress} = await helper.eth.createRFTCollection(owner, 'Limits', 'absolutely anything', 'INSI');
const limits = {
accountTokenOwnershipLimit: 1000,
sponsoredDataSize: 1024,
@@ -139,7 +139,7 @@
.methods.isCollectionExist(collectionAddressForNonexistentCollection).call())
.to.be.false;
- const {collectionAddress} = await helper.eth.createRefungibleCollection(owner, 'Exister', 'absolutely anything', 'WIWT');
+ const {collectionAddress} = await helper.eth.createRFTCollection(owner, 'Exister', 'absolutely anything', 'WIWT');
expect(await helper.ethNativeContract.collectionHelpers(collectionAddress)
.methods.isCollectionExist(collectionAddress).call())
.to.be.true;
@@ -202,7 +202,7 @@
itEth('(!negative test!) Check owner', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor);
const peasant = helper.eth.createAccount();
- const {collectionAddress} = await helper.eth.createRefungibleCollection(owner, 'Transgressed', 'absolutely anything', 'YVNE');
+ const {collectionAddress} = await helper.eth.createRFTCollection(owner, 'Transgressed', 'absolutely anything', 'YVNE');
const peasantCollection = helper.ethNativeContract.collection(collectionAddress, 'rft', peasant);
const EXPECTED_ERROR = 'NoPermission';
{
@@ -225,7 +225,7 @@
itEth('(!negative test!) Set limits', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor);
- const {collectionAddress} = await helper.eth.createRefungibleCollection(owner, 'Limits', 'absolutely anything', 'ISNI');
+ const {collectionAddress} = await helper.eth.createRFTCollection(owner, 'Limits', 'absolutely anything', 'ISNI');
const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'rft', owner);
await expect(collectionEvm.methods
.setCollectionLimit('badLimit', 'true')
tests/src/eth/evmCoder.test.tsdiffbeforeafterboth--- a/tests/src/eth/evmCoder.test.ts
+++ b/tests/src/eth/evmCoder.test.ts
@@ -65,7 +65,7 @@
itEth('Call non-existing function', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor);
- const collection = await helper.eth.createNonfungibleCollection(owner, 'EVMCODER', '', 'TEST');
+ const collection = await helper.eth.createNFTCollection(owner, 'EVMCODER', '', 'TEST');
const contract = await helper.ethContract.deployByCode(owner, 'Test', getContractSource(collection.collectionAddress, '0x1bfed5D614b886b9Ab2eA4CBAc22A96B7EC29c9c'));
const testContract = await helper.ethContract.deployByCode(owner, 'Test', getContractSource(collection.collectionAddress, contract.options.address));
{
tests/src/eth/fractionalizer/fractionalizer.test.tsdiffbeforeafterboth--- a/tests/src/eth/fractionalizer/fractionalizer.test.ts
+++ b/tests/src/eth/fractionalizer/fractionalizer.test.ts
@@ -62,7 +62,7 @@
const mintRFTToken = async (helper: EthUniqueHelper, owner: string, fractionalizer: Contract, amount: bigint): Promise<{
nftCollectionAddress: string, nftTokenId: number, rftTokenAddress: string
}> => {
- const nftCollection = await helper.eth.createNonfungibleCollection(owner, 'nft', 'NFT collection', 'NFT');
+ const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT');
const nftContract = helper.ethNativeContract.collection(nftCollection.collectionAddress, 'nft', owner);
const nftTokenId = await nftContract.methods.nextTokenId().call();
await nftContract.methods.mint(owner, nftTokenId).send({from: owner});
@@ -92,7 +92,7 @@
itEth('Set RFT collection', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor, 10n);
const fractionalizer = await deployContract(helper, owner);
- const rftCollection = await helper.eth.createRefungibleCollection(owner, 'rft', 'RFT collection', 'RFT');
+ const rftCollection = await helper.eth.createRFTCollection(owner, 'rft', 'RFT collection', 'RFT');
const rftContract = helper.ethNativeContract.collection(rftCollection.collectionAddress, 'rft', owner);
await rftContract.methods.addCollectionAdmin(fractionalizer.options.address).send({from: owner});
@@ -121,7 +121,7 @@
itEth('Set Allowlist', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor, 20n);
const {contract: fractionalizer} = await initContract(helper, owner);
- const nftCollection = await helper.eth.createNonfungibleCollection(owner, 'nft', 'NFT collection', 'NFT');
+ const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT');
const result1 = await fractionalizer.methods.setNftCollectionIsAllowed(nftCollection.collectionAddress, true).send({from: owner});
expect(result1.events).to.be.like({
@@ -146,7 +146,7 @@
itEth('NFT to RFT', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor, 20n);
- const nftCollection = await helper.eth.createNonfungibleCollection(owner, 'nft', 'NFT collection', 'NFT');
+ const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT');
const nftContract = helper.ethNativeContract.collection(nftCollection.collectionAddress, 'nft', owner);
const nftTokenId = await nftContract.methods.nextTokenId().call();
await nftContract.methods.mint(owner, nftTokenId).send({from: owner});
@@ -231,7 +231,7 @@
itEth('call setRFTCollection twice', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor, 20n);
- const rftCollection = await helper.eth.createRefungibleCollection(owner, 'rft', 'RFT collection', 'RFT');
+ const rftCollection = await helper.eth.createRFTCollection(owner, 'rft', 'RFT collection', 'RFT');
const refungibleContract = helper.ethNativeContract.collection(rftCollection.collectionAddress, 'rft', owner);
const fractionalizer = await deployContract(helper, owner);
@@ -244,7 +244,7 @@
itEth('call setRFTCollection with NFT collection', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor, 20n);
- const nftCollection = await helper.eth.createNonfungibleCollection(owner, 'nft', 'NFT collection', 'NFT');
+ const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT');
const nftContract = helper.ethNativeContract.collection(nftCollection.collectionAddress, 'nft', owner);
const fractionalizer = await deployContract(helper, owner);
@@ -257,7 +257,7 @@
itEth('call setRFTCollection while not collection admin', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor, 20n);
const fractionalizer = await deployContract(helper, owner);
- const rftCollection = await helper.eth.createRefungibleCollection(owner, 'rft', 'RFT collection', 'RFT');
+ const rftCollection = await helper.eth.createRFTCollection(owner, 'rft', 'RFT collection', 'RFT');
await expect(fractionalizer.methods.setRFTCollection(rftCollection.collectionAddress).call())
.to.be.rejectedWith(/Fractionalizer contract should be an admin of the collection$/g);
@@ -278,7 +278,7 @@
itEth('call nft2rft without setting RFT collection for contract', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor, 20n);
- const nftCollection = await helper.eth.createNonfungibleCollection(owner, 'nft', 'NFT collection', 'NFT');
+ const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT');
const nftContract = helper.ethNativeContract.collection(nftCollection.collectionAddress, 'nft', owner);
const nftTokenId = await nftContract.methods.nextTokenId().call();
await nftContract.methods.mint(owner, nftTokenId).send({from: owner});
@@ -293,7 +293,7 @@
const owner = await helper.eth.createAccountWithBalance(donor, 20n);
const nftOwner = await helper.eth.createAccountWithBalance(donor, 10n);
- const nftCollection = await helper.eth.createNonfungibleCollection(owner, 'nft', 'NFT collection', 'NFT');
+ const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT');
const nftContract = helper.ethNativeContract.collection(nftCollection.collectionAddress, 'nft', owner);
const nftTokenId = await nftContract.methods.nextTokenId().call();
await nftContract.methods.mint(owner, nftTokenId).send({from: owner});
@@ -310,7 +310,7 @@
itEth('call nft2rft while not in list of allowed accounts', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor, 20n);
- const nftCollection = await helper.eth.createNonfungibleCollection(owner, 'nft', 'NFT collection', 'NFT');
+ const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT');
const nftContract = helper.ethNativeContract.collection(nftCollection.collectionAddress, 'nft', owner);
const nftTokenId = await nftContract.methods.nextTokenId().call();
await nftContract.methods.mint(owner, nftTokenId).send({from: owner});
@@ -325,7 +325,7 @@
itEth('call nft2rft while fractionalizer doesnt have approval for nft token', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor, 20n);
- const nftCollection = await helper.eth.createNonfungibleCollection(owner, 'nft', 'NFT collection', 'NFT');
+ const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT');
const nftContract = helper.ethNativeContract.collection(nftCollection.collectionAddress, 'nft', owner);
const nftTokenId = await nftContract.methods.nextTokenId().call();
await nftContract.methods.mint(owner, nftTokenId).send({from: owner});
@@ -341,7 +341,7 @@
const owner = await helper.eth.createAccountWithBalance(donor, 20n);
const fractionalizer = await deployContract(helper, owner);
- const rftCollection = await helper.eth.createRefungibleCollection(owner, 'rft', 'RFT collection', 'RFT');
+ const rftCollection = await helper.eth.createRFTCollection(owner, 'rft', 'RFT collection', 'RFT');
const refungibleContract = helper.ethNativeContract.collection(rftCollection.collectionAddress, 'rft', owner);
const rftTokenId = await refungibleContract.methods.nextTokenId().call();
await refungibleContract.methods.mint(owner, rftTokenId).send({from: owner});
@@ -354,7 +354,7 @@
const owner = await helper.eth.createAccountWithBalance(donor, 20n);
const {contract: fractionalizer} = await initContract(helper, owner);
- const rftCollection = await helper.eth.createRefungibleCollection(owner, 'rft', 'RFT collection', 'RFT');
+ const rftCollection = await helper.eth.createRFTCollection(owner, 'rft', 'RFT collection', 'RFT');
const refungibleContract = helper.ethNativeContract.collection(rftCollection.collectionAddress, 'rft', owner);
const rftTokenId = await refungibleContract.methods.nextTokenId().call();
await refungibleContract.methods.mint(owner, rftTokenId).send({from: owner});
@@ -365,7 +365,7 @@
itEth('call rft2nft for RFT token that was not minted by fractionalizer contract', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor, 20n);
- const rftCollection = await helper.eth.createRefungibleCollection(owner, 'rft', 'RFT collection', 'RFT');
+ const rftCollection = await helper.eth.createRFTCollection(owner, 'rft', 'RFT collection', 'RFT');
const refungibleContract = helper.ethNativeContract.collection(rftCollection.collectionAddress, 'rft', owner);
const fractionalizer = await deployContract(helper, owner);
@@ -432,7 +432,7 @@
await fractionalizer.methods.setRFTCollection(rftCollectionAddress).send({from: owner});
await helper.executeExtrinsic(donor, 'api.tx.unique.setTransfersEnabledFlag', [rftCollection.collectionId, false], true);
- const nftCollection = await helper.eth.createNonfungibleCollection(owner, 'nft', 'NFT collection', 'NFT');
+ const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT');
const nftContract = helper.ethNativeContract.collection(nftCollection.collectionAddress, 'nft', owner);
const nftTokenId = await nftContract.methods.nextTokenId().call();
await nftContract.methods.mint(owner, nftTokenId).send({from: owner});
tests/src/eth/nesting/nest.test.tsdiffbeforeafterboth--- a/tests/src/eth/nesting/nest.test.ts
+++ b/tests/src/eth/nesting/nest.test.ts
@@ -7,7 +7,7 @@
helper: EthUniqueHelper,
owner: string,
): Promise<{ collectionId: number, collectionAddress: string, contract: Contract }> => {
- const {collectionAddress, collectionId} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C');
+ const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C');
const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
await contract.methods.setCollectionNesting(true).send({from: owner});
tests/src/eth/nonFungible.test.tsdiffbeforeafterboth--- a/tests/src/eth/nonFungible.test.ts
+++ b/tests/src/eth/nonFungible.test.ts
@@ -84,7 +84,7 @@
const receiver = helper.eth.createAccount();
const collectionHelper = helper.ethNativeContract.collectionHelpers(owner);
- let result = await collectionHelper.methods.createERC721MetadataCompatibleCollection('Mint collection', 'a', 'b', tokenPrefix).send({value: Number(2n * helper.balance.getOneTokenNominal())});
+ let result = await collectionHelper.methods.createERC721MetadataNFTCollection('Mint collection', 'a', 'b', tokenPrefix).send({value: Number(2n * helper.balance.getOneTokenNominal())});
const collectionAddress = helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId);
const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
@@ -146,7 +146,7 @@
const owner = await helper.eth.createAccountWithBalance(donor);
const receiver = helper.eth.createAccount();
- const {collectionAddress} = await helper.eth.createNonfungibleCollection(owner, 'Minty', '6', '6');
+ const {collectionAddress} = await helper.eth.createERC721MetadataNFTCollection(owner, 'Mint collection', '6', '6', '');
const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
const nextTokenId = await contract.methods.nextTokenId().call();
tests/src/eth/payable.test.tsdiffbeforeafterboth--- a/tests/src/eth/payable.test.ts
+++ b/tests/src/eth/payable.test.ts
@@ -146,7 +146,7 @@
const caller = await helper.eth.createAccountWithBalance(donor);
const contract = await deployProxyContract(helper, deployer);
- const collectionAddress = (await contract.methods.createNonfungibleCollection().send({from: caller, value: Number(CONTRACT_BALANCE)})).events.CollectionCreated.returnValues.collection;
+ const collectionAddress = (await contract.methods.createNFTCollection().send({from: caller, value: Number(CONTRACT_BALANCE)})).events.CollectionCreated.returnValues.collection;
const initialCallerBalance = await helper.balance.getEthereum(caller);
const initialContractBalance = await helper.balance.getEthereum(contract.options.address);
await contract.methods.mintNftToken(collectionAddress).send({from: caller});
@@ -164,7 +164,7 @@
const initialCallerBalance = await helper.balance.getEthereum(caller);
const initialContractBalance = await helper.balance.getEthereum(contract.options.address);
- await contract.methods.createNonfungibleCollection().send({from: caller, value: Number(CONTRACT_BALANCE)});
+ await contract.methods.createNFTCollection().send({from: caller, value: Number(CONTRACT_BALANCE)});
const finalCallerBalance = await helper.balance.getEthereum(caller);
const finalContractBalance = await helper.balance.getEthereum(contract.options.address);
expect(finalCallerBalance < initialCallerBalance).to.be.true;
@@ -177,8 +177,8 @@
const caller = await helper.eth.createAccountWithBalance(donor);
const collectionHelper = helper.ethNativeContract.collectionHelpers(caller);
- await expect(collectionHelper.methods.createNonfungibleCollection('A', 'B', 'C').call({value: Number(SMALL_FEE)})).to.be.rejectedWith('Sent amount not equals to collection creation price (2000000000000000000)');
- await expect(collectionHelper.methods.createNonfungibleCollection('A', 'B', 'C').call({value: Number(BIG_FEE)})).to.be.rejectedWith('Sent amount not equals to collection creation price (2000000000000000000)');
+ await expect(collectionHelper.methods.createNFTCollection('A', 'B', 'C').call({value: Number(SMALL_FEE)})).to.be.rejectedWith('Sent amount not equals to collection creation price (2000000000000000000)');
+ await expect(collectionHelper.methods.createNFTCollection('A', 'B', 'C').call({value: Number(BIG_FEE)})).to.be.rejectedWith('Sent amount not equals to collection creation price (2000000000000000000)');
});
itEth('Negative test: call createRFTCollection with wrong fee', async({helper}) => {
@@ -227,9 +227,9 @@
InnerContract(innerContract).flip();
}
- function createNonfungibleCollection() external payable {
+ function createNFTCollection() external payable {
address collectionHelpers = 0x6C4E9fE1AE37a41E93CEE429e8E1881aBdcbb54F;
- address nftCollection = CollectionHelpers(collectionHelpers).createNonfungibleCollection{value: msg.value}("A", "B", "C");
+ address nftCollection = CollectionHelpers(collectionHelpers).createNFTCollection{value: msg.value}("A", "B", "C");
emit CollectionCreated(nftCollection);
}
tests/src/eth/proxy/nonFungibleProxy.test.tsdiffbeforeafterboth--- a/tests/src/eth/proxy/nonFungibleProxy.test.ts
+++ b/tests/src/eth/proxy/nonFungibleProxy.test.ts
@@ -101,7 +101,7 @@
itEth('Can perform mint()', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor);
- const {collectionAddress} = await helper.eth.createNonfungibleCollection(owner, 'A', 'A', 'A');
+ const {collectionAddress} = await helper.eth.createERC721MetadataNFTCollection(owner, 'A', 'A', 'A', '');
const caller = await helper.eth.createAccountWithBalance(donor);
const receiver = helper.eth.createAccount();
tests/src/eth/reFungible.test.tsdiffbeforeafterboth--- a/tests/src/eth/reFungible.test.ts
+++ b/tests/src/eth/reFungible.test.ts
@@ -31,7 +31,7 @@
itEth('totalSupply', async ({helper}) => {
const caller = await helper.eth.createAccountWithBalance(donor);
- const {collectionAddress} = await helper.eth.createRefungibleCollection(caller, 'TotalSupply', '6', '6');
+ const {collectionAddress} = await helper.eth.createRFTCollection(caller, 'TotalSupply', '6', '6');
const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', caller);
const nextTokenId = await contract.methods.nextTokenId().call();
await contract.methods.mint(caller, nextTokenId).send();
@@ -41,7 +41,7 @@
itEth('balanceOf', async ({helper}) => {
const caller = await helper.eth.createAccountWithBalance(donor);
- const {collectionAddress} = await helper.eth.createRefungibleCollection(caller, 'BalanceOf', '6', '6');
+ const {collectionAddress} = await helper.eth.createRFTCollection(caller, 'BalanceOf', '6', '6');
const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', caller);
{
@@ -63,7 +63,7 @@
itEth('ownerOf', async ({helper}) => {
const caller = await helper.eth.createAccountWithBalance(donor);
- const {collectionAddress} = await helper.eth.createRefungibleCollection(caller, 'OwnerOf', '6', '6');
+ const {collectionAddress} = await helper.eth.createRFTCollection(caller, 'OwnerOf', '6', '6');
const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', caller);
const tokenId = await contract.methods.nextTokenId().call();
@@ -76,7 +76,7 @@
itEth('ownerOf after burn', async ({helper}) => {
const caller = await helper.eth.createAccountWithBalance(donor);
const receiver = helper.eth.createAccount();
- const {collectionId, collectionAddress} = await helper.eth.createRefungibleCollection(caller, 'OwnerOf-AfterBurn', '6', '6');
+ const {collectionId, collectionAddress} = await helper.eth.createRFTCollection(caller, 'OwnerOf-AfterBurn', '6', '6');
const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', caller);
const tokenId = await contract.methods.nextTokenId().call();
@@ -95,7 +95,7 @@
itEth('ownerOf for partial ownership', async ({helper}) => {
const caller = await helper.eth.createAccountWithBalance(donor);
const receiver = helper.eth.createAccount();
- const {collectionId, collectionAddress} = await helper.eth.createRefungibleCollection(caller, 'Partial-OwnerOf', '6', '6');
+ const {collectionId, collectionAddress} = await helper.eth.createRFTCollection(caller, 'Partial-OwnerOf', '6', '6');
const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', caller);
const tokenId = await contract.methods.nextTokenId().call();
@@ -124,7 +124,7 @@
itEth('Can perform mint()', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor);
const receiver = helper.eth.createAccount();
- const {collectionAddress} = await helper.eth.createRefungibleCollection(owner, 'Minty', '6', '6');
+ const {collectionAddress} = await helper.eth.createERC721MetadataRFTCollection(owner, 'Minty', '6', '6', '');
const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', owner);
const nextTokenId = await contract.methods.nextTokenId().call();
@@ -147,7 +147,7 @@
itEth('Can perform mintBulk()', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor);
const receiver = helper.eth.createAccount();
- const {collectionAddress} = await helper.eth.createRefungibleCollection(owner, 'MintBulky', '6', '6');
+ const {collectionAddress} = await helper.eth.createERC721MetadataRFTCollection(owner, 'MintBulky', '6', '6', '');
const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', owner);
{
@@ -179,7 +179,7 @@
itEth('Can perform burn()', async ({helper}) => {
const caller = await helper.eth.createAccountWithBalance(donor);
- const {collectionAddress} = await helper.eth.createRefungibleCollection(caller, 'Burny', '6', '6');
+ const {collectionAddress} = await helper.eth.createRFTCollection(caller, 'Burny', '6', '6');
const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', caller);
const tokenId = await contract.methods.nextTokenId().call();
@@ -197,7 +197,7 @@
itEth('Can perform transferFrom()', async ({helper}) => {
const caller = await helper.eth.createAccountWithBalance(donor);
const receiver = helper.eth.createAccount();
- const {collectionId, collectionAddress} = await helper.eth.createRefungibleCollection(caller, 'TransferFromy', '6', '6');
+ const {collectionId, collectionAddress} = await helper.eth.createRFTCollection(caller, 'TransferFromy', '6', '6');
const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', caller);
const tokenId = await contract.methods.nextTokenId().call();
@@ -241,7 +241,7 @@
itEth('Can perform transfer()', async ({helper}) => {
const caller = await helper.eth.createAccountWithBalance(donor);
const receiver = helper.eth.createAccount();
- const {collectionAddress} = await helper.eth.createRefungibleCollection(caller, 'Transferry', '6', '6');
+ const {collectionAddress} = await helper.eth.createRFTCollection(caller, 'Transferry', '6', '6');
const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', caller);
const tokenId = await contract.methods.nextTokenId().call();
@@ -271,7 +271,7 @@
itEth('transfer event on transfer from partial ownership to full ownership', async ({helper}) => {
const caller = await helper.eth.createAccountWithBalance(donor);
const receiver = helper.eth.createAccount();
- const {collectionId, collectionAddress} = await helper.eth.createRefungibleCollection(caller, 'Transferry-Partial-to-Full', '6', '6');
+ const {collectionId, collectionAddress} = await helper.eth.createRFTCollection(caller, 'Transferry-Partial-to-Full', '6', '6');
const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', caller);
const tokenId = await contract.methods.nextTokenId().call();
@@ -298,7 +298,7 @@
itEth('transfer event on transfer from full ownership to partial ownership', async ({helper}) => {
const caller = await helper.eth.createAccountWithBalance(donor);
const receiver = helper.eth.createAccount();
- const {collectionId, collectionAddress} = await helper.eth.createRefungibleCollection(caller, 'Transferry-Full-to-Partial', '6', '6');
+ const {collectionId, collectionAddress} = await helper.eth.createRFTCollection(caller, 'Transferry-Full-to-Partial', '6', '6');
const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', caller);
const tokenId = await contract.methods.nextTokenId().call();
@@ -336,7 +336,7 @@
itEth('transferFrom() call fee is less than 0.2UNQ', async ({helper}) => {
const caller = await helper.eth.createAccountWithBalance(donor);
const receiver = helper.eth.createAccount();
- const {collectionAddress} = await helper.eth.createRefungibleCollection(caller, 'Feeful-Transfer-From', '6', '6');
+ const {collectionAddress} = await helper.eth.createRFTCollection(caller, 'Feeful-Transfer-From', '6', '6');
const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', caller);
const tokenId = await contract.methods.nextTokenId().call();
@@ -350,7 +350,7 @@
itEth('transfer() call fee is less than 0.2UNQ', async ({helper}) => {
const caller = await helper.eth.createAccountWithBalance(donor);
const receiver = helper.eth.createAccount();
- const {collectionAddress} = await helper.eth.createRefungibleCollection(caller, 'Feeful-Transfer', '6', '6');
+ const {collectionAddress} = await helper.eth.createRFTCollection(caller, 'Feeful-Transfer', '6', '6');
const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', caller);
const tokenId = await contract.methods.nextTokenId().call();
@@ -386,8 +386,8 @@
itEth('Returns symbol name', async ({helper}) => {
const caller = await helper.eth.createAccountWithBalance(donor);
- const {collectionAddress} = await helper.eth.createRefungibleCollection(caller, 'Leviathan', '', '12');
- const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', caller);
+ const {collectionId} = await helper.rft.mintCollection(alice, {name: 'Leviathan', tokenPrefix: '12'});
+ const contract = helper.ethNativeContract.collectionById(collectionId, 'rft', caller);
const symbol = await contract.methods.symbol().call();
expect(symbol).to.equal('12');
});
tests/src/eth/reFungibleToken.test.tsdiffbeforeafterboth--- a/tests/src/eth/reFungibleToken.test.ts
+++ b/tests/src/eth/reFungibleToken.test.ts
@@ -81,7 +81,7 @@
const receiver = helper.eth.createAccount();
const collectionHelper = helper.ethNativeContract.collectionHelpers(owner);
- let result = await collectionHelper.methods.createERC721MetadataCompatibleCollection('Mint collection', 'a', 'b', tokenPrefix).send({value: Number(2n * helper.balance.getOneTokenNominal())});
+ let result = await collectionHelper.methods.createERC721MetadataNFTCollection('Mint collection', 'a', 'b', tokenPrefix).send({value: Number(2n * helper.balance.getOneTokenNominal())});
const collectionAddress = helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId);
const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', owner);
@@ -294,7 +294,7 @@
itEth('Receiving Transfer event on burning into full ownership', async ({helper}) => {
const caller = await helper.eth.createAccountWithBalance(donor);
const receiver = await helper.eth.createAccountWithBalance(donor);
- const {collectionId, collectionAddress} = await helper.eth.createRefungibleCollection(caller, 'Devastation', '6', '6');
+ const {collectionId, collectionAddress} = await helper.eth.createRFTCollection(caller, 'Devastation', '6', '6');
const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', caller);
const tokenId = await contract.methods.nextTokenId().call();
@@ -479,7 +479,7 @@
itEth('Default parent token address and id', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor);
- const {collectionId, collectionAddress} = await helper.eth.createRefungibleCollection(owner, 'Sands', '', 'GRAIN');
+ const {collectionId, collectionAddress} = await helper.eth.createRFTCollection(owner, 'Sands', '', 'GRAIN');
const collectionContract = helper.ethNativeContract.collection(collectionAddress, 'rft', owner);
const tokenId = await collectionContract.methods.nextTokenId().call();
tests/src/eth/util/playgrounds/unique.dev.tsdiffbeforeafterboth--- a/tests/src/eth/util/playgrounds/unique.dev.ts
+++ b/tests/src/eth/util/playgrounds/unique.dev.ts
@@ -174,11 +174,23 @@
return await this.helper.callRpc('api.rpc.eth.call', [{from: signer, to: contractAddress, data: abi}]);
}
- async createNonfungibleCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{collectionId: number, collectionAddress: string}> {
+ async createNFTCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{collectionId: number, collectionAddress: string}> {
const collectionCreationPrice = this.helper.balance.getCollectionCreationPrice();
const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer);
- const result = await collectionHelper.methods.createNonfungibleCollection(name, description, tokenPrefix).send({value: Number(collectionCreationPrice)});
+ const result = await collectionHelper.methods.createNFTCollection(name, description, tokenPrefix).send({value: Number(collectionCreationPrice)});
+
+ const collectionAddress = this.helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId);
+ const collectionId = this.helper.ethAddress.extractCollectionId(collectionAddress);
+
+ return {collectionId, collectionAddress};
+ }
+
+ async createERC721MetadataNFTCollection(signer: string, name: string, description: string, tokenPrefix: string, baseUri: string): Promise<{collectionId: number, collectionAddress: string}> {
+ const collectionCreationPrice = this.helper.balance.getCollectionCreationPrice();
+ const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer);
+
+ const result = await collectionHelper.methods.createERC721MetadataNFTCollection(name, description, tokenPrefix, baseUri).send({value: Number(collectionCreationPrice)});
const collectionAddress = this.helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId);
const collectionId = this.helper.ethAddress.extractCollectionId(collectionAddress);
@@ -186,7 +198,7 @@
return {collectionId, collectionAddress};
}
- async createRefungibleCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{collectionId: number, collectionAddress: string}> {
+ async createRFTCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{collectionId: number, collectionAddress: string}> {
const collectionCreationPrice = this.helper.balance.getCollectionCreationPrice();
const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer);
@@ -198,6 +210,18 @@
return {collectionId, collectionAddress};
}
+ async createERC721MetadataRFTCollection(signer: string, name: string, description: string, tokenPrefix: string, baseUri: string): Promise<{collectionId: number, collectionAddress: string}> {
+ const collectionCreationPrice = this.helper.balance.getCollectionCreationPrice();
+ const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer);
+
+ const result = await collectionHelper.methods.createERC721MetadataRFTCollection(name, description, tokenPrefix, baseUri).send({value: Number(collectionCreationPrice)});
+
+ const collectionAddress = this.helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId);
+ const collectionId = this.helper.ethAddress.extractCollectionId(collectionAddress);
+
+ return {collectionId, collectionAddress};
+ }
+
async deployCollectorContract(signer: string): Promise<Contract> {
return await this.helper.ethContract.deployByCode(signer, 'Collector', `
// SPDX-License-Identifier: UNLICENSED
tests/src/nesting/properties.test.tsdiffbeforeafterboth--- a/tests/src/nesting/properties.test.ts
+++ b/tests/src/nesting/properties.test.ts
@@ -18,6 +18,52 @@
import {itSub, Pallets, requirePalletsOrSkip, usingPlaygrounds, expect} from '../util/playgrounds';
import {UniqueHelper, UniqueBaseCollection, UniqueNFTCollection, UniqueNFToken, UniqueRFTCollection, UniqueRFToken} from '../util/playgrounds/unique';
+
+describe('Composite Properties Test', () => {
+ let alice: IKeyringPair;
+
+ before(async () => {
+ await usingPlaygrounds(async (helper, privateKey) => {
+ const donor = privateKey('//Alice');
+ [alice] = await helper.arrange.createAccounts([50n], donor);
+ });
+ });
+
+ async function testMakeSureSuppliesRequired(baseCollection: UniqueNFTCollection | UniqueRFTCollection) {
+
+ const collectionOption = await baseCollection.getOptions();
+ expect(collectionOption).is.not.null;
+ let collection = collectionOption;
+ expect(collection.tokenPropertyPermissions).to.be.empty;
+ expect(collection.properties).to.be.deep.equal([{key: 'ERC721Metadata', value: '1'}]);
+
+ const propertyPermissions = [
+ {key: 'mindgame', permission: {collectionAdmin: true, mutable: false, tokenOwner: true}},
+ {key: 'skullduggery', permission: {collectionAdmin: false, mutable: true, tokenOwner: false}},
+ ];
+ await expect(await baseCollection.setTokenPropertyPermissions(alice, propertyPermissions)).to.be.true;
+
+ const collectionProperties = [
+ {key: 'ERC721Metadata', value: '1'},
+ {key: 'black_hole', value: 'LIGO'},
+ {key: 'electron', value: 'come bond'},
+ ];
+
+ await expect(await baseCollection.setProperties(alice, collectionProperties)).to.be.true;
+
+ collection = await baseCollection.getOptions();
+ expect(collection.tokenPropertyPermissions).to.be.deep.equal(propertyPermissions);
+ expect(collection.properties).to.be.deep.equal(collectionProperties);
+ }
+
+ itSub('Makes sure collectionById supplies required fields for NFT', async ({helper}) => {
+ await testMakeSureSuppliesRequired(await helper.nft.mintCollection(alice, {name: 'A', description: 'B', tokenPrefix: 'C'}));
+ });
+
+ itSub.ifWithPallets('Makes sure collectionById supplies required fields for ReFungible', [Pallets.ReFungible], async ({helper}) => {
+ await testMakeSureSuppliesRequired(await helper.rft.mintCollection(alice, {name: 'A', description: 'B', tokenPrefix: 'C'}));
+ });
+});
// ---------- COLLECTION PROPERTIES
describe('Integration Test: Collection Properties', () => {
@@ -33,7 +79,11 @@
itSub('Properties are initially empty', async ({helper}) => {
const collection = await helper.nft.mintCollection(alice);
- expect(await collection.getProperties()).to.be.empty;
+ const properties = await collection.getProperties();
+ expect(properties).to.be.deep.equal([{
+ 'key': 'ERC721Metadata',
+ 'value': '1',
+ }]);
});
async function testSetsPropertiesForCollection(collection: UniqueBaseCollection) {
@@ -150,7 +200,11 @@
await expect(collection.setProperties(bob, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: 'LIGO'}]))
.to.be.rejectedWith(/common\.NoPermission/);
- expect(await collection.getProperties()).to.be.empty;
+ const properties = await collection.getProperties();
+ expect(properties).to.be.deep.equal([{
+ 'key': 'ERC721Metadata',
+ 'value': '1',
+ }]);
}
itSub('Fails to set properties in a NFT collection if not its onwer/administrator', async ({helper}) => {
@@ -202,7 +256,11 @@
await expect(collection.setProperties(alice, propertiesToBeSet)).
to.be.rejectedWith(/common\.PropertyLimitReached/);
- expect(await collection.getProperties()).to.be.empty;
+ const properties = await collection.getProperties();
+ expect(properties).to.be.deep.equal([{
+ 'key': 'ERC721Metadata',
+ 'value': '1',
+ }]);
}
itSub('Fails to set more properties than it is allowed (NFT)', async ({helper}) => {
tests/src/util/playgrounds/unique.tsdiffbeforeafterboth--- a/tests/src/util/playgrounds/unique.ts
+++ b/tests/src/util/playgrounds/unique.ts
@@ -981,6 +981,10 @@
return (await this.helper.callRpc('api.rpc.unique.collectionProperties', [collectionId, propertyKeys])).toHuman();
}
+ async getCollectionOptions(collectionId: number) {
+ return (await this.helper.callRpc('api.rpc.unique.collectionById', [collectionId])).toHuman();
+ }
+
/**
* Deletes onchain properties from the collection.
*
@@ -1293,6 +1297,7 @@
async mintCollection(signer: TSigner, collectionOptions: ICollectionCreationOptions, mode: 'NFT' | 'RFT'): Promise<UniqueBaseCollection> {
collectionOptions = JSON.parse(JSON.stringify(collectionOptions)) as ICollectionCreationOptions; // Clone object
collectionOptions.mode = (mode === 'NFT') ? {nft: null} : {refungible: null};
+ collectionOptions.properties = collectionOptions.properties || [{key: 'ERC721Metadata', value: '1'}];
for (const key of ['name', 'description', 'tokenPrefix']) {
if (typeof collectionOptions[key as 'name' | 'description' | 'tokenPrefix'] === 'string') collectionOptions[key as 'name' | 'description' | 'tokenPrefix'] = this.helper.util.str2vec(collectionOptions[key as 'name' | 'description' | 'tokenPrefix'] as string);
}
@@ -2476,6 +2481,10 @@
return await this.helper.collection.getTokenNextSponsored(this.collectionId, tokenId, addressObj);
}
+ async getOptions() {
+ return await this.helper.collection.getCollectionOptions(this.collectionId);
+ }
+
async setSponsor(signer: TSigner, sponsorAddress: TSubstrateAccount) {
return await this.helper.collection.setSponsor(signer, this.collectionId, sponsorAddress);
}