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

difftreelog

Merge pull request #776 from UniqueNetwork/feature/evm_set-get_tokenPropertyPermissions

ut-akuznetsov2022-12-16parents: #3f0dc65 #0ade15e.patch.diff
in: master
Feature/evm_set-get_tokenPropertyPermissions

22 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -6342,7 +6342,7 @@
 
 [[package]]
 name = "pallet-nonfungible"
-version = "0.1.9"
+version = "0.1.11"
 dependencies = [
  "ethereum 0.14.0",
  "evm-coder",
@@ -6501,7 +6501,7 @@
 
 [[package]]
 name = "pallet-refungible"
-version = "0.2.8"
+version = "0.2.10"
 dependencies = [
  "derivative",
  "ethereum 0.14.0",
modifiedcrates/evm-coder/tests/abi_derive_generation.rsdiffbeforeafterboth
--- a/crates/evm-coder/tests/abi_derive_generation.rs
+++ b/crates/evm-coder/tests/abi_derive_generation.rs
@@ -740,7 +740,7 @@
 	/// Some docs
 	/// At multi
 	/// line
-	#[derive(AbiCoder, Debug, PartialEq, Default)]
+	#[derive(AbiCoder, Debug, PartialEq, Default, Clone, Copy)]
 	#[repr(u8)]
 	enum Color {
 		/// Docs for Red
modifiedpallets/common/src/eth.rsdiffbeforeafterboth
--- a/pallets/common/src/eth.rs
+++ b/pallets/common/src/eth.rs
@@ -162,3 +162,18 @@
 	CollectionAdmin,
 	TokenOwner,
 }
+
+/// Ethereum representation of TokenPermissions (see [`up_data_structs::PropertyPermission`]) fields as an enumeration.
+#[derive(AbiCoder, Copy, Clone, Default, Debug)]
+#[repr(u8)]
+pub enum EthTokenPermissions {
+	/// Permission to change the property and property permission. See [`up_data_structs::PropertyPermission::mutable`]
+	#[default]
+	Mutable,
+
+	/// Change permission for the collection administrator. See [`up_data_structs::PropertyPermission::token_owner`]
+	TokenOwner,
+
+	/// Permission to change the property for the owner of the token. See [`up_data_structs::PropertyPermission::collection_admin`]
+	CollectionAdmin,
+}
modifiedpallets/nonfungible/CHANGELOG.mddiffbeforeafterboth
--- a/pallets/nonfungible/CHANGELOG.md
+++ b/pallets/nonfungible/CHANGELOG.md
@@ -4,6 +4,16 @@
 
 <!-- bureaucrate goes here -->
 
+## [0.1.11] - 2022-12-16
+
+### Added
+
+- The function `tokenPropertyPermissions` and `setTokenPropertyPermissions` to `TokenProperties` interface.
+
+### Changed
+
+- Hide `setTokenPropertyPermission` function in `TokenProperties` interface.
+
 ## [0.1.10] - 2022-11-18
 
 ### Added
modifiedpallets/nonfungible/Cargo.tomldiffbeforeafterboth
--- a/pallets/nonfungible/Cargo.toml
+++ b/pallets/nonfungible/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "pallet-nonfungible"
-version = "0.1.9"
+version = "0.1.11"
 license = "GPLv3"
 edition = "2021"
 
modifiedpallets/nonfungible/src/erc.rsdiffbeforeafterboth
--- a/pallets/nonfungible/src/erc.rs
+++ b/pallets/nonfungible/src/erc.rs
@@ -34,11 +34,11 @@
 	CollectionPropertiesVec,
 };
 use pallet_evm_coder_substrate::dispatch_to_evm;
-use sp_std::vec::Vec;
+use sp_std::{vec::Vec, vec};
 use pallet_common::{
 	CollectionHandle, CollectionPropertyPermissions, CommonCollectionOperations,
 	erc::{CommonEvmHandler, PrecompileResult, CollectionCall, static_property::key},
-	eth::EthCrossAccount,
+	eth::{EthCrossAccount, EthTokenPermissions},
 };
 use pallet_evm::{account::CrossAccountId, PrecompileHandle};
 use pallet_evm_coder_substrate::call;
@@ -60,6 +60,7 @@
 	/// @param collectionAdmin Permission to mutate property by collection admin if property is mutable.
 	/// @param tokenOwner Permission to mutate property by token owner if property is mutable.
 	#[weight(<SelfWeightOf<T>>::set_token_property_permissions(1))]
+	#[solidity(hide)]
 	fn set_token_property_permission(
 		&mut self,
 		caller: caller,
@@ -69,10 +70,10 @@
 		token_owner: bool,
 	) -> Result<()> {
 		let caller = T::CrossAccountId::from_eth(caller);
-		<Pallet<T>>::set_property_permission(
+		<Pallet<T>>::set_token_property_permissions(
 			self,
 			&caller,
-			PropertyKeyPermission {
+			vec![PropertyKeyPermission {
 				key: <Vec<u8>>::from(key)
 					.try_into()
 					.map_err(|_| "too long key")?,
@@ -81,11 +82,78 @@
 					collection_admin,
 					token_owner,
 				},
-			},
+			}],
 		)
 		.map_err(dispatch_to_evm::<T>)
 	}
 
+	/// @notice Set permissions for token property.
+	/// @dev Throws error if `msg.sender` is not admin or owner of the collection.
+	/// @param permissions Permissions for keys.
+	#[weight(<SelfWeightOf<T>>::set_token_property_permissions(permissions.len() as u32))]
+	fn set_token_property_permissions(
+		&mut self,
+		caller: caller,
+		permissions: Vec<(string, Vec<(EthTokenPermissions, bool)>)>,
+	) -> Result<()> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		const PERMISSIONS_FIELDS_COUNT: usize = 3;
+
+		let mut perms = Vec::new();
+
+		for (key, pp) in permissions {
+			if pp.len() > PERMISSIONS_FIELDS_COUNT {
+				return Err(alloc::format!(
+					"Actual number of fields {} for {}, which exceeds the maximum value of {}",
+					pp.len(),
+					stringify!(EthTokenPermissions),
+					PERMISSIONS_FIELDS_COUNT
+				)
+				.as_str()
+				.into());
+			}
+
+			let mut token_permission = PropertyPermission::default();
+
+			for (perm, value) in pp {
+				match perm {
+					EthTokenPermissions::Mutable => token_permission.mutable = value,
+					EthTokenPermissions::TokenOwner => token_permission.token_owner = value,
+					EthTokenPermissions::CollectionAdmin => {
+						token_permission.collection_admin = value
+					}
+				}
+			}
+
+			perms.push(PropertyKeyPermission {
+				key: key.into_bytes().try_into().map_err(|_| "too long key")?,
+				permission: token_permission,
+			});
+		}
+
+		<Pallet<T>>::set_token_property_permissions(self, &caller, perms)
+			.map_err(dispatch_to_evm::<T>)
+	}
+
+	/// @notice Get permissions for token properties.
+	fn token_property_permissions(
+		&self,
+	) -> Result<Vec<(string, Vec<(EthTokenPermissions, bool)>)>> {
+		let perms = <Pallet<T>>::token_property_permission(self.id);
+		Ok(perms
+			.into_iter()
+			.map(|(key, pp)| {
+				let key = string::from_utf8(key.into_inner()).expect("Stored key must be valid");
+				let pp = vec![
+					(EthTokenPermissions::Mutable, pp.mutable),
+					(EthTokenPermissions::TokenOwner, pp.token_owner),
+					(EthTokenPermissions::CollectionAdmin, pp.collection_admin),
+				];
+				(key, pp)
+			})
+			.collect())
+	}
+
 	/// @notice Set token property value.
 	/// @dev Throws error if `msg.sender` has no permission to edit the property.
 	/// @param tokenId ID of the token.
modifiedpallets/nonfungible/src/lib.rsdiffbeforeafterboth
--- a/pallets/nonfungible/src/lib.rs
+++ b/pallets/nonfungible/src/lib.rs
@@ -103,7 +103,7 @@
 	AccessMode, CollectionId, CollectionFlags, CustomDataLimit, TokenId, CreateCollectionData,
 	CreateNftExData, mapping::TokenAddressMapping, budget::Budget, Property, PropertyPermission,
 	PropertyKey, PropertyValue, PropertyKeyPermission, Properties, PropertyScope, TrySetProperty,
-	TokenChild, AuxPropertyValue,
+	TokenChild, AuxPropertyValue, PropertiesPermissionMap,
 };
 use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};
 use pallet_common::{
@@ -824,15 +824,8 @@
 		)
 	}
 
