difftreelog
fix set prop for not existed token (#933)
in: master
* fix: set prop for not existed token * optimize token checking * remove comments * test(token properties): on token non-existence * fix PR comments * rename value * refactor(modify token properties): readability + grammar * revert: unused import used for try-runtime * fix prop permission check * Add self_mint flag * Add LazyValue * fix tests * fix unit tests * fix docker * fix mintCross sponsoring * Generalize next_token_id * fix: set sponsored properties ---------
20 files changed
.docker/Dockerfile-chain-dev-unitdiffbeforeafterboth171718WORKDIR /dev_chain18WORKDIR /dev_chain191920CMD cargo test --features=limit-testing --workspace20CMD cargo test --features=limit-testing,tests --workspace2121pallets/common/Cargo.tomldiffbeforeafterboth36 "up-pov-estimate-rpc/std",36 "up-pov-estimate-rpc/std",37]37]38stubgen = ["evm-coder/stubgen"]38stubgen = ["evm-coder/stubgen"]39tests = []39try-runtime = ["frame-support/try-runtime"]40try-runtime = ["frame-support/try-runtime"]4041pallets/common/src/eth.rsdiffbeforeafterboth131 value: evm_coder::types::Bytes,131 value: evm_coder::types::Bytes,132}132}133134impl Property {135 /// Property key.136 pub fn key(&self) -> &str {137 self.key.as_str()138 }139140 /// Property value.141 pub fn value(&self) -> &[u8] {142 self.value.0.as_slice()143 }144}133145134impl TryFrom<up_data_structs::Property> for Property {146impl TryFrom<up_data_structs::Property> for Property {135 type Error = pallet_evm_coder_substrate::execution::Error;147 type Error = pallet_evm_coder_substrate::execution::Error;227 Some(value) => match value {239 Some(value) => match value {228 0 => Ok(Some(false)),240 0 => Ok(Some(false)),229 1 => Ok(Some(true)),241 1 => Ok(Some(true)),230 _ => {242 _ => Err(Self::Error::Revert(format!(231 return Err(Self::Error::Revert(format!(232 "can't convert value to boolean \"{value}\""243 "can't convert value to boolean \"{value}\""233 )))244 ))),234 }235 },245 },236 None => Ok(None),246 None => Ok(None),237 };247 };pallets/common/src/lib.rsdiffbeforeafterboth216 ///216 ///217 /// # Arguments217 /// # Arguments218 ///218 ///219 /// * `sender`: Caller's account.220 /// * `sponsor`: ID of the account of the sponsor-to-be.219 /// * `sponsor`: ID of the account of the sponsor-to-be.221 pub fn force_set_sponsor(&mut self, sponsor: T::AccountId) -> DispatchResult {220 pub fn force_set_sponsor(&mut self, sponsor: T::AccountId) -> DispatchResult {222 self.check_is_internal()?;221 self.check_is_internal()?;867 >;866 >;868}867}868869/// Represents the change mode for the token property.870pub enum SetPropertyMode {871 /// The token already exists.872 ExistingToken,873874 /// New token.875 NewToken {876 /// The creator of the token is the recipient.877 mint_target_is_sender: bool,878 },879}880881/// Value representation with delayed initialization time.882pub struct LazyValue<T, F: FnOnce() -> T> {883 value: Option<T>,884 f: Option<F>,885}886887impl<T, F: FnOnce() -> T> LazyValue<T, F> {888 /// Create a new LazyValue.889 pub fn new(f: F) -> Self {890 Self {891 value: None,892 f: Some(f),893 }894 }895896 /// Get the value. If it call furst time the value will be initialized.897 pub fn value(&mut self) -> &T {898 if self.value.is_none() {899 self.value = Some(self.f.take().unwrap()())900 }901902 self.value.as_ref().unwrap()903 }904905 /// Is value initialized.906 pub fn has_value(&self) -> bool {907 self.value.is_some()908 }909}910911fn check_token_permissions<T, FCA, FTO, FTE>(912 collection_admin_permitted: bool,913 token_owner_permitted: bool,914 is_collection_admin: &mut LazyValue<bool, FCA>,915 is_token_owner: &mut LazyValue<Result<bool, DispatchError>, FTO>,916 is_token_exist: &mut LazyValue<bool, FTE>,917) -> DispatchResult918where919 T: Config,920 FCA: FnOnce() -> bool,921 FTO: FnOnce() -> Result<bool, DispatchError>,922 FTE: FnOnce() -> bool,923{924 if !(collection_admin_permitted && *is_collection_admin.value()925 || token_owner_permitted && (*is_token_owner.value())?)926 {927 fail!(<Error<T>>::NoPermission);928 }929930 let token_certainly_exist = is_token_owner.has_value() && (*is_token_owner.value())?;931 if !token_certainly_exist && !is_token_exist.value() {932 fail!(<Error<T>>::TokenNotFound);933 }934 Ok(())935}869936870impl<T: Config> Pallet<T> {937impl<T: Config> Pallet<T> {871 /// Enshure that receiver address is correct.938 /// Enshure that receiver address is correct.1217 /// `properties_updates` contents:1284 /// `properties_updates` contents:1218 /// * sets a property under the <key> with the value provided `(<key>, Some(<value>))`1285 /// * sets a property under the <key> with the value provided `(<key>, Some(<value>))`1219 /// * removes a property under the <key> if the value is `None` `(<key>, None)`.1286 /// * removes a property under the <key> if the value is `None` `(<key>, None)`.1220 ///1221 /// - `nesting_budget`: Limit for searching parents in-depth to check ownership.1222 /// - `is_token_create`: Indicates that method is called during token initialization.1223 /// Allows to bypass ownership check.1224 ///1287 ///1225 /// All affected properties should have `mutable` permission1288 /// All affected properties should have `mutable` permission1226 /// to be **deleted** or to be **set more than once**,1289 /// to be **deleted** or to be **set more than once**,1229 /// This function fires an event for each property change.1292 /// This function fires an event for each property change.1230 /// In case of an error, all the changes (including the events) will be reverted1293 /// In case of an error, all the changes (including the events) will be reverted1231 /// since the function is transactional.1294 /// since the function is transactional.1295 #[allow(clippy::too_many_arguments)]1232 pub fn modify_token_properties(1296 pub fn modify_token_properties<FTO, FTE>(1233 collection: &CollectionHandle<T>,1297 collection: &CollectionHandle<T>,1234 sender: &T::CrossAccountId,1298 sender: &T::CrossAccountId,1235 token_id: TokenId,1299 token_id: TokenId,1300 is_token_exist: &mut LazyValue<bool, FTE>,1236 properties_updates: impl Iterator<Item = (PropertyKey, Option<PropertyValue>)>,1301 properties_updates: impl Iterator<Item = (PropertyKey, Option<PropertyValue>)>,1237 is_token_create: bool,1238 mut stored_properties: TokenProperties,1302 mut stored_properties: TokenProperties,1239 is_token_owner: impl Fn() -> Result<bool, DispatchError>,1303 is_token_owner: &mut LazyValue<Result<bool, DispatchError>, FTO>,1240 set_token_properties: impl FnOnce(TokenProperties),1304 set_token_properties: impl FnOnce(TokenProperties),1241 log: evm_coder::ethereum::Log,1305 log: evm_coder::ethereum::Log,1242 ) -> DispatchResult {1306 ) -> DispatchResult1307 where1308 FTO: FnOnce() -> Result<bool, DispatchError>,1309 FTE: FnOnce() -> bool,1310 {1243 let is_collection_admin = collection.is_owner_or_admin(sender);1311 let mut is_collection_admin = LazyValue::new(|| collection.is_owner_or_admin(sender));1244 let permissions = Self::property_permissions(collection.id);1312 let permissions = Self::property_permissions(collection.id);124513131246 let mut token_owner_result = None;1314 let mut changed = false;1247 let mut is_token_owner = || -> Result<bool, DispatchError> {1248 *token_owner_result.get_or_insert_with(&is_token_owner)1249 };12501251 for (key, value) in properties_updates {1315 for (key, value) in properties_updates {1252 let permission = permissions1316 let permission = permissions1253 .get(&key)1317 .get(&key)1254 .cloned()1318 .cloned()1255 .unwrap_or_else(PropertyPermission::none);1319 .unwrap_or_else(PropertyPermission::none);125613201257 let is_property_exists = stored_properties.get(&key).is_some();1321 let property_exists = stored_properties.get(&key).is_some();125813221259 match permission {1323 match permission {1260 PropertyPermission { mutable: false, .. } if is_property_exists => {1324 PropertyPermission { mutable: false, .. } if property_exists => {1261 return Err(<Error<T>>::NoPermission.into());1325 return Err(<Error<T>>::NoPermission.into());1262 }1326 }126313271264 PropertyPermission {1328 PropertyPermission {1265 collection_admin,1329 collection_admin,1266 token_owner,1330 token_owner,1267 ..1331 ..1268 } => {1332 } => check_token_permissions::<T, _, FTO, FTE>(1269 //TODO: investigate threats during public minting.1333 collection_admin,1270 let is_token_create =1271 is_token_create && (collection_admin || token_owner) && value.is_some();1272 if !(is_token_create1273 || (collection_admin && is_collection_admin)1274 || (token_owner && is_token_owner()?))1334 token_owner,1275 {1335 &mut is_collection_admin,1276 fail!(<Error<T>>::NoPermission);1336 is_token_owner,1277 }1337 is_token_exist,1278 }1338 )?,1279 }1339 }128013401281 match value {1341 match value {1293 }1353 }1294 }1354 }129513551296 <PalletEvm<T>>::deposit_log(log.clone());1356 changed = true;1297 }1357 }13581359 if changed {1360 <PalletEvm<T>>::deposit_log(log);1361 }129813621299 set_token_properties(stored_properties);1363 set_token_properties(stored_properties);130013642323 }2387 }2324}2388}23892390#[cfg(feature = "tests")]2391pub mod tests {2392 use crate::{DispatchResult, DispatchError, LazyValue, Config};23932394 const fn to_bool(u: u8) -> bool {2395 u != 02396 }23972398 #[derive(Debug)]2399 pub struct TestCase {2400 pub collection_admin: bool,2401 pub is_collection_admin: bool,2402 pub token_owner: bool,2403 pub is_token_owner: bool,2404 pub no_permission: bool,2405 }24062407 impl TestCase {2408 const fn new(2409 collection_admin: u8,2410 is_collection_admin: u8,2411 token_owner: u8,2412 is_token_owner: u8,2413 no_permission: u8,2414 ) -> Self {2415 Self {2416 collection_admin: to_bool(collection_admin),2417 is_collection_admin: to_bool(is_collection_admin),2418 token_owner: to_bool(token_owner),2419 is_token_owner: to_bool(is_token_owner),2420 no_permission: to_bool(no_permission),2421 }2422 }2423 }24242425 #[rustfmt::skip]2426 pub const table: [TestCase; 16] = [2427 // ┌╴collection_admin2428 // │ ┌╴is_collection_admin2429 // │ │ ┌╴token_owner2430 // │ │ │ ┌╴is_token_ownership2431 // │ │ │ │ ┌╴no_permission2432 /* 0*/ TestCase::new(0, 0, 0, 0, 1),2433 /* 1*/ TestCase::new(0, 0, 0, 1, 1),2434 /* 2*/ TestCase::new(0, 0, 1, 0, 1),2435 /* 3*/ TestCase::new(0, 0, 1, 1, 0),2436 /* 4*/ TestCase::new(0, 1, 0, 0, 1),2437 /* 5*/ TestCase::new(0, 1, 0, 1, 1),2438 /* 6*/ TestCase::new(0, 1, 1, 0, 1),2439 /* 7*/ TestCase::new(0, 1, 1, 1, 0),2440 /* 8*/ TestCase::new(1, 0, 0, 0, 1),2441 /* 9*/ TestCase::new(1, 0, 0, 1, 1),2442 /* 10*/ TestCase::new(1, 0, 1, 0, 1),2443 /* 11*/ TestCase::new(1, 0, 1, 1, 0),2444 /* 12*/ TestCase::new(1, 1, 0, 0, 0),2445 /* 13*/ TestCase::new(1, 1, 0, 1, 0),2446 /* 14*/ TestCase::new(1, 1, 1, 0, 0),2447 /* 15*/ TestCase::new(1, 1, 1, 1, 0),2448 ];24492450 pub fn check_token_permissions<T, FCA, FTO, FTE>(2451 collection_admin_permitted: bool,2452 token_owner_permitted: bool,2453 is_collection_admin: &mut LazyValue<bool, FCA>,2454 check_token_ownership: &mut LazyValue<Result<bool, DispatchError>, FTO>,2455 check_token_existence: &mut LazyValue<bool, FTE>,2456 ) -> DispatchResult2457 where2458 T: Config,2459 FCA: FnOnce() -> bool,2460 FTO: FnOnce() -> Result<bool, DispatchError>,2461 FTE: FnOnce() -> bool,2462 {2463 crate::check_token_permissions::<T, FCA, FTO, FTE>(2464 collection_admin_permitted,2465 token_owner_permitted,2466 is_collection_admin,2467 check_token_ownership,2468 check_token_existence,2469 )2470 }2471}23252472pallets/nonfungible/src/common.rsdiffbeforeafterboth245 &sender,245 &sender,246 token_id,246 token_id,247 properties.into_iter(),247 properties.into_iter(),248 false,248 pallet_common::SetPropertyMode::ExistingToken,249 nesting_budget,249 nesting_budget,250 ),250 ),251 weight,251 weight,pallets/nonfungible/src/erc.rsdiffbeforeafterboth194 &caller,194 &caller,195 TokenId(token_id),195 TokenId(token_id),196 properties.into_iter(),196 properties.into_iter(),197 false,197 pallet_common::SetPropertyMode::ExistingToken,198 &nesting_budget,198 &nesting_budget,199 )199 )200 .map_err(dispatch_to_evm::<T>)200 .map_err(dispatch_to_evm::<T>)939 /// @notice Returns next free NFT ID.939 /// @notice Returns next free NFT ID.940 fn next_token_id(&self) -> Result<U256> {940 fn next_token_id(&self) -> Result<U256> {941 self.consume_store_reads(1)?;941 self.consume_store_reads(1)?;942 Ok(<TokensMinted<T>>::get(self.id)942 Ok(<Pallet<T>>::next_token_id(self)943 .checked_add(1)944 .ok_or("item id overflow")?943 .map_err(dispatch_to_evm::<T>)?945 .into())944 .into())946 }945 }947946pallets/nonfungible/src/lib.rsdiffbeforeafterboth109use pallet_common::{109use pallet_common::{110 Error as CommonError, Pallet as PalletCommon, Event as CommonEvent, CollectionHandle,110 Error as CommonError, Pallet as PalletCommon, Event as CommonEvent, CollectionHandle,111 eth::collection_id_to_address, SelfWeightOf as PalletCommonWeightOf,111 eth::collection_id_to_address, SelfWeightOf as PalletCommonWeightOf,112 weights::WeightInfo as CommonWeightInfo, helpers::add_weight_to_post_info,112 weights::WeightInfo as CommonWeightInfo, helpers::add_weight_to_post_info, SetPropertyMode,113};113};114use pallet_structure::{Pallet as PalletStructure, Error as StructureError};114use pallet_structure::{Pallet as PalletStructure, Error as StructureError};115use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};115use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};585 /// A batch operation to add, edit or remove properties for a token.585 /// A batch operation to add, edit or remove properties for a token.586 ///586 ///587 /// - `nesting_budget`: Limit for searching parents in-depth to check ownership.587 /// - `nesting_budget`: Limit for searching parents in-depth to check ownership.588 /// - `is_token_create`: Indicates that method is called during token initialization.589 /// Allows to bypass ownership check.590 ///588 ///591 /// All affected properties should have `mutable` permission589 /// All affected properties should have `mutable` permission592 /// to be **deleted** or to be **set more than once**,590 /// to be **deleted** or to be **set more than once**,601 sender: &T::CrossAccountId,599 sender: &T::CrossAccountId,602 token_id: TokenId,600 token_id: TokenId,603 properties_updates: impl Iterator<Item = (PropertyKey, Option<PropertyValue>)>,601 properties_updates: impl Iterator<Item = (PropertyKey, Option<PropertyValue>)>,604 is_token_create: bool,602 mode: SetPropertyMode,605 nesting_budget: &dyn Budget,603 nesting_budget: &dyn Budget,606 ) -> DispatchResult {604 ) -> DispatchResult {607 let is_token_owner = || {605 let mut is_token_owner = pallet_common::LazyValue::new(|| {606 if let SetPropertyMode::NewToken {607 mint_target_is_sender,608 } = mode609 {610 return Ok(mint_target_is_sender);611 }612608 let is_owned = <PalletStructure<T>>::check_indirectly_owned(613 let is_owned = <PalletStructure<T>>::check_indirectly_owned(609 sender.clone(),614 sender.clone(),614 )?;619 )?;615620616 Ok(is_owned)621 Ok(is_owned)617 };622 });623624 let mut is_token_exist =625 pallet_common::LazyValue::new(|| Self::token_exists(collection, token_id));618626619 let stored_properties = <TokenProperties<T>>::get((collection.id, token_id));627 let stored_properties = <TokenProperties<T>>::get((collection.id, token_id));620628621 <PalletCommon<T>>::modify_token_properties(629 <PalletCommon<T>>::modify_token_properties(622 collection,630 collection,623 sender,631 sender,624 token_id,632 token_id,633 &mut is_token_exist,625 properties_updates,634 properties_updates,626 is_token_create,627 stored_properties,635 stored_properties,628 is_token_owner,636 &mut is_token_owner,629 |properties| <TokenProperties<T>>::set((collection.id, token_id), properties),637 |properties| <TokenProperties<T>>::set((collection.id, token_id), properties),630 erc::ERC721TokenEvent::TokenChanged {638 erc::ERC721TokenEvent::TokenChanged {631 token_id: token_id.into(),639 token_id: token_id.into(),634 )642 )635 }643 }644645 pub fn next_token_id(collection: &NonfungibleHandle<T>) -> Result<TokenId, DispatchError> {646 let next_token_id = <TokensMinted<T>>::get(collection.id)647 .checked_add(1)648 .ok_or(<CommonError<T>>::CollectionTokenLimitExceeded)?;649650 ensure!(651 collection.limits.token_limit() >= next_token_id,652 <CommonError<T>>::CollectionTokenLimitExceeded653 );654655 Ok(TokenId(next_token_id))656 }636657637 /// Batch operation to add or edit properties for the token658 /// Batch operation to add or edit properties for the token638 ///659 ///644 sender: &T::CrossAccountId,665 sender: &T::CrossAccountId,645 token_id: TokenId,666 token_id: TokenId,646 properties: impl Iterator<Item = Property>,667 properties: impl Iterator<Item = Property>,647 is_token_create: bool,668 mode: SetPropertyMode,648 nesting_budget: &dyn Budget,669 nesting_budget: &dyn Budget,649 ) -> DispatchResult {670 ) -> DispatchResult {650 Self::modify_token_properties(671 Self::modify_token_properties(651 collection,672 collection,652 sender,673 sender,653 token_id,674 token_id,654 properties.map(|p| (p.key, Some(p.value))),675 properties.map(|p| (p.key, Some(p.value))),655 is_token_create,676 mode,656 nesting_budget,677 nesting_budget,657 )678 )658 }679 }669 property: Property,690 property: Property,670 nesting_budget: &dyn Budget,691 nesting_budget: &dyn Budget,671 ) -> DispatchResult {692 ) -> DispatchResult {672 let is_token_create = false;673674 Self::set_token_properties(693 Self::set_token_properties(675 collection,694 collection,676 sender,695 sender,677 token_id,696 token_id,678 [property].into_iter(),697 [property].into_iter(),679 is_token_create,698 SetPropertyMode::ExistingToken,680 nesting_budget,699 nesting_budget,681 )700 )682 }701 }693 property_keys: impl Iterator<Item = PropertyKey>,712 property_keys: impl Iterator<Item = PropertyKey>,694 nesting_budget: &dyn Budget,713 nesting_budget: &dyn Budget,695 ) -> DispatchResult {714 ) -> DispatchResult {696 let is_token_create = false;697698 Self::modify_token_properties(715 Self::modify_token_properties(699 collection,716 collection,700 sender,717 sender,701 token_id,718 token_id,702 property_keys.into_iter().map(|key| (key, None)),719 property_keys.into_iter().map(|key| (key, None)),703 is_token_create,720 SetPropertyMode::ExistingToken,704 nesting_budget,721 nesting_budget,705 )722 )706 }723 }985 sender,1002 sender,986 TokenId(token),1003 TokenId(token),987 data.properties.clone().into_iter(),1004 data.properties.clone().into_iter(),1005 SetPropertyMode::NewToken {1006 mint_target_is_sender: sender.conv_eq(&data.owner),988 true,1007 },989 nesting_budget,1008 nesting_budget,990 ) {1009 ) {991 return TransactionOutcome::Rollback(Err(e));1010 return TransactionOutcome::Rollback(Err(e));pallets/refungible/src/common.rsdiffbeforeafterboth399 &sender,399 &sender,400 token_id,400 token_id,401 properties.into_iter(),401 properties.into_iter(),402 false,402 pallet_common::SetPropertyMode::ExistingToken,403 nesting_budget,403 nesting_budget,404 ),404 ),405 weight,405 weight,pallets/refungible/src/erc.rsdiffbeforeafterboth196 &caller,196 &caller,197 TokenId(token_id),197 TokenId(token_id),198 properties.into_iter(),198 properties.into_iter(),199 false,199 pallet_common::SetPropertyMode::ExistingToken,200 &nesting_budget,200 &nesting_budget,201 )201 )202 .map_err(dispatch_to_evm::<T>)202 .map_err(dispatch_to_evm::<T>)973 /// @notice Returns next free RFT ID.973 /// @notice Returns next free RFT ID.974 fn next_token_id(&self) -> Result<U256> {974 fn next_token_id(&self) -> Result<U256> {975 self.consume_store_reads(1)?;975 self.consume_store_reads(1)?;976 Ok(<TokensMinted<T>>::get(self.id)976 Ok(<Pallet<T>>::next_token_id(self)977 .checked_add(1)978 .ok_or("item id overflow")?977 .map_err(dispatch_to_evm::<T>)?979 .into())978 .into())980 }979 }981980pallets/refungible/src/lib.rsdiffbeforeafterboth97use pallet_evm_coder_substrate::WithRecorder;97use pallet_evm_coder_substrate::WithRecorder;98use pallet_common::{98use pallet_common::{99 CommonCollectionOperations, Error as CommonError, eth::collection_id_to_address,99 CommonCollectionOperations, Error as CommonError, eth::collection_id_to_address,100 Event as CommonEvent, Pallet as PalletCommon,100 Event as CommonEvent, Pallet as PalletCommon, SetPropertyMode,101};101};102use pallet_structure::Pallet as PalletStructure;102use pallet_structure::Pallet as PalletStructure;103use sp_core::{Get, H160};103use sp_core::{Get, H160};521 /// * removes a property under the <key> if the value is `None` `(<key>, None)`.521 /// * removes a property under the <key> if the value is `None` `(<key>, None)`.522 ///522 ///523 /// - `nesting_budget`: Limit for searching parents in-depth to check ownership.523 /// - `nesting_budget`: Limit for searching parents in-depth to check ownership.524 /// - `is_token_create`: Indicates that method is called during token initialization.525 /// Allows to bypass ownership check.526 ///524 ///527 /// All affected properties should have `mutable` permission525 /// All affected properties should have `mutable` permission528 /// to be **deleted** or to be **set more than once**,526 /// to be **deleted** or to be **set more than once**,537 sender: &T::CrossAccountId,535 sender: &T::CrossAccountId,538 token_id: TokenId,536 token_id: TokenId,539 properties_updates: impl Iterator<Item = (PropertyKey, Option<PropertyValue>)>,537 properties_updates: impl Iterator<Item = (PropertyKey, Option<PropertyValue>)>,540 is_token_create: bool,538 mode: SetPropertyMode,541 nesting_budget: &dyn Budget,539 nesting_budget: &dyn Budget,542 ) -> DispatchResult {540 ) -> DispatchResult {543 let is_token_owner = || -> Result<bool, DispatchError> {541 let mut is_token_owner =542 pallet_common::LazyValue::new(|| -> Result<bool, DispatchError> {543 if let SetPropertyMode::NewToken {544 mint_target_is_sender,545 } = mode546 {547 return Ok(mint_target_is_sender);548 }549544 let balance = collection.balance(sender.clone(), token_id);550 let balance = collection.balance(sender.clone(), token_id);545 let total_pieces: u128 =551 let total_pieces: u128 =557 )?;563 )?;558564559 Ok(is_bundle_owner)565 Ok(is_bundle_owner)560 };566 });567568 let mut is_token_exist =569 pallet_common::LazyValue::new(|| Self::token_exists(collection, token_id));561570562 let stored_properties = <TokenProperties<T>>::get((collection.id, token_id));571 let stored_properties = <TokenProperties<T>>::get((collection.id, token_id));563572564 <PalletCommon<T>>::modify_token_properties(573 <PalletCommon<T>>::modify_token_properties(565 collection,574 collection,566 sender,575 sender,567 token_id,576 token_id,577 &mut is_token_exist,568 properties_updates,578 properties_updates,569 is_token_create,570 stored_properties,579 stored_properties,571 is_token_owner,580 &mut is_token_owner,572 |properties| <TokenProperties<T>>::set((collection.id, token_id), properties),581 |properties| <TokenProperties<T>>::set((collection.id, token_id), properties),573 erc::ERC721TokenEvent::TokenChanged {582 erc::ERC721TokenEvent::TokenChanged {574 token_id: token_id.into(),583 token_id: token_id.into(),577 )586 )578 }587 }588589 pub fn next_token_id(collection: &RefungibleHandle<T>) -> Result<TokenId, DispatchError> {590 let next_token_id = <TokensMinted<T>>::get(collection.id)591 .checked_add(1)592 .ok_or(<CommonError<T>>::CollectionTokenLimitExceeded)?;593594 ensure!(595 collection.limits.token_limit() >= next_token_id,596 <CommonError<T>>::CollectionTokenLimitExceeded597 );598599 Ok(TokenId(next_token_id))600 }579601580 pub fn set_token_properties(602 pub fn set_token_properties(581 collection: &RefungibleHandle<T>,603 collection: &RefungibleHandle<T>,582 sender: &T::CrossAccountId,604 sender: &T::CrossAccountId,583 token_id: TokenId,605 token_id: TokenId,584 properties: impl Iterator<Item = Property>,606 properties: impl Iterator<Item = Property>,585 is_token_create: bool,607 mode: SetPropertyMode,586 nesting_budget: &dyn Budget,608 nesting_budget: &dyn Budget,587 ) -> DispatchResult {609 ) -> DispatchResult {588 Self::modify_token_properties(610 Self::modify_token_properties(589 collection,611 collection,590 sender,612 sender,591 token_id,613 token_id,592 properties.map(|p| (p.key, Some(p.value))),614 properties.map(|p| (p.key, Some(p.value))),593 is_token_create,615 mode,594 nesting_budget,616 nesting_budget,595 )617 )596 }618 }602 property: Property,624 property: Property,603 nesting_budget: &dyn Budget,625 nesting_budget: &dyn Budget,604 ) -> DispatchResult {626 ) -> DispatchResult {605 let is_token_create = false;606607 Self::set_token_properties(627 Self::set_token_properties(608 collection,628 collection,609 sender,629 sender,610 token_id,630 token_id,611 [property].into_iter(),631 [property].into_iter(),612 is_token_create,632 SetPropertyMode::ExistingToken,613 nesting_budget,633 nesting_budget,614 )634 )615 }635 }621 property_keys: impl Iterator<Item = PropertyKey>,641 property_keys: impl Iterator<Item = PropertyKey>,622 nesting_budget: &dyn Budget,642 nesting_budget: &dyn Budget,623 ) -> DispatchResult {643 ) -> DispatchResult {624 let is_token_create = false;625626 Self::modify_token_properties(644 Self::modify_token_properties(627 collection,645 collection,628 sender,646 sender,629 token_id,647 token_id,630 property_keys.into_iter().map(|key| (key, None)),648 property_keys.into_iter().map(|key| (key, None)),631 is_token_create,649 SetPropertyMode::ExistingToken,632 nesting_budget,650 nesting_budget,633 )651 )634 }652 }914 let token_id = first_token_id + i as u32 + 1;932 let token_id = first_token_id + i as u32 + 1;915 <TotalSupply<T>>::insert((collection.id, token_id), totals[i]);933 <TotalSupply<T>>::insert((collection.id, token_id), totals[i]);916934935 let mut mint_target_is_sender = true;917 for (user, amount) in data.users.iter() {936 for (user, amount) in data.users.iter() {918 if *amount == 0 {937 if *amount == 0 {919 continue;938 continue;920 }939 }940941 mint_target_is_sender = mint_target_is_sender && sender.conv_eq(user);942921 <Balance<T>>::insert((collection.id, token_id, &user), amount);943 <Balance<T>>::insert((collection.id, token_id, &user), amount);922 <Owned<T>>::insert((collection.id, &user, TokenId(token_id)), true);944 <Owned<T>>::insert((collection.id, &user, TokenId(token_id)), true);932 sender,954 sender,933 TokenId(token_id),955 TokenId(token_id),934 data.properties.clone().into_iter(),956 data.properties.clone().into_iter(),957 SetPropertyMode::NewToken {958 mint_target_is_sender,935 true,959 },936 nesting_budget,960 nesting_budget,937 ) {961 ) {938 return TransactionOutcome::Rollback(Err(e));962 return TransactionOutcome::Rollback(Err(e));runtime/common/ethereum/sponsoring.rsdiffbeforeafterboth22use pallet_evm::account::CrossAccountId;22use pallet_evm::account::CrossAccountId;23use pallet_evm_transaction_payment::CallContext;23use pallet_evm_transaction_payment::CallContext;24use pallet_nonfungible::{24use pallet_nonfungible::{25 Config as NonfungibleConfig,25 Config as NonfungibleConfig, Pallet as NonfungiblePallet, NonfungibleHandle,26 erc::{26 erc::{27 UniqueNFTCall, ERC721UniqueExtensionsCall, ERC721UniqueMintableCall, ERC721Call,27 UniqueNFTCall, ERC721UniqueExtensionsCall, ERC721UniqueMintableCall, ERC721Call,28 TokenPropertiesCall,28 TokenPropertiesCall,56pub struct UniqueEthSponsorshipHandler<T: UniqueConfig>(PhantomData<*const T>);56pub struct UniqueEthSponsorshipHandler<T: UniqueConfig>(PhantomData<*const T>);57impl<T: UniqueConfig + FungibleConfig + NonfungibleConfig + RefungibleConfig>57impl<T: UniqueConfig + FungibleConfig + NonfungibleConfig + RefungibleConfig>58 SponsorshipHandler<T::CrossAccountId, CallContext> for UniqueEthSponsorshipHandler<T>58 SponsorshipHandler<T::CrossAccountId, CallContext> for UniqueEthSponsorshipHandler<T>59where60 T::AccountId: From<[u8; 32]>,59{61{60 fn get_sponsor(62 fn get_sponsor(61 who: &T::CrossAccountId,63 who: &T::CrossAccountId,67 let (method_id, mut reader) = AbiReader::new_call(&call_context.input).ok()?;69 let (method_id, mut reader) = AbiReader::new_call(&call_context.input).ok()?;68 Some(T::CrossAccountId::from_sub(match &collection.mode {70 Some(T::CrossAccountId::from_sub(match &collection.mode {69 CollectionMode::NFT => {71 CollectionMode::NFT => {72 let collection = NonfungibleHandle::cast(collection);70 let call = <UniqueNFTCall<T>>::parse(method_id, &mut reader).ok()??;73 let call = <UniqueNFTCall<T>>::parse(method_id, &mut reader).ok()??;71 match call {74 match call {72 UniqueNFTCall::TokenProperties(TokenPropertiesCall::SetProperty {75 UniqueNFTCall::TokenProperties(call) => match call {76 TokenPropertiesCall::SetProperty {73 token_id,77 token_id,74 key,78 key,75 value,79 value,76 ..80 ..77 }) => {81 } => {78 let token_id: TokenId = token_id.try_into().ok()?;82 let token_id: TokenId = token_id.try_into().ok()?;79 withdraw_set_token_property::<T>(83 withdraw_set_existing_token_property::<T>(80 &collection,84 &collection,81 who,85 who,82 &token_id,86 &token_id,83 key.len() + value.len(),87 key.len() + value.len(),84 )88 )85 .map(|()| sponsor)89 .map(|()| sponsor)86 }90 }91 TokenPropertiesCall::SetProperties {92 token_id,93 properties,94 ..95 } => {96 let token_id: TokenId = token_id.try_into().ok()?;97 let data_size = properties98 .into_iter()99 .map(|p| p.key().len() + p.value().len())100 .sum();101102 withdraw_set_existing_token_property::<T>(103 &collection,104 who,105 &token_id,106 data_size,107 )108 .map(|()| sponsor)109 }110 _ => None,111 },87 UniqueNFTCall::ERC721UniqueExtensions(112 UniqueNFTCall::ERC721UniqueExtensions(call) => match call {88 ERC721UniqueExtensionsCall::Transfer { token_id, .. },113 ERC721UniqueExtensionsCall::Transfer { token_id, .. } => {89 ) => {90 let token_id: TokenId = token_id.try_into().ok()?;114 let token_id: TokenId = token_id.try_into().ok()?;91 withdraw_transfer::<T>(&collection, who, &token_id).map(|()| sponsor)115 withdraw_transfer::<T>(&collection, who, &token_id)116 .map(|()| sponsor)92 }117 }118 ERC721UniqueExtensionsCall::MintCross { properties, .. } => {119 withdraw_create_item::<T>(120 &collection,121 who,122 &CreateItemData::NFT(CreateNftData::default()),123 )?;124125 let token_id =126 <NonfungiblePallet<T>>::next_token_id(&collection).ok()?;127 let data_size: usize = properties128 .into_iter()129 .map(|p| p.key().len() + p.value().len())130 .sum();131132 withdraw_set_token_property::<T>(&collection, &token_id, data_size)133 .map(|()| sponsor)134 }135 _ => None,136 },93 UniqueNFTCall::ERC721UniqueMintable(137 UniqueNFTCall::ERC721UniqueMintable(94 ERC721UniqueMintableCall::Mint { .. }138 ERC721UniqueMintableCall::Mint { .. }95 | ERC721UniqueMintableCall::MintCheckId { .. }139 | ERC721UniqueMintableCall::MintCheckId { .. }runtime/common/ethereum/sponsoring/refungible.rsdiffbeforeafterboth94 ..94 ..95 } => {95 } => {96 let token_id = TokenId::try_from(token_id).ok()?;96 let token_id = TokenId::try_from(token_id).ok()?;97 withdraw_set_token_property::<T>(&collection, who, &token_id, key.len() + value.len())97 withdraw_set_existing_token_property::<T>(98 &collection,99 who,100 &token_id,runtime/common/sponsoring.rsdiffbeforeafterboth39impl<T> Config for T where T: UniqueConfig + FungibleConfig + NonfungibleConfig + RefungibleConfig {}39impl<T> Config for T where T: UniqueConfig + FungibleConfig + NonfungibleConfig + RefungibleConfig {}404041// TODO: permission check?41// TODO: permission check?42pub fn withdraw_set_token_property<T: Config>(42pub fn withdraw_set_existing_token_property<T: Config>(43 collection: &CollectionHandle<T>,43 collection: &CollectionHandle<T>,44 who: &T::CrossAccountId,44 who: &T::CrossAccountId,45 item_id: &TokenId,45 item_id: &TokenId,46 data_size: usize,46 data_size: usize,47) -> Option<()> {47) -> Option<()> {48 // preliminary sponsoring correctness check48 // preliminary sponsoring correctness check49 match collection.mode {50 CollectionMode::NFT => {51 let owner = pallet_nonfungible::TokenData::<T>::get((collection.id, item_id))?.owner;52 if !owner.conv_eq(who) {53 return None;54 }55 }56 CollectionMode::Fungible(_) => {57 // Fungible tokens have no properties58 return None;59 }60 CollectionMode::ReFungible => {61 if !<pallet_refungible::Owned<T>>::get((collection.id, who, item_id)) {62 return None;63 }64 }65 }6667 withdraw_set_token_property(collection, item_id, data_size)68}6970pub fn withdraw_set_token_property<T: Config>(49 match collection.mode {71 collection: &CollectionHandle<T>,50 CollectionMode::NFT => {72 item_id: &TokenId,51 let owner = pallet_nonfungible::TokenData::<T>::get((collection.id, item_id))?.owner;73 data_size: usize,52 if !owner.conv_eq(who) {74) -> Option<()> {53 return None;75 if data_size == 0 {54 }76 return Some(());55 }77 }56 CollectionMode::Fungible(_) => {57 // Fungible tokens have no properties58 return None;59 }60 CollectionMode::ReFungible => {61 if !<pallet_refungible::Owned<T>>::get((collection.id, who, item_id)) {62 return None;63 }64 }65 }6667 if data_size > collection.limits.sponsored_data_size() as usize {78 if data_size > collection.limits.sponsored_data_size() as usize {68 return None;79 return None;81 <TokenPropertyBasket<T>>::insert(collection.id, item_id, block_number);92 <TokenPropertyBasket<T>>::insert(collection.id, item_id, block_number);829383 Some(())94 Some(())84}95}859686pub fn withdraw_transfer<T: Config>(97pub fn withdraw_transfer<T: Config>(87 collection: &CollectionHandle<T>,98 collection: &CollectionHandle<T>,237 ..247 ..238 } => {248 } => {239 let (sponsor, collection) = load::<T>(*collection_id)?;249 let (sponsor, collection) = load::<T>(*collection_id)?;240 withdraw_set_token_property(250 withdraw_set_existing_token_property(241 &collection,251 &collection,242 &T::CrossAccountId::from_sub(who.clone()),252 &T::CrossAccountId::from_sub(who.clone()),243 token_id,253 token_id,runtime/tests/Cargo.tomldiffbeforeafterboth556[features]6[features]7default = ['refungible']7default = ['refungible']8tests = ['pallet-common/tests']899refungible = []10refungible = []1011runtime/tests/src/tests.rsdiffbeforeafterboth1737 let collection_id = create_test_collection(&CollectionMode::NFT, CollectionId(1));1737 let collection_id = create_test_collection(&CollectionMode::NFT, CollectionId(1));173817381739 let origin1 = RuntimeOrigin::signed(1);1739 let origin1 = RuntimeOrigin::signed(1);1740 assert_ok!(Unique::add_collection_admin(1741 origin1.clone(),1742 collection_id,1743 account(1)1744 ));174017451741 let data = default_nft_data();1746 let data = default_nft_data();1742 create_test_item(collection_id, &data.into());1747 create_test_item(collection_id, &data.into());2611 });2616 });2612}2617}26182619mod check_token_permissions {2620 use super::*;2621 use frame_support::once_cell::sync::Lazy;2622 use pallet_common::LazyValue;2623 use sp_runtime::DispatchError;26242625 fn test<FTE: FnOnce() -> bool>(2626 i: usize,2627 test_case: &pallet_common::tests::TestCase,2628 check_token_existence: &mut LazyValue<bool, FTE>,2629 ) {2630 let collection_admin = test_case.collection_admin;2631 let mut is_collection_admin = LazyValue::new(|| test_case.is_collection_admin);2632 let token_owner = test_case.token_owner;2633 let mut is_token_owner = LazyValue::new(|| Ok(test_case.is_token_owner));2634 let is_no_permission = test_case.no_permission;26352636 let result = pallet_common::tests::check_token_permissions::<Test, _, _, FTE>(2637 collection_admin,2638 token_owner,2639 &mut is_collection_admin,2640 &mut is_token_owner,2641 check_token_existence,2642 );26432644 if is_no_permission {2645 assert!(2646 result.is_err(),2647 "{i}: {test_case:?}, token_exist: {}",2648 check_token_existence.value()2649 );2650 assert_err!(result, pallet_common::Error::<Test>::NoPermission,);2651 } else if check_token_existence.has_value() && !check_token_existence.value() {2652 assert!(2653 result.is_err(),2654 "{i}: {test_case:?}, token_exist: {}",2655 check_token_existence.value()2656 );2657 assert_err!(result, pallet_common::Error::<Test>::TokenNotFound,);2658 }2659 }26602661 #[test]2662 fn no_permission_only() {2663 new_test_ext().execute_with(|| {2664 let mut check_token_existence = LazyValue::new(|| true);2665 for (i, row) in pallet_common::tests::table.iter().enumerate() {2666 test(i, row, &mut check_token_existence);2667 }2668 });2669 }26702671 #[test]2672 fn no_permission_and_token_not_found() {2673 new_test_ext().execute_with(|| {2674 for (i, row) in pallet_common::tests::table.iter().enumerate() {2675 // This is inside the loop to keep track of whether the lambda was called2676 let mut check_token_existence = LazyValue::new(|| false);2677 test(i, row, &mut check_token_existence);2678 }2679 });2680 }2681}26132682tests/src/createMultipleItemsEx.test.tsdiffbeforeafterboth195 description: 'descr',195 description: 'descr',196 tokenPrefix: 'COL',196 tokenPrefix: 'COL',197 tokenPropertyPermissions: [197 tokenPropertyPermissions: [198 {key: 'k', permission: {tokenOwner: true, mutable: false, collectionAdmin: false}},198 {key: 'k', permission: {tokenOwner: false, mutable: false, collectionAdmin: true}},199 ],199 ],200 });200 });201201tests/src/eth/collectionSponsoring.test.tsdiffbeforeafterboth17import {IKeyringPair} from '@polkadot/types/types';17import {IKeyringPair} from '@polkadot/types/types';18import {Pallets, requirePalletsOrSkip, usingPlaygrounds} from '../util/index';18import {Pallets, requirePalletsOrSkip, usingPlaygrounds} from '../util/index';19import {itEth, expect} from './util';19import {itEth, expect} from './util';20import {CollectionLimitField, TokenPermissionField} from './util/playgrounds/types';202121describe('evm nft collection sponsoring', () => {22describe('evm nft collection sponsoring', () => {22 let donor: IKeyringPair;23 let donor: IKeyringPair;138 expect(sponsorship.Confirmed).to.be.eq(helper.address.ethToSubstrate(sponsorEth, true));139 expect(sponsorship.Confirmed).to.be.eq(helper.address.ethToSubstrate(sponsorEth, true));139140140 // Create user with no balance:141 // Create user with no balance:141 const user = helper.eth.createAccount();142 const user = helper.ethCrossAccount.createAccount();142 const userCross = helper.ethCrossAccount.fromAddress(user);143 const nextTokenId = await collectionEvm.methods.nextTokenId().call();143 const nextTokenId = await collectionEvm.methods.nextTokenId().call();144 expect(nextTokenId).to.be.equal('1');144 expect(nextTokenId).to.be.equal('1');145145149 expect(oldPermissions.access).to.be.equal('Normal');149 expect(oldPermissions.access).to.be.equal('Normal');150150151 await collectionEvm.methods.setCollectionAccess(1 /*'AllowList'*/).send({from: owner});151 await collectionEvm.methods.setCollectionAccess(1 /*'AllowList'*/).send({from: owner});152 await collectionEvm.methods.addToCollectionAllowListCross(userCross).send({from: owner});152 await collectionEvm.methods.addToCollectionAllowListCross(user).send({from: owner});153 await collectionEvm.methods.setCollectionMintMode(true).send({from: owner});153 await collectionEvm.methods.setCollectionMintMode(true).send({from: owner});154 await collectionEvm.methods.setCollectionLimit({field: CollectionLimitField.SponsoredDataRateLimit, value: {status: true, value: 30}}).send();154155155 const newPermissions = (await collectionSub.getData())!.raw.permissions;156 const newPermissions = (await collectionSub.getData())!.raw.permissions;156 expect(newPermissions.mintMode).to.be.true;157 expect(newPermissions.mintMode).to.be.true;157 expect(newPermissions.access).to.be.equal('AllowList');158 expect(newPermissions.access).to.be.equal('AllowList');159160 // Set token permissions161 await collectionEvm.methods.setTokenPropertyPermissions([162 ['key', [163 [TokenPermissionField.TokenOwner, true],164 ],165 ],166 ]).send({from: owner});158167159 const ownerBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner));168 const ownerBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner));160 const sponsorBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsorEth));169 const sponsorBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsorEth));161 const userBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(user));170 const userBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(user.eth));162171163 // User can mint token without balance:172 // User can mint token without balance:164 {173 {165 const result = await collectionEvm.methods.mintWithTokenURI(user, 'Test URI').send({from: user});174 const result = await collectionEvm.methods.mintCross(user, [{key: 'key', value: Buffer.from('Value')}]).send({from: user.eth});166 const event = helper.eth.normalizeEvents(result.events)175 const event = helper.eth.normalizeEvents(result.events)167 .find(event => event.event === 'Transfer');176 .find(event => event.event === 'Transfer');168177171 event: 'Transfer',180 event: 'Transfer',172 args: {181 args: {173 from: '0x0000000000000000000000000000000000000000',182 from: '0x0000000000000000000000000000000000000000',174 to: user,183 to: user.eth,175 tokenId: '1',184 tokenId: '1',176 },185 },177 });186 });187188 // await collectionEvm.methods.setProperties(1, [{key: 'key', value: Buffer.from('Value1')}]).send({from: user.eth});178189179 const ownerBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner));190 const ownerBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner));180 const sponsorBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsorEth));191 const sponsorBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsorEth));181 const userBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(user));192 const userBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(user.eth));182193183 expect(await collectionEvm.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI');194 expect(await collectionEvm.methods.properties(nextTokenId, []).call())195 .to.be.like([196 [197 'key',198 '0x' + Buffer.from('Value').toString('hex'),199 ],200 ]);184 expect(ownerBalanceBefore).to.be.eq(ownerBalanceAfter);201 expect(ownerBalanceBefore).to.be.eq(ownerBalanceAfter);185 expect(userBalanceAfter).to.be.eq(userBalanceBefore);202 expect(userBalanceAfter).to.be.eq(userBalanceBefore);186 expect(sponsorBalanceBefore > sponsorBalanceAfter).to.be.true;203 expect(sponsorBalanceBefore > sponsorBalanceAfter).to.be.true;187 }204 }188 }));205 }));206207 itEth('Can sponsor [set token properties] via access list', async ({helper}) => {208 const owner = await helper.eth.createAccountWithBalance(donor);209 const sponsorEth = await helper.eth.createAccountWithBalance(donor);210 const sponsorCrossEth = helper.ethCrossAccount.fromAddress(sponsorEth);211212 const {collectionAddress} = await helper.eth.createERC721MetadataCompatibleNFTCollection(owner, 'Sponsor collection', '1', '1', '');213 const collectionEvm = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner, false);214215 // Set collection sponsor:216 await collectionEvm.methods.setCollectionSponsorCross(sponsorCrossEth).send({from: owner});217218 // Sponsor can confirm sponsorship:219 await collectionEvm.methods.confirmCollectionSponsorship().send({from: sponsorEth});220221 // Create user with no balance:222 const user = helper.ethCrossAccount.createAccount();223 const nextTokenId = await collectionEvm.methods.nextTokenId().call();224 expect(nextTokenId).to.be.equal('1');225226 // Set collection permissions:227 await collectionEvm.methods.setCollectionAccess(1 /*'AllowList'*/).send({from: owner});228 await collectionEvm.methods.addToCollectionAllowListCross(user).send({from: owner});229 await collectionEvm.methods.setCollectionMintMode(true).send({from: owner});230 await collectionEvm.methods.setCollectionLimit({field: CollectionLimitField.SponsoredDataRateLimit, value: {status: true, value: 30}}).send();231232 // Set token permissions233 await collectionEvm.methods.setTokenPropertyPermissions([234 ['key', [235 [TokenPermissionField.TokenOwner, true],236 ],237 ],238 ]).send({from: owner});239240 const ownerBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner));241 const sponsorBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsorEth));242 const userBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(user.eth));243244 // User can mint token without balance:245 {246 const result = await collectionEvm.methods.mintCross(user, []).send({from: user.eth});247 const event = helper.eth.normalizeEvents(result.events)248 .find(event => event.event === 'Transfer');249250 expect(event).to.be.deep.equal({251 address: collectionAddress,252 event: 'Transfer',253 args: {254 from: '0x0000000000000000000000000000000000000000',255 to: user.eth,256 tokenId: '1',257 },258 });259260 await collectionEvm.methods.setProperties(1, [{key: 'key', value: Buffer.from('Value')}]).send({from: user.eth});261262 const ownerBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner));263 const sponsorBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsorEth));264 const userBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(user.eth));265266 expect(await collectionEvm.methods.properties(nextTokenId, []).call())267 .to.be.like([268 [269 'key',270 '0x' + Buffer.from('Value').toString('hex'),271 ],272 ]);273 expect(ownerBalanceBefore).to.be.eq(ownerBalanceAfter);274 expect(userBalanceAfter).to.be.eq(userBalanceBefore);275 expect(sponsorBalanceBefore > sponsorBalanceAfter).to.be.true;276 }277 });189278190 // TODO: Temprorary off. Need refactor279 // TODO: Temprorary off. Need refactor191 // itWeb3('Sponsoring collection from substrate address via access list', async ({api, web3, privateKeyWrapper}) => {280 // itWeb3('Sponsoring collection from substrate address via access list', async ({api, web3, privateKeyWrapper}) => {456 expect(newPermissions.mintMode).to.be.true;545 expect(newPermissions.mintMode).to.be.true;457 expect(newPermissions.access).to.be.equal('AllowList');546 expect(newPermissions.access).to.be.equal('AllowList');547548 // Set token permissions549 await collectionEvm.methods.setTokenPropertyPermissions([550 ['URI', [551 [TokenPermissionField.TokenOwner, true],552 [TokenPermissionField.CollectionAdmin, true],553 ],554 ],555 ]).send({from: owner});458556459 const ownerBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner));557 const ownerBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner));460 const sponsorBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsorEth));558 const sponsorBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsorEth));623 await collectionEvm.methods.addToCollectionAllowListCross(userCross).send({from: owner});721 await collectionEvm.methods.addToCollectionAllowListCross(userCross).send({from: owner});624 await collectionEvm.methods.setCollectionMintMode(true).send({from: owner});722 await collectionEvm.methods.setCollectionMintMode(true).send({from: owner});723724 // Set token permissions725 await collectionEvm.methods.setTokenPropertyPermissions([726 ['URI', [727 [TokenPermissionField.TokenOwner, true],728 [TokenPermissionField.CollectionAdmin, true],729 ],730 ],731 ]).send({from: owner});625732626 const ownerBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner));733 const ownerBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner));627 const sponsorBalanceBefore = await helper.balance.getSubstrate(sponsor.address);734 const sponsorBalanceBefore = await helper.balance.getSubstrate(sponsor.address);tests/src/eth/tokenProperties.test.tsdiffbeforeafterboth554 }554 }555 }));555 }));556557 [558 {mode: 'nft' as const, requiredPallets: []},559 {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]},560 ].map(testCase =>561 itEth.ifWithPallets(`[${testCase.mode}] Can't be multiple set/read for non-existent token`, testCase.requiredPallets, async({helper}) => {562 const caller = await helper.eth.createAccountWithBalance(donor);563564 const properties = Array(5).fill(0).map((_, i) => { return {key: `key_${i}`, value: Buffer.from(`value_${i}`)}; });565 const permissions: ITokenPropertyPermission[] = properties.map(p => { return {key: p.key, permission: {tokenOwner: true,566 collectionAdmin: true,567 mutable: true}}; });568569 const collection = await helper[testCase.mode].mintCollection(alice, {570 tokenPrefix: 'ethp',571 tokenPropertyPermissions: permissions,572 }) as UniqueNFTCollection | UniqueRFTCollection;573574 await collection.addAdmin(alice, {Ethereum: caller});575576 const address = helper.ethAddress.fromCollectionId(collection.collectionId);577 const contract = await helper.ethNativeContract.collection(address, testCase.mode, caller);578579 await expect(contract.methods.setProperties(1, properties).call({from: caller})).to.be.rejectedWith('TokenNotFound');580 }));581582 [583 {mode: 'nft' as const, requiredPallets: []},584 {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]},585 ].map(testCase =>586 itEth.ifWithPallets(`[${testCase.mode}] Can't be deleted for non-existent token`, testCase.requiredPallets, async({helper}) => {587 const caller = await helper.eth.createAccountWithBalance(donor);588 const collection = await helper[testCase.mode].mintCollection(alice, {589 tokenPropertyPermissions: [{590 key: 'testKey',591 permission: {592 mutable: true,593 collectionAdmin: true,594 },595 },596 {597 key: 'testKey_1',598 permission: {599 mutable: true,600 collectionAdmin: true,601 },602 }],603 });604605606 await collection.addAdmin(alice, {Ethereum: caller});607608 const address = helper.ethAddress.fromCollectionId(collection.collectionId);609 const contract = await helper.ethNativeContract.collection(address, testCase.mode, caller);610611 await expect(contract.methods.deleteProperties(1, ['testKey', 'testKey_1']).call({from: caller})).to.be.rejectedWith('TokenNotFound');612 }));556});613});557614558615tests/src/getPropertiesRpc.test.tsdiffbeforeafterboth121 });121 });122});122});123123124[125 {mode: 'nft' as const},126 {mode: 'rft' as const},127].map(testCase =>128 describe('negative properties', () => {129 let alice: IKeyringPair;130131 before(async () => {132 await usingPlaygrounds(async (_, privateKey) => {133 alice = await privateKey({url: import.meta.url});134 });135 });136137 itSub(`[${testCase.mode}] set token property for non-existent token`, async ({helper}) => {138 const collection = await helper[testCase.mode].mintCollection(alice);139 await collection.setTokenPropertyPermissions(alice, [{key: 'key', permission: {mutable: true, tokenOwner: true, collectionAdmin: true}}]);140 await expect(collection.setTokenProperties(alice, 1, [{key: 'key', value: 'value'}])).to.be.rejectedWith('common.TokenNotFound');141 expect(await collection.getTokenProperties(1, ['key'])).to.be.empty;142 });143144 itSub(`[${testCase.mode}] delete token property for non-existent token`, async ({helper}) => {145 const collection = await helper[testCase.mode].mintCollection(alice);146 await collection.setTokenPropertyPermissions(alice, [{key: 'key', permission: {mutable: true, tokenOwner: true, collectionAdmin: true}}]);147 await expect(collection.deleteTokenProperties(alice, 1, ['key'])).to.be.rejectedWith('common.TokenNotFound');148 expect(await collection.getTokenProperties(1, ['key'])).to.be.empty;149 });150 }));tests/src/nesting/tokenProperties.test.tsdiffbeforeafterboth449 expect(consumedSpace).to.be.equal(sizeOfProperty(biggerProp) - expectedConsumedSpaceDiff);449 expect(consumedSpace).to.be.equal(sizeOfProperty(biggerProp) - expectedConsumedSpaceDiff);450 }));450 }));451452 itSub('Set sponsored properties', async({helper}) => {453 const collection = await helper.nft.mintCollection(alice, {tokenPropertyPermissions: [{key: 'k', permission: {tokenOwner: true}}]});454455 await collection.setSponsor(alice, alice.address);456 await collection.confirmSponsorship(alice);457 await collection.setPermissions(alice, {access: 'AllowList', mintMode: true});458 await collection.addToAllowList(alice, {Substrate: bob.address});459 await collection.setLimits(alice, {sponsoredDataRateLimit: {blocks: 30}});460461 const token = await collection.mintToken(alice, {Substrate: bob.address});462463 const aliceBalanceBefore = await helper.balance.getSubstrate(alice.address);464 const bobBalanceBefore = await helper.balance.getSubstrate(bob.address);465466 await token.setProperties(bob, [{key: 'k', value: 'val'}]);467468 const aliceBalanceAfter = await helper.balance.getSubstrate(alice.address);469 const bobBalanceAfter = await helper.balance.getSubstrate(bob.address);470471 expect(bobBalanceAfter).to.be.equal(bobBalanceBefore);472 expect(aliceBalanceBefore > aliceBalanceAfter).to.be.true;473 });451});474});452475453describe('Negative Integration Test: Token Properties', () => {476describe('Negative Integration Test: Token Properties', () => {475 });498 });476 });499 });500501 [502 {mode: 'nft' as const, requiredPallets: [Pallets.NFT]},503 {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]},504 ].map(testCase =>505 itSub.ifWithPallets(`Forbids adding/deleting properties of a token if token doesn't exist (${testCase.mode.toLocaleUpperCase})`, testCase.requiredPallets, async({helper}) => {506 const collection = await helper[testCase.mode].mintCollection(alice, {507 tokenPropertyPermissions: constitution.slice(0, 1).map(({permission}) => ({key: '1', permission})),508 });509 const nonExistentToken = collection.getTokenObject(1);510511 await expect(512 nonExistentToken.setProperties(alice, [{key: '1', value: 'Serotonin increase'}]),513 'on expecting failure whilst adding a property by alice',514 ).to.be.rejectedWith(/common\.TokenNotFound/);515516 await expect(517 nonExistentToken.deleteProperties(alice, ['1']),518 'on expecting failure whilst deleting a property by alice',519 ).to.be.rejectedWith(/common\.TokenNotFound/);520 }));477521478 async function mintCollectionWithAllPermissionsAndToken(helper: UniqueHelper, mode: 'NFT' | 'RFT'): Promise<[UniqueNFToken | UniqueRFToken, bigint]> {522 async function mintCollectionWithAllPermissionsAndToken(helper: UniqueHelper, mode: 'NFT' | 'RFT'): Promise<[UniqueNFToken | UniqueRFToken, bigint]> {479 const collection = await (mode == 'NFT' ? helper.nft : helper.rft).mintCollection(alice, {523 const collection = await (mode == 'NFT' ? helper.nft : helper.rft).mintCollection(alice, {