git.delta.rocks / unique-network / refs/commits / e1f20e1894f3

difftreelog

fix set prop for not existed token (#933)

bugrazoid2023-06-14parent: #807186d.patch.diff
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

modified.docker/Dockerfile-chain-dev-unitdiffbeforeafterboth
1717
18WORKDIR /dev_chain18WORKDIR /dev_chain
1919
20CMD cargo test --features=limit-testing --workspace20CMD cargo test --features=limit-testing,tests --workspace
2121
modifiedpallets/common/Cargo.tomldiffbeforeafterboth
36 "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"]
4041
modifiedpallets/common/src/eth.rsdiffbeforeafterboth
131 value: evm_coder::types::Bytes,131 value: evm_coder::types::Bytes,
132}132}
133
134impl Property {
135 /// Property key.
136 pub fn key(&self) -> &str {
137 self.key.as_str()
138 }
139
140 /// Property value.
141 pub fn value(&self) -> &[u8] {
142 self.value.0.as_slice()
143 }
144}
133145
134impl 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 };
modifiedpallets/common/src/lib.rsdiffbeforeafterboth
216 ///216 ///
217 /// # Arguments217 /// # Arguments
218 ///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}
868
869/// Represents the change mode for the token property.
870pub enum SetPropertyMode {
871 /// The token already exists.
872 ExistingToken,
873
874 /// New token.
875 NewToken {
876 /// The creator of the token is the recipient.
877 mint_target_is_sender: bool,
878 },
879}
880
881/// Value representation with delayed initialization time.
882pub struct LazyValue<T, F: FnOnce() -> T> {
883 value: Option<T>,
884 f: Option<F>,
885}
886
887impl<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 }
895
896 /// 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 }
901
902 self.value.as_ref().unwrap()
903 }
904
905 /// Is value initialized.
906 pub fn has_value(&self) -> bool {
907 self.value.is_some()
908 }
909}
910
911fn 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) -> DispatchResult
918where
919 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 }
929
930 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}
869936
870impl<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` permission
1226 /// 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 reverted
1231 /// 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 ) -> DispatchResult
1307 where
1308 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);
12451313
1246 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 };
1250
1251 for (key, value) in properties_updates {1315 for (key, value) in properties_updates {
1252 let permission = permissions1316 let permission = permissions
1253 .get(&key)1317 .get(&key)
1254 .cloned()1318 .cloned()
1255 .unwrap_or_else(PropertyPermission::none);1319 .unwrap_or_else(PropertyPermission::none);
12561320
1257 let is_property_exists = stored_properties.get(&key).is_some();1321 let property_exists = stored_properties.get(&key).is_some();
12581322
1259 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 }
12631327
1264 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_create
1273 || (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 }
12801340
1281 match value {1341 match value {
1293 }1353 }
1294 }1354 }
12951355
1296 <PalletEvm<T>>::deposit_log(log.clone());1356 changed = true;
1297 }1357 }
1358
1359 if changed {
1360 <PalletEvm<T>>::deposit_log(log);
1361 }
12981362
1299 set_token_properties(stored_properties);1363 set_token_properties(stored_properties);
13001364
2323 }2387 }
2324}2388}
2389
2390#[cfg(feature = "tests")]
2391pub mod tests {
2392 use crate::{DispatchResult, DispatchError, LazyValue, Config};
2393
2394 const fn to_bool(u: u8) -> bool {
2395 u != 0
2396 }
2397
2398 #[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 }
2406
2407 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 }
2424
2425 #[rustfmt::skip]
2426 pub const table: [TestCase; 16] = [
2427 // ┌╴collection_admin
2428 // │ ┌╴is_collection_admin
2429 // │ │ ┌╴token_owner
2430 // │ │ │ ┌╴is_token_ownership
2431 // │ │ │ │ ┌╴no_permission
2432 /* 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 ];
2449
2450 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 ) -> DispatchResult
2457 where
2458 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}
23252472
modifiedpallets/nonfungible/src/common.rsdiffbeforeafterboth
245 &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,
modifiedpallets/nonfungible/src/erc.rsdiffbeforeafterboth
194 &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 }
947946
modifiedpallets/nonfungible/src/lib.rsdiffbeforeafterboth
109use 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` permission
592 /// 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 } = mode
609 {
610 return Ok(mint_target_is_sender);
611 }
612
608 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 )?;
615620
616 Ok(is_owned)621 Ok(is_owned)
617 };622 });
623
624 let mut is_token_exist =
625 pallet_common::LazyValue::new(|| Self::token_exists(collection, token_id));
618626
619 let stored_properties = <TokenProperties<T>>::get((collection.id, token_id));627 let stored_properties = <TokenProperties<T>>::get((collection.id, token_id));
620628
621 <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 }
644
645 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)?;
649
650 ensure!(
651 collection.limits.token_limit() >= next_token_id,
652 <CommonError<T>>::CollectionTokenLimitExceeded
653 );
654
655 Ok(TokenId(next_token_id))
656 }
636657
637 /// Batch operation to add or edit properties for the token658 /// Batch operation to add or edit properties for the token
638 ///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;
673
674 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;
697
698 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));
modifiedpallets/refungible/src/common.rsdiffbeforeafterboth
399 &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,
modifiedpallets/refungible/src/erc.rsdiffbeforeafterboth
196 &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 }
981980
modifiedpallets/refungible/src/lib.rsdiffbeforeafterboth
97use 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` permission
528 /// 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 } = mode
546 {
547 return Ok(mint_target_is_sender);
548 }
549
544 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 )?;
558564
559 Ok(is_bundle_owner)565 Ok(is_bundle_owner)
560 };566 });
567
568 let mut is_token_exist =
569 pallet_common::LazyValue::new(|| Self::token_exists(collection, token_id));
561570
562 let stored_properties = <TokenProperties<T>>::get((collection.id, token_id));571 let stored_properties = <TokenProperties<T>>::get((collection.id, token_id));
563572
564 <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 }
588
589 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)?;
593
594 ensure!(
595 collection.limits.token_limit() >= next_token_id,
596 <CommonError<T>>::CollectionTokenLimitExceeded
597 );
598
599 Ok(TokenId(next_token_id))
600 }
579601
580 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;
606
607 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;
625
626 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]);
916934
935 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 }
940
941 mint_target_is_sender = mint_target_is_sender && sender.conv_eq(user);
942
921 <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));
modifiedruntime/common/ethereum/sponsoring.rsdiffbeforeafterboth
22use 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>
59where
60 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 = properties
98 .into_iter()
99 .map(|p| p.key().len() + p.value().len())
100 .sum();
101
102 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 )?;
124
125 let token_id =
126 <NonfungiblePallet<T>>::next_token_id(&collection).ok()?;
127 let data_size: usize = properties
128 .into_iter()
129 .map(|p| p.key().len() + p.value().len())
130 .sum();
131
132 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 { .. }
modifiedruntime/common/ethereum/sponsoring/refungible.rsdiffbeforeafterboth
94 ..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,
modifiedruntime/common/sponsoring.rsdiffbeforeafterboth
39impl<T> Config for T where T: UniqueConfig + FungibleConfig + NonfungibleConfig + RefungibleConfig {}39impl<T> Config for T where T: UniqueConfig + FungibleConfig + NonfungibleConfig + RefungibleConfig {}
4040
41// 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 check
49 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 properties
58 return None;
59 }
60 CollectionMode::ReFungible => {
61 if !<pallet_refungible::Owned<T>>::get((collection.id, who, item_id)) {
62 return None;
63 }
64 }
65 }
66
67 withdraw_set_token_property(collection, item_id, data_size)
68}
69
70pub 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 properties
58 return None;
59 }
60 CollectionMode::ReFungible => {
61 if !<pallet_refungible::Owned<T>>::get((collection.id, who, item_id)) {
62 return None;
63 }
64 }
65 }
66
67 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);
8293
83 Some(())94 Some(())
84}95}
8596
86pub 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,
modifiedruntime/tests/Cargo.tomldiffbeforeafterboth
55
6[features]6[features]
7default = ['refungible']7default = ['refungible']
8tests = ['pallet-common/tests']
89
9refungible = []10refungible = []
1011
modifiedruntime/tests/src/tests.rsdiffbeforeafterboth
1737 let collection_id = create_test_collection(&CollectionMode::NFT, CollectionId(1));1737 let collection_id = create_test_collection(&CollectionMode::NFT, CollectionId(1));
17381738
1739 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 ));
17401745
1741 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}
2618
2619mod check_token_permissions {
2620 use super::*;
2621 use frame_support::once_cell::sync::Lazy;
2622 use pallet_common::LazyValue;
2623 use sp_runtime::DispatchError;
2624
2625 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;
2635
2636 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 );
2643
2644 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 }
2660
2661 #[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 }
2670
2671 #[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 called
2676 let mut check_token_existence = LazyValue::new(|| false);
2677 test(i, row, &mut check_token_existence);
2678 }
2679 });
2680 }
2681}
26132682
modifiedtests/src/createMultipleItemsEx.test.tsdiffbeforeafterboth
195 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 });
201201
modifiedtests/src/eth/collectionSponsoring.test.tsdiffbeforeafterboth
17import {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';
2021
21describe('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));
139140
140 // 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');
145145
149 expect(oldPermissions.access).to.be.equal('Normal');149 expect(oldPermissions.access).to.be.equal('Normal');
150150
151 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();
154155
155 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');
159
160 // Set token permissions
161 await collectionEvm.methods.setTokenPropertyPermissions([
162 ['key', [
163 [TokenPermissionField.TokenOwner, true],
164 ],
165 ],
166 ]).send({from: owner});
158167
159 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));
162171
163 // 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');
168177
171 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 });
187
188 // await collectionEvm.methods.setProperties(1, [{key: 'key', value: Buffer.from('Value1')}]).send({from: user.eth});
178189
179 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));
182193
183 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 }));
206
207 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);
211
212 const {collectionAddress} = await helper.eth.createERC721MetadataCompatibleNFTCollection(owner, 'Sponsor collection', '1', '1', '');
213 const collectionEvm = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner, false);
214
215 // Set collection sponsor:
216 await collectionEvm.methods.setCollectionSponsorCross(sponsorCrossEth).send({from: owner});
217
218 // Sponsor can confirm sponsorship:
219 await collectionEvm.methods.confirmCollectionSponsorship().send({from: sponsorEth});
220
221 // 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');
225
226 // 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();
231
232 // Set token permissions
233 await collectionEvm.methods.setTokenPropertyPermissions([
234 ['key', [
235 [TokenPermissionField.TokenOwner, true],
236 ],
237 ],
238 ]).send({from: owner});
239
240 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));
243
244 // 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');
249
250 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 });
259
260 await collectionEvm.methods.setProperties(1, [{key: 'key', value: Buffer.from('Value')}]).send({from: user.eth});
261
262 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));
265
266 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 });
189278
190 // TODO: Temprorary off. Need refactor279 // TODO: Temprorary off. Need refactor
191 // 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');
547
548 // Set token permissions
549 await collectionEvm.methods.setTokenPropertyPermissions([
550 ['URI', [
551 [TokenPermissionField.TokenOwner, true],
552 [TokenPermissionField.CollectionAdmin, true],
553 ],
554 ],
555 ]).send({from: owner});
458556
459 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});
723
724 // Set token permissions
725 await collectionEvm.methods.setTokenPropertyPermissions([
726 ['URI', [
727 [TokenPermissionField.TokenOwner, true],
728 [TokenPermissionField.CollectionAdmin, true],
729 ],
730 ],
731 ]).send({from: owner});
625732
626 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);
modifiedtests/src/eth/tokenProperties.test.tsdiffbeforeafterboth
554 }554 }
555 }));555 }));
556
557 [
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);
563
564 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}}; });
568
569 const collection = await helper[testCase.mode].mintCollection(alice, {
570 tokenPrefix: 'ethp',
571 tokenPropertyPermissions: permissions,
572 }) as UniqueNFTCollection | UniqueRFTCollection;
573
574 await collection.addAdmin(alice, {Ethereum: caller});
575
576 const address = helper.ethAddress.fromCollectionId(collection.collectionId);
577 const contract = await helper.ethNativeContract.collection(address, testCase.mode, caller);
578
579 await expect(contract.methods.setProperties(1, properties).call({from: caller})).to.be.rejectedWith('TokenNotFound');
580 }));
581
582 [
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 });
604
605
606 await collection.addAdmin(alice, {Ethereum: caller});
607
608 const address = helper.ethAddress.fromCollectionId(collection.collectionId);
609 const contract = await helper.ethNativeContract.collection(address, testCase.mode, caller);
610
611 await expect(contract.methods.deleteProperties(1, ['testKey', 'testKey_1']).call({from: caller})).to.be.rejectedWith('TokenNotFound');
612 }));
556});613});
557614
558615
modifiedtests/src/getPropertiesRpc.test.tsdiffbeforeafterboth
121 });121 });
122});122});
123123
124[
125 {mode: 'nft' as const},
126 {mode: 'rft' as const},
127].map(testCase =>
128 describe('negative properties', () => {
129 let alice: IKeyringPair;
130
131 before(async () => {
132 await usingPlaygrounds(async (_, privateKey) => {
133 alice = await privateKey({url: import.meta.url});
134 });
135 });
136
137 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 });
143
144 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 }));
modifiedtests/src/nesting/tokenProperties.test.tsdiffbeforeafterboth
449 expect(consumedSpace).to.be.equal(sizeOfProperty(biggerProp) - expectedConsumedSpaceDiff);449 expect(consumedSpace).to.be.equal(sizeOfProperty(biggerProp) - expectedConsumedSpaceDiff);
450 }));450 }));
451
452 itSub('Set sponsored properties', async({helper}) => {
453 const collection = await helper.nft.mintCollection(alice, {tokenPropertyPermissions: [{key: 'k', permission: {tokenOwner: true}}]});
454
455 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}});
460
461 const token = await collection.mintToken(alice, {Substrate: bob.address});
462
463 const aliceBalanceBefore = await helper.balance.getSubstrate(alice.address);
464 const bobBalanceBefore = await helper.balance.getSubstrate(bob.address);
465
466 await token.setProperties(bob, [{key: 'k', value: 'val'}]);
467
468 const aliceBalanceAfter = await helper.balance.getSubstrate(alice.address);
469 const bobBalanceAfter = await helper.balance.getSubstrate(bob.address);
470
471 expect(bobBalanceAfter).to.be.equal(bobBalanceBefore);
472 expect(aliceBalanceBefore > aliceBalanceAfter).to.be.true;
473 });
451});474});
452475
453describe('Negative Integration Test: Token Properties', () => {476describe('Negative Integration Test: Token Properties', () => {
475 });498 });
476 });499 });
500
501 [
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);
510
511 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/);
515
516 await expect(
517 nonExistentToken.deleteProperties(alice, ['1']),
518 'on expecting failure whilst deleting a property by alice',
519 ).to.be.rejectedWith(/common\.TokenNotFound/);
520 }));
477521
478 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, {