-	/// Set property permissions for the collection.
-	///
-	/// Sender should be the owner or admin of the collection.
-	pub fn set_property_permission(
-		collection: &CollectionHandle<T>,
-		sender: &T::CrossAccountId,
-		permission: PropertyKeyPermission,
-	) -> DispatchResult {
-		<PalletCommon<T>>::set_property_permission(collection, sender, permission)
+	pub fn token_property_permission(collection_id: CollectionId) -> PropertiesPermissionMap {
+		<PalletCommon<T>>::property_permissions(collection_id)
 	}
 
 	pub fn check_token_immediate_ownership(
modifiedpallets/nonfungible/src/stubs/UniqueNFT.soldiffbeforeafterboth
--- a/pallets/nonfungible/src/stubs/UniqueNFT.sol
+++ b/pallets/nonfungible/src/stubs/UniqueNFT.sol
@@ -18,30 +18,44 @@
 }
 
 /// @title A contract that allows to set and delete token properties and change token property permissions.
-/// @dev the ERC-165 identifier for this interface is 0x91a97a68
+/// @dev the ERC-165 identifier for this interface is 0xde0695c2
 contract TokenProperties is Dummy, ERC165 {
+	// /// @notice Set permissions for token property.
+	// /// @dev Throws error if `msg.sender` is not admin or owner of the collection.
+	// /// @param key Property key.
+	// /// @param isMutable Permission to mutate property.
+	// /// @param collectionAdmin Permission to mutate property by collection admin if property is mutable.
+	// /// @param tokenOwner Permission to mutate property by token owner if property is mutable.
+	// /// @dev EVM selector for this function is: 0x222d97fa,
+	// ///  or in textual repr: setTokenPropertyPermission(string,bool,bool,bool)
+	// function setTokenPropertyPermission(string memory key, bool isMutable, bool collectionAdmin, bool tokenOwner) public {
+	// 	require(false, stub_error);
+	// 	key;
+	// 	isMutable;
+	// 	collectionAdmin;
+	// 	tokenOwner;
+	// 	dummy = 0;
+	// }
+
 	/// @notice Set permissions for token property.
 	/// @dev Throws error if `msg.sender` is not admin or owner of the collection.
-	/// @param key Property key.
-	/// @param isMutable Permission to mutate property.
-	/// @param collectionAdmin Permission to mutate property by collection admin if property is mutable.
-	/// @param tokenOwner Permission to mutate property by token owner if property is mutable.
-	/// @dev EVM selector for this function is: 0x222d97fa,
-	///  or in textual repr: setTokenPropertyPermission(string,bool,bool,bool)
-	function setTokenPropertyPermission(
-		string memory key,
-		bool isMutable,
-		bool collectionAdmin,
-		bool tokenOwner
-	) public {
+	/// @param permissions Permissions for keys.
+	/// @dev EVM selector for this function is: 0xbd92983a,
+	///  or in textual repr: setTokenPropertyPermissions((string,(uint8,bool)[])[])
+	function setTokenPropertyPermissions(Tuple48[] memory permissions) public {
 		require(false, stub_error);
-		key;
-		isMutable;
-		collectionAdmin;
-		tokenOwner;
+		permissions;
 		dummy = 0;
 	}
 
+	/// @dev EVM selector for this function is: 0xf23d7790,
+	///  or in textual repr: tokenPropertyPermissions()
+	function tokenPropertyPermissions() public view returns (Tuple48[] memory) {
+		require(false, stub_error);
+		dummy;
+		return new Tuple48[](0);
+	}
+
 	// /// @notice Set token property value.
 	// /// @dev Throws error if `msg.sender` has no permission to edit the property.
 	// /// @param tokenId ID of the token.
@@ -118,6 +132,24 @@
 	bytes value;
 }
 
+enum EthTokenPermissions {
+	Mutable,
+	TokenOwner,
+	CollectionAdmin
+}
+
+/// @dev anonymous struct
+struct Tuple48 {
+	string field_0;
+	Tuple46[] field_1;
+}
+
+/// @dev anonymous struct
+struct Tuple46 {
+	EthTokenPermissions field_0;
+	bool field_1;
+}
+
 /// @title A contract that allows you to work with collections.
 /// @dev the ERC-165 identifier for this interface is 0xb5e1747f
 contract Collection is Dummy, ERC165 {
modifiedpallets/refungible/CHANGELOG.mddiffbeforeafterboth
--- a/pallets/refungible/CHANGELOG.md
+++ b/pallets/refungible/CHANGELOG.md
@@ -4,6 +4,16 @@
 
 <!-- bureaucrate goes here -->
 
+## [0.2.10] - 2022-12-16
+
+### Added
+
+- The function `tokenPropertyPermissions` and `setTokenPropertyPermissions` to `TokenProperties` interface.
+
+### Changed
+
+- Hide `setTokenPropertyPermission` function in `TokenProperties` interface.
+
 ## [0.2.9] - 2022-11-18
 
 ### Added
modifiedpallets/refungible/Cargo.tomldiffbeforeafterboth
--- a/pallets/refungible/Cargo.toml
+++ b/pallets/refungible/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "pallet-refungible"
-version = "0.2.8"
+version = "0.2.10"
 license = "GPLv3"
 edition = "2021"
 
modifiedpallets/refungible/src/erc.rsdiffbeforeafterboth
--- a/pallets/refungible/src/erc.rs
+++ b/pallets/refungible/src/erc.rs
@@ -33,7 +33,7 @@
 use pallet_common::{
 	CollectionHandle, CollectionPropertyPermissions, CommonCollectionOperations,
 	erc::{CommonEvmHandler, CollectionCall, static_property::key},
-	eth::EthCrossAccount,
+	eth::{EthCrossAccount, EthTokenPermissions},
 	Error as CommonError,
 };
 use pallet_evm::{account::CrossAccountId, PrecompileHandle};
@@ -63,6 +63,7 @@
 	/// @param collectionAdmin Permission to mutate property by collection admin if property is mutable.
 	/// @param tokenOwner Permission to mutate property by token owner if property is mutable.
 	#[weight(<SelfWeightOf<T>>::set_token_property_permissions(1))]
+	#[solidity(hide)]
 	fn set_token_property_permission(
 		&mut self,
 		caller: caller,
@@ -89,6 +90,77 @@
 		.map_err(dispatch_to_evm::<T>)
 	}
 
+	/// @notice Set permissions for token property.
+	/// @dev Throws error if `msg.sender` is not admin or owner of the collection.
+	/// @param permissions Permissions for keys.
+	#[weight(<SelfWeightOf<T>>::set_token_property_permissions(permissions.len() as u32))]
+	fn set_token_property_permissions(
+		&mut self,
+		caller: caller,
+		permissions: Vec<(string, Vec<(EthTokenPermissions, bool)>)>,
+	) -> Result<()> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		const PERMISSIONS_FIELDS_COUNT: usize = 3;
+
+		let mut perms = Vec::new();
+
+		for (key, pp) in permissions {
+			if pp.len() > PERMISSIONS_FIELDS_COUNT {
+				return Err(alloc::format!(
+					"Actual number of fields {} for {}, which exceeds the maximum value of {}",
+					pp.len(),
+					stringify!(EthTokenPermissions),
+					PERMISSIONS_FIELDS_COUNT
+				)
+				.as_str()
+				.into());
+			}
+
+			let mut token_permission = PropertyPermission {
+				mutable: false,
+				collection_admin: false,
+				token_owner: false,
+			};
+
+			for (perm, value) in pp {
+				match perm {
+					EthTokenPermissions::Mutable => token_permission.mutable = value,
+					EthTokenPermissions::TokenOwner => token_permission.token_owner = value,
+					EthTokenPermissions::CollectionAdmin => {
+						token_permission.collection_admin = value
+					}
+				}
+			}
+
+			perms.push(PropertyKeyPermission {
+				key: key.into_bytes().try_into().map_err(|_| "too long key")?,
+				permission: token_permission,
+			});
+		}
+
+		<Pallet<T>>::set_token_property_permissions(self, &caller, perms)
+			.map_err(dispatch_to_evm::<T>)
+	}
+
+	/// @notice Get permissions for token properties.
+	fn token_property_permissions(
+		&self,
+	) -> Result<Vec<(string, Vec<(EthTokenPermissions, bool)>)>> {
+		let perms = <Pallet<T>>::token_property_permission(self.id);
+		Ok(perms
+			.into_iter()
+			.map(|(key, pp)| {
+				let key = string::from_utf8(key.into_inner()).expect("Stored key must be valid");
+				let pp = vec![
+					(EthTokenPermissions::Mutable, pp.mutable),
+					(EthTokenPermissions::TokenOwner, pp.token_owner),
+					(EthTokenPermissions::CollectionAdmin, pp.collection_admin),
+				];
+				(key, pp)
+			})
+			.collect())
+	}
+
 	/// @notice Set token property value.
 	/// @dev Throws error if `msg.sender` has no permission to edit the property.
 	/// @param tokenId ID of the token.
modifiedpallets/refungible/src/lib.rsdiffbeforeafterboth
--- a/pallets/refungible/src/lib.rs
+++ b/pallets/refungible/src/lib.rs
@@ -113,7 +113,7 @@
 	AccessMode, budget::Budget, CollectionId, CollectionFlags, CollectionPropertiesVec,
 	CreateCollectionData, CustomDataLimit, mapping::TokenAddressMapping, MAX_ITEMS_PER_BATCH,
 	MAX_REFUNGIBLE_PIECES, Property, PropertyKey, PropertyKeyPermission, PropertyPermission,
-	PropertyScope, PropertyValue, TokenId, TrySetProperty,
+	PropertyScope, PropertyValue, TokenId, TrySetProperty, PropertiesPermissionMap,
 };
 
 pub use pallet::*;
@@ -1378,6 +1378,10 @@
 		<PalletCommon<T>>::set_token_property_permissions(collection, sender, property_permissions)
 	}
 
+	pub fn token_property_permission(collection_id: CollectionId) -> PropertiesPermissionMap {
+		<PalletCommon<T>>::property_permissions(collection_id)
+	}
+
 	pub fn set_scoped_token_property_permissions(
 		collection: &RefungibleHandle<T>,
 		sender: &T::CrossAccountId,
modifiedpallets/refungible/src/stubs/UniqueRefungible.rawdiffbeforeafterboth

binary blob — no preview

modifiedpallets/refungible/src/stubs/UniqueRefungible.soldiffbeforeafterboth
--- a/pallets/refungible/src/stubs/UniqueRefungible.sol
+++ b/pallets/refungible/src/stubs/UniqueRefungible.sol
@@ -18,30 +18,45 @@
 }
 
 /// @title A contract that allows to set and delete token properties and change token property permissions.
