git.delta.rocks / unique-network / refs/commits / 448629b28a12

difftreelog

Merge pull request #986 from UniqueNetwork/feature/add_mint_bulk_cross

Yaroslav Bolyukin2023-09-25parents: #6486f2a #752e2b0.patch.diff
in: master
Add mintBulkCross to NFT and RFT collections

20 files changed

modifiednode/cli/src/command.rsdiffbeforeafterboth
--- a/node/cli/src/command.rs
+++ b/node/cli/src/command.rs
@@ -425,7 +425,7 @@
 				.map(|cfg| &cfg.registry);
 			let task_manager =
 				sc_service::TaskManager::new(runner.config().tokio_handle.clone(), *registry)
-					.map_err(|e| format!("Error: {:?}", e))?;
+					.map_err(|e| format!("Error: {e:?}"))?;
 			let info_provider = Some(timestamp_with_aura_info(12000));
 
 			runner.async_run(|config| -> Result<(Pin<Box<dyn Future<Output = _>>>, _)> {
modifiedpallets/common/src/eth.rsdiffbeforeafterboth
--- a/pallets/common/src/eth.rs
+++ b/pallets/common/src/eth.rs
@@ -119,7 +119,7 @@
 		} else if self.sub == Default::default() {
 			Ok(Some(T::CrossAccountId::from_eth(self.eth)))
 		} else {
-			Err(format!("All fields of cross account is non zeroed {:?}", self).into())
+			Err(format!("All fields of cross account is non zeroed {self:?}").into())
 		}
 	}
 
modifiedpallets/nonfungible/src/erc.rsdiffbeforeafterboth
--- a/pallets/nonfungible/src/erc.rs
+++ b/pallets/nonfungible/src/erc.rs
@@ -26,7 +26,7 @@
 	char::{REPLACEMENT_CHARACTER, decode_utf16},
 	convert::TryInto,
 };
-use evm_coder::{abi::AbiType, ToLog, generate_stubgen, solidity_interface, types::*};
+use evm_coder::{abi::AbiType, AbiCoder, ToLog, generate_stubgen, solidity_interface, types::*};
 use frame_support::BoundedVec;
 use up_data_structs::{
 	TokenId, PropertyPermission, PropertyKeyPermission, Property, CollectionId, PropertyKey,
@@ -64,6 +64,15 @@
 	},
 }
 
+/// Token minting parameters
+#[derive(AbiCoder, Default, Debug)]
+pub struct MintTokenData {
+	/// Minted token owner
+	pub owner: eth::CrossAddress,
+	/// Minted token properties
+	pub properties: Vec<eth::Property>,
+}
+
 frontier_contract! {
 	macro_rules! NonfungibleHandle_result {...}
 	impl<T: Config> Contract for NonfungibleHandle<T> {...}
@@ -981,13 +990,41 @@
 		Ok(true)
 	}
 
+	/// @notice Function to mint a token.
+	/// @param data Array of pairs of token owner and token's properties for minted token
+	#[weight(<SelfWeightOf<T>>::create_multiple_items(data.len() as u32) + <SelfWeightOf<T>>::set_token_properties(data.len() as u32))]
+	fn mint_bulk_cross(&mut self, caller: Caller, data: Vec<MintTokenData>) -> Result<bool> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let budget = self
+			.recorder
+			.weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+		let mut create_nft_data = Vec::with_capacity(data.len());
+		for MintTokenData { owner, properties } in data {
+			let owner = owner.into_sub_cross_account::<T>()?;
+			create_nft_data.push(CreateItemData::<T> {
+				properties: properties
+					.into_iter()
+					.map(|property| property.try_into())
+					.collect::<Result<Vec<_>>>()?
+					.try_into()
+					.map_err(|_| "too many properties")?,
+				owner,
+			});
+		}
+
+		<Pallet<T>>::create_multiple_items(self, &caller, create_nft_data, &budget)
+			.map_err(dispatch_to_evm::<T>)?;
+		Ok(true)
+	}
+
 	/// @notice Function to mint multiple tokens with the given tokenUris.
 	/// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive
 	///  numbers and first number should be obtained with `nextTokenId` method
 	/// @param to The new owner
 	/// @param tokens array of pairs of token ID and token URI for minted tokens
 	#[solidity(hide, rename_selector = "mintBulkWithTokenURI")]
-	#[weight(<SelfWeightOf<T>>::create_multiple_items(tokens.len() as u32)  + <SelfWeightOf<T>>::set_token_properties(tokens.len() as u32))]
+	#[weight(<SelfWeightOf<T>>::create_multiple_items(tokens.len() as u32) + <SelfWeightOf<T>>::set_token_properties(tokens.len() as u32))]
 	fn mint_bulk_with_token_uri(
 		&mut self,
 		caller: Caller,
modifiedpallets/nonfungible/src/stubs/UniqueNFT.rawdiffbeforeafterboth

binary blob — no preview

modifiedpallets/nonfungible/src/stubs/UniqueNFT.soldiffbeforeafterboth
--- a/pallets/nonfungible/src/stubs/UniqueNFT.sol
+++ b/pallets/nonfungible/src/stubs/UniqueNFT.sol
@@ -800,7 +800,7 @@
 }
 
 /// @title Unique extensions for ERC721.
-/// @dev the ERC-165 identifier for this interface is 0x307b061a
+/// @dev the ERC-165 identifier for this interface is 0x9b397d16
 contract ERC721UniqueExtensions is Dummy, ERC165 {
 	/// @notice A descriptive name for a collection of NFTs in this contract
 	/// @dev EVM selector for this function is: 0x06fdde03,
@@ -997,6 +997,17 @@
 	// 	return false;
 	// }
 
+	/// @notice Function to mint a token.
+	/// @param data Array of pairs of token owner and token's properties for minted token
+	/// @dev EVM selector for this function is: 0xab427b0c,
+	///  or in textual repr: mintBulkCross(((address,uint256),(string,bytes)[])[])
+	function mintBulkCross(MintTokenData[] memory data) public returns (bool) {
+		require(false, stub_error);
+		data;
+		dummy = 0;
+		return false;
+	}
+
 	// /// @notice Function to mint multiple tokens with the given tokenUris.
 	// /// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive
 	// ///  numbers and first number should be obtained with `nextTokenId` method
@@ -1044,6 +1055,14 @@
 	string uri;
 }
 
+/// Token minting parameters
+struct MintTokenData {
+	/// Minted token owner
+	CrossAddress owner;
+	/// Minted token properties
+	Property[] properties;
+}
+
 /// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension
 /// @dev See https://eips.ethereum.org/EIPS/eip-721
 /// @dev the ERC-165 identifier for this interface is 0x780e9d63
modifiedpallets/refungible/src/erc.rsdiffbeforeafterboth
--- a/pallets/refungible/src/erc.rs
+++ b/pallets/refungible/src/erc.rs
@@ -26,7 +26,7 @@
 	char::{REPLACEMENT_CHARACTER, decode_utf16},
 	convert::TryInto,
 };
-use evm_coder::{abi::AbiType, ToLog, generate_stubgen, solidity_interface, types::*};
+use evm_coder::{abi::AbiType, AbiCoder, ToLog, generate_stubgen, solidity_interface, types::*};
 use frame_support::{BoundedBTreeMap, BoundedVec};
 use pallet_common::{
 	CollectionHandle, CollectionPropertyPermissions, CommonCollectionOperations,
@@ -71,6 +71,24 @@
 	},
 }
 
+/// Token minting parameters
+#[derive(AbiCoder, Default, Debug)]
+pub struct OwnerPieces {
+	/// Minted token owner
+	pub owner: eth::CrossAddress,
+	/// Number of token pieces
+	pub pieces: u128,
+}
+
+/// Token minting parameters
+#[derive(AbiCoder, Default, Debug)]
+pub struct MintTokenData {
+	/// Minted token owner and number of pieces
+	pub owners: Vec<OwnerPieces>,
+	/// Minted token properties
+	pub properties: Vec<eth::Property>,
+}
+
 /// @title A contract that allows to set and delete token properties and change token property permissions.
 #[solidity_interface(name = TokenProperties, events(ERC721TokenEvent), enum(derive(PreDispatch)), enum_attr(weight))]
 impl<T: Config> RefungibleHandle<T> {
@@ -1021,6 +1039,55 @@
 		Ok(true)
 	}
 
+	/// @notice Function to mint a token.
+	/// @param tokenProperties Properties of minted token
+	#[weight(if token_properties.len() == 1 {
+		<SelfWeightOf<T>>::create_multiple_items_ex_multiple_owners(token_properties.iter().next().unwrap().owners.len() as u32)
+	} else {
+		<SelfWeightOf<T>>::create_multiple_items_ex_multiple_items(token_properties.len() as u32)
+	} + <SelfWeightOf<T>>::set_token_properties(token_properties.len() as u32))]
+	fn mint_bulk_cross(
+		&mut self,
+		caller: Caller,
+		token_properties: Vec<MintTokenData>,
+	) -> Result<bool> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let budget = self
+			.recorder
+			.weight_calls_budget(<StructureWeight<T>>::find_parent());
+		let has_multiple_tokens = token_properties.len() > 1;
+
+		let mut create_rft_data = Vec::with_capacity(token_properties.len());
+		for MintTokenData { owners, properties } in token_properties {
+			let has_multiple_owners = owners.len() > 1;
+			if has_multiple_tokens & has_multiple_owners {
+				return Err(
+					"creation of multiple tokens supported only if they have single owner each"
+						.into(),
+				);
+			}
+			let users: BoundedBTreeMap<_, _, _> = owners
+				.into_iter()
+				.map(|data| Ok((data.owner.into_sub_cross_account::<T>()?, data.pieces)))
+				.collect::<Result<BTreeMap<_, _>>>()?
+				.try_into()
+				.map_err(|_| "too many users")?;
+			create_rft_data.push(CreateItemData::<T> {
+				properties: properties
+					.into_iter()
+					.map(|property| property.try_into())
+					.collect::<Result<Vec<_>>>()?
+					.try_into()
+					.map_err(|_| "too many properties")?,
+				users,
+			});
+		}
+
+		<Pallet<T>>::create_multiple_items(self, &caller, create_rft_data, &budget)
+			.map_err(dispatch_to_evm::<T>)?;
+		Ok(true)
+	}
+
 	/// @notice Function to mint multiple tokens with the given tokenUris.
 	/// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive
 	///  numbers and first number should be obtained with `nextTokenId` method
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
@@ -800,7 +800,7 @@
 }
 
 /// @title Unique extensions for ERC721.
-/// @dev the ERC-165 identifier for this interface is 0x95c0f66c
+/// @dev the ERC-165 identifier for this interface is 0x4abaabdb
 contract ERC721UniqueExtensions is Dummy, ERC165 {
 	/// @notice A descriptive name for a collection of NFTs in this contract
 	/// @dev EVM selector for this function is: 0x06fdde03,
@@ -986,6 +986,17 @@
 	// 	return false;
 	// }
 
+	/// @notice Function to mint a token.
+	/// @param tokenProperties Properties of minted token
+	/// @dev EVM selector for this function is: 0xdf7a5db7,
+	///  or in textual repr: mintBulkCross((((address,uint256),uint128)[],(string,bytes)[])[])
+	function mintBulkCross(MintTokenData[] memory tokenProperties) public returns (bool) {
+		require(false, stub_error);
+		tokenProperties;
+		dummy = 0;
+		return false;
+	}
+
 	// /// @notice Function to mint multiple tokens with the given tokenUris.
 	// /// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive
 	// ///  numbers and first number should be obtained with `nextTokenId` method
@@ -1045,6 +1056,22 @@
 	string uri;
 }
 
+/// Token minting parameters
+struct MintTokenData {
+	/// Minted token owner and number of pieces
+	OwnerPieces[] owners;
+	/// Minted token properties
+	Property[] properties;
+}
+
+/// Token minting parameters
+struct OwnerPieces {
+	/// Minted token owner
+	CrossAddress owner;
+	/// Number of token pieces
+	uint128 pieces;
+}
+
 /// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension
 /// @dev See https://eips.ethereum.org/EIPS/eip-721
 /// @dev the ERC-165 identifier for this interface is 0x780e9d63
modifiedpallets/scheduler-v2/src/benchmarking.rsdiffbeforeafterboth
--- a/pallets/scheduler-v2/src/benchmarking.rs
+++ b/pallets/scheduler-v2/src/benchmarking.rs
@@ -136,8 +136,10 @@
 	let bound = EncodedCall::bound() as u32;
 	let mut len = match maybe_lookup_len {
 		Some(len) => {
-			len.min(<T::Preimages as PreimageRecipient<T::Hash>>::MaxSize::get() - 2)
-				.max(bound) - 3
+			len.clamp(
+				bound,
+				<T::Preimages as PreimageRecipient<T::Hash>>::MaxSize::get() - 2,
+			) - 3
 		}
 		None => bound.saturating_sub(4),
 	};
modifiedpallets/unique/src/eth/stubs/CollectionHelpers.rawdiffbeforeafterboth