-/// @dev the ERC-165 identifier for this interface is 0x91a97a68
+/// @dev the ERC-165 identifier for this interface is 0xde0695c2
 contract TokenProperties is Dummy, ERC165 {
+	// /// @notice Set permissions for token property.
+	// /// @dev Throws error if `msg.sender` is not admin or owner of the collection.
+	// /// @param key Property key.
+	// /// @param isMutable Permission to mutate property.
+	// /// @param collectionAdmin Permission to mutate property by collection admin if property is mutable.
+	// /// @param tokenOwner Permission to mutate property by token owner if property is mutable.
+	// /// @dev EVM selector for this function is: 0x222d97fa,
+	// ///  or in textual repr: setTokenPropertyPermission(string,bool,bool,bool)
+	// function setTokenPropertyPermission(string memory key, bool isMutable, bool collectionAdmin, bool tokenOwner) public {
+	// 	require(false, stub_error);
+	// 	key;
+	// 	isMutable;
+	// 	collectionAdmin;
+	// 	tokenOwner;
+	// 	dummy = 0;
+	// }
+
 	/// @notice Set permissions for token property.
 	/// @dev Throws error if `msg.sender` is not admin or owner of the collection.
-	/// @param key Property key.
-	/// @param isMutable Permission to mutate property.
-	/// @param collectionAdmin Permission to mutate property by collection admin if property is mutable.
-	/// @param tokenOwner Permission to mutate property by token owner if property is mutable.
-	/// @dev EVM selector for this function is: 0x222d97fa,
-	///  or in textual repr: setTokenPropertyPermission(string,bool,bool,bool)
-	function setTokenPropertyPermission(
-		string memory key,
-		bool isMutable,
-		bool collectionAdmin,
-		bool tokenOwner
-	) public {
+	/// @param permissions Permissions for keys.
+	/// @dev EVM selector for this function is: 0xbd92983a,
+	///  or in textual repr: setTokenPropertyPermissions((string,(uint8,bool)[])[])
+	function setTokenPropertyPermissions(Tuple53[] memory permissions) public {
 		require(false, stub_error);
-		key;
-		isMutable;
-		collectionAdmin;
-		tokenOwner;
+		permissions;
 		dummy = 0;
 	}
 
+	/// @notice Get permissions for token properties.
+	/// @dev EVM selector for this function is: 0xf23d7790,
+	///  or in textual repr: tokenPropertyPermissions()
+	function tokenPropertyPermissions() public view returns (Tuple53[] memory) {
+		require(false, stub_error);
+		dummy;
+		return new Tuple53[](0);
+	}
+
 	// /// @notice Set token property value.
 	// /// @dev Throws error if `msg.sender` has no permission to edit the property.
 	// /// @param tokenId ID of the token.
@@ -118,6 +133,24 @@
 	bytes value;
 }
 
+enum EthTokenPermissions {
+	Mutable,
+	TokenOwner,
+	CollectionAdmin
+}
+
+/// @dev anonymous struct
+struct Tuple53 {
+	string field_0;
+	Tuple51[] field_1;
+}
+
+/// @dev anonymous struct
+struct Tuple51 {
+	EthTokenPermissions field_0;
+	bool field_1;
+}
+
 /// @title A contract that allows you to work with collections.
 /// @dev the ERC-165 identifier for this interface is 0xb5e1747f
 contract Collection is Dummy, ERC165 {
modifiedprimitives/data-structs/src/lib.rsdiffbeforeafterboth
--- a/primitives/data-structs/src/lib.rs
+++ b/primitives/data-structs/src/lib.rs
@@ -1010,7 +1010,7 @@
 pub type PropertyValue = BoundedBytes<ConstU32<MAX_PROPERTY_VALUE_LENGTH>>;
 
 /// Property permission.
-#[derive(Encode, Decode, TypeInfo, Debug, MaxEncodedLen, PartialEq, Clone)]
+#[derive(Encode, Decode, TypeInfo, Debug, MaxEncodedLen, PartialEq, Clone, Default)]
 #[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
 pub struct PropertyPermission {
 	/// Permission to change the property and property permission.
modifiedtests/src/eth/abi/nonFungible.jsondiffbeforeafterboth
--- a/tests/src/eth/abi/nonFungible.json
+++ b/tests/src/eth/abi/nonFungible.json
@@ -697,12 +697,29 @@
   },
   {
     "inputs": [
-      { "internalType": "string", "name": "key", "type": "string" },
-      { "internalType": "bool", "name": "isMutable", "type": "bool" },
-      { "internalType": "bool", "name": "collectionAdmin", "type": "bool" },
-      { "internalType": "bool", "name": "tokenOwner", "type": "bool" }
+      {
+        "components": [
+          { "internalType": "string", "name": "field_0", "type": "string" },
+          {
+            "components": [
+              {
+                "internalType": "enum EthTokenPermissions",
+                "name": "field_0",
+                "type": "uint8"
+              },
+              { "internalType": "bool", "name": "field_1", "type": "bool" }
+            ],
+            "internalType": "struct Tuple46[]",
+            "name": "field_1",
+            "type": "tuple[]"
+          }
+        ],
+        "internalType": "struct Tuple48[]",
+        "name": "permissions",
+        "type": "tuple[]"
+      }
     ],
-    "name": "setTokenPropertyPermission",
+    "name": "setTokenPropertyPermissions",
     "outputs": [],
     "stateMutability": "nonpayable",
     "type": "function"
@@ -743,6 +760,35 @@
     "type": "function"
   },
   {
+    "inputs": [],
+    "name": "tokenPropertyPermissions",
+    "outputs": [
+      {
+        "components": [
+          { "internalType": "string", "name": "field_0", "type": "string" },
+          {
+            "components": [
+              {
+                "internalType": "enum EthTokenPermissions",
+                "name": "field_0",
+                "type": "uint8"
+              },
+              { "internalType": "bool", "name": "field_1", "type": "bool" }
+            ],
+            "internalType": "struct Tuple46[]",
+            "name": "field_1",
+            "type": "tuple[]"
+          }
+        ],
+        "internalType": "struct Tuple48[]",
+        "name": "",
+        "type": "tuple[]"
+      }
+    ],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
     "inputs": [
       { "internalType": "uint256", "name": "tokenId", "type": "uint256" }
     ],
modifiedtests/src/eth/abi/reFungible.jsondiffbeforeafterboth
before · tests/src/eth/abi/reFungible.json
1[2  {3    "anonymous": false,4    "inputs": [5      {6        "indexed": true,7        "internalType": "address",8        "name": "owner",9        "type": "address"10      },11      {12        "indexed": true,13        "internalType": "address",14        "name": "approved",15        "type": "address"16      },17      {18        "indexed": true,19        "internalType": "uint256",20        "name": "tokenId",21        "type": "uint256"22      }23    ],24    "name": "Approval",25    "type": "event"26  },27  {28    "anonymous": false,29    "inputs": [30      {31        "indexed": true,32        "internalType": "address",33        "name": "owner",34        "type": "address"35      },36      {37        "indexed": true,38        "internalType": "address",39        "name": "operator",40        "type": "address"41      },42      {43        "indexed": false,44        "internalType": "bool",45        "name": "approved",46        "type": "bool"47      }48    ],49    "name": "ApprovalForAll",50    "type": "event"51  },52  {53    "anonymous": false,54    "inputs": [],55    "name": "MintingFinished",56    "type": "event"57  },58  {59    "anonymous": false,60    "inputs": [61      {62        "indexed": true,63        "internalType": "address",64        "name": "from",65        "type": "address"66      },67      {68        "indexed": true,69        "internalType": "address",70        "name": "to",71        "type": "address"72      },73      {74        "indexed": true,75        "internalType": "uint256",76        "name": "tokenId",77        "type": "uint256"78      }79    ],80    "name": "Transfer",81    "type": "event"82  },83  {84    "inputs": [85      {86        "components": [87          { "internalType": "address", "name": "eth", "type": "address" },88          { "internalType": "uint256", "name": "sub", "type": "uint256" }89        ],90        "internalType": "struct EthCrossAccount",91        "name": "newAdmin",92        "type": "tuple"93      }94    ],95    "name": "addCollectionAdminCross",96    "outputs": [],97    "stateMutability": "nonpayable",98    "type": "function"99  },100  {101    "inputs": [102      {103        "components": [104          { "internalType": "address", "name": "eth", "type": "address" },105          { "internalType": "uint256", "name": "sub", "type": "uint256" }106        ],107        "internalType": "struct EthCrossAccount",108        "name": "user",109        "type": "tuple"110      }111    ],112    "name": "addToCollectionAllowListCross",113    "outputs": [],114    "stateMutability": "nonpayable",115    "type": "function"116  },117  {118    "inputs": [119      {120        "components": [121          { "internalType": "address", "name": "eth", "type": "address" },122          { "internalType": "uint256", "name": "sub", "type": "uint256" }123        ],124        "internalType": "struct EthCrossAccount",125        "name": "user",126        "type": "tuple"127      }128    ],129    "name": "allowlistedCross",130    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],131    "stateMutability": "view",132    "type": "function"133  },134  {135    "inputs": [136      { "internalType": "address", "name": "approved", "type": "address" },137      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }138    ],139    "name": "approve",140    "outputs": [],141    "stateMutability": "nonpayable",142    "type": "function"143  },144  {145    "inputs": [146      { "internalType": "address", "name": "owner", "type": "address" }147    ],148    "name": "balanceOf",149    "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],150    "stateMutability": "view",151    "type": "function"152  },153  {154    "inputs": [155      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }156    ],157    "name": "burn",158    "outputs": [],159    "stateMutability": "nonpayable",160    "type": "function"161  },162  {163    "inputs": [164      {165        "components": [166          { "internalType": "address", "name": "eth", "type": "address" },167          { "internalType": "uint256", "name": "sub", "type": "uint256" }168        ],169        "internalType": "struct EthCrossAccount",170        "name": "from",171        "type": "tuple"172      },173      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }174    ],175    "name": "burnFromCross",176    "outputs": [],177    "stateMutability": "nonpayable",178    "type": "function"179  },180  {181    "inputs": [182      {183        "components": [184          { "internalType": "address", "name": "eth", "type": "address" },185          { "internalType": "uint256", "name": "sub", "type": "uint256" }186        ],187        "internalType": "struct EthCrossAccount",188        "name": "newOwner",189        "type": "tuple"190      }191    ],192    "name": "changeCollectionOwnerCross",193    "outputs": [],194    "stateMutability": "nonpayable",195    "type": "function"196  },197  {198    "inputs": [],199    "name": "collectionAdmins",200    "outputs": [201      {202        "components": [203          { "internalType": "address", "name": "eth", "type": "address" },204          { "internalType": "uint256", "name": "sub", "type": "uint256" }205        ],206        "internalType": "struct EthCrossAccount[]",207        "name": "",208        "type": "tuple[]"209      }210    ],211    "stateMutability": "view",212    "type": "function"213  },214  {215    "inputs": [],216    "name": "collectionHelperAddress",217    "outputs": [{ "internalType": "address", "name": "", "type": "address" }],218    "stateMutability": "view",219    "type": "function"220  },221  {222    "inputs": [],223    "name": "collectionNestingPermissions",224    "outputs": [225      {226        "components": [227          {228            "internalType": "enum CollectionPermissions",229            "name": "field_0",230            "type": "uint8"231          },232          { "internalType": "bool", "name": "field_1", "type": "bool" }233        ],234        "internalType": "struct Tuple36[]",235        "name": "",236        "type": "tuple[]"237      }238    ],239    "stateMutability": "view",240    "type": "function"241  },242  {243    "inputs": [],244    "name": "collectionNestingRestrictedCollectionIds",245    "outputs": [246      {247        "components": [248          { "internalType": "bool", "name": "field_0", "type": "bool" },249          {250            "internalType": "uint256[]",251            "name": "field_1",252            "type": "uint256[]"253          }254        ],255        "internalType": "struct Tuple33",256        "name": "",257        "type": "tuple"258      }259    ],260    "stateMutability": "view",261    "type": "function"262  },263  {264    "inputs": [],265    "name": "collectionOwner",266    "outputs": [267      {268        "components": [269          { "internalType": "address", "name": "eth", "type": "address" },270          { "internalType": "uint256", "name": "sub", "type": "uint256" }271        ],272        "internalType": "struct EthCrossAccount",273        "name": "",274        "type": "tuple"275      }276    ],277    "stateMutability": "view",278    "type": "function"279  },280  {281    "inputs": [282      { "internalType": "string[]", "name": "keys", "type": "string[]" }283    ],284    "name": "collectionProperties",285    "outputs": [286      {287        "components": [288          { "internalType": "string", "name": "key", "type": "string" },289          { "internalType": "bytes", "name": "value", "type": "bytes" }290        ],291        "internalType": "struct Property[]",292        "name": "",293        "type": "tuple[]"294      }295    ],296    "stateMutability": "view",297    "type": "function"298  },299  {300    "inputs": [{ "internalType": "string", "name": "key", "type": "string" }],301    "name": "collectionProperty",302    "outputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }],303    "stateMutability": "view",304    "type": "function"305  },306  {307    "inputs": [],308    "name": "collectionSponsor",309    "outputs": [310      {311        "components": [312          { "internalType": "address", "name": "field_0", "type": "address" },313          { "internalType": "uint256", "name": "field_1", "type": "uint256" }314        ],315        "internalType": "struct Tuple29",316        "name": "",317        "type": "tuple"318      }319    ],320    "stateMutability": "view",321    "type": "function"322  },323  {324    "inputs": [],325    "name": "confirmCollectionSponsorship",326    "outputs": [],327    "stateMutability": "nonpayable",328    "type": "function"329  },330  {331    "inputs": [],332    "name": "contractAddress",333    "outputs": [{ "internalType": "address", "name": "", "type": "address" }],334    "stateMutability": "view",335    "type": "function"336  },337  {338    "inputs": [339      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }340    ],341    "name": "crossOwnerOf",342    "outputs": [343      {344        "components": [345          { "internalType": "address", "name": "eth", "type": "address" },346          { "internalType": "uint256", "name": "sub", "type": "uint256" }347        ],348        "internalType": "struct EthCrossAccount",349        "name": "",350        "type": "tuple"351      }352    ],353    "stateMutability": "view",354    "type": "function"355  },356  {357    "inputs": [358      { "internalType": "string[]", "name": "keys", "type": "string[]" }359    ],360    "name": "deleteCollectionProperties",361    "outputs": [],362    "stateMutability": "nonpayable",363    "type": "function"364  },365  {366    "inputs": [367      { "internalType": "uint256", "name": "tokenId", "type": "uint256" },368      { "internalType": "string[]", "name": "keys", "type": "string[]" }369    ],370    "name": "deleteProperties",371    "outputs": [],372    "stateMutability": "nonpayable",373    "type": "function"374  },375  {376    "inputs": [],377    "name": "description",378    "outputs": [{ "internalType": "string", "name": "", "type": "string" }],379    "stateMutability": "view",380    "type": "function"381  },382  {383    "inputs": [],384    "name": "finishMinting",385    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],386    "stateMutability": "nonpayable",387    "type": "function"388  },389  {390    "inputs": [391      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }392    ],393    "name": "getApproved",394    "outputs": [{ "internalType": "address", "name": "", "type": "address" }],395    "stateMutability": "view",396    "type": "function"397  },398  {399    "inputs": [],400    "name": "hasCollectionPendingSponsor",401    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],402    "stateMutability": "view",403    "type": "function"404  },405  {406    "inputs": [407      { "internalType": "address", "name": "owner", "type": "address" },408      { "internalType": "address", "name": "operator", "type": "address" }409    ],410    "name": "isApprovedForAll",411    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],412    "stateMutability": "view",413    "type": "function"414  },415  {416    "inputs": [417      {418        "components": [419          { "internalType": "address", "name": "eth", "type": "address" },420          { "internalType": "uint256", "name": "sub", "type": "uint256" }421        ],422        "internalType": "struct EthCrossAccount",423        "name": "user",424        "type": "tuple"425      }426    ],427    "name": "isOwnerOrAdminCross",428    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],429    "stateMutability": "view",430    "type": "function"431  },432  {433    "inputs": [{ "internalType": "address", "name": "to", "type": "address" }],434    "name": "mint",435    "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],436    "stateMutability": "nonpayable",437    "type": "function"438  },439  {440    "inputs": [441      { "internalType": "address", "name": "to", "type": "address" },442      { "internalType": "string", "name": "tokenUri", "type": "string" }443    ],444    "name": "mintWithTokenURI",445    "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],446    "stateMutability": "nonpayable",447    "type": "function"448  },449  {450    "inputs": [],451    "name": "mintingFinished",452    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],453    "stateMutability": "view",454    "type": "function"455  },456  {457    "inputs": [],458    "name": "name",459    "outputs": [{ "internalType": "string", "name": "", "type": "string" }],460    "stateMutability": "view",461    "type": "function"462  },463  {464    "inputs": [],465    "name": "nextTokenId",466    "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],467    "stateMutability": "view",468    "type": "function"469  },470  {471    "inputs": [472      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }473    ],474    "name": "ownerOf",475    "outputs": [{ "internalType": "address", "name": "", "type": "address" }],476    "stateMutability": "view",477    "type": "function"478  },479  {480    "inputs": [481      { "internalType": "uint256", "name": "tokenId", "type": "uint256" },482      { "internalType": "string[]", "name": "keys", "type": "string[]" }483    ],484    "name": "properties",485    "outputs": [486      {487        "components": [488          { "internalType": "string", "name": "key", "type": "string" },489          { "internalType": "bytes", "name": "value", "type": "bytes" }490        ],491        "internalType": "struct Property[]",492        "name": "",493        "type": "tuple[]"494      }495    ],496    "stateMutability": "view",497    "type": "function"498  },499  {500    "inputs": [501      { "internalType": "uint256", "name": "tokenId", "type": "uint256" },502      { "internalType": "string", "name": "key", "type": "string" }503    ],504    "name": "property",505    "outputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }],506    "stateMutability": "view",507    "type": "function"508  },509  {510    "inputs": [511      {512        "components": [513          { "internalType": "address", "name": "eth", "type": "address" },514          { "internalType": "uint256", "name": "sub", "type": "uint256" }515        ],516        "internalType": "struct EthCrossAccount",517        "name": "admin",518        "type": "tuple"519      }520    ],521    "name": "removeCollectionAdminCross",522    "outputs": [],523    "stateMutability": "nonpayable",524    "type": "function"525  },526  {527    "inputs": [],528    "name": "removeCollectionSponsor",529    "outputs": [],530    "stateMutability": "nonpayable",531    "type": "function"532  },533  {534    "inputs": [535      {536        "components": [537          { "internalType": "address", "name": "eth", "type": "address" },538          { "internalType": "uint256", "name": "sub", "type": "uint256" }539        ],540        "internalType": "struct EthCrossAccount",541        "name": "user",542        "type": "tuple"543      }544    ],545    "name": "removeFromCollectionAllowListCross",546    "outputs": [],547    "stateMutability": "nonpayable",548    "type": "function"549  },550  {551    "inputs": [552      { "internalType": "address", "name": "from", "type": "address" },553      { "internalType": "address", "name": "to", "type": "address" },554      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }555    ],556    "name": "safeTransferFrom",557    "outputs": [],558    "stateMutability": "nonpayable",559    "type": "function"560  },561  {562    "inputs": [563      { "internalType": "address", "name": "from", "type": "address" },564      { "internalType": "address", "name": "to", "type": "address" },565      { "internalType": "uint256", "name": "tokenId", "type": "uint256" },566      { "internalType": "bytes", "name": "data", "type": "bytes" }567    ],568    "name": "safeTransferFromWithData",569    "outputs": [],570    "stateMutability": "nonpayable",571    "type": "function"572  },573  {574    "inputs": [575      { "internalType": "address", "name": "operator", "type": "address" },576      { "internalType": "bool", "name": "approved", "type": "bool" }577    ],578    "name": "setApprovalForAll",579    "outputs": [],580    "stateMutability": "nonpayable",581    "type": "function"582  },583  {584    "inputs": [{ "internalType": "uint8", "name": "mode", "type": "uint8" }],585    "name": "setCollectionAccess",586    "outputs": [],587    "stateMutability": "nonpayable",588    "type": "function"589  },590  {591    "inputs": [592      { "internalType": "string", "name": "limit", "type": "string" },593      { "internalType": "uint256", "name": "value", "type": "uint256" }594    ],595    "name": "setCollectionLimit",596    "outputs": [],597    "stateMutability": "nonpayable",598    "type": "function"599  },600  {601    "inputs": [{ "internalType": "bool", "name": "mode", "type": "bool" }],602    "name": "setCollectionMintMode",603    "outputs": [],604    "stateMutability": "nonpayable",605    "type": "function"606  },607  {608    "inputs": [{ "internalType": "bool", "name": "enable", "type": "bool" }],609    "name": "setCollectionNesting",610    "outputs": [],611    "stateMutability": "nonpayable",612    "type": "function"613  },614  {615    "inputs": [616      { "internalType": "bool", "name": "enable", "type": "bool" },617      {618        "internalType": "address[]",619        "name": "collections",620        "type": "address[]"621      }622    ],623    "name": "setCollectionNesting",624    "outputs": [],625    "stateMutability": "nonpayable",626    "type": "function"627  },628  {629    "inputs": [630      {631        "components": [632          { "internalType": "string", "name": "key", "type": "string" },633          { "internalType": "bytes", "name": "value", "type": "bytes" }634        ],635        "internalType": "struct Property[]",636        "name": "properties",637        "type": "tuple[]"638      }639    ],640    "name": "setCollectionProperties",641    "outputs": [],642    "stateMutability": "nonpayable",643    "type": "function"644  },645  {646    "inputs": [647      {648        "components": [649          { "internalType": "address", "name": "eth", "type": "address" },650          { "internalType": "uint256", "name": "sub", "type": "uint256" }651        ],652        "internalType": "struct EthCrossAccount",653        "name": "sponsor",654        "type": "tuple"655      }656    ],657    "name": "setCollectionSponsorCross",658    "outputs": [],659    "stateMutability": "nonpayable",660    "type": "function"661  },662  {663    "inputs": [664      { "internalType": "uint256", "name": "tokenId", "type": "uint256" },665      {666        "components": [667          { "internalType": "string", "name": "key", "type": "string" },668          { "internalType": "bytes", "name": "value", "type": "bytes" }669        ],670        "internalType": "struct Property[]",671        "name": "properties",672        "type": "tuple[]"673      }674    ],675    "name": "setProperties",676    "outputs": [],677    "stateMutability": "nonpayable",678    "type": "function"679  },680  {681    "inputs": [682      { "internalType": "string", "name": "key", "type": "string" },683      { "internalType": "bool", "name": "isMutable", "type": "bool" },684      { "internalType": "bool", "name": "collectionAdmin", "type": "bool" },685      { "internalType": "bool", "name": "tokenOwner", "type": "bool" }686    ],687    "name": "setTokenPropertyPermission",688    "outputs": [],689    "stateMutability": "nonpayable",690    "type": "function"691  },692  {693    "inputs": [694      { "internalType": "bytes4", "name": "interfaceID", "type": "bytes4" }695    ],696    "name": "supportsInterface",697    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],698    "stateMutability": "view",699    "type": "function"700  },701  {702    "inputs": [],703    "name": "symbol",704    "outputs": [{ "internalType": "string", "name": "", "type": "string" }],705    "stateMutability": "view",706    "type": "function"707  },708  {709    "inputs": [710      { "internalType": "uint256", "name": "index", "type": "uint256" }711    ],712    "name": "tokenByIndex",713    "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],714    "stateMutability": "view",715    "type": "function"716  },717  {718    "inputs": [719      { "internalType": "uint256", "name": "token", "type": "uint256" }720    ],721    "name": "tokenContractAddress",722    "outputs": [{ "internalType": "address", "name": "", "type": "address" }],723    "stateMutability": "view",724    "type": "function"725  },726  {727    "inputs": [728      { "internalType": "address", "name": "owner", "type": "address" },729      { "internalType": "uint256", "name": "index", "type": "uint256" }730    ],731    "name": "tokenOfOwnerByIndex",732    "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],733    "stateMutability": "view",734    "type": "function"735  },736  {737    "inputs": [738      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }739    ],740    "name": "tokenURI",741    "outputs": [{ "internalType": "string", "name": "", "type": "string" }],742    "stateMutability": "view",743    "type": "function"744  },745  {746    "inputs": [],747    "name": "totalSupply",748    "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],749    "stateMutability": "view",750    "type": "function"751  },752  {753    "inputs": [754      { "internalType": "address", "name": "to", "type": "address" },755      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }756    ],757    "name": "transfer",758    "outputs": [],759    "stateMutability": "nonpayable",760    "type": "function"761  },762  {763    "inputs": [764      {765        "components": [766          { "internalType": "address", "name": "eth", "type": "address" },767          { "internalType": "uint256", "name": "sub", "type": "uint256" }768        ],769        "internalType": "struct EthCrossAccount",770        "name": "to",771        "type": "tuple"772      },773      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }774    ],775    "name": "transferCross",776    "outputs": [],777    "stateMutability": "nonpayable",778    "type": "function"779  },780  {781    "inputs": [782      { "internalType": "address", "name": "from", "type": "address" },783      { "internalType": "address", "name": "to", "type": "address" },784      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }785    ],786    "name": "transferFrom",787    "outputs": [],788    "stateMutability": "nonpayable",789    "type": "function"790  },791  {792    "inputs": [793      {794        "components": [795          { "internalType": "address", "name": "eth", "type": "address" },796          { "internalType": "uint256", "name": "sub", "type": "uint256" }797        ],798        "internalType": "struct EthCrossAccount",799        "name": "from",800        "type": "tuple"801      },802      {803        "components": [804          { "internalType": "address", "name": "eth", "type": "address" },805          { "internalType": "uint256", "name": "sub", "type": "uint256" }806        ],807        "internalType": "struct EthCrossAccount",808        "name": "to",809        "type": "tuple"810      },811      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }812    ],813    "name": "transferFromCross",814    "outputs": [],815    "stateMutability": "nonpayable",816    "type": "function"817  },818  {819    "inputs": [],820    "name": "uniqueCollectionType",821    "outputs": [{ "internalType": "string", "name": "", "type": "string" }],822    "stateMutability": "view",823    "type": "function"824  }825]
after · tests/src/eth/abi/reFungible.json
1[2  {3    "anonymous": false,4    "inputs": [5      {6        "indexed": true,7        "internalType": "address",8        "name": "owner",9        "type": "address"10      },11      {12        "indexed": true,13        "internalType": "address",14        "name": "approved",15        "type": "address"16      },17      {18        "indexed": true,19        "internalType": "uint256",20        "name": "tokenId",21        "type": "uint256"22      }23    ],24    "name": "Approval",25    "type": "event"26  },27  {28    "anonymous": false,29    "inputs": [30      {31        "indexed": true,32        "internalType": "address",33        "name": "owner",34        "type": "address"35      },36      {37        "indexed": true,38        "internalType": "address",39        "name": "operator",40        "type": "address"41      },42      {43        "indexed": false,44        "internalType": "bool",45        "name": "approved",46        "type": "bool"47      }48    ],49    "name": "ApprovalForAll",50    "type": "event"51  },52  {53    "anonymous": false,54    "inputs": [],55    "name": "MintingFinished",56    "type": "event"57  },58  {59    "anonymous": false,60    "inputs": [61      {62        "indexed": true,63        "internalType": "address",64        "name": "from",65        "type": "address"66      },67      {68        "indexed": true,69        "internalType": "address",70        "name": "to",71        "type": "address"72      },73      {74        "indexed": true,75        "internalType": "uint256",76        "name": "tokenId",77        "type": "uint256"78      }79    ],80    "name": "Transfer",81    "type": "event"82  },83  {84    "inputs": [85      {86        "components": [87          { "internalType": "address", "name": "eth", "type": "address" },88          { "internalType": "uint256", "name": "sub", "type": "uint256" }89        ],90        "internalType": "struct EthCrossAccount",91        "name": "newAdmin",92        "type": "tuple"93      }94    ],95    "name": "addCollectionAdminCross",96    "outputs": [],97    "stateMutability": "nonpayable",98    "type": "function"99  },100  {101    "inputs": [102      {103        "components": [104          { "internalType": "address", "name": "eth", "type": "address" },105          { "internalType": "uint256", "name": "sub", "type": "uint256" }106        ],107        "internalType": "struct EthCrossAccount",108        "name": "user",109        "type": "tuple"110      }111    ],112    "name": "addToCollectionAllowListCross",113    "outputs": [],114    "stateMutability": "nonpayable",115    "type": "function"116  },117  {118    "inputs": [119      {120        "components": [121          { "internalType": "address", "name": "eth", "type": "address" },122          { "internalType": "uint256", "name": "sub", "type": "uint256" }123        ],124        "internalType": "struct EthCrossAccount",125        "name": "user",126        "type": "tuple"127      }128    ],129    "name": "allowlistedCross",130    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],131    "stateMutability": "view",132    "type": "function"133  },134  {135    "inputs": [136      { "internalType": "address", "name": "approved", "type": "address" },137      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }138    ],139    "name": "approve",140    "outputs": [],141    "stateMutability": "nonpayable",142    "type": "function"143  },144  {145    "inputs": [146      { "internalType": "address", "name": "owner", "type": "address" }147    ],148    "name": "balanceOf",149    "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],150    "stateMutability": "view",151    "type": "function"152  },153  {154    "inputs": [155      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }156    ],157    "name": "burn",158    "outputs": [],159    "stateMutability": "nonpayable",160    "type": "function"161  },162  {163    "inputs": [164      {165        "components": [166          { "internalType": "address", "name": "eth", "type": "address" },167          { "internalType": "uint256", "name": "sub", "type": "uint256" }168        ],169        "internalType": "struct EthCrossAccount",170        "name": "from",171        "type": "tuple"172      },173      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }174    ],175    "name": "burnFromCross",176    "outputs": [],177    "stateMutability": "nonpayable",178    "type": "function"179  },180  {181    "inputs": [182      {183        "components": [184          { "internalType": "address", "name": "eth", "type": "address" },185          { "internalType": "uint256", "name": "sub", "type": "uint256" }186        ],187        "internalType": "struct EthCrossAccount",188        "name": "newOwner",189        "type": "tuple"190      }191    ],192    "name": "changeCollectionOwnerCross",193    "outputs": [],194    "stateMutability": "nonpayable",195    "type": "function"196  },197  {198    "inputs": [],199    "name": "collectionAdmins",200    "outputs": [201      {202        "components": [203          { "internalType": "address", "name": "eth", "type": "address" },204          { "internalType": "uint256", "name": "sub", "type": "uint256" }205        ],206        "internalType": "struct EthCrossAccount[]",207        "name": "",208        "type": "tuple[]"209      }210    ],211    "stateMutability": "view",212    "type": "function"213  },214  {215    "inputs": [],216    "name": "collectionHelperAddress",217    "outputs": [{ "internalType": "address", "name": "", "type": "address" }],218    "stateMutability": "view",219    "type": "function"220  },221  {222    "inputs": [],223    "name": "collectionNestingPermissions",224    "outputs": [225      {226        "components": [227          {228            "internalType": "enum CollectionPermissions",229            "name": "field_0",230            "type": "uint8"231          },232          { "internalType": "bool", "name": "field_1", "type": "bool" }233        ],234        "internalType": "struct Tuple36[]",235        "name": "",236        "type": "tuple[]"237      }238    ],239    "stateMutability": "view",240    "type": "function"241  },242  {243    "inputs": [],244    "name": "collectionNestingRestrictedCollectionIds",245    "outputs": [246      {247        "components": [248          { "internalType": "bool", "name": "field_0", "type": "bool" },249          {250            "internalType": "uint256[]",251            "name": "field_1",252            "type": "uint256[]"253          }254        ],255        "internalType": "struct Tuple33",256        "name": "",257        "type": "tuple"258      }259    ],260    "stateMutability": "view",261    "type": "function"262  },263  {264    "inputs": [],265    "name": "collectionOwner",266    "outputs": [267      {268        "components": [269          { "internalType": "address", "name": "eth", "type": "address" },270          { "internalType": "uint256", "name": "sub", "type": "uint256" }271        ],272        "internalType": "struct EthCrossAccount",273        "name": "",274        "type": "tuple"275      }276    ],277    "stateMutability": "view",278    "type": "function"279  },280  {281    "inputs": [282      { "internalType": "string[]", "name": "keys", "type": "string[]" }283    ],284    "name": "collectionProperties",285    "outputs": [286      {287        "components": [288          { "internalType": "string", "name": "key", "type": "string" },289          { "internalType": "bytes", "name": "value", "type": "bytes" }290        ],291        "internalType": "struct Property[]",292        "name": "",293        "type": "tuple[]"294      }295    ],296    "stateMutability": "view",297    "type": "function"298  },299  {300    "inputs": [{ "internalType": "string", "name": "key", "type": "string" }],301    "name": "collectionProperty",302    "outputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }],303    "stateMutability": "view",304    "type": "function"305  },306  {307    "inputs": [],308    "name": "collectionSponsor",309    "outputs": [310      {311        "components": [312          { "internalType": "address", "name": "field_0", "type": "address" },313          { "internalType": "uint256", "name": "field_1", "type": "uint256" }314        ],315        "internalType": "struct Tuple29",316        "name": "",317        "type": "tuple"318      }319    ],320    "stateMutability": "view",321    "type": "function"322  },323  {324    "inputs": [],325    "name": "confirmCollectionSponsorship",326    "outputs": [],327    "stateMutability": "nonpayable",328    "type": "function"329  },330  {331    "inputs": [],332    "name": "contractAddress",333    "outputs": [{ "internalType": "address", "name": "", "type": "address" }],334    "stateMutability": "view",335    "type": "function"336  },337  {338    "inputs": [339      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }340    ],341    "name": "crossOwnerOf",342    "outputs": [343      {344        "components": [345          { "internalType": "address", "name": "eth", "type": "address" },346          { "internalType": "uint256", "name": "sub", "type": "uint256" }347        ],348        "internalType": "struct EthCrossAccount",349        "name": "",350        "type": "tuple"351      }352    ],353    "stateMutability": "view",354    "type": "function"355  },356  {357    "inputs": [358      { "internalType": "string[]", "name": "keys", "type": "string[]" }359    ],360    "name": "deleteCollectionProperties",361    "outputs": [],362    "stateMutability": "nonpayable",363    "type": "function"364  },365  {366    "inputs": [367      { "internalType": "uint256", "name": "tokenId", "type": "uint256" },368      { "internalType": "string[]", "name": "keys", "type": "string[]" }369    ],370    "name": "deleteProperties",371    "outputs": [],372    "stateMutability": "nonpayable",373    "type": "function"374  },375  {376    "inputs": [],377    "name": "description",378    "outputs": [{ "internalType": "string", "name": "", "type": "string" }],379    "stateMutability": "view",380    "type": "function"381  },382  {383    "inputs": [],384    "name": "finishMinting",385    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],386    "stateMutability": "nonpayable",387    "type": "function"388  },389  {390    "inputs": [391      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }392    ],393    "name": "getApproved",394    "outputs": [{ "internalType": "address", "name": "", "type": "address" }],395    "stateMutability": "view",396    "type": "function"397  },398  {399    "inputs": [],400    "name": "hasCollectionPendingSponsor",401    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],402    "stateMutability": "view",403    "type": "function"404  },405  {406    "inputs": [407      { "internalType": "address", "name": "owner", "type": "address" },408      { "internalType": "address", "name": "operator", "type": "address" }409    ],410    "name": "isApprovedForAll",411    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],412    "stateMutability": "view",413    "type": "function"414  },415  {416    "inputs": [417      {418        "components": [419          { "internalType": "address", "name": "eth", "type": "address" },420          { "internalType": "uint256", "name": "sub", "type": "uint256" }421        ],422        "internalType": "struct EthCrossAccount",423        "name": "user",424        "type": "tuple"425      }426    ],427    "name": "isOwnerOrAdminCross",428    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],429    "stateMutability": "view",430    "type": "function"431  },432  {433    "inputs": [{ "internalType": "address", "name": "to", "type": "address" }],434    "name": "mint",435    "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],436    "stateMutability": "nonpayable",437    "type": "function"438  },439  {440    "inputs": [441      { "internalType": "address", "name": "to", "type": "address" },442      { "internalType": "string", "name": "tokenUri", "type": "string" }443    ],444    "name": "mintWithTokenURI",445    "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],446    "stateMutability": "nonpayable",447    "type": "function"448  },449  {450    "inputs": [],451    "name": "mintingFinished",452    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],453    "stateMutability": "view",454    "type": "function"455  },456  {457    "inputs": [],458    "name": "name",459    "outputs": [{ "internalType": "string", "name": "", "type": "string" }],460    "stateMutability": "view",461    "type": "function"462  },463  {464    "inputs": [],465    "name": "nextTokenId",466    "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],467    "stateMutability": "view",468    "type": "function"469  },470  {471    "inputs": [472      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }473    ],474    "name": "ownerOf",475    "outputs": [{ "internalType": "address", "name": "", "type": "address" }],476    "stateMutability": "view",477    "type": "function"478  },479  {480    "inputs": [481      { "internalType": "uint256", "name": "tokenId", "type": "uint256" },482      { "internalType": "string[]", "name": "keys", "type": "string[]" }483    ],484    "name": "properties",485    "outputs": [486      {487        "components": [488          { "internalType": "string", "name": "key", "type": "string" },489          { "internalType": "bytes", "name": "value", "type": "bytes" }490        ],491        "internalType": "struct Property[]",492        "name": "",493        "type": "tuple[]"494      }495    ],496    "stateMutability": "view",497    "type": "function"498  },499  {500    "inputs": [501      { "internalType": "uint256", "name": "tokenId", "type": "uint256" },502      { "internalType": "string", "name": "key", "type": "string" }503    ],504    "name": "property",505    "outputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }],506    "stateMutability": "view",507    "type": "function"508  },509  {510    "inputs": [511      {512        "components": [513          { "internalType": "address", "name": "eth", "type": "address" },514          { "internalType": "uint256", "name": "sub", "type": "uint256" }515        ],516        "internalType": "struct EthCrossAccount",517        "name": "admin",518        "type": "tuple"519      }520    ],521    "name": "removeCollectionAdminCross",522    "outputs": [],523    "stateMutability": "nonpayable",524    "type": "function"525  },526  {527    "inputs": [],528    "name": "removeCollectionSponsor",529    "outputs": [],530    "stateMutability": "nonpayable",531    "type": "function"532  },533  {534    "inputs": [535      {536        "components": [537          { "internalType": "address", "name": "eth", "type": "address" },538          { "internalType": "uint256", "name": "sub", "type": "uint256" }539        ],540        "internalType": "struct EthCrossAccount",541        "name": "user",542        "type": "tuple"543      }544    ],545    "name": "removeFromCollectionAllowListCross",546    "outputs": [],547    "stateMutability": "nonpayable",548    "type": "function"549  },550  {551    "inputs": [552      { "internalType": "address", "name": "from", "type": "address" },553      { "internalType": "address", "name": "to", "type": "address" },554      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }555    ],556    "name": "safeTransferFrom",557    "outputs": [],558    "stateMutability": "nonpayable",559    "type": "function"560  },561  {562    "inputs": [563      { "internalType": "address", "name": "from", "type": "address" },564      { "internalType": "address", "name": "to", "type": "address" },565      { "internalType": "uint256", "name": "tokenId", "type": "uint256" },566      { "internalType": "bytes", "name": "data", "type": "bytes" }567    ],568    "name": "safeTransferFromWithData",569    "outputs": [],570    "stateMutability": "nonpayable",571    "type": "function"572  },573  {574    "inputs": [575      { "internalType": "address", "name": "operator", "type": "address" },576      { "internalType": "bool", "name": "approved", "type": "bool" }577    ],578    "name": "setApprovalForAll",579    "outputs": [],580    "stateMutability": "nonpayable",581    "type": "function"582  },583  {584    "inputs": [{ "internalType": "uint8", "name": "mode", "type": "uint8" }],585    "name": "setCollectionAccess",586    "outputs": [],587    "stateMutability": "nonpayable",588    "type": "function"589  },590  {591    "inputs": [592      { "internalType": "string", "name": "limit", "type": "string" },593      { "internalType": "uint256", "name": "value", "type": "uint256" }594    ],595    "name": "setCollectionLimit",596    "outputs": [],597    "stateMutability": "nonpayable",598    "type": "function"599  },600  {601    "inputs": [{ "internalType": "bool", "name": "mode", "type": "bool" }],602    "name": "setCollectionMintMode",603    "outputs": [],604    "stateMutability": "nonpayable",605    "type": "function"606  },607  {608    "inputs": [{ "internalType": "bool", "name": "enable", "type": "bool" }],609    "name": "setCollectionNesting",610    "outputs": [],611    "stateMutability": "nonpayable",612    "type": "function"613  },614  {615    "inputs": [616      { "internalType": "bool", "name": "enable", "type": "bool" },617      {618        "internalType": "address[]",619        "name": "collections",620        "type": "address[]"621      }622    ],623    "name": "setCollectionNesting",624    "outputs": [],625    "stateMutability": "nonpayable",626    "type": "function"627  },628  {629    "inputs": [630      {631        "components": [632          { "internalType": "string", "name": "key", "type": "string" },633          { "internalType": "bytes", "name": "value", "type": "bytes" }634        ],635        "internalType": "struct Property[]",636        "name": "properties",637        "type": "tuple[]"638      }639    ],640    "name": "setCollectionProperties",641    "outputs": [],642    "stateMutability": "nonpayable",643    "type": "function"644  },645  {646    "inputs": [647      {648        "components": [649          { "internalType": "address", "name": "eth", "type": "address" },650          { "internalType": "uint256", "name": "sub", "type": "uint256" }651        ],652        "internalType": "struct EthCrossAccount",653        "name": "sponsor",654        "type": "tuple"655      }656    ],657    "name": "setCollectionSponsorCross",658    "outputs": [],659    "stateMutability": "nonpayable",660    "type": "function"661  },662  {663    "inputs": [664      { "internalType": "uint256", "name": "tokenId", "type": "uint256" },665      {666        "components": [667          { "internalType": "string", "name": "key", "type": "string" },668          { "internalType": "bytes", "name": "value", "type": "bytes" }669        ],670        "internalType": "struct Property[]",671        "name": "properties",672        "type": "tuple[]"673      }674    ],675    "name": "setProperties",676    "outputs": [],677    "stateMutability": "nonpayable",678    "type": "function"679  },680  {681    "inputs": [682      {683        "components": [684          { "internalType": "string", "name": "field_0", "type": "string" },685          {686            "components": [687              {688                "internalType": "enum EthTokenPermissions",689                "name": "field_0",690                "type": "uint8"691              },692              { "internalType": "bool", "name": "field_1", "type": "bool" }693            ],694            "internalType": "struct Tuple51[]",695            "name": "field_1",696            "type": "tuple[]"697          }698        ],699        "internalType": "struct Tuple53[]",700        "name": "permissions",701        "type": "tuple[]"702      }703    ],704    "name": "setTokenPropertyPermissions",705    "outputs": [],706    "stateMutability": "nonpayable",707    "type": "function"708  },709  {710    "inputs": [711      { "internalType": "bytes4", "name": "interfaceID", "type": "bytes4" }712    ],713    "name": "supportsInterface",714    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],715    "stateMutability": "view",716    "type": "function"717  },718  {719    "inputs": [],720    "name": "symbol",721    "outputs": [{ "internalType": "string", "name": "", "type": "string" }],722    "stateMutability": "view",723    "type": "function"724  },725  {726    "inputs": [727      { "internalType": "uint256", "name": "index", "type": "uint256" }728    ],729    "name": "tokenByIndex",730    "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],731    "stateMutability": "view",732    "type": "function"733  },734  {735    "inputs": [736      { "internalType": "uint256", "name": "token", "type": "uint256" }737    ],738    "name": "tokenContractAddress",739    "outputs": [{ "internalType": "address", "name": "", "type": "address" }],740    "stateMutability": "view",741    "type": "function"742  },743  {744    "inputs": [745      { "internalType": "address", "name": "owner", "type": "address" },746      { "internalType": "uint256", "name": "index", "type": "uint256" }747    ],748    "name": "tokenOfOwnerByIndex",749    "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],750    "stateMutability": "view",751    "type": "function"752  },753  {754    "inputs": [],755    "name": "tokenPropertyPermissions",756    "outputs": [757      {758        "components": [759          { "internalType": "string", "name": "field_0", "type": "string" },760          {761            "components": [762              {763                "internalType": "enum EthTokenPermissions",764                "name": "field_0",765                "type": "uint8"766              },767              { "internalType": "bool", "name": "field_1", "type": "bool" }768            ],769            "internalType": "struct Tuple51[]",770            "name": "field_1",771            "type": "tuple[]"772          }773        ],774        "internalType": "struct Tuple53[]",775        "name": "",776        "type": "tuple[]"777      }778    ],779    "stateMutability": "view",780    "type": "function"781  },782  {783    "inputs": [784      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }785    ],786    "name": "tokenURI",787    "outputs": [{ "internalType": "string", "name": "", "type": "string" }],788    "stateMutability": "view",789    "type": "function"790  },791  {792    "inputs": [],793    "name": "totalSupply",794    "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],795    "stateMutability": "view",796    "type": "function"797  },798  {799    "inputs": [800      { "internalType": "address", "name": "to", "type": "address" },801      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }802    ],803    "name": "transfer",804    "outputs": [],805    "stateMutability": "nonpayable",806    "type": "function"807  },808  {809    "inputs": [810      {811        "components": [812          { "internalType": "address", "name": "eth", "type": "address" },813          { "internalType": "uint256", "name": "sub", "type": "uint256" }814        ],815        "internalType": "struct EthCrossAccount",816        "name": "to",817        "type": "tuple"818      },819      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }820    ],821    "name": "transferCross",822    "outputs": [],823    "stateMutability": "nonpayable",824    "type": "function"825  },826  {827    "inputs": [828      { "internalType": "address", "name": "from", "type": "address" },829      { "internalType": "address", "name": "to", "type": "address" },830      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }831    ],832    "name": "transferFrom",833    "outputs": [],834    "stateMutability": "nonpayable",835    "type": "function"836  },837  {838    "inputs": [839      {840        "components": [841          { "internalType": "address", "name": "eth", "type": "address" },842          { "internalType": "uint256", "name": "sub", "type": "uint256" }843        ],844        "internalType": "struct EthCrossAccount",845        "name": "from",846        "type": "tuple"847      },848      {849        "components": [850          { "internalType": "address", "name": "eth", "type": "address" },851          { "internalType": "uint256", "name": "sub", "type": "uint256" }852        ],853        "internalType": "struct EthCrossAccount",854        "name": "to",855        "type": "tuple"856      },857      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }858    ],859    "name": "transferFromCross",860    "outputs": [],861    "stateMutability": "nonpayable",862    "type": "function"863  },864  {865    "inputs": [],866    "name": "uniqueCollectionType",867    "outputs": [{ "internalType": "string", "name": "", "type": "string" }],868    "stateMutability": "view",869    "type": "function"870  }871]
modifiedtests/src/eth/api/UniqueNFT.soldiffbeforeafterboth
--- a/tests/src/eth/api/UniqueNFT.sol
+++ b/tests/src/eth/api/UniqueNFT.sol
@@ -13,22 +13,28 @@
 }
 
 /// @title A contract that allows to set and delete token properties and change token property permissions.
-/// @dev the ERC-165 identifier for this interface is 0x91a97a68
+/// @dev the ERC-165 identifier for this interface is 0xde0695c2
 interface TokenProperties is Dummy, ERC165 {
+	// /// @notice Set permissions for token property.
+	// /// @dev Throws error if `msg.sender` is not admin or owner of the collection.
+	// /// @param key Property key.
+	// /// @param isMutable Permission to mutate property.
+	// /// @param collectionAdmin Permission to mutate property by collection admin if property is mutable.
+	// /// @param tokenOwner Permission to mutate property by token owner if property is mutable.
+	// /// @dev EVM selector for this function is: 0x222d97fa,
+	// ///  or in textual repr: setTokenPropertyPermission(string,bool,bool,bool)
+	// function setTokenPropertyPermission(string memory key, bool isMutable, bool collectionAdmin, bool tokenOwner) external;
+
 	/// @notice Set permissions for token property.
 	/// @dev Throws error if `msg.sender` is not admin or owner of the collection.
-	/// @param key Property key.
-	/// @param isMutable Permission to mutate property.
-	/// @param collectionAdmin Permission to mutate property by collection admin if property is mutable.
-	/// @param tokenOwner Permission to mutate property by token owner if property is mutable.
-	/// @dev EVM selector for this function is: 0x222d97fa,
-	///  or in textual repr: setTokenPropertyPermission(string,bool,bool,bool)
-	function setTokenPropertyPermission(
-		string memory key,
-		bool isMutable,
-		bool collectionAdmin,
-		bool tokenOwner
-	) external;
+	/// @param permissions Permissions for keys.
+	/// @dev EVM selector for this function is: 0xbd92983a,
+	///  or in textual repr: setTokenPropertyPermissions((string,(uint8,bool)[])[])
+	function setTokenPropertyPermissions(Tuple43[] memory permissions) external;
+
+	/// @dev EVM selector for this function is: 0xf23d7790,
+	///  or in textual repr: tokenPropertyPermissions()
+	function tokenPropertyPermissions() external view returns (Tuple43[] memory);
 
 	// /// @notice Set token property value.
 	// /// @dev Throws error if `msg.sender` has no permission to edit the property.
@@ -79,6 +85,24 @@
 	bytes value;
 }
 