binary blob — no preview

modifiedpallets/unique/src/eth/stubs/CollectionHelpers.soldiffbeforeafterboth
--- a/pallets/unique/src/eth/stubs/CollectionHelpers.sol
+++ b/pallets/unique/src/eth/stubs/CollectionHelpers.sol
@@ -25,12 +25,12 @@
 }
 
 /// @title Contract, which allows users to operate with collections
-/// @dev the ERC-165 identifier for this interface is 0x4135fff1
+/// @dev the ERC-165 identifier for this interface is 0x94e5af0d
 contract CollectionHelpers is Dummy, ERC165, CollectionHelpersEvents {
 	/// Create a collection
 	/// @return address Address of the newly created collection
-	/// @dev EVM selector for this function is: 0xa765ee5b,
-	///  or in textual repr: createCollection(((address,uint256),string,string,string,uint8,uint8,(string,bytes)[],(string,(uint8,bool)[])[],(address,uint256)[],(bool,bool,address[]),(uint8,uint256)[],uint8))
+	/// @dev EVM selector for this function is: 0x72b5bea7,
+	///  or in textual repr: createCollection((string,string,string,uint8,uint8,(string,bytes)[],(string,(uint8,bool)[])[],(address,uint256)[],(bool,bool,address[]),(uint8,uint256)[],(address,uint256),uint8))
 	function createCollection(CreateCollectionData memory data) public payable returns (address) {
 		require(false, stub_error);
 		data;
@@ -170,8 +170,6 @@
 
 /// Collection properties
 struct CreateCollectionData {
-	/// Collection sponsor
-	CrossAddress pending_sponsor;
 	/// Collection name
 	string name;
 	/// Collection description
@@ -192,11 +190,12 @@
 	CollectionNestingAndPermission nesting_settings;
 	/// Collection limits
 	CollectionLimitValue[] limits;
+	/// Collection sponsor
+	CrossAddress pending_sponsor;
 	/// Extra collection flags
 	CollectionFlags flags;
 }
 
-/// Cross account struct
 type CollectionFlags is uint8;
 
 library CollectionFlagsLib {
@@ -207,13 +206,19 @@
 	/// External collections can't be managed using `unique` api
 	CollectionFlags constant externalField = CollectionFlags.wrap(1);
 
-	/// Reserved bits
+	/// Reserved flags
 	function reservedField(uint8 value) public pure returns (CollectionFlags) {
 		require(value < 1 << 5, "out of bound value");
 		return CollectionFlags.wrap(value << 1);
 	}
 }
 
+/// Cross account struct
+struct CrossAddress {
+	address eth;
+	uint256 sub;
+}
+
 /// [`CollectionLimits`](up_data_structs::CollectionLimits) field representation for EVM.
 struct CollectionLimitValue {
 	CollectionLimitField field;
@@ -250,12 +255,6 @@
 	bool collection_admin;
 	/// If set - only tokens from specified collections can be nested.
 	address[] restricted;
-}
-
-/// Cross account struct
-struct CrossAddress {
-	address eth;
-	uint256 sub;
 }
 
 /// Ethereum representation of Token Property Permissions.
@@ -292,10 +291,10 @@
 
 /// Type of tokens in collection
 enum CollectionMode {
-	/// Fungible
-	Fungible,
 	/// Nonfungible
 	Nonfungible,
+	/// Fungible
+	Fungible,
 	/// Refungible
 	Refungible
 }
modifiedruntime/common/ethereum/sponsoring/refungible.rsdiffbeforeafterboth
--- a/runtime/common/ethereum/sponsoring/refungible.rs
+++ b/runtime/common/ethereum/sponsoring/refungible.rs
@@ -242,6 +242,7 @@
 			BurnFrom { .. }
 			| BurnFromCross { .. }
 			| MintBulk { .. }
+			| MintBulkCross { .. }
 			| MintBulkWithTokenUri { .. } => None,
 
 			MintCross { .. } => withdraw_create_item::<T>(
modifiedruntime/common/tests/mod.rsdiffbeforeafterboth
--- a/runtime/common/tests/mod.rs
+++ b/runtime/common/tests/mod.rs
@@ -33,7 +33,7 @@
 const PARA_ID: u32 = 2037;
 
 fn get_from_seed<TPublic: Public>(seed: &str) -> <TPublic::Pair as Pair>::Public {
-	TPublic::Pair::from_string(&format!("//{}", seed), None)
+	TPublic::Pair::from_string(&format!("//{seed}"), None)
 		.expect("static values are valid; qed")
 		.public()
 }
modifiedtests/src/eth/abi/nonFungible.jsondiffbeforeafterboth
--- a/tests/src/eth/abi/nonFungible.json
+++ b/tests/src/eth/abi/nonFungible.json
@@ -469,6 +469,39 @@
     "inputs": [
       {
         "components": [
+          {
+            "components": [
+              { "internalType": "address", "name": "eth", "type": "address" },
+              { "internalType": "uint256", "name": "sub", "type": "uint256" }
+            ],
+            "internalType": "struct CrossAddress",
+            "name": "owner",
+            "type": "tuple"
+          },
+          {
+            "components": [
+              { "internalType": "string", "name": "key", "type": "string" },
+              { "internalType": "bytes", "name": "value", "type": "bytes" }
+            ],
+            "internalType": "struct Property[]",
+            "name": "properties",
+            "type": "tuple[]"
+          }
+        ],
+        "internalType": "struct MintTokenData[]",
+        "name": "data",
+        "type": "tuple[]"
+      }
+    ],
+    "name": "mintBulkCross",
+    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      {
+        "components": [
           { "internalType": "address", "name": "eth", "type": "address" },
           { "internalType": "uint256", "name": "sub", "type": "uint256" }
         ],
modifiedtests/src/eth/abi/reFungible.jsondiffbeforeafterboth
--- a/tests/src/eth/abi/reFungible.json
+++ b/tests/src/eth/abi/reFungible.json
@@ -451,6 +451,55 @@
     "inputs": [
       {
         "components": [
+          {
+            "components": [
+              {
+                "components": [
+                  {
+                    "internalType": "address",
+                    "name": "eth",
+                    "type": "address"
+                  },
+                  {
+                    "internalType": "uint256",
+                    "name": "sub",
+                    "type": "uint256"
+                  }
+                ],
+                "internalType": "struct CrossAddress",
+                "name": "owner",
+                "type": "tuple"
+              },
+              { "internalType": "uint128", "name": "pieces", "type": "uint128" }
+            ],
+            "internalType": "struct OwnerPieces[]",
+            "name": "owners",
+            "type": "tuple[]"
+          },
+          {
+            "components": [
+              { "internalType": "string", "name": "key", "type": "string" },
+              { "internalType": "bytes", "name": "value", "type": "bytes" }
+            ],
+            "internalType": "struct Property[]",
+            "name": "properties",
+            "type": "tuple[]"
+          }
+        ],
+        "internalType": "struct MintTokenData[]",
+        "name": "tokenProperties",
+        "type": "tuple[]"
+      }
+    ],
+    "name": "mintBulkCross",
+    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      {
+        "components": [
           { "internalType": "address", "name": "eth", "type": "address" },
           { "internalType": "uint256", "name": "sub", "type": "uint256" }
         ],
modifiedtests/src/eth/api/CollectionHelpers.soldiffbeforeafterboth
--- a/tests/src/eth/api/CollectionHelpers.sol
+++ b/tests/src/eth/api/CollectionHelpers.sol
@@ -20,12 +20,12 @@
 }
 
 /// @title Contract, which allows users to operate with collections
-/// @dev the ERC-165 identifier for this interface is 0x4135fff1
+/// @dev the ERC-165 identifier for this interface is 0x94e5af0d
 interface CollectionHelpers is Dummy, ERC165, CollectionHelpersEvents {
 	/// Create a collection
 	/// @return address Address of the newly created collection
-	/// @dev EVM selector for this function is: 0xa765ee5b,
-	///  or in textual repr: createCollection(((address,uint256),string,string,string,uint8,uint8,(string,bytes)[],(string,(uint8,bool)[])[],(address,uint256)[],(bool,bool,address[]),(uint8,uint256)[],uint8))
+	/// @dev EVM selector for this function is: 0x72b5bea7,
+	///  or in textual repr: createCollection((string,string,string,uint8,uint8,(string,bytes)[],(string,(uint8,bool)[])[],(address,uint256)[],(bool,bool,address[]),(uint8,uint256)[],(address,uint256),uint8))
 	function createCollection(CreateCollectionData memory data) external payable returns (address);
 
 	/// Create an NFT collection
@@ -103,8 +103,6 @@
 
 /// Collection properties
 struct CreateCollectionData {
-	/// Collection sponsor
-	CrossAddress pending_sponsor;
 	/// Collection name
 	string name;
 	/// Collection description
@@ -125,11 +123,12 @@
 	CollectionNestingAndPermission nesting_settings;
 	/// Collection limits
 	CollectionLimitValue[] limits;
+	/// Collection sponsor
+	CrossAddress pending_sponsor;
 	/// Extra collection flags
 	CollectionFlags flags;
 }
 
-/// Cross account struct
 type CollectionFlags is uint8;
 
 library CollectionFlagsLib {
@@ -140,13 +139,19 @@
 	/// External collections can't be managed using `unique` api
 	CollectionFlags constant externalField = CollectionFlags.wrap(1);
 
-	/// Reserved bits
+	/// Reserved flags
 	function reservedField(uint8 value) public pure returns (CollectionFlags) {
 		require(value < 1 << 5, "out of bound value");
 		return CollectionFlags.wrap(value << 1);
 	}
 }
 
+/// Cross account struct
+struct CrossAddress {
+	address eth;
+	uint256 sub;
+}
+
 /// [`CollectionLimits`](up_data_structs::CollectionLimits) field representation for EVM.
 struct CollectionLimitValue {
 	CollectionLimitField field;
@@ -183,12 +188,6 @@
 	bool collection_admin;
 	/// If set - only tokens from specified collections can be nested.
 	address[] restricted;
-}
-
-/// Cross account struct
-struct CrossAddress {
-	address eth;
-	uint256 sub;
 }
 
 /// Ethereum representation of Token Property Permissions.
@@ -225,10 +224,10 @@
 
 /// Type of tokens in collection
 enum CollectionMode {
-	/// Fungible
-	Fungible,
 	/// Nonfungible
 	Nonfungible,
+	/// Fungible
+	Fungible,
 	/// Refungible
 	Refungible
 }
modifiedtests/src/eth/api/UniqueNFT.soldiffbeforeafterboth
--- a/tests/src/eth/api/UniqueNFT.sol
+++ b/tests/src/eth/api/UniqueNFT.sol
@@ -551,7 +551,7 @@
 }
 
 /// @title Unique extensions for ERC721.
-/// @dev the ERC-165 identifier for this interface is 0x307b061a
+/// @dev the ERC-165 identifier for this interface is 0x9b397d16
 interface ERC721UniqueExtensions is Dummy, ERC165 {
 	/// @notice A descriptive name for a collection of NFTs in this contract
 	/// @dev EVM selector for this function is: 0x06fdde03,
@@ -674,6 +674,12 @@
 	// ///  or in textual repr: mintBulk(address,uint256[])
 	// function mintBulk(address to, uint256[] memory tokenIds) external returns (bool);
 
+	/// @notice Function to mint a token.
+	/// @param data Array of pairs of token owner and token's properties for minted token
+	/// @dev EVM selector for this function is: 0xab427b0c,
+	///  or in textual repr: mintBulkCross(((address,uint256),(string,bytes)[])[])
+	function mintBulkCross(MintTokenData[] memory data) external returns (bool);
+
 	// /// @notice Function to mint multiple tokens with the given tokenUris.
 	// /// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive
 	// ///  numbers and first number should be obtained with `nextTokenId` method
@@ -705,6 +711,14 @@
 	string uri;
 }
 
+/// Token minting parameters
+struct MintTokenData {
+	/// Minted token owner
+	CrossAddress owner;
+	/// Minted token properties
+	Property[] properties;
+}
+
 /// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension
 /// @dev See https://eips.ethereum.org/EIPS/eip-721
 /// @dev the ERC-165 identifier for this interface is 0x780e9d63
modifiedtests/src/eth/api/UniqueRefungible.soldiffbeforeafterboth
--- a/tests/src/eth/api/UniqueRefungible.sol
+++ b/tests/src/eth/api/UniqueRefungible.sol
@@ -551,7 +551,7 @@
 }
 
 /// @title Unique extensions for ERC721.
-/// @dev the ERC-165 identifier for this interface is 0x95c0f66c
+/// @dev the ERC-165 identifier for this interface is 0x4abaabdb
 interface ERC721UniqueExtensions is Dummy, ERC165 {
 	/// @notice A descriptive name for a collection of NFTs in this contract
 	/// @dev EVM selector for this function is: 0x06fdde03,
@@ -668,6 +668,12 @@
 	// ///  or in textual repr: mintBulk(address,uint256[])
 	// function mintBulk(address to, uint256[] memory tokenIds) external returns (bool);
 
+	/// @notice Function to mint a token.
+	/// @param tokenProperties Properties of minted token
+	/// @dev EVM selector for this function is: 0xdf7a5db7,
+	///  or in textual repr: mintBulkCross((((address,uint256),uint128)[],(string,bytes)[])[])
+	function mintBulkCross(MintTokenData[] memory tokenProperties) external returns (bool);
+
 	// /// @notice Function to mint multiple tokens with the given tokenUris.
 	// /// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive
 	// ///  numbers and first number should be obtained with `nextTokenId` method
@@ -706,6 +712,22 @@
 	string uri;
 }
 
+/// Token minting parameters
+struct MintTokenData {
+	/// Minted token owner and number of pieces
+	OwnerPieces[] owners;
+	/// Minted token properties
+	Property[] properties;
+}
+
+/// Token minting parameters
+struct OwnerPieces {
+	/// Minted token owner
+	CrossAddress owner;
+	/// Number of token pieces
+	uint128 pieces;
+}
+
 /// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension
 /// @dev See https://eips.ethereum.org/EIPS/eip-721
 /// @dev the ERC-165 identifier for this interface is 0x780e9d63
modifiedtests/src/eth/nonFungible.test.tsdiffbeforeafterboth
--- a/tests/src/eth/nonFungible.test.ts
+++ b/tests/src/eth/nonFungible.test.ts
@@ -18,6 +18,7 @@
 import {IKeyringPair} from '@polkadot/types/types';
 import {Contract} from 'web3-eth-contract';
 import {ITokenPropertyPermission} from '../util/playgrounds/types';
+import {CREATE_COLLECTION_DATA_DEFAULTS, CollectionMode, CreateCollectionData, TokenPermissionField} from './util/playgrounds/types';
 
 describe('Check ERC721 token URI for NFT', () => {
   let donor: IKeyringPair;
@@ -197,6 +198,96 @@
     }
   });
 
+  itEth('Can perform mintBulkCross()', async ({helper}) => {
+    const caller = await helper.eth.createAccountWithBalance(donor);
+    const callerCross = helper.ethCrossAccount.fromAddress(caller);
+    const receiver = helper.eth.createAccount();
+    const receiverCross = helper.ethCrossAccount.fromAddress(receiver);
+
+    const permissions = [
+      {code: TokenPermissionField.Mutable, value: true},
+      {code: TokenPermissionField.TokenOwner, value: true},
+      {code: TokenPermissionField.CollectionAdmin, value: true},
+    ];
+    const {collectionAddress} = await helper.eth.createCollection(
+      caller,
+      {
+        ...CREATE_COLLECTION_DATA_DEFAULTS,
+        name: 'A',
+        description: 'B',
+        tokenPrefix: 'C',
+        collectionMode: 'nft',
+        adminList: [callerCross],
+        tokenPropertyPermissions: [
+          {key: 'key_0_0', permissions},
+          {key: 'key_1_0', permissions},
+          {key: 'key_1_1', permissions},
+          {key: 'key_2_0', permissions},
+          {key: 'key_2_1', permissions},
+          {key: 'key_2_2', permissions},
+        ],
+      },
+    ).send();
+
+    const contract = await helper.ethNativeContract.collection(collectionAddress, 'nft', caller);
+    {
+      const nextTokenId = await contract.methods.nextTokenId().call();
+      expect(nextTokenId).to.be.equal('1');
+      const result = await contract.methods.mintBulkCross([
+        {
+          owner: receiverCross,
+          properties: [
+            {key: 'key_0_0', value: Buffer.from('value_0_0')},
+          ],
+        },
+        {
+          owner: receiverCross,
+          properties: [
+            {key: 'key_1_0', value: Buffer.from('value_1_0')},
+            {key: 'key_1_1', value: Buffer.from('value_1_1')},
+          ],
+        },
+        {
+          owner: receiverCross,
+          properties: [
+            {key: 'key_2_0', value: Buffer.from('value_2_0')},
+            {key: 'key_2_1', value: Buffer.from('value_2_1')},
+            {key: 'key_2_2', value: Buffer.from('value_2_2')},
+          ],
+        },
+      ]).send({from: caller});
+      const events = result.events.Transfer.sort((a: any, b: any) => +a.returnValues.tokenId - b.returnValues.tokenId);
+      const bulkSize = 3;
+      for(let i = 0; i < bulkSize; i++) {
+        const event = events[i];
+        expect(event.address).to.equal(collectionAddress);
+        expect(event.returnValues.from).to.equal('0x0000000000000000000000000000000000000000');
+        expect(event.returnValues.to).to.equal(receiver);
+        expect(event.returnValues.tokenId).to.equal(`${+nextTokenId + i}`);
+      }
+
+      const properties = [
+        await contract.methods.properties(+nextTokenId, []).call(),
+        await contract.methods.properties(+nextTokenId + 1, []).call(),
+        await contract.methods.properties(+nextTokenId + 2, []).call(),
+      ];
+      expect(properties).to.be.deep.equal([
+        [
+          ['key_0_0', helper.getWeb3().utils.toHex('value_0_0')],
+        ],
+        [
+          ['key_1_0', helper.getWeb3().utils.toHex('value_1_0')],
+          ['key_1_1', helper.getWeb3().utils.toHex('value_1_1')],
+        ],
+        [
+          ['key_2_0', helper.getWeb3().utils.toHex('value_2_0')],
+          ['key_2_1', helper.getWeb3().utils.toHex('value_2_1')],
+          ['key_2_2', helper.getWeb3().utils.toHex('value_2_2')],
+        ],
+      ]);
+    }
+  });
+
   itEth('Can perform burn()', async ({helper}) => {
     const caller = await helper.eth.createAccountWithBalance(donor);
 
modifiedtests/src/eth/reFungible.test.tsdiffbeforeafterboth
before · tests/src/eth/reFungible.test.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617import {Pallets, requirePalletsOrSkip} from '../util';18import {expect, itEth, usingEthPlaygrounds} from './util';19import {IKeyringPair} from '@polkadot/types/types';20import {ITokenPropertyPermission} from '../util/playgrounds/types';2122describe('Refungible: Plain calls', () => {23  let donor: IKeyringPair;24  let minter: IKeyringPair;25  let bob: IKeyringPair;26  let charlie: IKeyringPair;2728  before(async function() {29    await usingEthPlaygrounds(async (helper, privateKey) => {30      requirePalletsOrSkip(this, helper, [Pallets.ReFungible]);3132      donor = await privateKey({url: import.meta.url});33      [minter, bob, charlie] = await helper.arrange.createAccounts([100n, 100n, 100n], donor);34    });35  });3637  [38    'substrate' as const,39    'ethereum' as const,40  ].map(testCase => {41    itEth(`Can perform mintCross() for ${testCase} address`, async ({helper}) => {42      const collectionAdmin = await helper.eth.createAccountWithBalance(donor);4344      const receiverEth = helper.eth.createAccount();45      const receiverCrossEth = helper.ethCrossAccount.fromAddress(receiverEth);46      const receiverSub = bob;47      const receiverCrossSub = helper.ethCrossAccount.fromKeyringPair(receiverSub);4849      const properties = Array(5).fill(0).map((_, i) => ({key: `key_${i}`, value: Buffer.from(`value_${i}`)}));50      const permissions: ITokenPropertyPermission[] = properties.map(p => ({key: p.key, permission: {51        tokenOwner: false,52        collectionAdmin: true,53        mutable: false}}));545556      const collection = await helper.rft.mintCollection(minter, {57        tokenPrefix: 'ethp',58        tokenPropertyPermissions: permissions,59      });60      await collection.addAdmin(minter, {Ethereum: collectionAdmin});6162      const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);63      const contract = await helper.ethNativeContract.collection(collectionAddress, 'rft', collectionAdmin, true);64      let expectedTokenId = await contract.methods.nextTokenId().call();65      let result = await contract.methods.mintCross(testCase === 'ethereum' ? receiverCrossEth : receiverCrossSub, []).send();66      let tokenId = result.events.Transfer.returnValues.tokenId;67      expect(tokenId).to.be.equal(expectedTokenId);6869      let event = result.events.Transfer;70      expect(event.address).to.be.equal(collectionAddress);71      expect(event.returnValues.from).to.be.equal('0x0000000000000000000000000000000000000000');72      expect(event.returnValues.to).to.be.equal(testCase === 'ethereum' ? receiverEth : helper.address.substrateToEth(bob.address));73      expect(await contract.methods.properties(tokenId, []).call()).to.be.like([]);7475      expectedTokenId = await contract.methods.nextTokenId().call();76      result = await contract.methods.mintCross(testCase === 'ethereum' ? receiverCrossEth : receiverCrossSub, properties).send();77      event = result.events.Transfer;78      expect(event.address).to.be.equal(collectionAddress);79      expect(event.returnValues.from).to.be.equal('0x0000000000000000000000000000000000000000');80      expect(event.returnValues.to).to.be.equal(testCase === 'ethereum' ? receiverEth : helper.address.substrateToEth(bob.address));81      expect(await contract.methods.properties(tokenId, []).call()).to.be.like([]);8283      tokenId = result.events.Transfer.returnValues.tokenId;8485      expect(tokenId).to.be.equal(expectedTokenId);8687      expect(await contract.methods.properties(tokenId, []).call()).to.be.like(properties88        .map(p => helper.ethProperty.property(p.key, p.value.toString())));8990      expect(await helper.nft.getTokenOwner(collection.collectionId, tokenId))91        .to.deep.eq(testCase === 'ethereum' ? {Ethereum: receiverEth.toLowerCase()} : {Substrate: receiverSub.address});92    });93  });9495  itEth.skip('Can perform mintBulk()', async ({helper}) => {96    const owner = await helper.eth.createAccountWithBalance(donor);97    const receiver = helper.eth.createAccount();98    const {collectionAddress} = await helper.eth.createERC721MetadataCompatibleRFTCollection(owner, 'MintBulky', '6', '6', '');99    const contract = await helper.ethNativeContract.collection(collectionAddress, 'rft', owner);100101    {102      const nextTokenId = await contract.methods.nextTokenId().call();103      expect(nextTokenId).to.be.equal('1');104      const result = await contract.methods.mintBulkWithTokenURI(105        receiver,106        [107          [nextTokenId, 'Test URI 0'],108          [+nextTokenId + 1, 'Test URI 1'],109          [+nextTokenId + 2, 'Test URI 2'],110        ],111      ).send();112113      const events = result.events.Transfer;114      for(let i = 0; i < 2; i++) {115        const event = events[i];116        expect(event.address).to.equal(collectionAddress);117        expect(event.returnValues.from).to.equal('0x0000000000000000000000000000000000000000');118        expect(event.returnValues.to).to.equal(receiver);119        expect(event.returnValues.tokenId).to.equal(String(+nextTokenId + i));120      }121122      expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI 0');123      expect(await contract.methods.tokenURI(+nextTokenId + 1).call()).to.be.equal('Test URI 1');124      expect(await contract.methods.tokenURI(+nextTokenId + 2).call()).to.be.equal('Test URI 2');125    }126  });127128  itEth('Can perform setApprovalForAll()', async ({helper}) => {129    const owner = await helper.eth.createAccountWithBalance(donor);130    const operator = helper.eth.createAccount();131132    const collection = await helper.rft.mintCollection(minter, {});133134    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);135    const contract = await helper.ethNativeContract.collection(collectionAddress, 'rft', owner);136137    const approvedBefore = await contract.methods.isApprovedForAll(owner, operator).call();138    expect(approvedBefore).to.be.equal(false);139140    {141      const result = await contract.methods.setApprovalForAll(operator, true).send({from: owner});142143      expect(result.events.ApprovalForAll).to.be.like({144        address: collectionAddress,145        event: 'ApprovalForAll',146        returnValues: {147          owner,148          operator,149          approved: true,150        },151      });152153      const approvedAfter = await contract.methods.isApprovedForAll(owner, operator).call();154      expect(approvedAfter).to.be.equal(true);155    }156157    {158      const result = await contract.methods.setApprovalForAll(operator, false).send({from: owner});159160      expect(result.events.ApprovalForAll).to.be.like({161        address: collectionAddress,162        event: 'ApprovalForAll',163        returnValues: {164          owner,165          operator,166          approved: false,167        },168      });169170      const approvedAfter = await contract.methods.isApprovedForAll(owner, operator).call();171      expect(approvedAfter).to.be.equal(false);172    }173  });174175  itEth('Can perform burn with ApprovalForAll', async ({helper}) => {176    const collection = await helper.rft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});177178    const owner = await helper.eth.createAccountWithBalance(donor);179    const operator = await helper.eth.createAccountWithBalance(donor);180181    const token = await collection.mintToken(minter, 100n, {Ethereum: owner});182183    const address = helper.ethAddress.fromCollectionId(collection.collectionId);184    const contract = await helper.ethNativeContract.collection(address, 'rft');185186    {187      await contract.methods.setApprovalForAll(operator, true).send({from: owner});188      const ownerCross = helper.ethCrossAccount.fromAddress(owner);189      const result = await contract.methods.burnFromCross(ownerCross, token.tokenId).send({from: operator});190      const events = result.events.Transfer;191192      expect(events).to.be.like({193        address,194        event: 'Transfer',195        returnValues: {196          from: owner,197          to: '0x0000000000000000000000000000000000000000',198          tokenId: token.tokenId.toString(),199        },200      });201    }202  });203204  itEth('Can perform burn with approve and approvalForAll', async ({helper}) => {205    const collection = await helper.rft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});206207    const owner = await helper.eth.createAccountWithBalance(donor);208    const operator = await helper.eth.createAccountWithBalance(donor);209210    const token = await collection.mintToken(minter, 100n, {Ethereum: owner});211212    const address = helper.ethAddress.fromCollectionId(collection.collectionId);213    const contract = await helper.ethNativeContract.collection(address, 'rft');214215    const rftToken = await helper.ethNativeContract.rftTokenById(token.collectionId, token.tokenId, owner, true);216217    {218      await rftToken.methods.approve(operator, 15n).send({from: owner});219      await contract.methods.setApprovalForAll(operator, true).send({from: owner});220      await rftToken.methods.burnFrom(owner, 10n).send({from: operator});221    }222    {223      const allowance = await rftToken.methods.allowance(owner, operator).call();224      expect(+allowance).to.be.equal(5);225    }226    {227      const ownerCross = helper.ethCrossAccount.fromAddress(owner);228      const operatorCross = helper.ethCrossAccount.fromAddress(operator);229      const allowance = await rftToken.methods.allowanceCross(ownerCross, operatorCross).call();230      expect(+allowance).to.equal(5);231    }232  });233234  itEth('Can perform transfer with ApprovalForAll', async ({helper}) => {235    const collection = await helper.rft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});236237    const owner = await helper.eth.createAccountWithBalance(donor);238    const operator = await helper.eth.createAccountWithBalance(donor);239    const receiver = charlie;240241    const token = await collection.mintToken(minter, 100n, {Ethereum: owner});242243    const address = helper.ethAddress.fromCollectionId(collection.collectionId);244    const contract = await helper.ethNativeContract.collection(address, 'rft');245246    {247      await contract.methods.setApprovalForAll(operator, true).send({from: owner});248      const ownerCross = helper.ethCrossAccount.fromAddress(owner);249      const recieverCross = helper.ethCrossAccount.fromKeyringPair(receiver);250      const result = await contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: operator});251      const event = result.events.Transfer;252      expect(event).to.be.like({253        address: helper.ethAddress.fromCollectionId(collection.collectionId),254        event: 'Transfer',255        returnValues: {256          from: owner,257          to: helper.address.substrateToEth(receiver.address),258          tokenId: token.tokenId.toString(),259        },260      });261    }262263    expect(await token.getTop10Owners()).to.be.like([{Substrate: receiver.address}]);264  });265266  itEth('Can perform burn()', async ({helper}) => {267    const caller = await helper.eth.createAccountWithBalance(donor);268    const {collectionAddress} = await helper.eth.createRFTCollection(caller, 'Burny', '6', '6');269    const contract = await helper.ethNativeContract.collection(collectionAddress, 'rft', caller);270271    const result = await contract.methods.mint(caller).send();272    const tokenId = result.events.Transfer.returnValues.tokenId;273    {274      const result = await contract.methods.burn(tokenId).send();275      const event = result.events.Transfer;276      expect(event.address).to.equal(collectionAddress);277      expect(event.returnValues.from).to.equal(caller);278      expect(event.returnValues.to).to.equal('0x0000000000000000000000000000000000000000');279      expect(event.returnValues.tokenId).to.equal(tokenId.toString());280    }281  });282283  itEth('Can perform transferFrom()', async ({helper}) => {284    const caller = await helper.eth.createAccountWithBalance(donor);285    const receiver = helper.eth.createAccount();286    const {collectionId, collectionAddress} = await helper.eth.createRFTCollection(caller, 'TransferFromy', '6', '6');287    const contract = await helper.ethNativeContract.collection(collectionAddress, 'rft', caller);288289    const result = await contract.methods.mint(caller).send();290    const tokenId = result.events.Transfer.returnValues.tokenId;291292    const tokenAddress = helper.ethAddress.fromTokenId(collectionId, tokenId);293294    const tokenContract = await helper.ethNativeContract.rftToken(tokenAddress, caller);295    await tokenContract.methods.repartition(15).send();296297    {298      const tokenEvents: any = [];299      tokenContract.events.allEvents((_: any, event: any) => {300        tokenEvents.push(event);301      });302      const result = await contract.methods.transferFrom(caller, receiver, tokenId).send();303      if(tokenEvents.length == 0) await helper.wait.newBlocks(1);304305      let event = result.events.Transfer;306      expect(event.address).to.equal(collectionAddress);307      expect(event.returnValues.from).to.equal(caller);308      expect(event.returnValues.to).to.equal(receiver);309      expect(event.returnValues.tokenId).to.equal(tokenId.toString());310311      event = tokenEvents[0];312      expect(event.address).to.equal(tokenAddress);313      expect(event.returnValues.from).to.equal(caller);314      expect(event.returnValues.to).to.equal(receiver);315      expect(event.returnValues.value).to.equal('15');316    }317318    {319      const balance = await contract.methods.balanceOf(receiver).call();320      expect(+balance).to.equal(1);321    }322323    {324      const balance = await contract.methods.balanceOf(caller).call();325      expect(+balance).to.equal(0);326    }327  });328329  // Soft-deprecated330  itEth('Can perform burnFrom()', async ({helper}) => {331    const collection = await helper.rft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});332333    const owner = await helper.eth.createAccountWithBalance(donor);334    const spender = await helper.eth.createAccountWithBalance(donor);335336    const token = await collection.mintToken(minter, 100n, {Ethereum: owner});337338    const address = helper.ethAddress.fromCollectionId(collection.collectionId);339    const contract = await helper.ethNativeContract.collection(address, 'rft', spender, true);340341    const tokenAddress = helper.ethAddress.fromTokenId(collection.collectionId, token.tokenId);342    const tokenContract = await helper.ethNativeContract.rftToken(tokenAddress, owner);343    await tokenContract.methods.repartition(15).send();344    await tokenContract.methods.approve(spender, 15).send();345346    {347      const result = await contract.methods.burnFrom(owner, token.tokenId).send();348      const event = result.events.Transfer;349      expect(event).to.be.like({350        address: helper.ethAddress.fromCollectionId(collection.collectionId),351        event: 'Transfer',352        returnValues: {353          from: owner,354          to: '0x0000000000000000000000000000000000000000',355          tokenId: token.tokenId.toString(),356        },357      });358    }359360    expect(await collection.getTokenBalance(token.tokenId, {Ethereum: owner})).to.be.eq(0n);361  });362363  itEth('Can perform burnFromCross()', async ({helper}) => {364    const collection = await helper.rft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});365366    const owner = bob;367    const spender = await helper.eth.createAccountWithBalance(donor);368369    const token = await collection.mintToken(minter, 100n, {Substrate: owner.address});370371    const address = helper.ethAddress.fromCollectionId(collection.collectionId);372    const contract = await helper.ethNativeContract.collection(address, 'rft');373374    await token.repartition(owner, 15n);375    await token.approve(owner, {Ethereum: spender}, 15n);376377    {378      const ownerCross = helper.ethCrossAccount.fromKeyringPair(owner);379      const result = await contract.methods.burnFromCross(ownerCross, token.tokenId).send({from: spender});380      const event = result.events.Transfer;381      expect(event).to.be.like({382        address: helper.ethAddress.fromCollectionId(collection.collectionId),383        event: 'Transfer',384        returnValues: {385          from: helper.address.substrateToEth(owner.address),386          to: '0x0000000000000000000000000000000000000000',387          tokenId: token.tokenId.toString(),388        },389      });390    }391392    expect(await collection.getTokenBalance(token.tokenId, {Substrate: owner.address})).to.be.eq(0n);393  });394395  itEth('Can perform transferFromCross()', async ({helper}) => {396    const collection = await helper.rft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});397398    const owner = bob;399    const spender = await helper.eth.createAccountWithBalance(donor);400    const receiver = charlie;401402    const token = await collection.mintToken(minter, 100n, {Substrate: owner.address});403404    const address = helper.ethAddress.fromCollectionId(collection.collectionId);405    const contract = await helper.ethNativeContract.collection(address, 'rft');406407    await token.repartition(owner, 15n);408    await token.approve(owner, {Ethereum: spender}, 15n);409410    {411      const ownerCross = helper.ethCrossAccount.fromKeyringPair(owner);412      const recieverCross = helper.ethCrossAccount.fromKeyringPair(receiver);413      const result = await contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: spender});414      const event = result.events.Transfer;415      expect(event).to.be.like({416        address: helper.ethAddress.fromCollectionId(collection.collectionId),417        event: 'Transfer',418        returnValues: {419          from: helper.address.substrateToEth(owner.address),420          to: helper.address.substrateToEth(receiver.address),421          tokenId: token.tokenId.toString(),422        },423      });424    }425426    expect(await token.getTop10Owners()).to.be.like([{Substrate: receiver.address}]);427  });428429  itEth('Can perform transfer()', async ({helper}) => {430    const caller = await helper.eth.createAccountWithBalance(donor);431    const receiver = helper.eth.createAccount();432    const {collectionAddress} = await helper.eth.createRFTCollection(caller, 'Transferry', '6', '6');433    const contract = await helper.ethNativeContract.collection(collectionAddress, 'rft', caller);434435    const result = await contract.methods.mint(caller).send();436    const tokenId = result.events.Transfer.returnValues.tokenId;437438    {439      const result = await contract.methods.transfer(receiver, tokenId).send();440441      const event = result.events.Transfer;442      expect(event.address).to.equal(collectionAddress);443      expect(event.returnValues.from).to.equal(caller);444      expect(event.returnValues.to).to.equal(receiver);445      expect(event.returnValues.tokenId).to.equal(tokenId.toString());446    }447448    {449      const balance = await contract.methods.balanceOf(caller).call();450      expect(+balance).to.equal(0);451    }452453    {454      const balance = await contract.methods.balanceOf(receiver).call();455      expect(+balance).to.equal(1);456    }457  });458459  itEth('Can perform transferCross()', async ({helper}) => {460    const sender = await helper.eth.createAccountWithBalance(donor);461    const receiverEth = await helper.eth.createAccountWithBalance(donor);462    const receiverCrossEth = helper.ethCrossAccount.fromAddress(receiverEth);463    const receiverCrossSub = helper.ethCrossAccount.fromKeyringPair(minter);464465    const collection = await helper.rft.mintCollection(minter, {});466    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);467    const collectionEvm = await helper.ethNativeContract.collection(collectionAddress, 'rft', sender);468469    const token = await collection.mintToken(minter, 50n, {Ethereum: sender});470471    {472      // Can transferCross to ethereum address:473      const result = await collectionEvm.methods.transferCross(receiverCrossEth, token.tokenId).send({from: sender});474      // Check events:475      const event = result.events.Transfer;476      expect(event.address).to.equal(collectionAddress);477      expect(event.returnValues.from).to.equal(sender);478      expect(event.returnValues.to).to.equal(receiverEth);479      expect(event.returnValues.tokenId).to.equal(token.tokenId.toString());480      // Sender's balance decreased:481      const senderBalance = await collectionEvm.methods.balanceOf(sender).call();482      expect(+senderBalance).to.equal(0);483      expect(await token.getBalance({Ethereum: sender})).to.eq(0n);484      // Receiver's balance increased:485      const receiverBalance = await collectionEvm.methods.balanceOf(receiverEth).call();486      expect(+receiverBalance).to.equal(1);487      expect(await token.getBalance({Ethereum: receiverEth})).to.eq(50n);488    }489490    {491      // Can transferCross to substrate address:492      const substrateResult = await collectionEvm.methods.transferCross(receiverCrossSub, token.tokenId).send({from: receiverEth});493      // Check events:494      const event = substrateResult.events.Transfer;495      expect(event.address).to.be.equal(collectionAddress);496      expect(event.returnValues.from).to.be.equal(receiverEth);497      expect(event.returnValues.to).to.be.equal(helper.address.substrateToEth(minter.address));498      expect(event.returnValues.tokenId).to.be.equal(`${token.tokenId}`);499      // Sender's balance decreased:500      const senderBalance = await collectionEvm.methods.balanceOf(receiverEth).call();501      expect(+senderBalance).to.equal(0);502      expect(await token.getBalance({Ethereum: receiverEth})).to.eq(0n);503      // Receiver's balance increased:504      const receiverBalance = await helper.nft.getTokensByAddress(collection.collectionId, {Substrate: minter.address});505      expect(receiverBalance).to.contain(token.tokenId);506      expect(await token.getBalance({Substrate: minter.address})).to.eq(50n);507    }508  });509510  ['transfer', 'transferCross'].map(testCase => itEth(`Cannot ${testCase} non-owned token`, async ({helper}) => {511    const sender = await helper.eth.createAccountWithBalance(donor);512    const tokenOwner = await helper.eth.createAccountWithBalance(donor);513    const receiverSub = minter;514    const receiverCrossSub = helper.ethCrossAccount.fromKeyringPair(minter);515516    const collection = await helper.rft.mintCollection(minter, {});517    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);518    const collectionEvm = await helper.ethNativeContract.collection(collectionAddress, 'rft', sender);519520    await collection.mintToken(minter, 50n, {Ethereum: sender});521    const nonSendersToken = await collection.mintToken(minter, 50n, {Ethereum: tokenOwner});522523    // Cannot transferCross someone else's token:524    const receiver = testCase === 'transfer' ? helper.address.substrateToEth(receiverSub.address) : receiverCrossSub;525    await expect(collectionEvm.methods[testCase](receiver, nonSendersToken.tokenId).send({from: sender})).to.be.rejected;526    // Cannot transfer token if it does not exist:527    await expect(collectionEvm.methods[testCase](receiver, 999999).send({from: sender})).to.be.rejected;528  }));529530  itEth('transfer event on transfer from partial ownership to full ownership', async ({helper}) => {531    const caller = await helper.eth.createAccountWithBalance(donor);532    const receiver = helper.eth.createAccount();533    const {collectionId, collectionAddress} = await helper.eth.createRFTCollection(caller, 'Transferry-Partial-to-Full', '6', '6');534    const contract = await helper.ethNativeContract.collection(collectionAddress, 'rft', caller);535536    const result = await contract.methods.mint(caller).send();537    const tokenId = result.events.Transfer.returnValues.tokenId;538539    const tokenContract = await helper.ethNativeContract.rftTokenById(collectionId, tokenId, caller);540541    await tokenContract.methods.repartition(2).send();542    await tokenContract.methods.transfer(receiver, 1).send();543544    const events: any = [];545    contract.events.allEvents((_: any, event: any) => {546      events.push(event);547    });548549    await tokenContract.methods.transfer(receiver, 1).send();550    if(events.length == 0) await helper.wait.newBlocks(1);551    const event = events[0];552553    expect(event.address).to.equal(collectionAddress);554    expect(event.returnValues.from).to.equal('0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF');555    expect(event.returnValues.to).to.equal(receiver);556    expect(event.returnValues.tokenId).to.equal(tokenId.toString());557  });558559  itEth('transfer event on transfer from full ownership to partial ownership', async ({helper}) => {560    const caller = await helper.eth.createAccountWithBalance(donor);561    const receiver = helper.eth.createAccount();562    const {collectionId, collectionAddress} = await helper.eth.createRFTCollection(caller, 'Transferry-Full-to-Partial', '6', '6');563    const contract = await helper.ethNativeContract.collection(collectionAddress, 'rft', caller);564565    const result = await contract.methods.mint(caller).send();566    const tokenId = result.events.Transfer.returnValues.tokenId;567568    const tokenContract = await helper.ethNativeContract.rftTokenById(collectionId, tokenId, caller);569570    await tokenContract.methods.repartition(2).send();571572    const events: any = [];573    contract.events.allEvents((_: any, event: any) => {574      events.push(event);575    });576577    await tokenContract.methods.transfer(receiver, 1).send();578    if(events.length == 0) await helper.wait.newBlocks(1);579    const event = events[0];580581    expect(event.address).to.equal(collectionAddress);582    expect(event.returnValues.from).to.equal(caller);583    expect(event.returnValues.to).to.equal('0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF');584    expect(event.returnValues.tokenId).to.equal(tokenId.toString());585  });586587  itEth('Check balanceOfCross()', async ({helper}) => {588    const collection = await helper.rft.mintCollection(minter, {});589    const owner = await helper.ethCrossAccount.createAccountWithBalance(donor);590    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);591    const collectionEvm = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner.eth);592593    expect(await collectionEvm.methods.balanceOfCross(owner).call({from: owner.eth})).to.be.eq('0');594595    for(let i = 1n; i < 10n; i++) {596      await collection.mintToken(minter, 100n, {Ethereum: owner.eth});597      expect(await collectionEvm.methods.balanceOfCross(owner).call({from: owner.eth})).to.be.eq(i.toString());598    }599  });600601  itEth('Check ownerOfCross()', async ({helper}) => {602    const collection = await helper.rft.mintCollection(minter, {});603    let owner = await helper.ethCrossAccount.createAccountWithBalance(donor);604    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);605    const collectionEvm = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner.eth);606    const {tokenId} = await collection.mintToken(minter, 100n,{Ethereum: owner.eth});607608    for(let i = 1n; i < 10n; i++) {609      const ownerCross = await collectionEvm.methods.ownerOfCross(tokenId).call({from: owner.eth});610      expect(ownerCross.eth).to.be.eq(owner.eth);611      expect(ownerCross.sub).to.be.eq(owner.sub);612613      const newOwner = await helper.ethCrossAccount.createAccountWithBalance(donor);614      await collectionEvm.methods.transferCross(newOwner, tokenId).send({from: owner.eth});615      owner = newOwner;616    }617618    const tokenAddress = helper.ethAddress.fromTokenId(collection.collectionId, tokenId);619    const tokenContract = await helper.ethNativeContract.rftToken(tokenAddress, owner.eth, true);620    const newOwner = await helper.ethCrossAccount.createAccountWithBalance(donor);621    await tokenContract.methods.transferCross(newOwner, 50).send({from: owner.eth});622    const ownerCross = await collectionEvm.methods.ownerOfCross(tokenId).call({from: owner.eth});623    expect(ownerCross.eth.toUpperCase()).to.be.eq('0XFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF');624    expect(ownerCross.sub).to.be.eq('0');625  });626});627628describe('RFT: Fees', () => {629  let donor: IKeyringPair;630631  before(async function() {632    await usingEthPlaygrounds(async (helper, privateKey) => {633      requirePalletsOrSkip(this, helper, [Pallets.ReFungible]);634635      donor = await privateKey({url: import.meta.url});636    });637  });638639  itEth('transferFrom() call fee is less than 0.2UNQ', async ({helper}) => {640    const caller = await helper.eth.createAccountWithBalance(donor);641    const receiver = helper.eth.createAccount();642    const {collectionAddress} = await helper.eth.createRFTCollection(caller, 'Feeful-Transfer-From', '6', '6');643    const contract = await helper.ethNativeContract.collection(collectionAddress, 'rft', caller);644645    const result = await contract.methods.mint(caller).send();646    const tokenId = result.events.Transfer.returnValues.tokenId;647648    const cost = await helper.eth.recordCallFee(caller, () => contract.methods.transferFrom(caller, receiver, tokenId).send());649    expect(cost < BigInt(0.2 * Number(helper.balance.getOneTokenNominal())));650    expect(cost > 0n);651  });652653  itEth('transfer() call fee is less than 0.2UNQ', async ({helper}) => {654    const caller = await helper.eth.createAccountWithBalance(donor);655    const receiver = helper.eth.createAccount();656    const {collectionAddress} = await helper.eth.createRFTCollection(caller, 'Feeful-Transfer', '6', '6');657    const contract = await helper.ethNativeContract.collection(collectionAddress, 'rft', caller);658659    const result = await contract.methods.mint(caller).send();660    const tokenId = result.events.Transfer.returnValues.tokenId;661662    const cost = await helper.eth.recordCallFee(caller, () => contract.methods.transfer(receiver, tokenId).send());663    expect(cost < BigInt(0.2 * Number(helper.balance.getOneTokenNominal())));664    expect(cost > 0n);665  });666});667668describe('Common metadata', () => {669  let donor: IKeyringPair;670  let alice: IKeyringPair;671672  before(async function() {673    await usingEthPlaygrounds(async (helper, privateKey) => {674      requirePalletsOrSkip(this, helper, [Pallets.ReFungible]);675676      donor = await privateKey({url: import.meta.url});677      [alice] = await helper.arrange.createAccounts([1000n], donor);678    });679  });680681  itEth('Returns collection name', async ({helper}) => {682    const caller = helper.eth.createAccount();683    const tokenPropertyPermissions = [{684      key: 'URI',685      permission: {686        mutable: true,687        collectionAdmin: true,688        tokenOwner: false,689      },690    }];691    const collection = await helper.rft.mintCollection(692      alice,693      {694        name: 'Leviathan',695        tokenPrefix: '11',696        properties: [{key: 'ERC721Metadata', value: '1'}],697        tokenPropertyPermissions,698      },699    );700701    const contract = helper.ethNativeContract.collectionById(collection.collectionId, 'rft', caller);702    const name = await contract.methods.name().call();703    expect(name).to.equal('Leviathan');704  });705706  itEth('Returns symbol name', async ({helper}) => {707    const caller = await helper.eth.createAccountWithBalance(donor);708    const tokenPropertyPermissions = [{709      key: 'URI',710      permission: {711        mutable: true,712        collectionAdmin: true,713        tokenOwner: false,714      },715    }];716    const {collectionId} = await helper.rft.mintCollection(717      alice,718      {719        name: 'Leviathan',720        tokenPrefix: '12',721        properties: [{key: 'ERC721Metadata', value: '1'}],722        tokenPropertyPermissions,723      },724    );725726    const contract = await helper.ethNativeContract.collectionById(collectionId, 'rft', caller);727    const symbol = await contract.methods.symbol().call();728    expect(symbol).to.equal('12');729  });730});731732describe('Negative tests', () => {733  let donor: IKeyringPair;734  let minter: IKeyringPair;735  let alice: IKeyringPair;736737  before(async function() {738    await usingEthPlaygrounds(async (helper, privateKey) => {739      requirePalletsOrSkip(this, helper, [Pallets.ReFungible]);740741      donor = await privateKey({url: import.meta.url});742      [minter, alice] = await helper.arrange.createAccounts([100n, 100n], donor);743    });744  });745746  itEth('[negative] Cant perform burn without approval', async ({helper}) => {747    const collection = await helper.rft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});748749    const owner = await helper.eth.createAccountWithBalance(donor);750    const spender = await helper.eth.createAccountWithBalance(donor);751752    const token = await collection.mintToken(minter, 100n, {Ethereum: owner});753754    const address = helper.ethAddress.fromCollectionId(collection.collectionId);755    const contract = await helper.ethNativeContract.collection(address, 'rft');756757    const ownerCross = helper.ethCrossAccount.fromAddress(owner);758759    await expect(contract.methods.burnFromCross(ownerCross, token.tokenId).send({from: spender})).to.be.rejected;760761    await contract.methods.setApprovalForAll(spender, true).send({from: owner});762    await contract.methods.setApprovalForAll(spender, false).send({from: owner});763764    await expect(contract.methods.burnFromCross(ownerCross, token.tokenId).send({from: spender})).to.be.rejected;765  });766767  itEth('[negative] Cant perform transfer without approval', async ({helper}) => {768    const collection = await helper.rft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});769    const owner = await helper.eth.createAccountWithBalance(donor);770    const receiver = alice;771772    const spender = await helper.eth.createAccountWithBalance(donor);773774    const token = await collection.mintToken(minter, 100n, {Ethereum: owner});775776    const address = helper.ethAddress.fromCollectionId(collection.collectionId);777    const contract = await helper.ethNativeContract.collection(address, 'rft');778779    const ownerCross = helper.ethCrossAccount.fromAddress(owner);780    const recieverCross = helper.ethCrossAccount.fromKeyringPair(receiver);781782    await expect(contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: spender})).to.be.rejected;783784    await contract.methods.setApprovalForAll(spender, true).send({from: owner});785    await contract.methods.setApprovalForAll(spender, false).send({from: owner});786787    await expect(contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: spender})).to.be.rejected;788  });789});
after · tests/src/eth/reFungible.test.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617import {Pallets, requirePalletsOrSkip} from '../util';18import {expect, itEth, usingEthPlaygrounds} from './util';19import {IKeyringPair} from '@polkadot/types/types';20import {ITokenPropertyPermission} from '../util/playgrounds/types';21import {CREATE_COLLECTION_DATA_DEFAULTS, TokenPermissionField} from './util/playgrounds/types';2223describe('Refungible: Plain calls', () => {24  let donor: IKeyringPair;25  let minter: IKeyringPair;26  let bob: IKeyringPair;27  let charlie: IKeyringPair;2829  before(async function() {30    await usingEthPlaygrounds(async (helper, privateKey) => {31      requirePalletsOrSkip(this, helper, [Pallets.ReFungible]);3233      donor = await privateKey({url: import.meta.url});34      [minter, bob, charlie] = await helper.arrange.createAccounts([100n, 100n, 100n], donor);35    });36  });3738  [39    'substrate' as const,40    'ethereum' as const,41  ].map(testCase => {42    itEth(`Can perform mintCross() for ${testCase} address`, async ({helper}) => {43      const collectionAdmin = await helper.eth.createAccountWithBalance(donor);4445      const receiverEth = helper.eth.createAccount();46      const receiverCrossEth = helper.ethCrossAccount.fromAddress(receiverEth);47      const receiverSub = bob;48      const receiverCrossSub = helper.ethCrossAccount.fromKeyringPair(receiverSub);4950      const properties = Array(5).fill(0).map((_, i) => ({key: `key_${i}`, value: Buffer.from(`value_${i}`)}));51      const permissions: ITokenPropertyPermission[] = properties.map(p => ({key: p.key, permission: {52        tokenOwner: false,53        collectionAdmin: true,54        mutable: false}}));555657      const collection = await helper.rft.mintCollection(minter, {58        tokenPrefix: 'ethp',59        tokenPropertyPermissions: permissions,60      });61      await collection.addAdmin(minter, {Ethereum: collectionAdmin});6263      const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);64      const contract = await helper.ethNativeContract.collection(collectionAddress, 'rft', collectionAdmin, true);65      let expectedTokenId = await contract.methods.nextTokenId().call();66      let result = await contract.methods.mintCross(testCase === 'ethereum' ? receiverCrossEth : receiverCrossSub, []).send();67      let tokenId = result.events.Transfer.returnValues.tokenId;68      expect(tokenId).to.be.equal(expectedTokenId);6970      let event = result.events.Transfer;71      expect(event.address).to.be.equal(collectionAddress);72      expect(event.returnValues.from).to.be.equal('0x0000000000000000000000000000000000000000');73      expect(event.returnValues.to).to.be.equal(testCase === 'ethereum' ? receiverEth : helper.address.substrateToEth(bob.address));74      expect(await contract.methods.properties(tokenId, []).call()).to.be.like([]);7576      expectedTokenId = await contract.methods.nextTokenId().call();77      result = await contract.methods.mintCross(testCase === 'ethereum' ? receiverCrossEth : receiverCrossSub, properties).send();78      event = result.events.Transfer;79      expect(event.address).to.be.equal(collectionAddress);80      expect(event.returnValues.from).to.be.equal('0x0000000000000000000000000000000000000000');81      expect(event.returnValues.to).to.be.equal(testCase === 'ethereum' ? receiverEth : helper.address.substrateToEth(bob.address));82      expect(await contract.methods.properties(tokenId, []).call()).to.be.like([]);8384      tokenId = result.events.Transfer.returnValues.tokenId;8586      expect(tokenId).to.be.equal(expectedTokenId);8788      expect(await contract.methods.properties(tokenId, []).call()).to.be.like(properties89        .map(p => helper.ethProperty.property(p.key, p.value.toString())));9091      expect(await helper.nft.getTokenOwner(collection.collectionId, tokenId))92        .to.deep.eq(testCase === 'ethereum' ? {Ethereum: receiverEth.toLowerCase()} : {Substrate: receiverSub.address});93    });94  });9596  itEth.skip('Can perform mintBulk()', async ({helper}) => {97    const owner = await helper.eth.createAccountWithBalance(donor);98    const receiver = helper.eth.createAccount();99    const {collectionAddress} = await helper.eth.createERC721MetadataCompatibleRFTCollection(owner, 'MintBulky', '6', '6', '');100    const contract = await helper.ethNativeContract.collection(collectionAddress, 'rft', owner);101102    {103      const nextTokenId = await contract.methods.nextTokenId().call();104      expect(nextTokenId).to.be.equal('1');105      const result = await contract.methods.mintBulkWithTokenURI(106        receiver,107        [108          [nextTokenId, 'Test URI 0'],109          [+nextTokenId + 1, 'Test URI 1'],110          [+nextTokenId + 2, 'Test URI 2'],111        ],112      ).send();113114      const events = result.events.Transfer;115      for(let i = 0; i < 2; i++) {116        const event = events[i];117        expect(event.address).to.equal(collectionAddress);118        expect(event.returnValues.from).to.equal('0x0000000000000000000000000000000000000000');119        expect(event.returnValues.to).to.equal(receiver);120        expect(event.returnValues.tokenId).to.equal(String(+nextTokenId + i));121      }122123      expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI 0');124      expect(await contract.methods.tokenURI(+nextTokenId + 1).call()).to.be.equal('Test URI 1');125      expect(await contract.methods.tokenURI(+nextTokenId + 2).call()).to.be.equal('Test URI 2');126    }127  });128129  itEth('Can perform mintBulkCross() with multiple tokens', async ({helper}) => {130    const caller = await helper.eth.createAccountWithBalance(donor);131    const callerCross = helper.ethCrossAccount.fromAddress(caller);132    const receiver = helper.eth.createAccount();133    const receiverCross = helper.ethCrossAccount.fromAddress(receiver);134135    const permissions = [136      {code: TokenPermissionField.Mutable, value: true},137      {code: TokenPermissionField.TokenOwner, value: true},138      {code: TokenPermissionField.CollectionAdmin, value: true},139    ];140    const {collectionAddress} = await helper.eth.createCollection(141      caller,142      {143        ...CREATE_COLLECTION_DATA_DEFAULTS,144        name: 'A',145        description: 'B',146        tokenPrefix: 'C',147        collectionMode: 'rft',148        adminList: [callerCross],149        tokenPropertyPermissions: [150          {key: 'key_0_0', permissions},151          {key: 'key_1_0', permissions},152          {key: 'key_1_1', permissions},153          {key: 'key_2_0', permissions},154          {key: 'key_2_1', permissions},155          {key: 'key_2_2', permissions},156        ],157      },158    ).send();159160    const contract = await helper.ethNativeContract.collection(collectionAddress, 'rft', caller);161    const nextTokenId = await contract.methods.nextTokenId().call();162    expect(nextTokenId).to.be.equal('1');163    const result = await contract.methods.mintBulkCross([164      {165        owners: [{166          owner: receiverCross,167          pieces: 1,168        }],169        properties: [170          {key: 'key_0_0', value: Buffer.from('value_0_0')},171        ],172      },173      {174        owners: [{175          owner: receiverCross,176          pieces: 2,177        }],178        properties: [179          {key: 'key_1_0', value: Buffer.from('value_1_0')},180          {key: 'key_1_1', value: Buffer.from('value_1_1')},181        ],182      },183      {184        owners: [{185          owner: receiverCross,186          pieces: 1,187        }],188        properties: [189          {key: 'key_2_0', value: Buffer.from('value_2_0')},190          {key: 'key_2_1', value: Buffer.from('value_2_1')},191          {key: 'key_2_2', value: Buffer.from('value_2_2')},192        ],193      },194    ]).send({from: caller});195    const events = result.events.Transfer.sort((a: any, b: any) => +a.returnValues.tokenId - b.returnValues.tokenId);196    const bulkSize = 3;197    for(let i = 0; i < bulkSize; i++) {198      const event = events[i];199      expect(event.address).to.equal(collectionAddress);200      expect(event.returnValues.from).to.equal('0x0000000000000000000000000000000000000000');201      expect(event.returnValues.to).to.equal(receiver);202      expect(event.returnValues.tokenId).to.equal(`${+nextTokenId + i}`);203    }204205    const properties = [206      await contract.methods.properties(+nextTokenId, []).call(),207      await contract.methods.properties(+nextTokenId + 1, []).call(),208      await contract.methods.properties(+nextTokenId + 2, []).call(),209    ];210    expect(properties).to.be.deep.equal([211      [212        ['key_0_0', helper.getWeb3().utils.toHex('value_0_0')],213      ],214      [215        ['key_1_0', helper.getWeb3().utils.toHex('value_1_0')],216        ['key_1_1', helper.getWeb3().utils.toHex('value_1_1')],217      ],218      [219        ['key_2_0', helper.getWeb3().utils.toHex('value_2_0')],220        ['key_2_1', helper.getWeb3().utils.toHex('value_2_1')],221        ['key_2_2', helper.getWeb3().utils.toHex('value_2_2')],222      ],223    ]);224  });225226  itEth('Can perform mintBulkCross() with multiple owners', async ({helper}) => {227    const caller = await helper.eth.createAccountWithBalance(donor);228    const callerCross = helper.ethCrossAccount.fromAddress(caller);229    const receiver = helper.eth.createAccount();230    const receiverCross = helper.ethCrossAccount.fromAddress(receiver);231    const receiver2 = helper.eth.createAccount();232    const receiver2Cross = helper.ethCrossAccount.fromAddress(receiver2);233234    const permissions = [235      {code: TokenPermissionField.Mutable, value: true},236      {code: TokenPermissionField.TokenOwner, value: true},237      {code: TokenPermissionField.CollectionAdmin, value: true},238    ];239    const {collectionAddress} = await helper.eth.createCollection(240      caller,241      {242        ...CREATE_COLLECTION_DATA_DEFAULTS,243        name: 'A',244        description: 'B',245        tokenPrefix: 'C',246        collectionMode: 'rft',247        adminList: [callerCross],248        tokenPropertyPermissions: [249          {key: 'key_2_0', permissions},250          {key: 'key_2_1', permissions},251          {key: 'key_2_2', permissions},252        ],253      },254    ).send();255256    const contract = await helper.ethNativeContract.collection(collectionAddress, 'rft', caller);257    const nextTokenId = await contract.methods.nextTokenId().call();258    expect(nextTokenId).to.be.equal('1');259    const result = await contract.methods.mintBulkCross([{260      owners: [261        {262          owner: receiverCross,263          pieces: 1,264        },265        {266          owner: receiver2Cross,267          pieces: 2,268        },269      ],270      properties: [271        {key: 'key_2_0', value: Buffer.from('value_2_0')},272        {key: 'key_2_1', value: Buffer.from('value_2_1')},273        {key: 'key_2_2', value: Buffer.from('value_2_2')},274      ],275    }]).send({from: caller});276    const event = result.events.Transfer;277    expect(event.address).to.equal(collectionAddress);278    expect(event.returnValues.from).to.equal('0x0000000000000000000000000000000000000000');279    expect(event.returnValues.to).to.equal('0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF');280    expect(event.returnValues.tokenId).to.equal(`${+nextTokenId}`);281282    const properties = [283      await contract.methods.properties(+nextTokenId, []).call(),284    ];285    expect(properties).to.be.deep.equal([[286      ['key_2_0', helper.getWeb3().utils.toHex('value_2_0')],287      ['key_2_1', helper.getWeb3().utils.toHex('value_2_1')],288      ['key_2_2', helper.getWeb3().utils.toHex('value_2_2')],289    ]]);290  });291292  itEth('Can perform setApprovalForAll()', async ({helper}) => {293    const owner = await helper.eth.createAccountWithBalance(donor);294    const operator = helper.eth.createAccount();295296    const collection = await helper.rft.mintCollection(minter, {});297298    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);299    const contract = await helper.ethNativeContract.collection(collectionAddress, 'rft', owner);300301    const approvedBefore = await contract.methods.isApprovedForAll(owner, operator).call();302    expect(approvedBefore).to.be.equal(false);303304    {305      const result = await contract.methods.setApprovalForAll(operator, true).send({from: owner});306307      expect(result.events.ApprovalForAll).to.be.like({308        address: collectionAddress,309        event: 'ApprovalForAll',310        returnValues: {311          owner,312          operator,313          approved: true,314        },315      });316317      const approvedAfter = await contract.methods.isApprovedForAll(owner, operator).call();318      expect(approvedAfter).to.be.equal(true);319    }320321    {322      const result = await contract.methods.setApprovalForAll(operator, false).send({from: owner});323324      expect(result.events.ApprovalForAll).to.be.like({325        address: collectionAddress,326        event: 'ApprovalForAll',327        returnValues: {328          owner,329          operator,330          approved: false,331        },332      });333334      const approvedAfter = await contract.methods.isApprovedForAll(owner, operator).call();335      expect(approvedAfter).to.be.equal(false);336    }337  });338339  itEth('Can perform burn with ApprovalForAll', async ({helper}) => {340    const collection = await helper.rft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});341342    const owner = await helper.eth.createAccountWithBalance(donor);343    const operator = await helper.eth.createAccountWithBalance(donor);344345    const token = await collection.mintToken(minter, 100n, {Ethereum: owner});346347    const address = helper.ethAddress.fromCollectionId(collection.collectionId);348    const contract = await helper.ethNativeContract.collection(address, 'rft');349350    {351      await contract.methods.setApprovalForAll(operator, true).send({from: owner});352      const ownerCross = helper.ethCrossAccount.fromAddress(owner);353      const result = await contract.methods.burnFromCross(ownerCross, token.tokenId).send({from: operator});354      const events = result.events.Transfer;355356      expect(events).to.be.like({357        address,358        event: 'Transfer',359        returnValues: {360          from: owner,361          to: '0x0000000000000000000000000000000000000000',362          tokenId: token.tokenId.toString(),363        },364      });365    }366  });367368  itEth('Can perform burn with approve and approvalForAll', async ({helper}) => {369    const collection = await helper.rft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});370371    const owner = await helper.eth.createAccountWithBalance(donor);372    const operator = await helper.eth.createAccountWithBalance(donor);373374    const token = await collection.mintToken(minter, 100n, {Ethereum: owner});375376    const address = helper.ethAddress.fromCollectionId(collection.collectionId);377    const contract = await helper.ethNativeContract.collection(address, 'rft');378379    const rftToken = await helper.ethNativeContract.rftTokenById(token.collectionId, token.tokenId, owner, true);380381    {382      await rftToken.methods.approve(operator, 15n).send({from: owner});383      await contract.methods.setApprovalForAll(operator, true).send({from: owner});384      await rftToken.methods.burnFrom(owner, 10n).send({from: operator});385    }386    {387      const allowance = await rftToken.methods.allowance(owner, operator).call();388      expect(+allowance).to.be.equal(5);389    }390    {391      const ownerCross = helper.ethCrossAccount.fromAddress(owner);392      const operatorCross = helper.ethCrossAccount.fromAddress(operator);393      const allowance = await rftToken.methods.allowanceCross(ownerCross, operatorCross).call();394      expect(+allowance).to.equal(5);395    }396  });397398  itEth('Can perform transfer with ApprovalForAll', async ({helper}) => {399    const collection = await helper.rft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});400401    const owner = await helper.eth.createAccountWithBalance(donor);402    const operator = await helper.eth.createAccountWithBalance(donor);403    const receiver = charlie;404405    const token = await collection.mintToken(minter, 100n, {Ethereum: owner});406407    const address = helper.ethAddress.fromCollectionId(collection.collectionId);408    const contract = await helper.ethNativeContract.collection(address, 'rft');409410    {411      await contract.methods.setApprovalForAll(operator, true).send({from: owner});412      const ownerCross = helper.ethCrossAccount.fromAddress(owner);413      const recieverCross = helper.ethCrossAccount.fromKeyringPair(receiver);414      const result = await contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: operator});415      const event = result.events.Transfer;416      expect(event).to.be.like({417        address: helper.ethAddress.fromCollectionId(collection.collectionId),418        event: 'Transfer',419        returnValues: {420          from: owner,421          to: helper.address.substrateToEth(receiver.address),422          tokenId: token.tokenId.toString(),423        },424      });425    }426427    expect(await token.getTop10Owners()).to.be.like([{Substrate: receiver.address}]);428  });429430  itEth('Can perform burn()', async ({helper}) => {431    const caller = await helper.eth.createAccountWithBalance(donor);432    const {collectionAddress} = await helper.eth.createRFTCollection(caller, 'Burny', '6', '6');433    const contract = await helper.ethNativeContract.collection(collectionAddress, 'rft', caller);434435    const result = await contract.methods.mint(caller).send();436    const tokenId = result.events.Transfer.returnValues.tokenId;437    {438      const result = await contract.methods.burn(tokenId).send();439      const event = result.events.Transfer;440      expect(event.address).to.equal(collectionAddress);441      expect(event.returnValues.from).to.equal(caller);442      expect(event.returnValues.to).to.equal('0x0000000000000000000000000000000000000000');443      expect(event.returnValues.tokenId).to.equal(tokenId.toString());444    }445  });446447  itEth('Can perform transferFrom()', async ({helper}) => {448    const caller = await helper.eth.createAccountWithBalance(donor);449    const receiver = helper.eth.createAccount();450    const {collectionId, collectionAddress} = await helper.eth.createRFTCollection(caller, 'TransferFromy', '6', '6');451    const contract = await helper.ethNativeContract.collection(collectionAddress, 'rft', caller);452453    const result = await contract.methods.mint(caller).send();454    const tokenId = result.events.Transfer.returnValues.tokenId;455456    const tokenAddress = helper.ethAddress.fromTokenId(collectionId, tokenId);457458    const tokenContract = await helper.ethNativeContract.rftToken(tokenAddress, caller);459    await tokenContract.methods.repartition(15).send();460461    {462      const tokenEvents: any = [];463      tokenContract.events.allEvents((_: any, event: any) => {464        tokenEvents.push(event);465      });466      const result = await contract.methods.transferFrom(caller, receiver, tokenId).send();467      if(tokenEvents.length == 0) await helper.wait.newBlocks(1);468469      let event = result.events.Transfer;470      expect(event.address).to.equal(collectionAddress);471      expect(event.returnValues.from).to.equal(caller);472      expect(event.returnValues.to).to.equal(receiver);473      expect(event.returnValues.tokenId).to.equal(tokenId.toString());474475      event = tokenEvents[0];476      expect(event.address).to.equal(tokenAddress);477      expect(event.returnValues.from).to.equal(caller);478      expect(event.returnValues.to).to.equal(receiver);479      expect(event.returnValues.value).to.equal('15');480    }481482    {483      const balance = await contract.methods.balanceOf(receiver).call();484      expect(+balance).to.equal(1);485    }486487    {488      const balance = await contract.methods.balanceOf(caller).call();489      expect(+balance).to.equal(0);490    }491  });492493  // Soft-deprecated494  itEth('Can perform burnFrom()', async ({helper}) => {495    const collection = await helper.rft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});496497    const owner = await helper.eth.createAccountWithBalance(donor);498    const spender = await helper.eth.createAccountWithBalance(donor);499500    const token = await collection.mintToken(minter, 100n, {Ethereum: owner});501502    const address = helper.ethAddress.fromCollectionId(collection.collectionId);503    const contract = await helper.ethNativeContract.collection(address, 'rft', spender, true);504505    const tokenAddress = helper.ethAddress.fromTokenId(collection.collectionId, token.tokenId);506    const tokenContract = await helper.ethNativeContract.rftToken(tokenAddress, owner);507    await tokenContract.methods.repartition(15).send();508    await tokenContract.methods.approve(spender, 15).send();509510    {511      const result = await contract.methods.burnFrom(owner, token.tokenId).send();512      const event = result.events.Transfer;513      expect(event).to.be.like({514        address: helper.ethAddress.fromCollectionId(collection.collectionId),515        event: 'Transfer',516        returnValues: {517          from: owner,518          to: '0x0000000000000000000000000000000000000000',519          tokenId: token.tokenId.toString(),520        },521      });522    }523524    expect(await collection.getTokenBalance(token.tokenId, {Ethereum: owner})).to.be.eq(0n);525  });526527  itEth('Can perform burnFromCross()', async ({helper}) => {528    const collection = await helper.rft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});529530    const owner = bob;531    const spender = await helper.eth.createAccountWithBalance(donor);532533    const token = await collection.mintToken(minter, 100n, {Substrate: owner.address});534535    const address = helper.ethAddress.fromCollectionId(collection.collectionId);536    const contract = await helper.ethNativeContract.collection(address, 'rft');537538    await token.repartition(owner, 15n);539    await token.approve(owner, {Ethereum: spender}, 15n);540541    {542      const ownerCross = helper.ethCrossAccount.fromKeyringPair(owner);543      const result = await contract.methods.burnFromCross(ownerCross, token.tokenId).send({from: spender});544      const event = result.events.Transfer;545      expect(event).to.be.like({546        address: helper.ethAddress.fromCollectionId(collection.collectionId),547        event: 'Transfer',548        returnValues: {549          from: helper.address.substrateToEth(owner.address),550          to: '0x0000000000000000000000000000000000000000',551          tokenId: token.tokenId.toString(),552        },553      });554    }555556    expect(await collection.getTokenBalance(token.tokenId, {Substrate: owner.address})).to.be.eq(0n);557  });558559  itEth('Can perform transferFromCross()', async ({helper}) => {560    const collection = await helper.rft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});561562    const owner = bob;563    const spender = await helper.eth.createAccountWithBalance(donor);564    const receiver = charlie;565566    const token = await collection.mintToken(minter, 100n, {Substrate: owner.address});567568    const address = helper.ethAddress.fromCollectionId(collection.collectionId);569    const contract = await helper.ethNativeContract.collection(address, 'rft');570571    await token.repartition(owner, 15n);572    await token.approve(owner, {Ethereum: spender}, 15n);573574    {575      const ownerCross = helper.ethCrossAccount.fromKeyringPair(owner);576      const recieverCross = helper.ethCrossAccount.fromKeyringPair(receiver);577      const result = await contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: spender});578      const event = result.events.Transfer;579      expect(event).to.be.like({580        address: helper.ethAddress.fromCollectionId(collection.collectionId),581        event: 'Transfer',582        returnValues: {583          from: helper.address.substrateToEth(owner.address),584          to: helper.address.substrateToEth(receiver.address),585          tokenId: token.tokenId.toString(),586        },587      });588    }589590    expect(await token.getTop10Owners()).to.be.like([{Substrate: receiver.address}]);591  });592593  itEth('Can perform transfer()', async ({helper}) => {594    const caller = await helper.eth.createAccountWithBalance(donor);595    const receiver = helper.eth.createAccount();596    const {collectionAddress} = await helper.eth.createRFTCollection(caller, 'Transferry', '6', '6');597    const contract = await helper.ethNativeContract.collection(collectionAddress, 'rft', caller);598599    const result = await contract.methods.mint(caller).send();600    const tokenId = result.events.Transfer.returnValues.tokenId;601602    {603      const result = await contract.methods.transfer(receiver, tokenId).send();604605      const event = result.events.Transfer;606      expect(event.address).to.equal(collectionAddress);607      expect(event.returnValues.from).to.equal(caller);608      expect(event.returnValues.to).to.equal(receiver);609      expect(event.returnValues.tokenId).to.equal(tokenId.toString());610    }611612    {613      const balance = await contract.methods.balanceOf(caller).call();614      expect(+balance).to.equal(0);615    }616617    {618      const balance = await contract.methods.balanceOf(receiver).call();619      expect(+balance).to.equal(1);620    }621  });622623  itEth('Can perform transferCross()', async ({helper}) => {624    const sender = await helper.eth.createAccountWithBalance(donor);625    const receiverEth = await helper.eth.createAccountWithBalance(donor);626    const receiverCrossEth = helper.ethCrossAccount.fromAddress(receiverEth);627    const receiverCrossSub = helper.ethCrossAccount.fromKeyringPair(minter);628629    const collection = await helper.rft.mintCollection(minter, {});630    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);631    const collectionEvm = await helper.ethNativeContract.collection(collectionAddress, 'rft', sender);632633    const token = await collection.mintToken(minter, 50n, {Ethereum: sender});634635    {636      // Can transferCross to ethereum address:637      const result = await collectionEvm.methods.transferCross(receiverCrossEth, token.tokenId).send({from: sender});638      // Check events:639      const event = result.events.Transfer;640      expect(event.address).to.equal(collectionAddress);641      expect(event.returnValues.from).to.equal(sender);642      expect(event.returnValues.to).to.equal(receiverEth);643      expect(event.returnValues.tokenId).to.equal(token.tokenId.toString());644      // Sender's balance decreased:645      const senderBalance = await collectionEvm.methods.balanceOf(sender).call();646      expect(+senderBalance).to.equal(0);647      expect(await token.getBalance({Ethereum: sender})).to.eq(0n);648      // Receiver's balance increased:649      const receiverBalance = await collectionEvm.methods.balanceOf(receiverEth).call();650      expect(+receiverBalance).to.equal(1);651      expect(await token.getBalance({Ethereum: receiverEth})).to.eq(50n);652    }653654    {655      // Can transferCross to substrate address:656      const substrateResult = await collectionEvm.methods.transferCross(receiverCrossSub, token.tokenId).send({from: receiverEth});657      // Check events:658      const event = substrateResult.events.Transfer;659      expect(event.address).to.be.equal(collectionAddress);660      expect(event.returnValues.from).to.be.equal(receiverEth);661      expect(event.returnValues.to).to.be.equal(helper.address.substrateToEth(minter.address));662      expect(event.returnValues.tokenId).to.be.equal(`${token.tokenId}`);663      // Sender's balance decreased:664      const senderBalance = await collectionEvm.methods.balanceOf(receiverEth).call();665      expect(+senderBalance).to.equal(0);666      expect(await token.getBalance({Ethereum: receiverEth})).to.eq(0n);667      // Receiver's balance increased:668      const receiverBalance = await helper.nft.getTokensByAddress(collection.collectionId, {Substrate: minter.address});669      expect(receiverBalance).to.contain(token.tokenId);670      expect(await token.getBalance({Substrate: minter.address})).to.eq(50n);671    }672  });673674  ['transfer', 'transferCross'].map(testCase => itEth(`Cannot ${testCase} non-owned token`, async ({helper}) => {675    const sender = await helper.eth.createAccountWithBalance(donor);676    const tokenOwner = await helper.eth.createAccountWithBalance(donor);677    const receiverSub = minter;678    const receiverCrossSub = helper.ethCrossAccount.fromKeyringPair(minter);679680    const collection = await helper.rft.mintCollection(minter, {});681    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);682    const collectionEvm = await helper.ethNativeContract.collection(collectionAddress, 'rft', sender);683684    await collection.mintToken(minter, 50n, {Ethereum: sender});685    const nonSendersToken = await collection.mintToken(minter, 50n, {Ethereum: tokenOwner});686687    // Cannot transferCross someone else's token:688    const receiver = testCase === 'transfer' ? helper.address.substrateToEth(receiverSub.address) : receiverCrossSub;689    await expect(collectionEvm.methods[testCase](receiver, nonSendersToken.tokenId).send({from: sender})).to.be.rejected;690    // Cannot transfer token if it does not exist:691    await expect(collectionEvm.methods[testCase](receiver, 999999).send({from: sender})).to.be.rejected;692  }));693694  itEth('transfer event on transfer from partial ownership to full ownership', async ({helper}) => {695    const caller = await helper.eth.createAccountWithBalance(donor);696    const receiver = helper.eth.createAccount();697    const {collectionId, collectionAddress} = await helper.eth.createRFTCollection(caller, 'Transferry-Partial-to-Full', '6', '6');698    const contract = await helper.ethNativeContract.collection(collectionAddress, 'rft', caller);699700    const result = await contract.methods.mint(caller).send();701    const tokenId = result.events.Transfer.returnValues.tokenId;702703    const tokenContract = await helper.ethNativeContract.rftTokenById(collectionId, tokenId, caller);704705    await tokenContract.methods.repartition(2).send();706    await tokenContract.methods.transfer(receiver, 1).send();707708    const events: any = [];709    contract.events.allEvents((_: any, event: any) => {710      events.push(event);711    });712713    await tokenContract.methods.transfer(receiver, 1).send();714    if(events.length == 0) await helper.wait.newBlocks(1);715    const event = events[0];716717    expect(event.address).to.equal(collectionAddress);718    expect(event.returnValues.from).to.equal('0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF');719    expect(event.returnValues.to).to.equal(receiver);720    expect(event.returnValues.tokenId).to.equal(tokenId.toString());721  });722723  itEth('transfer event on transfer from full ownership to partial ownership', async ({helper}) => {724    const caller = await helper.eth.createAccountWithBalance(donor);725    const receiver = helper.eth.createAccount();726    const {collectionId, collectionAddress} = await helper.eth.createRFTCollection(caller, 'Transferry-Full-to-Partial', '6', '6');727    const contract = await helper.ethNativeContract.collection(collectionAddress, 'rft', caller);728729    const result = await contract.methods.mint(caller).send();730    const tokenId = result.events.Transfer.returnValues.tokenId;731732    const tokenContract = await helper.ethNativeContract.rftTokenById(collectionId, tokenId, caller);733734    await tokenContract.methods.repartition(2).send();735736    const events: any = [];737    contract.events.allEvents((_: any, event: any) => {738      events.push(event);739    });740741    await tokenContract.methods.transfer(receiver, 1).send();742    if(events.length == 0) await helper.wait.newBlocks(1);743    const event = events[0];744745    expect(event.address).to.equal(collectionAddress);746    expect(event.returnValues.from).to.equal(caller);747    expect(event.returnValues.to).to.equal('0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF');748    expect(event.returnValues.tokenId).to.equal(tokenId.toString());749  });750751  itEth('Check balanceOfCross()', async ({helper}) => {752    const collection = await helper.rft.mintCollection(minter, {});753    const owner = await helper.ethCrossAccount.createAccountWithBalance(donor);754    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);755    const collectionEvm = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner.eth);756757    expect(await collectionEvm.methods.balanceOfCross(owner).call({from: owner.eth})).to.be.eq('0');758759    for(let i = 1n; i < 10n; i++) {760      await collection.mintToken(minter, 100n, {Ethereum: owner.eth});761      expect(await collectionEvm.methods.balanceOfCross(owner).call({from: owner.eth})).to.be.eq(i.toString());762    }763  });764765  itEth('Check ownerOfCross()', async ({helper}) => {766    const collection = await helper.rft.mintCollection(minter, {});767    let owner = await helper.ethCrossAccount.createAccountWithBalance(donor);768    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);769    const collectionEvm = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner.eth);770    const {tokenId} = await collection.mintToken(minter, 100n,{Ethereum: owner.eth});771772    for(let i = 1n; i < 10n; i++) {773      const ownerCross = await collectionEvm.methods.ownerOfCross(tokenId).call({from: owner.eth});774      expect(ownerCross.eth).to.be.eq(owner.eth);775      expect(ownerCross.sub).to.be.eq(owner.sub);776777      const newOwner = await helper.ethCrossAccount.createAccountWithBalance(donor);778      await collectionEvm.methods.transferCross(newOwner, tokenId).send({from: owner.eth});779      owner = newOwner;780    }781782    const tokenAddress = helper.ethAddress.fromTokenId(collection.collectionId, tokenId);783    const tokenContract = await helper.ethNativeContract.rftToken(tokenAddress, owner.eth, true);784    const newOwner = await helper.ethCrossAccount.createAccountWithBalance(donor);785    await tokenContract.methods.transferCross(newOwner, 50).send({from: owner.eth});786    const ownerCross = await collectionEvm.methods.ownerOfCross(tokenId).call({from: owner.eth});787    expect(ownerCross.eth.toUpperCase()).to.be.eq('0XFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF');788    expect(ownerCross.sub).to.be.eq('0');789  });790});791792describe('RFT: Fees', () => {793  let donor: IKeyringPair;794795  before(async function() {796    await usingEthPlaygrounds(async (helper, privateKey) => {797      requirePalletsOrSkip(this, helper, [Pallets.ReFungible]);798799      donor = await privateKey({url: import.meta.url});800    });801  });802803  itEth('transferFrom() call fee is less than 0.2UNQ', async ({helper}) => {804    const caller = await helper.eth.createAccountWithBalance(donor);805    const receiver = helper.eth.createAccount();806    const {collectionAddress} = await helper.eth.createRFTCollection(caller, 'Feeful-Transfer-From', '6', '6');807    const contract = await helper.ethNativeContract.collection(collectionAddress, 'rft', caller);808809    const result = await contract.methods.mint(caller).send();810    const tokenId = result.events.Transfer.returnValues.tokenId;811812    const cost = await helper.eth.recordCallFee(caller, () => contract.methods.transferFrom(caller, receiver, tokenId).send());813    expect(cost < BigInt(0.2 * Number(helper.balance.getOneTokenNominal())));814    expect(cost > 0n);815  });816817  itEth('transfer() call fee is less than 0.2UNQ', async ({helper}) => {818    const caller = await helper.eth.createAccountWithBalance(donor);819    const receiver = helper.eth.createAccount();820    const {collectionAddress} = await helper.eth.createRFTCollection(caller, 'Feeful-Transfer', '6', '6');821    const contract = await helper.ethNativeContract.collection(collectionAddress, 'rft', caller);822823    const result = await contract.methods.mint(caller).send();824    const tokenId = result.events.Transfer.returnValues.tokenId;825826    const cost = await helper.eth.recordCallFee(caller, () => contract.methods.transfer(receiver, tokenId).send());827    expect(cost < BigInt(0.2 * Number(helper.balance.getOneTokenNominal())));828    expect(cost > 0n);829  });830});831832describe('Common metadata', () => {833  let donor: IKeyringPair;834  let alice: IKeyringPair;835836  before(async function() {837    await usingEthPlaygrounds(async (helper, privateKey) => {838      requirePalletsOrSkip(this, helper, [Pallets.ReFungible]);839840      donor = await privateKey({url: import.meta.url});841      [alice] = await helper.arrange.createAccounts([1000n], donor);842    });843  });844845  itEth('Returns collection name', async ({helper}) => {846    const caller = helper.eth.createAccount();847    const tokenPropertyPermissions = [{848      key: 'URI',849      permission: {850        mutable: true,851        collectionAdmin: true,852        tokenOwner: false,853      },854    }];855    const collection = await helper.rft.mintCollection(856      alice,857      {858        name: 'Leviathan',859        tokenPrefix: '11',860        properties: [{key: 'ERC721Metadata', value: '1'}],861        tokenPropertyPermissions,862      },863    );864865    const contract = helper.ethNativeContract.collectionById(collection.collectionId, 'rft', caller);866    const name = await contract.methods.name().call();867    expect(name).to.equal('Leviathan');868  });869870  itEth('Returns symbol name', async ({helper}) => {871    const caller = await helper.eth.createAccountWithBalance(donor);872    const tokenPropertyPermissions = [{873      key: 'URI',874      permission: {875        mutable: true,876        collectionAdmin: true,877        tokenOwner: false,878      },879    }];880    const {collectionId} = await helper.rft.mintCollection(881      alice,882      {883        name: 'Leviathan',884        tokenPrefix: '12',885        properties: [{key: 'ERC721Metadata', value: '1'}],886        tokenPropertyPermissions,887      },888    );889890    const contract = await helper.ethNativeContract.collectionById(collectionId, 'rft', caller);891    const symbol = await contract.methods.symbol().call();892    expect(symbol).to.equal('12');893  });894});895896describe('Negative tests', () => {897  let donor: IKeyringPair;898  let minter: IKeyringPair;899  let alice: IKeyringPair;900901  before(async function() {902    await usingEthPlaygrounds(async (helper, privateKey) => {903      requirePalletsOrSkip(this, helper, [Pallets.ReFungible]);904905      donor = await privateKey({url: import.meta.url});906      [minter, alice] = await helper.arrange.createAccounts([100n, 100n], donor);907    });908  });909910  itEth('[negative] Cant perform burn without approval', async ({helper}) => {911    const collection = await helper.rft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});912913    const owner = await helper.eth.createAccountWithBalance(donor);914    const spender = await helper.eth.createAccountWithBalance(donor);915916    const token = await collection.mintToken(minter, 100n, {Ethereum: owner});917918    const address = helper.ethAddress.fromCollectionId(collection.collectionId);919    const contract = await helper.ethNativeContract.collection(address, 'rft');920921    const ownerCross = helper.ethCrossAccount.fromAddress(owner);922923    await expect(contract.methods.burnFromCross(ownerCross, token.tokenId).send({from: spender})).to.be.rejected;924925    await contract.methods.setApprovalForAll(spender, true).send({from: owner});926    await contract.methods.setApprovalForAll(spender, false).send({from: owner});927928    await expect(contract.methods.burnFromCross(ownerCross, token.tokenId).send({from: spender})).to.be.rejected;929  });930931  itEth('[negative] Cant perform transfer without approval', async ({helper}) => {932    const collection = await helper.rft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});933    const owner = await helper.eth.createAccountWithBalance(donor);934    const receiver = alice;935936    const spender = await helper.eth.createAccountWithBalance(donor);937938    const token = await collection.mintToken(minter, 100n, {Ethereum: owner});939940    const address = helper.ethAddress.fromCollectionId(collection.collectionId);941    const contract = await helper.ethNativeContract.collection(address, 'rft');942943    const ownerCross = helper.ethCrossAccount.fromAddress(owner);944    const recieverCross = helper.ethCrossAccount.fromKeyringPair(receiver);945946    await expect(contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: spender})).to.be.rejected;947948    await contract.methods.setApprovalForAll(spender, true).send({from: owner});949    await contract.methods.setApprovalForAll(spender, false).send({from: owner});950951    await expect(contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: spender})).to.be.rejected;952  });953954  itEth('[negative] Can perform mintBulkCross() with multiple owners and multiple tokens', async ({helper}) => {955    const caller = await helper.eth.createAccountWithBalance(donor);956    const callerCross = helper.ethCrossAccount.fromAddress(caller);957    const receiver = helper.eth.createAccount();958    const receiverCross = helper.ethCrossAccount.fromAddress(receiver);959    const receiver2 = helper.eth.createAccount();960    const receiver2Cross = helper.ethCrossAccount.fromAddress(receiver2);961962    const permissions = [963      {code: TokenPermissionField.Mutable, value: true},964      {code: TokenPermissionField.TokenOwner, value: true},965      {code: TokenPermissionField.CollectionAdmin, value: true},966    ];967    const {collectionAddress} = await helper.eth.createCollection(968      caller,969      {970        ...CREATE_COLLECTION_DATA_DEFAULTS,971        name: 'A',972        description: 'B',973        tokenPrefix: 'C',974        collectionMode: 'rft',975        adminList: [callerCross],976        tokenPropertyPermissions: [977          {key: 'key_0_0', permissions},978          {key: 'key_2_0', permissions},979          {key: 'key_2_1', permissions},980          {key: 'key_2_2', permissions},981        ],982      },983    ).send();984985    const contract = await helper.ethNativeContract.collection(collectionAddress, 'rft', caller);986    const nextTokenId = await contract.methods.nextTokenId().call();987    expect(nextTokenId).to.be.equal('1');988    const createData = [989      {990        owners: [{991          owner: receiverCross,992          pieces: 1,993        }],994        properties: [995          {key: 'key_0_0', value: Buffer.from('value_0_0')},996        ],997      },998      {999        owners: [1000          {1001            owner: receiverCross,1002            pieces: 1,1003          },1004          {1005            owner: receiver2Cross,1006            pieces: 2,1007          },1008        ],1009        properties: [1010          {key: 'key_2_0', value: Buffer.from('value_2_0')},1011          {key: 'key_2_1', value: Buffer.from('value_2_1')},1012          {key: 'key_2_2', value: Buffer.from('value_2_2')},1013        ],1014      },1015    ];10161017    await expect(contract.methods.mintBulkCross(createData).call({from: caller})).to.be.rejectedWith('creation of multiple tokens supported only if they have single owner each');1018  });1019});