+enum EthTokenPermissions {
+	Mutable,
+	TokenOwner,
+	CollectionAdmin
+}
+
+/// @dev anonymous struct
+struct Tuple43 {
+	string field_0;
+	Tuple41[] field_1;
+}
+
+/// @dev anonymous struct
+struct Tuple41 {
+	EthTokenPermissions field_0;
+	bool field_1;
+}
+
 /// @title A contract that allows you to work with collections.
 /// @dev the ERC-165 identifier for this interface is 0xb5e1747f
 interface Collection is Dummy, ERC165 {
modifiedtests/src/eth/api/UniqueRefungible.soldiffbeforeafterboth
--- a/tests/src/eth/api/UniqueRefungible.sol
+++ b/tests/src/eth/api/UniqueRefungible.sol
@@ -13,22 +13,29 @@
 }
 
 /// @title A contract that allows to set and delete token properties and change token property permissions.
-/// @dev the ERC-165 identifier for this interface is 0x91a97a68
+/// @dev the ERC-165 identifier for this interface is 0xde0695c2
 interface TokenProperties is Dummy, ERC165 {
+	// /// @notice Set permissions for token property.
+	// /// @dev Throws error if `msg.sender` is not admin or owner of the collection.
+	// /// @param key Property key.
+	// /// @param isMutable Permission to mutate property.
+	// /// @param collectionAdmin Permission to mutate property by collection admin if property is mutable.
+	// /// @param tokenOwner Permission to mutate property by token owner if property is mutable.
+	// /// @dev EVM selector for this function is: 0x222d97fa,
+	// ///  or in textual repr: setTokenPropertyPermission(string,bool,bool,bool)
+	// function setTokenPropertyPermission(string memory key, bool isMutable, bool collectionAdmin, bool tokenOwner) external;
+
 	/// @notice Set permissions for token property.
 	/// @dev Throws error if `msg.sender` is not admin or owner of the collection.
-	/// @param key Property key.
-	/// @param isMutable Permission to mutate property.
-	/// @param collectionAdmin Permission to mutate property by collection admin if property is mutable.
-	/// @param tokenOwner Permission to mutate property by token owner if property is mutable.
-	/// @dev EVM selector for this function is: 0x222d97fa,
-	///  or in textual repr: setTokenPropertyPermission(string,bool,bool,bool)
-	function setTokenPropertyPermission(
-		string memory key,
-		bool isMutable,
-		bool collectionAdmin,
-		bool tokenOwner
-	) external;
+	/// @param permissions Permissions for keys.
+	/// @dev EVM selector for this function is: 0xbd92983a,
+	///  or in textual repr: setTokenPropertyPermissions((string,(uint8,bool)[])[])
+	function setTokenPropertyPermissions(Tuple47[] memory permissions) external;
+
+	/// @notice Get permissions for token properties.
+	/// @dev EVM selector for this function is: 0xf23d7790,
+	///  or in textual repr: tokenPropertyPermissions()
+	function tokenPropertyPermissions() external view returns (Tuple47[] memory);
 
 	// /// @notice Set token property value.
 	// /// @dev Throws error if `msg.sender` has no permission to edit the property.
@@ -79,6 +86,24 @@
 	bytes value;
 }
 
+enum EthTokenPermissions {
+	Mutable,
+	TokenOwner,
+	CollectionAdmin
+}
+
+/// @dev anonymous struct
+struct Tuple47 {
+	string field_0;
+	Tuple45[] field_1;
+}
+
+/// @dev anonymous struct
+struct Tuple45 {
+	EthTokenPermissions field_0;
+	bool field_1;
+}
+
 /// @title A contract that allows you to work with collections.
 /// @dev the ERC-165 identifier for this interface is 0xb5e1747f
 interface Collection is Dummy, ERC165 {
modifiedtests/src/eth/events.test.tsdiffbeforeafterboth
--- a/tests/src/eth/events.test.ts
+++ b/tests/src/eth/events.test.ts
@@ -19,7 +19,7 @@
 import {EthUniqueHelper, itEth, usingEthPlaygrounds} from './util';
 import {IEvent, TCollectionMode} from '../util/playgrounds/types';
 import {Pallets, requirePalletsOrSkip} from '../util';
-import {NormalizedEvent} from './util/playgrounds/types';
+import {EthTokenPermissions, NormalizedEvent} from './util/playgrounds/types';
 
 let donor: IKeyringPair;
   
@@ -119,7 +119,13 @@
     ethEvents.push(event);
   });
   const {unsubscribe, collectedEvents: subEvents} = await helper.subscribeEvents([{section: 'common', names: ['PropertyPermissionSet']}]);
-  await collection.methods.setTokenPropertyPermission('testKey', true, true, true).send({from: owner});
+  await collection.methods.setTokenPropertyPermissions([
+    ['A', [
+      [EthTokenPermissions.Mutable, true], 
+      [EthTokenPermissions.TokenOwner, true], 
+      [EthTokenPermissions.CollectionAdmin, true]],
+    ],
+  ]).send({from: owner});
   await helper.wait.newBlocks(1);
   expect(ethEvents).to.be.like([
     {
@@ -374,7 +380,13 @@
   const collectionHelper = helper.ethNativeContract.collectionHelpers(owner);
   const result = await collection.methods.mint(owner).send({from: owner});
   const tokenId = result.events.Transfer.returnValues.tokenId;
-  await collection.methods.setTokenPropertyPermission('A', true, true, true).send({from: owner});
+  await collection.methods.setTokenPropertyPermissions([
+    ['A', [
+      [EthTokenPermissions.Mutable, true], 
+      [EthTokenPermissions.TokenOwner, true], 
+      [EthTokenPermissions.CollectionAdmin, true]],
+    ],
+  ]).send({from: owner});
 
 
   const ethEvents: any = [];
modifiedtests/src/eth/tokenProperties.test.tsdiffbeforeafterboth
--- a/tests/src/eth/tokenProperties.test.ts
+++ b/tests/src/eth/tokenProperties.test.ts
@@ -20,6 +20,7 @@
 import {ITokenPropertyPermission} from '../util/playgrounds/types';
 import {Pallets} from '../util';
 import {UniqueNFTCollection, UniqueNFToken, UniqueRFTCollection} from '../util/playgrounds/unique';
+import {EthTokenPermissions} from './util/playgrounds/types';
 
 describe('EVM token properties', () => {
   let donor: IKeyringPair;
@@ -32,25 +33,170 @@
     });
   });
 
-  itEth('Can be reconfigured', async({helper}) => {
-    const caller = await helper.eth.createAccountWithBalance(donor);
-    for(const [mutable,collectionAdmin, tokenOwner] of cartesian([], [false, true], [false, true], [false, true])) {
-      const collection = await helper.nft.mintCollection(alice);
-      await collection.addAdmin(alice, {Ethereum: caller});
+  [
+    {mode: 'nft' as const, requiredPallets: []},
+    {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]},
+  ].map(testCase =>
+    itEth.ifWithPallets(`[${testCase.mode}] Set and get token property permissions`, testCase.requiredPallets, async({helper}) => {
+      const owner = await helper.eth.createAccountWithBalance(donor);
+      const caller = await helper.ethCrossAccount.createAccountWithBalance(donor);
+      for(const [mutable,collectionAdmin, tokenOwner] of cartesian([], [false, true], [false, true], [false, true])) {
+        const {collectionId, collectionAddress} = await helper.eth.createCollection(testCase.mode, owner, 'A', 'B', 'C');
+        const collection = await helper.ethNativeContract.collection(collectionAddress, testCase.mode, owner);
+        await collection.methods.addCollectionAdminCross(caller).send({from: owner});
+
+        await collection.methods.setTokenPropertyPermissions([
+          ['testKey', [
+            [EthTokenPermissions.Mutable, mutable], 
+            [EthTokenPermissions.TokenOwner, tokenOwner], 
+            [EthTokenPermissions.CollectionAdmin, collectionAdmin]],
+          ],
+        ]).send({from: caller.eth});
       
-      const address = helper.ethAddress.fromCollectionId(collection.collectionId);
-      const contract = helper.ethNativeContract.collection(address, 'nft', caller);
-  
-      await contract.methods.setTokenPropertyPermission('testKey', mutable, collectionAdmin, tokenOwner).send({from: caller});
-  
-      expect(await collection.getPropertyPermissions()).to.be.deep.equal([{
-        key: 'testKey',
-        permission: {mutable, collectionAdmin, tokenOwner},
-      }]);
-    }
-  });
+        expect(await helper[testCase.mode].getPropertyPermissions(collectionId)).to.be.deep.equal([{
+          key: 'testKey',
+          permission: {mutable, collectionAdmin, tokenOwner},
+        }]);
 
+        expect(await collection.methods.tokenPropertyPermissions().call({from: caller.eth})).to.be.like([
+          ['testKey', [
+            [EthTokenPermissions.Mutable.toString(), mutable], 
+            [EthTokenPermissions.TokenOwner.toString(), tokenOwner], 
+            [EthTokenPermissions.CollectionAdmin.toString(), collectionAdmin]],
+          ],
+        ]);
+      }
+    }));
+
   [
+    {mode: 'nft' as const, requiredPallets: []},
+    {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]},
+  ].map(testCase =>
+    itEth.ifWithPallets(`[${testCase.mode}] Set and get multiple token property permissions as owner`, testCase.requiredPallets, async({helper}) => {
+      const owner = await helper.eth.createAccountWithBalance(donor);
+      
+      const {collectionId, collectionAddress} = await helper.eth.createCollection(testCase.mode, owner, 'A', 'B', 'C');
+      const collection = await helper.ethNativeContract.collection(collectionAddress, testCase.mode, owner);
+
+      await collection.methods.setTokenPropertyPermissions([
+        ['testKey_0', [
+          [EthTokenPermissions.Mutable, true], 
+          [EthTokenPermissions.TokenOwner, true], 
+          [EthTokenPermissions.CollectionAdmin, true]],
+        ],
+        ['testKey_1', [
+          [EthTokenPermissions.Mutable, true], 
+          [EthTokenPermissions.TokenOwner, false], 
+          [EthTokenPermissions.CollectionAdmin, true]],
+        ],
+        ['testKey_2', [
+          [EthTokenPermissions.Mutable, false], 
+          [EthTokenPermissions.TokenOwner, true], 
+          [EthTokenPermissions.CollectionAdmin, false]],
+        ],
+      ]).send({from: owner});
+      
+      expect(await helper[testCase.mode].getPropertyPermissions(collectionId)).to.be.deep.equal([
+        {
+          key: 'testKey_0',
+          permission: {mutable: true, tokenOwner: true, collectionAdmin: true},
+        },
+        {
+          key: 'testKey_1',
+          permission: {mutable: true, tokenOwner: false, collectionAdmin: true},
+        },
+        {
+          key: 'testKey_2',
+          permission: {mutable: false, tokenOwner: true, collectionAdmin: false},
+        },
+      ]);
+
+      expect(await collection.methods.tokenPropertyPermissions().call({from: owner})).to.be.like([
+        ['testKey_0', [
+          [EthTokenPermissions.Mutable.toString(), true], 
+          [EthTokenPermissions.TokenOwner.toString(), true], 
+          [EthTokenPermissions.CollectionAdmin.toString(), true]],
+        ],
+        ['testKey_1', [
+          [EthTokenPermissions.Mutable.toString(), true], 
+          [EthTokenPermissions.TokenOwner.toString(), false], 
+          [EthTokenPermissions.CollectionAdmin.toString(), true]],
+        ],
+        ['testKey_2', [
+          [EthTokenPermissions.Mutable.toString(), false], 
+          [EthTokenPermissions.TokenOwner.toString(), true], 
+          [EthTokenPermissions.CollectionAdmin.toString(), false]],
+        ],
+      ]);
+      
+    }));
+
+  [
+    {mode: 'nft' as const, requiredPallets: []},
+    {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]},
+  ].map(testCase =>
+    itEth.ifWithPallets(`[${testCase.mode}] Set and get multiple token property permissions as admin`, testCase.requiredPallets, async({helper}) => {
+      const owner = await helper.eth.createAccountWithBalance(donor);
+      const caller = await helper.ethCrossAccount.createAccountWithBalance(donor);
+      
+      const {collectionId, collectionAddress} = await helper.eth.createCollection(testCase.mode, owner, 'A', 'B', 'C');
+      const collection = await helper.ethNativeContract.collection(collectionAddress, testCase.mode, owner);
+      await collection.methods.addCollectionAdminCross(caller).send({from: owner});
+
+      await collection.methods.setTokenPropertyPermissions([
+        ['testKey_0', [
+          [EthTokenPermissions.Mutable, true], 
+          [EthTokenPermissions.TokenOwner, true], 
+          [EthTokenPermissions.CollectionAdmin, true]],
+        ],
+        ['testKey_1', [
+          [EthTokenPermissions.Mutable, true], 
+          [EthTokenPermissions.TokenOwner, false], 
+          [EthTokenPermissions.CollectionAdmin, true]],
+        ],
+        ['testKey_2', [
+          [EthTokenPermissions.Mutable, false], 
+          [EthTokenPermissions.TokenOwner, true], 
+          [EthTokenPermissions.CollectionAdmin, false]],
+        ],
+      ]).send({from: caller.eth});
+      
+      expect(await helper[testCase.mode].getPropertyPermissions(collectionId)).to.be.deep.equal([
+        {
+          key: 'testKey_0',
+          permission: {mutable: true, tokenOwner: true, collectionAdmin: true},
+        },
+        {
+          key: 'testKey_1',
+          permission: {mutable: true, tokenOwner: false, collectionAdmin: true},
+        },
+        {
+          key: 'testKey_2',
+          permission: {mutable: false, tokenOwner: true, collectionAdmin: false},
+        },
+      ]);
+
+      expect(await collection.methods.tokenPropertyPermissions().call({from: caller.eth})).to.be.like([
+        ['testKey_0', [
+          [EthTokenPermissions.Mutable.toString(), true], 
+          [EthTokenPermissions.TokenOwner.toString(), true], 
+          [EthTokenPermissions.CollectionAdmin.toString(), true]],
+        ],
+        ['testKey_1', [
+          [EthTokenPermissions.Mutable.toString(), true], 
+          [EthTokenPermissions.TokenOwner.toString(), false], 
+          [EthTokenPermissions.CollectionAdmin.toString(), true]],
+        ],
+        ['testKey_2', [
+          [EthTokenPermissions.Mutable.toString(), false], 
+          [EthTokenPermissions.TokenOwner.toString(), true], 
+          [EthTokenPermissions.CollectionAdmin.toString(), false]],
+        ],
+      ]);
+      
+    }));
+
+  [
     {
       method: 'setProperties',
       methodParams: [[{key: 'testKey1', value: Buffer.from('testValue1')}, {key: 'testKey2', value: Buffer.from('testValue2')}]],
@@ -301,6 +447,47 @@
       const actualProps = await collectionEvm.methods.properties(token.tokenId, []).call();
       expect(actualProps).to.deep.eq(expectedProps);
     }));
+
+  [
+    {mode: 'nft' as const, requiredPallets: []},
+    {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]},
+  ].map(testCase =>
+    itEth.ifWithPallets(`[${testCase.mode}] Cant set token property permissions as non owner or admin`, testCase.requiredPallets, async({helper}) => {
+      const owner = await helper.eth.createAccountWithBalance(donor);
+      const caller = await helper.eth.createAccountWithBalance(donor);
+        
+      const {collectionAddress} = await helper.eth.createCollection(testCase.mode, owner, 'A', 'B', 'C');
+      const collection = await helper.ethNativeContract.collection(collectionAddress, testCase.mode, owner);
+  
+      await expect(collection.methods.setTokenPropertyPermissions([
+        ['testKey_0', [
+          [EthTokenPermissions.Mutable, true], 
+          [EthTokenPermissions.TokenOwner, true], 
+          [EthTokenPermissions.CollectionAdmin, true]],
+        ],
+      ]).call({from: caller})).to.be.rejectedWith('NoPermission');  
+    }));
+
+  [
+    {mode: 'nft' as const, requiredPallets: []},
+    {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]},
+  ].map(testCase =>
+    itEth.ifWithPallets(`[${testCase.mode}] Cant set token property permissions with invalid character`, testCase.requiredPallets, async({helper}) => {
+      const owner = await helper.eth.createAccountWithBalance(donor);
+        
+      const {collectionAddress} = await helper.eth.createCollection(testCase.mode, owner, 'A', 'B', 'C');
+      const collection = await helper.ethNativeContract.collection(collectionAddress, testCase.mode, owner);
+  
+      await expect(collection.methods.setTokenPropertyPermissions([
+        // "Space" is invalid character
+        ['testKey 0', [
+          [EthTokenPermissions.Mutable, true], 
+          [EthTokenPermissions.TokenOwner, true], 
+          [EthTokenPermissions.CollectionAdmin, true]],
+        ],
+      ]).call({from: owner})).to.be.rejectedWith('InvalidCharacterInPropertyKey');  
+    }));
+  
 });
 
 
modifiedtests/src/eth/util/playgrounds/types.tsdiffbeforeafterboth
--- a/tests/src/eth/util/playgrounds/types.ts
+++ b/tests/src/eth/util/playgrounds/types.ts
@@ -20,3 +20,8 @@
 
 export type EthProperty = string[];
 
+export enum EthTokenPermissions {
+  Mutable,
+  TokenOwner,
+  CollectionAdmin
+}
\ No newline at end of file