--- a/Cargo.lock +++ b/Cargo.lock @@ -5731,7 +5731,7 @@ [[package]] name = "pallet-common" -version = "0.1.2" +version = "0.1.3" dependencies = [ "ethereum", "evm-coder", @@ -6198,7 +6198,7 @@ [[package]] name = "pallet-nonfungible" -version = "0.1.1" +version = "0.1.2" dependencies = [ "ethereum", "evm-coder", @@ -6638,7 +6638,7 @@ [[package]] name = "pallet-unique" -version = "0.1.0" +version = "0.1.1" dependencies = [ "ethereum", "evm-coder", @@ -6649,6 +6649,7 @@ "pallet-evm", "pallet-evm-coder-substrate", "pallet-nonfungible", + "pallet-refungible", "parity-scale-codec 3.1.5", "scale-info", "serde", @@ -12736,7 +12737,7 @@ [[package]] name = "up-data-structs" -version = "0.1.1" +version = "0.1.2" dependencies = [ "derivative", "frame-support", --- a/Makefile +++ b/Makefile @@ -9,10 +9,12 @@ FUNGIBLE_EVM_STUBS=./pallets/fungible/src/stubs FUNGIBLE_EVM_ABI=./tests/src/eth/fungibleAbi.json +REFUNGIBLE_EVM_STUBS=./pallets/refungible/src/stubs +REFUNGIBLE_EVM_ABI=./tests/src/eth/refungibleAbi.json + NONFUNGIBLE_EVM_STUBS=./pallets/nonfungible/src/stubs NONFUNGIBLE_EVM_ABI=./tests/src/eth/nonFungibleAbi.json -REFUNGIBLE_EVM_STUBS=./pallets/refungible/src/stubs RENFUNGIBLE_EVM_ABI=./tests/src/eth/reFungibleAbi.json RENFUNGIBLE_TOKEN_EVM_ABI=./tests/src/eth/reFungibleTokenAbi.json @@ -25,7 +27,7 @@ TESTS_API=./tests/src/eth/api/ .PHONY: regenerate_solidity -regenerate_solidity: UniqueFungible.sol UniqueNFT.sol UniqueRefungibleToken.sol ContractHelpers.sol CollectionHelpers.sol +regenerate_solidity: UniqueFungible.sol UniqueNFT.sol UniqueRefungible.sol UniqueRefungibleToken.sol ContractHelpers.sol CollectionHelpers.sol UniqueFungible.sol: PACKAGE=pallet-fungible NAME=erc::gen_iface OUTPUT=$(TESTS_API)/$@ ./.maintain/scripts/generate_sol.sh @@ -35,6 +37,10 @@ PACKAGE=pallet-nonfungible NAME=erc::gen_iface OUTPUT=$(TESTS_API)/$@ ./.maintain/scripts/generate_sol.sh PACKAGE=pallet-nonfungible NAME=erc::gen_impl OUTPUT=$(NONFUNGIBLE_EVM_STUBS)/$@ ./.maintain/scripts/generate_sol.sh +UniqueRefungible.sol: + PACKAGE=pallet-refungible NAME=erc::gen_iface OUTPUT=$(TESTS_API)/$@ ./.maintain/scripts/generate_sol.sh + PACKAGE=pallet-refungible NAME=erc::gen_impl OUTPUT=$(REFUNGIBLE_EVM_STUBS)/$@ ./.maintain/scripts/generate_sol.sh + UniqueRefungibleToken.sol: PACKAGE=pallet-refungible NAME=erc_token::gen_iface OUTPUT=$(TESTS_API)/$@ ./.maintain/scripts/generate_sol.sh PACKAGE=pallet-refungible NAME=erc_token::gen_impl OUTPUT=$(REFUNGIBLE_EVM_STUBS)/$@ ./.maintain/scripts/generate_sol.sh @@ -59,6 +65,10 @@ INPUT=$(REFUNGIBLE_EVM_STUBS)/$< OUTPUT=$(REFUNGIBLE_EVM_STUBS)/UniqueRefungibleToken.raw ./.maintain/scripts/compile_stub.sh INPUT=$(REFUNGIBLE_EVM_STUBS)/$< OUTPUT=$(RENFUNGIBLE_TOKEN_EVM_ABI) ./.maintain/scripts/generate_abi.sh +UniqueRefungible: UniqueRefungible.sol + INPUT=$(REFUNGIBLE_EVM_STUBS)/$< OUTPUT=$(REFUNGIBLE_EVM_STUBS)/UniqueRefungible.raw ./.maintain/scripts/compile_stub.sh + INPUT=$(REFUNGIBLE_EVM_STUBS)/$< OUTPUT=$(REFUNGIBLE_EVM_ABI) ./.maintain/scripts/generate_abi.sh + ContractHelpers: ContractHelpers.sol INPUT=$(CONTRACT_HELPERS_STUBS)/$< OUTPUT=$(CONTRACT_HELPERS_STUBS)/ContractHelpers.raw ./.maintain/scripts/compile_stub.sh INPUT=$(CONTRACT_HELPERS_STUBS)/$< OUTPUT=$(CONTRACT_HELPERS_ABI) ./.maintain/scripts/generate_abi.sh @@ -67,7 +77,7 @@ INPUT=$(COLLECTION_HELPER_STUBS)/$< OUTPUT=$(COLLECTION_HELPER_STUBS)/CollectionHelpers.raw ./.maintain/scripts/compile_stub.sh INPUT=$(COLLECTION_HELPER_STUBS)/$< OUTPUT=$(COLLECTION_HELPER_ABI) ./.maintain/scripts/generate_abi.sh -evm_stubs: UniqueFungible UniqueNFT UniqueRefungibleToken ContractHelpers CollectionHelpers +evm_stubs: UniqueFungible UniqueNFT UniqueRefungible UniqueRefungibleToken ContractHelpers CollectionHelpers .PHONY: _bench _bench: --- a/pallets/common/CHANGELOG.MD +++ b/pallets/common/CHANGELOG.MD @@ -2,6 +2,10 @@ All notable changes to this project will be documented in this file. +## [0.1.3] - 2022-07-25 +### Add +- Some static property keys and values. + ## [0.1.2] - 2022-07-20 ### Fixed --- a/pallets/common/Cargo.toml +++ b/pallets/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-common" -version = "0.1.2" +version = "0.1.3" license = "GPLv3" edition = "2021" --- a/pallets/common/src/dispatch.rs +++ b/pallets/common/src/dispatch.rs @@ -8,6 +8,7 @@ weights::Pays, traits::Get, }; +use sp_runtime::DispatchError; use up_data_structs::{CollectionId, CreateCollectionData}; use crate::{pallet::Config, CommonCollectionOperations, CollectionHandle}; @@ -78,7 +79,7 @@ fn create( sender: T::CrossAccountId, data: CreateCollectionData, - ) -> DispatchResult; + ) -> Result; /// Delete the collection. /// --- a/pallets/common/src/erc.rs +++ b/pallets/common/src/erc.rs @@ -430,10 +430,70 @@ Ok(()) } -/// Get the "tokenURI" key as [PropertyKey](up_data_structs::PropertyKey). -pub fn token_uri_key() -> up_data_structs::PropertyKey { - b"tokenURI" - .to_vec() - .try_into() - .expect("length < limit; qed") +/// Contains static property keys and values. +pub mod static_property { + use evm_coder::{ + execution::{Result, Error}, + }; + use alloc::format; + + const EXPECT_CONVERT_ERROR: &str = "length < limit"; + + /// Keys. + pub mod key { + use super::*; + + /// Key "schemaName". + pub fn schema_name() -> up_data_structs::PropertyKey { + property_key_from_bytes(b"schemaName").expect(EXPECT_CONVERT_ERROR) + } + + /// Key "baseURI". + pub fn base_uri() -> up_data_structs::PropertyKey { + property_key_from_bytes(b"baseURI").expect(EXPECT_CONVERT_ERROR) + } + + /// Key "url". + pub fn url() -> up_data_structs::PropertyKey { + property_key_from_bytes(b"url").expect(EXPECT_CONVERT_ERROR) + } + + /// Key "suffix". + pub fn suffix() -> up_data_structs::PropertyKey { + property_key_from_bytes(b"suffix").expect(EXPECT_CONVERT_ERROR) + } + } + + /// Values. + pub mod value { + use super::*; + + /// Value "ERC721Metadata". + pub const ERC721_METADATA: &[u8] = b"ERC721Metadata"; + + /// Value for [`ERC721_METADATA`]. + pub fn erc721() -> up_data_structs::PropertyValue { + property_value_from_bytes(ERC721_METADATA).expect(EXPECT_CONVERT_ERROR) + } + } + + /// Convert `byte` to [`PropertyKey`]. + pub fn property_key_from_bytes(bytes: &[u8]) -> Result { + bytes.to_vec().try_into().map_err(|_| { + Error::Revert(format!( + "Property key is too long. Max length is {}.", + up_data_structs::PropertyKey::bound() + )) + }) + } + + /// Convert `bytes` to [`PropertyValue`]. + pub fn property_value_from_bytes(bytes: &[u8]) -> Result { + bytes.to_vec().try_into().map_err(|_| { + Error::Revert(format!( + "Property key is too long. Max length is {}.", + up_data_structs::PropertyKey::bound() + )) + }) + } } --- a/pallets/nonfungible/CHANGELOG.md +++ b/pallets/nonfungible/CHANGELOG.md @@ -2,10 +2,20 @@ All notable changes to this project will be documented in this file. -## [0.1.1] - 2022-07-14 +## [0.1.2] - 2022-07-25 +### Changed +- New `token_uri` retrieval logic: + + If the collection has a `url` property and it is not empty, it is returned. + Else If the collection does not have a property with key `schemaName` or its value is not equal to `ERC721Metadata`, it return an error `tokenURI not set`. + + If the property `baseURI` is empty or absent, return "" (empty string) + otherwise, if property `suffix` present and is non-empty, return concatenation of baseURI and suffix + otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings). +## [0.1.1] - 2022-07-14 ### Added - Implementation of RPC method `token_owners`. For reasons of compatibility with this pallet, returns only one owner if token exists. - This was an internal request to improve the web interface and support fractionalization event. \ No newline at end of file + This was an internal request to improve the web interface and support fractionalization event. --- a/pallets/nonfungible/Cargo.toml +++ b/pallets/nonfungible/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-nonfungible" -version = "0.1.1" +version = "0.1.2" license = "GPLv3" edition = "2021" --- a/pallets/nonfungible/src/erc.rs +++ b/pallets/nonfungible/src/erc.rs @@ -33,12 +33,16 @@ use pallet_evm_coder_substrate::dispatch_to_evm; use sp_std::vec::Vec; use pallet_common::{ - erc::{CommonEvmHandler, PrecompileResult, CollectionCall, token_uri_key}, + erc::{ + CommonEvmHandler, PrecompileResult, CollectionCall, + static_property::{key, value as property_value}, + }, CollectionHandle, CollectionPropertyPermissions, }; use pallet_evm::{account::CrossAccountId, PrecompileHandle}; use pallet_evm_coder_substrate::call; use pallet_structure::{SelfWeightOf as StructureWeight, weights::WeightInfo as _}; +use alloc::string::ToString; use crate::{ AccountBalance, Config, CreateItemData, NonfungibleHandle, Pallet, TokenData, TokensMinted, @@ -212,27 +216,47 @@ } /// @notice A distinct Uniform Resource Identifier (URI) for a given asset. - /// @dev Throws if `tokenId` is not a valid NFT. URIs are defined in RFC - /// 3986. The URI may point to a JSON file that conforms to the "ERC721 - /// Metadata JSON Schema". + /// + /// @dev If the token has a `url` property and it is not empty, it is returned. + /// Else If the collection does not have a property with key `schemaName` or its value is not equal to `ERC721Metadata`, it return an error `tokenURI not set`. + /// If the collection property `baseURI` is empty or absent, return "" (empty string) + /// otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix + /// otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings). + /// /// @return token's const_metadata #[solidity(rename_selector = "tokenURI")] fn token_uri(&self, token_id: uint256) -> Result { - let key = token_uri_key(); - if !has_token_permission::(self.id, &key) { - return Err("No tokenURI permission".into()); + let token_id_u32: u32 = token_id.try_into().map_err(|_| "token id overflow")?; + + if let Ok(url) = get_token_property(self, token_id_u32, &key::url()) { + if !url.is_empty() { + return Ok(url); + } + } else if !is_erc721_metadata_compatible::(self.id) { + return Err("tokenURI not set".into()); } - self.consume_store_reads(1)?; - let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?; + if let Some(base_uri) = + pallet_common::Pallet::::get_collection_property(self.id, &key::base_uri()) + { + if !base_uri.is_empty() { + let base_uri = string::from_utf8(base_uri.into_inner()).map_err(|e| { + Error::Revert(alloc::format!( + "Can not convert value \"baseURI\" to string with error \"{}\"", + e + )) + })?; + if let Ok(suffix) = get_token_property(self, token_id_u32, &key::suffix()) { + if !suffix.is_empty() { + return Ok(base_uri + suffix.as_str()); + } + } - let properties = >::try_get((self.id, token_id)) - .map_err(|_| Error::Revert("Token properties not found".into()))?; - if let Some(property) = properties.get(&key) { - return Ok(string::from_utf8_lossy(property).into()); + return Ok(base_uri + token_id.to_string().as_str()); + } } - Err("Property tokenURI not found".into()) + Ok("".into()) } } @@ -469,7 +493,7 @@ token_id: uint256, token_uri: string, ) -> Result { - let key = token_uri_key(); + let key = key::url(); let permission = get_token_permission::(self.id, &key)?; if !permission.collection_admin { return Err("Operation is not allowed".into()); @@ -520,6 +544,32 @@ } } +fn get_token_property( + collection: &CollectionHandle, + token_id: u32, + key: &up_data_structs::PropertyKey, +) -> Result { + collection.consume_store_reads(1)?; + let properties = >::try_get((collection.id, token_id)) + .map_err(|_| Error::Revert("Token properties not found".into()))?; + if let Some(property) = properties.get(key) { + return Ok(string::from_utf8_lossy(property).into()); + } + + Err("Property tokenURI not found".into()) +} + +fn is_erc721_metadata_compatible(collection_id: CollectionId) -> bool { + if let Some(shema_name) = + pallet_common::Pallet::::get_collection_property(collection_id, &key::schema_name()) + { + let shema_name = shema_name.into_inner(); + shema_name == property_value::ERC721_METADATA + } else { + false + } +} + fn get_token_permission( collection_id: CollectionId, key: &PropertyKey, @@ -528,8 +578,11 @@ .map_err(|_| Error::Revert("No permissions for collection".into()))?; let a = token_property_permissions .get(key) - .map(|p| p.clone()) - .ok_or_else(|| Error::Revert("No permission".into()))?; + .map(Clone::clone) + .ok_or_else(|| { + let key = string::from_utf8(key.clone().into_inner()).unwrap_or_default(); + Error::Revert(alloc::format!("No permission for key {}", key)) + })?; Ok(a) } @@ -656,7 +709,7 @@ to: address, tokens: Vec<(uint256, string)>, ) -> Result { - let key = token_uri_key(); + let key = key::url(); let caller = T::CrossAccountId::from_eth(caller); let to = T::CrossAccountId::from_eth(to); let mut expected_index = >::get(self.id) --- a/pallets/refungible/CHANGELOG.md +++ b/pallets/refungible/CHANGELOG.md @@ -10,6 +10,8 @@ test(refungible-pallet): add tests for ERC-20 EVM API for RFT token pieces ([#413](https://github.com/UniqueNetwork/unique-chain/pull/413)) ## [v0.1.1] - 2022-07-14 +### Added +- Support for properties for RFT collections and tokens. ### Other changes --- a/pallets/refungible/Changelog.md +++ /dev/null @@ -1,3 +0,0 @@ -### 0.1.1 ---- -* Added support for properties for RFT collections and tokens. --- a/pallets/refungible/src/erc.rs +++ b/pallets/refungible/src/erc.rs @@ -25,14 +25,14 @@ use crate::{Config, RefungibleHandle}; #[solidity_interface( - name = "UniqueRFT", + name = "UniqueRefungible", is(via("CollectionHandle", common_mut, Collection),) )] impl RefungibleHandle where T::AccountId: From<[u8; 32]> {} // Not a tests, but code generators -generate_stubgen!(gen_impl, UniqueRFTCall<()>, true); -generate_stubgen!(gen_iface, UniqueRFTCall<()>, false); +generate_stubgen!(gen_impl, UniqueRefungibleCall<()>, true); +generate_stubgen!(gen_iface, UniqueRefungibleCall<()>, false); impl CommonEvmHandler for RefungibleHandle where @@ -43,6 +43,6 @@ self, handle: &mut impl PrecompileHandle, ) -> Option { - call::, _, _>(handle, self) + call::, _, _>(handle, self) } } --- /dev/null +++ b/pallets/refungible/src/stubs/UniqueRefungible.sol @@ -0,0 +1,251 @@ +// SPDX-License-Identifier: OTHER +// This code is automatically generated + +pragma solidity >=0.8.0 <0.9.0; + +// Common stubs holder +contract Dummy { + uint8 dummy; + string stub_error = "this contract is implemented in native"; +} + +contract ERC165 is Dummy { + function supportsInterface(bytes4 interfaceID) + external + view + returns (bool) + { + require(false, stub_error); + interfaceID; + return true; + } +} + +// Selector: 7d9262e6 +contract Collection is Dummy, ERC165 { + // Set collection property. + // + // @param key Property key. + // @param value Propery value. + // + // Selector: setCollectionProperty(string,bytes) 2f073f66 + function setCollectionProperty(string memory key, bytes memory value) + public + { + require(false, stub_error); + key; + value; + dummy = 0; + } + + // Delete collection property. + // + // @param key Property key. + // + // Selector: deleteCollectionProperty(string) 7b7debce + function deleteCollectionProperty(string memory key) public { + require(false, stub_error); + key; + dummy = 0; + } + + // Get collection property. + // + // @dev Throws error if key not found. + // + // @param key Property key. + // @return bytes The property corresponding to the key. + // + // Selector: collectionProperty(string) cf24fd6d + function collectionProperty(string memory key) + public + view + returns (bytes memory) + { + require(false, stub_error); + key; + dummy; + return hex""; + } + + // Set the sponsor of the collection. + // + // @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. + // + // @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract. + // + // Selector: setCollectionSponsor(address) 7623402e + function setCollectionSponsor(address sponsor) public { + require(false, stub_error); + sponsor; + dummy = 0; + } + + // Collection sponsorship confirmation. + // + // @dev After setting the sponsor for the collection, it must be confirmed with this function. + // + // Selector: confirmCollectionSponsorship() 3c50e97a + function confirmCollectionSponsorship() public { + require(false, stub_error); + dummy = 0; + } + + // Set limits for the collection. + // @dev Throws error if limit not found. + // @param limit Name of the limit. Valid names: + // "accountTokenOwnershipLimit", + // "sponsoredDataSize", + // "sponsoredDataRateLimit", + // "tokenLimit", + // "sponsorTransferTimeout", + // "sponsorApproveTimeout" + // @param value Value of the limit. + // + // Selector: setCollectionLimit(string,uint32) 6a3841db + function setCollectionLimit(string memory limit, uint32 value) public { + require(false, stub_error); + limit; + value; + dummy = 0; + } + + // Set limits for the collection. + // @dev Throws error if limit not found. + // @param limit Name of the limit. Valid names: + // "ownerCanTransfer", + // "ownerCanDestroy", + // "transfersEnabled" + // @param value Value of the limit. + // + // Selector: setCollectionLimit(string,bool) 993b7fba + function setCollectionLimit(string memory limit, bool value) public { + require(false, stub_error); + limit; + value; + dummy = 0; + } + + // Get contract address. + // + // Selector: contractAddress() f6b4dfb4 + function contractAddress() public view returns (address) { + require(false, stub_error); + dummy; + return 0x0000000000000000000000000000000000000000; + } + + // Add collection admin by substrate address. + // @param new_admin Substrate administrator address. + // + // Selector: addCollectionAdminSubstrate(uint256) 5730062b + function addCollectionAdminSubstrate(uint256 newAdmin) public { + require(false, stub_error); + newAdmin; + dummy = 0; + } + + // Remove collection admin by substrate address. + // @param admin Substrate administrator address. + // + // Selector: removeCollectionAdminSubstrate(uint256) 4048fcf9 + function removeCollectionAdminSubstrate(uint256 admin) public { + require(false, stub_error); + admin; + dummy = 0; + } + + // Add collection admin. + // @param new_admin Address of the added administrator. + // + // Selector: addCollectionAdmin(address) 92e462c7 + function addCollectionAdmin(address newAdmin) public { + require(false, stub_error); + newAdmin; + dummy = 0; + } + + // Remove collection admin. + // + // @param new_admin Address of the removed administrator. + // + // Selector: removeCollectionAdmin(address) fafd7b42 + function removeCollectionAdmin(address admin) public { + require(false, stub_error); + admin; + dummy = 0; + } + + // Toggle accessibility of collection nesting. + // + // @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled' + // + // Selector: setCollectionNesting(bool) 112d4586 + function setCollectionNesting(bool enable) public { + require(false, stub_error); + enable; + dummy = 0; + } + + // Toggle accessibility of collection nesting. + // + // @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled' + // @param collections Addresses of collections that will be available for nesting. + // + // Selector: setCollectionNesting(bool,address[]) 64872396 + function setCollectionNesting(bool enable, address[] memory collections) + public + { + require(false, stub_error); + enable; + collections; + dummy = 0; + } + + // Set the collection access method. + // @param mode Access mode + // 0 for Normal + // 1 for AllowList + // + // Selector: setCollectionAccess(uint8) 41835d4c + function setCollectionAccess(uint8 mode) public { + require(false, stub_error); + mode; + dummy = 0; + } + + // Add the user to the allowed list. + // + // @param user Address of a trusted user. + // + // Selector: addToCollectionAllowList(address) 67844fe6 + function addToCollectionAllowList(address user) public { + require(false, stub_error); + user; + dummy = 0; + } + + // Remove the user from the allowed list. + // + // @param user Address of a removed user. + // + // Selector: removeFromCollectionAllowList(address) 85c51acb + function removeFromCollectionAllowList(address user) public { + require(false, stub_error); + user; + dummy = 0; + } + + // Switch permission for minting. + // + // @param mode Enable if "true". + // + // Selector: setCollectionMintMode(bool) 00018e84 + function setCollectionMintMode(bool mode) public { + require(false, stub_error); + mode; + dummy = 0; + } +} + +contract UniqueRefungible is Dummy, ERC165, Collection {} --- a/pallets/refungible/src/stubs/UniqueRefungibleToken.sol +++ b/pallets/refungible/src/stubs/UniqueRefungibleToken.sol @@ -33,6 +33,8 @@ // Selector: 942e8b22 contract ERC20 is Dummy, ERC165, ERC20Events { + // @return the name of the token. + // // Selector: name() 06fdde03 function name() public view returns (string memory) { require(false, stub_error); @@ -40,6 +42,8 @@ return ""; } + // @return the symbol of the token. + // // Selector: symbol() 95d89b41 function symbol() public view returns (string memory) { require(false, stub_error); @@ -47,6 +51,8 @@ return ""; } + // @dev Total number of tokens in existence + // // Selector: totalSupply() 18160ddd function totalSupply() public view returns (uint256) { require(false, stub_error); @@ -54,6 +60,8 @@ return 0; } + // @dev Not supported + // // Selector: decimals() 313ce567 function decimals() public view returns (uint8) { require(false, stub_error); @@ -61,6 +69,10 @@ return 0; } + // @dev Gets the balance of the specified address. + // @param owner The address to query the balance of. + // @return An uint256 representing the amount owned by the passed address. + // // Selector: balanceOf(address) 70a08231 function balanceOf(address owner) public view returns (uint256) { require(false, stub_error); @@ -69,6 +81,10 @@ return 0; } + // @dev Transfer token for a specified address + // @param to The address to transfer to. + // @param amount The amount to be transferred. + // // Selector: transfer(address,uint256) a9059cbb function transfer(address to, uint256 amount) public returns (bool) { require(false, stub_error); @@ -78,6 +94,11 @@ return false; } + // @dev Transfer tokens from one address to another + // @param from address The address which you want to send tokens from + // @param to address The address which you want to transfer to + // @param amount uint256 the amount of tokens to be transferred + // // Selector: transferFrom(address,address,uint256) 23b872dd function transferFrom( address from, @@ -92,6 +113,14 @@ return false; } + // @dev Approve the passed address to spend the specified amount of tokens on behalf of `msg.sender`. + // Beware that changing an allowance with this method brings the risk that someone may use both the old + // and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this + // race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards: + // https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + // @param spender The address which will spend the funds. + // @param amount The amount of tokens to be spent. + // // Selector: approve(address,uint256) 095ea7b3 function approve(address spender, uint256 amount) public returns (bool) { require(false, stub_error); @@ -101,6 +130,11 @@ return false; } + // @dev Function to check the amount of tokens that an owner allowed to a spender. + // @param owner address The address which owns the funds. + // @param spender address The address which will spend the funds. + // @return A uint256 specifying the amount of tokens still available for the spender. + // // Selector: allowance(address,address) dd62ed3e function allowance(address owner, address spender) public @@ -117,6 +151,11 @@ // Selector: ab8deb37 contract ERC20UniqueExtensions is Dummy, ERC165 { + // @dev Function that burns an amount of the token of a given account, + // deducting from the sender's allowance for said account. + // @param from The account whose tokens will be burnt. + // @param amount The amount that will be burnt. + // // Selector: burnFrom(address,uint256) 79cc6790 function burnFrom(address from, uint256 amount) public returns (bool) { require(false, stub_error); @@ -126,6 +165,10 @@ return false; } + // @dev Function that changes total amount of the tokens. + // Throws if `msg.sender` doesn't owns all of the tokens. + // @param amount New total amount of the tokens. + // // Selector: repartition(uint256) d2418ca7 function repartition(uint256 amount) public returns (bool) { require(false, stub_error); --- /dev/null +++ b/pallets/unique/CHANGELOG.md @@ -0,0 +1,9 @@ +# Change Log + +All notable changes to this project will be documented in this file. + +## [v0.1.1] - 2022-07-25 +### Added +- Method for creating `ERC721Metadata` compatible NFT collection. +- Method for creating `ERC721Metadata` compatible ReFungible collection. +- Method for creating ReFungible collection. --- a/pallets/unique/Cargo.toml +++ b/pallets/unique/Cargo.toml @@ -9,7 +9,7 @@ license = 'GPLv3' name = 'pallet-unique' repository = 'https://github.com/UniqueNetwork/unique-chain' -version = '0.1.0' +version = '0.1.1' [package.metadata.docs.rs] targets = ['x86_64-unknown-linux-gnu'] @@ -103,3 +103,4 @@ evm-coder = { default-features = false, path = '../../crates/evm-coder' } pallet-evm-coder-substrate = { default-features = false, path = '../../pallets/evm-coder-substrate' } pallet-nonfungible = { default-features = false, path = '../../pallets/nonfungible' } +pallet-refungible = { default-features = false, path = '../../pallets/refungible' } --- a/pallets/unique/src/eth/mod.rs +++ b/pallets/unique/src/eth/mod.rs @@ -17,18 +17,22 @@ //! Implementation of CollectionHelpers contract. use core::marker::PhantomData; -use evm_coder::{execution::*, generate_stubgen, solidity_interface, weight, types::*}; +use evm_coder::{execution::*, generate_stubgen, solidity_interface, solidity, weight, types::*}; use ethereum as _; use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder}; use pallet_evm::{OnMethodCall, PrecompileResult, account::CrossAccountId, PrecompileHandle}; use up_data_structs::{ - CreateCollectionData, MAX_COLLECTION_DESCRIPTION_LENGTH, MAX_TOKEN_PREFIX_LENGTH, - MAX_COLLECTION_NAME_LENGTH, + CollectionName, CollectionDescription, CollectionTokenPrefix, CreateCollectionData, + CollectionMode, PropertyValue, }; use frame_support::traits::Get; use pallet_common::{ CollectionById, - erc::{token_uri_key, CollectionHelpersEvents}, + erc::{ + static_property::{key, value as property_value}, + CollectionHelpersEvents, + }, + dispatch::CollectionDispatch, }; use crate::{SelfWeightOf, Config, weights::WeightInfo}; @@ -47,9 +51,112 @@ } } +fn convert_data( + caller: caller, + name: string, + description: string, + token_prefix: string, + base_uri: string, +) -> Result<( + T::CrossAccountId, + CollectionName, + CollectionDescription, + CollectionTokenPrefix, + PropertyValue, +)> { + let caller = T::CrossAccountId::from_eth(caller); + let name = name + .encode_utf16() + .collect::>() + .try_into() + .map_err(|_| error_field_too_long(stringify!(name), CollectionName::bound()))?; + let description = description + .encode_utf16() + .collect::>() + .try_into() + .map_err(|_| { + error_field_too_long(stringify!(description), CollectionDescription::bound()) + })?; + let token_prefix = token_prefix.into_bytes().try_into().map_err(|_| { + error_field_too_long(stringify!(token_prefix), CollectionTokenPrefix::bound()) + })?; + let base_uri_value = base_uri + .into_bytes() + .try_into() + .map_err(|_| error_field_too_long(stringify!(token_prefix), PropertyValue::bound()))?; + Ok((caller, name, description, token_prefix, base_uri_value)) +} + +fn make_data( + name: CollectionName, + mode: CollectionMode, + description: CollectionDescription, + token_prefix: CollectionTokenPrefix, + base_uri_value: PropertyValue, + add_properties: bool, +) -> Result> { + let mut properties = up_data_structs::CollectionPropertiesVec::default(); + let mut token_property_permissions = + up_data_structs::CollectionPropertiesPermissionsVec::default(); + + token_property_permissions + .try_push(up_data_structs::PropertyKeyPermission { + key: key::url(), + permission: up_data_structs::PropertyPermission { + mutable: false, + collection_admin: true, + token_owner: false, + }, + }) + .map_err(|e| Error::Revert(format!("{:?}", e)))?; + + if add_properties { + token_property_permissions + .try_push(up_data_structs::PropertyKeyPermission { + key: key::suffix(), + permission: up_data_structs::PropertyPermission { + mutable: false, + collection_admin: true, + token_owner: false, + }, + }) + .map_err(|e| Error::Revert(format!("{:?}", e)))?; + + properties + .try_push(up_data_structs::Property { + key: key::schema_name(), + value: property_value::erc721(), + }) + .map_err(|e| Error::Revert(format!("{:?}", e)))?; + + if !base_uri_value.is_empty() { + properties + .try_push(up_data_structs::Property { + key: key::base_uri(), + value: base_uri_value, + }) + .map_err(|e| Error::Revert(format!("{:?}", e)))?; + } + } + + let data = CreateCollectionData { + name, + mode, + description, + token_prefix, + token_property_permissions, + properties, + ..Default::default() + }; + Ok(data) +} + /// @title Contract, which allows users to operate with collections #[solidity_interface(name = "CollectionHelpers", events(CollectionHelpersEvents))] -impl EvmCollectionHelpers { +impl EvmCollectionHelpers +where + T: Config + pallet_nonfungible::Config + pallet_refungible::Config, +{ /// Create an NFT collection /// @param name Name of the collection /// @param description Informative description of the collection @@ -63,47 +170,97 @@ description: string, token_prefix: string, ) -> Result
{ - let caller = T::CrossAccountId::from_eth(caller); - let name = name - .encode_utf16() - .collect::>() - .try_into() - .map_err(|_| error_feild_too_long(stringify!(name), MAX_COLLECTION_NAME_LENGTH))?; - let description = description - .encode_utf16() - .collect::>() - .try_into() - .map_err(|_| { - error_feild_too_long(stringify!(description), MAX_COLLECTION_DESCRIPTION_LENGTH) - })?; - let token_prefix = token_prefix - .into_bytes() - .try_into() - .map_err(|_| error_feild_too_long(stringify!(token_prefix), MAX_TOKEN_PREFIX_LENGTH))?; + let (caller, name, description, token_prefix, _base_uri_value) = + convert_data::(caller, name, description, token_prefix, "".into())?; + let data = make_data::( + name, + CollectionMode::NFT, + description, + token_prefix, + Default::default(), + false, + )?; + let collection_id = T::CollectionDispatch::create(caller, data) + .map_err(pallet_evm_coder_substrate::dispatch_to_evm::)?; - let key = token_uri_key(); - let permission = up_data_structs::PropertyPermission { - mutable: true, - collection_admin: true, - token_owner: false, - }; - let mut token_property_permissions = - up_data_structs::CollectionPropertiesPermissionsVec::default(); - token_property_permissions - .try_push(up_data_structs::PropertyKeyPermission { key, permission }) - .map_err(|e| Error::Revert(format!("{:?}", e)))?; + let address = pallet_common::eth::collection_id_to_address(collection_id); + Ok(address) + } - let data = CreateCollectionData { + #[weight(>::create_collection())] + #[solidity(rename_selector = "createERC721MetadataCompatibleCollection")] + fn create_nonfungible_collection_with_properties( + &mut self, + caller: caller, + name: string, + description: string, + token_prefix: string, + base_uri: string, + ) -> Result
{ + let (caller, name, description, token_prefix, base_uri_value) = + convert_data::(caller, name, description, token_prefix, base_uri)?; + let data = make_data::( name, + CollectionMode::NFT, description, token_prefix, - token_property_permissions, - ..Default::default() - }; + base_uri_value, + true, + )?; + let collection_id = T::CollectionDispatch::create(caller, data) + .map_err(pallet_evm_coder_substrate::dispatch_to_evm::)?; - let collection_id = - >::init_collection(caller.clone(), data, false) - .map_err(pallet_evm_coder_substrate::dispatch_to_evm::)?; + let address = pallet_common::eth::collection_id_to_address(collection_id); + Ok(address) + } + + #[weight(>::create_collection())] + fn create_refungible_collection( + &self, + caller: caller, + name: string, + description: string, + token_prefix: string, + ) -> Result
{ + let (caller, name, description, token_prefix, _base_uri) = + convert_data::(caller, name, description, token_prefix, "".into())?; + let data = make_data::( + name, + CollectionMode::ReFungible, + description, + token_prefix, + Default::default(), + false, + )?; + let collection_id = T::CollectionDispatch::create(caller, data) + .map_err(pallet_evm_coder_substrate::dispatch_to_evm::)?; + + let address = pallet_common::eth::collection_id_to_address(collection_id); + Ok(address) + } + + #[weight(>::create_collection())] + #[solidity(rename_selector = "createERC721MetadataCompatibleRFTCollection")] + fn create_refungible_collection_with_properties( + &mut self, + caller: caller, + name: string, + description: string, + token_prefix: string, + base_uri: string, + ) -> Result
{ + let (caller, name, description, token_prefix, base_uri_value) = + convert_data::(caller, name, description, token_prefix, base_uri)?; + let data = make_data::( + name, + CollectionMode::NFT, + description, + token_prefix, + base_uri_value, + true, + )?; + let collection_id = T::CollectionDispatch::create(caller, data) + .map_err(pallet_evm_coder_substrate::dispatch_to_evm::)?; let address = pallet_common::eth::collection_id_to_address(collection_id); Ok(address) @@ -124,7 +281,9 @@ /// Implements [`OnMethodCall`], which delegates call to [`EvmCollectionHelpers`] pub struct CollectionHelpersOnMethodCall(PhantomData<*const T>); -impl OnMethodCall for CollectionHelpersOnMethodCall { +impl OnMethodCall + for CollectionHelpersOnMethodCall +{ fn is_reserved(contract: &sp_core::H160) -> bool { contract == &T::ContractAddress::get() } @@ -152,6 +311,6 @@ generate_stubgen!(collection_helper_impl, CollectionHelpersCall<()>, true); generate_stubgen!(collection_helper_iface, CollectionHelpersCall<()>, false); -fn error_feild_too_long(feild: &str, bound: u32) -> Error { +fn error_field_too_long(feild: &str, bound: usize) -> Error { Error::Revert(format!("{} is too long. Max length is {}.", feild, bound)) } --- a/pallets/unique/src/eth/stubs/CollectionHelpers.sol +++ b/pallets/unique/src/eth/stubs/CollectionHelpers.sol @@ -29,7 +29,7 @@ ); } -// Selector: 20947cd0 +// Selector: c20653fc contract CollectionHelpers is Dummy, ERC165, CollectionHelpersEvents { // Selector: createNonfungibleCollection(string,string,string) e34a6844 function createNonfungibleCollection( @@ -45,6 +45,36 @@ return 0x0000000000000000000000000000000000000000; } + // Selector: createERC721MetadataCompatibleCollection(string,string,string,string) a634a5f9 + function createERC721MetadataCompatibleCollection( + string memory name, + string memory description, + string memory tokenPrefix, + string memory baseUri + ) public returns (address) { + require(false, stub_error); + name; + description; + tokenPrefix; + baseUri; + dummy = 0; + return 0x0000000000000000000000000000000000000000; + } + + // Selector: createRefungibleCollection(string,string,string) 44a68ad5 + function createRefungibleCollection( + string memory name, + string memory description, + string memory tokenPrefix + ) public view returns (address) { + require(false, stub_error); + name; + description; + tokenPrefix; + dummy; + return 0x0000000000000000000000000000000000000000; + } + // Selector: isCollectionExist(address) c3de1494 function isCollectionExist(address collectionAddress) public --- a/pallets/unique/src/lib.rs +++ b/pallets/unique/src/lib.rs @@ -352,7 +352,7 @@ // ========= - T::CollectionDispatch::create(T::CrossAccountId::from_sub(sender), data)?; + let _id = T::CollectionDispatch::create(T::CrossAccountId::from_sub(sender), data)?; Ok(()) } --- /dev/null +++ b/primitives/data-structs/CHANGELOG.md @@ -0,0 +1,11 @@ +# Change Log + +All notable changes to this project will be documented in this file. + + +## [v0.1.2] - 2022-07-25 +### Added +- Type aliases `CollectionName`, `CollectionDescription`, `CollectionTokenPrefix` +## [v0.1.1] - 2022-07-22 +### Added +- Аields with properties to `CreateReFungibleData` and `CreateRefungibleExData`. \ No newline at end of file --- a/primitives/data-structs/Cargo.toml +++ b/primitives/data-structs/Cargo.toml @@ -6,7 +6,7 @@ license = 'GPLv3' homepage = "https://unique.network" repository = 'https://github.com/UniqueNetwork/unique-chain' -version = '0.1.1' +version = '0.1.2' [dependencies] scale-info = { version = "2.0.1", default-features = false, features = [ --- a/primitives/data-structs/Changelog.md +++ /dev/null @@ -1,3 +0,0 @@ -### 0.1.1 ---- -* Added fields with properties to `CreateReFungibleData` and `CreateRefungibleExData`. \ No newline at end of file --- a/primitives/data-structs/src/lib.rs +++ b/primitives/data-structs/src/lib.rs @@ -286,6 +286,10 @@ } } +pub type CollectionName = BoundedVec>; +pub type CollectionDescription = BoundedVec>; +pub type CollectionTokenPrefix = BoundedVec>; + /// Collection parameters, used in storage (see [`RpcCollection`] for the RPC version). #[struct_versioning::versioned(version = 2, upper)] #[derive(Encode, Decode, Clone, PartialEq, TypeInfo, MaxEncodedLen)] @@ -294,9 +298,9 @@ pub mode: CollectionMode, #[version(..2)] pub access: AccessMode, - pub name: BoundedVec>, - pub description: BoundedVec>, - pub token_prefix: BoundedVec>, + pub name: CollectionName, + pub description: CollectionDescription, + pub token_prefix: CollectionTokenPrefix, #[version(..2)] pub mint_mode: bool, @@ -350,9 +354,9 @@ #[derivative(Default(value = "CollectionMode::NFT"))] pub mode: CollectionMode, pub access: Option, - pub name: BoundedVec>, - pub description: BoundedVec>, - pub token_prefix: BoundedVec>, + pub name: CollectionName, + pub description: CollectionDescription, + pub token_prefix: CollectionTokenPrefix, pub pending_sponsor: Option, pub limits: Option, pub permissions: Option, --- a/runtime/common/src/dispatch.rs +++ b/runtime/common/src/dispatch.rs @@ -17,6 +17,7 @@ use frame_support::{dispatch::DispatchResult, ensure}; use pallet_evm::{PrecompileHandle, PrecompileResult}; use sp_core::H160; +use sp_runtime::DispatchError; use sp_std::{borrow::ToOwned, vec::Vec}; use pallet_common::{ CollectionById, CollectionHandle, CommonCollectionOperations, erc::CommonEvmHandler, @@ -30,6 +31,7 @@ }; use up_data_structs::{ CollectionMode, CreateCollectionData, MAX_DECIMAL_POINTS, mapping::TokenAddressMapping, + CollectionId, }; pub enum CollectionDispatchT @@ -51,8 +53,8 @@ fn create( sender: T::CrossAccountId, data: CreateCollectionData, - ) -> DispatchResult { - let _id = match data.mode { + ) -> Result { + let id = match data.mode { CollectionMode::NFT => >::init_collection(sender, data, false)?, CollectionMode::Fungible(decimal_points) => { // check params @@ -64,7 +66,7 @@ } CollectionMode::ReFungible => >::init_collection(sender, data)?, }; - Ok(()) + Ok(id) } fn destroy(sender: T::CrossAccountId, collection: CollectionHandle) -> DispatchResult { --- a/tests/src/eth/api/CollectionHelpers.sol +++ b/tests/src/eth/api/CollectionHelpers.sol @@ -20,7 +20,7 @@ ); } -// Selector: 20947cd0 +// Selector: c20653fc interface CollectionHelpers is Dummy, ERC165, CollectionHelpersEvents { // Selector: createNonfungibleCollection(string,string,string) e34a6844 function createNonfungibleCollection( @@ -29,6 +29,21 @@ string memory tokenPrefix ) external returns (address); + // Selector: createERC721MetadataCompatibleCollection(string,string,string,string) a634a5f9 + function createERC721MetadataCompatibleCollection( + string memory name, + string memory description, + string memory tokenPrefix, + string memory baseUri + ) external returns (address); + + // Selector: createRefungibleCollection(string,string,string) 44a68ad5 + function createRefungibleCollection( + string memory name, + string memory description, + string memory tokenPrefix + ) external view returns (address); + // Selector: isCollectionExist(address) c3de1494 function isCollectionExist(address collectionAddress) external --- /dev/null +++ b/tests/src/eth/api/UniqueRFT.sol @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: OTHER +// This code is automatically generated + +pragma solidity >=0.8.0 <0.9.0; + +// Common stubs holder +interface Dummy { + +} + +interface ERC165 is Dummy { + function supportsInterface(bytes4 interfaceID) external view returns (bool); +} + +// Selector: 7d9262e6 +interface Collection is Dummy, ERC165 { + // Set collection property. + // + // @param key Property key. + // @param value Propery value. + // + // Selector: setCollectionProperty(string,bytes) 2f073f66 + function setCollectionProperty(string memory key, bytes memory value) + external; + + // Delete collection property. + // + // @param key Property key. + // + // Selector: deleteCollectionProperty(string) 7b7debce + function deleteCollectionProperty(string memory key) external; + + // Get collection property. + // + // @dev Throws error if key not found. + // + // @param key Property key. + // @return bytes The property corresponding to the key. + // + // Selector: collectionProperty(string) cf24fd6d + function collectionProperty(string memory key) + external + view + returns (bytes memory); + + // Set the sponsor of the collection. + // + // @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. + // + // @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract. + // + // Selector: setCollectionSponsor(address) 7623402e + function setCollectionSponsor(address sponsor) external; + + // Collection sponsorship confirmation. + // + // @dev After setting the sponsor for the collection, it must be confirmed with this function. + // + // Selector: confirmCollectionSponsorship() 3c50e97a + function confirmCollectionSponsorship() external; + + // Set limits for the collection. + // @dev Throws error if limit not found. + // @param limit Name of the limit. Valid names: + // "accountTokenOwnershipLimit", + // "sponsoredDataSize", + // "sponsoredDataRateLimit", + // "tokenLimit", + // "sponsorTransferTimeout", + // "sponsorApproveTimeout" + // @param value Value of the limit. + // + // Selector: setCollectionLimit(string,uint32) 6a3841db + function setCollectionLimit(string memory limit, uint32 value) external; + + // Set limits for the collection. + // @dev Throws error if limit not found. + // @param limit Name of the limit. Valid names: + // "ownerCanTransfer", + // "ownerCanDestroy", + // "transfersEnabled" + // @param value Value of the limit. + // + // Selector: setCollectionLimit(string,bool) 993b7fba + function setCollectionLimit(string memory limit, bool value) external; + + // Get contract address. + // + // Selector: contractAddress() f6b4dfb4 + function contractAddress() external view returns (address); + + // Add collection admin by substrate address. + // @param new_admin Substrate administrator address. + // + // Selector: addCollectionAdminSubstrate(uint256) 5730062b + function addCollectionAdminSubstrate(uint256 newAdmin) external; + + // Remove collection admin by substrate address. + // @param admin Substrate administrator address. + // + // Selector: removeCollectionAdminSubstrate(uint256) 4048fcf9 + function removeCollectionAdminSubstrate(uint256 admin) external; + + // Add collection admin. + // @param new_admin Address of the added administrator. + // + // Selector: addCollectionAdmin(address) 92e462c7 + function addCollectionAdmin(address newAdmin) external; + + // Remove collection admin. + // + // @param new_admin Address of the removed administrator. + // + // Selector: removeCollectionAdmin(address) fafd7b42 + function removeCollectionAdmin(address admin) external; + + // Toggle accessibility of collection nesting. + // + // @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled' + // + // Selector: setCollectionNesting(bool) 112d4586 + function setCollectionNesting(bool enable) external; + + // Toggle accessibility of collection nesting. + // + // @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled' + // @param collections Addresses of collections that will be available for nesting. + // + // Selector: setCollectionNesting(bool,address[]) 64872396 + function setCollectionNesting(bool enable, address[] memory collections) + external; + + // Set the collection access method. + // @param mode Access mode + // 0 for Normal + // 1 for AllowList + // + // Selector: setCollectionAccess(uint8) 41835d4c + function setCollectionAccess(uint8 mode) external; + + // Add the user to the allowed list. + // + // @param user Address of a trusted user. + // + // Selector: addToCollectionAllowList(address) 67844fe6 + function addToCollectionAllowList(address user) external; + + // Remove the user from the allowed list. + // + // @param user Address of a removed user. + // + // Selector: removeFromCollectionAllowList(address) 85c51acb + function removeFromCollectionAllowList(address user) external; + + // Switch permission for minting. + // + // @param mode Enable if "true". + // + // Selector: setCollectionMintMode(bool) 00018e84 + function setCollectionMintMode(bool mode) external; +} + +interface UniqueRFT is Dummy, ERC165, Collection {} --- /dev/null +++ b/tests/src/eth/api/UniqueRefungible.sol @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: OTHER +// This code is automatically generated + +pragma solidity >=0.8.0 <0.9.0; + +// Common stubs holder +interface Dummy { + +} + +interface ERC165 is Dummy { + function supportsInterface(bytes4 interfaceID) external view returns (bool); +} + +// Selector: 7d9262e6 +interface Collection is Dummy, ERC165 { + // Set collection property. + // + // @param key Property key. + // @param value Propery value. + // + // Selector: setCollectionProperty(string,bytes) 2f073f66 + function setCollectionProperty(string memory key, bytes memory value) + external; + + // Delete collection property. + // + // @param key Property key. + // + // Selector: deleteCollectionProperty(string) 7b7debce + function deleteCollectionProperty(string memory key) external; + + // Get collection property. + // + // @dev Throws error if key not found. + // + // @param key Property key. + // @return bytes The property corresponding to the key. + // + // Selector: collectionProperty(string) cf24fd6d + function collectionProperty(string memory key) + external + view + returns (bytes memory); + + // Set the sponsor of the collection. + // + // @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. + // + // @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract. + // + // Selector: setCollectionSponsor(address) 7623402e + function setCollectionSponsor(address sponsor) external; + + // Collection sponsorship confirmation. + // + // @dev After setting the sponsor for the collection, it must be confirmed with this function. + // + // Selector: confirmCollectionSponsorship() 3c50e97a + function confirmCollectionSponsorship() external; + + // Set limits for the collection. + // @dev Throws error if limit not found. + // @param limit Name of the limit. Valid names: + // "accountTokenOwnershipLimit", + // "sponsoredDataSize", + // "sponsoredDataRateLimit", + // "tokenLimit", + // "sponsorTransferTimeout", + // "sponsorApproveTimeout" + // @param value Value of the limit. + // + // Selector: setCollectionLimit(string,uint32) 6a3841db + function setCollectionLimit(string memory limit, uint32 value) external; + + // Set limits for the collection. + // @dev Throws error if limit not found. + // @param limit Name of the limit. Valid names: + // "ownerCanTransfer", + // "ownerCanDestroy", + // "transfersEnabled" + // @param value Value of the limit. + // + // Selector: setCollectionLimit(string,bool) 993b7fba + function setCollectionLimit(string memory limit, bool value) external; + + // Get contract address. + // + // Selector: contractAddress() f6b4dfb4 + function contractAddress() external view returns (address); + + // Add collection admin by substrate address. + // @param new_admin Substrate administrator address. + // + // Selector: addCollectionAdminSubstrate(uint256) 5730062b + function addCollectionAdminSubstrate(uint256 newAdmin) external; + + // Remove collection admin by substrate address. + // @param admin Substrate administrator address. + // + // Selector: removeCollectionAdminSubstrate(uint256) 4048fcf9 + function removeCollectionAdminSubstrate(uint256 admin) external; + + // Add collection admin. + // @param new_admin Address of the added administrator. + // + // Selector: addCollectionAdmin(address) 92e462c7 + function addCollectionAdmin(address newAdmin) external; + + // Remove collection admin. + // + // @param new_admin Address of the removed administrator. + // + // Selector: removeCollectionAdmin(address) fafd7b42 + function removeCollectionAdmin(address admin) external; + + // Toggle accessibility of collection nesting. + // + // @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled' + // + // Selector: setCollectionNesting(bool) 112d4586 + function setCollectionNesting(bool enable) external; + + // Toggle accessibility of collection nesting. + // + // @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled' + // @param collections Addresses of collections that will be available for nesting. + // + // Selector: setCollectionNesting(bool,address[]) 64872396 + function setCollectionNesting(bool enable, address[] memory collections) + external; + + // Set the collection access method. + // @param mode Access mode + // 0 for Normal + // 1 for AllowList + // + // Selector: setCollectionAccess(uint8) 41835d4c + function setCollectionAccess(uint8 mode) external; + + // Add the user to the allowed list. + // + // @param user Address of a trusted user. + // + // Selector: addToCollectionAllowList(address) 67844fe6 + function addToCollectionAllowList(address user) external; + + // Remove the user from the allowed list. + // + // @param user Address of a removed user. + // + // Selector: removeFromCollectionAllowList(address) 85c51acb + function removeFromCollectionAllowList(address user) external; + + // Switch permission for minting. + // + // @param mode Enable if "true". + // + // Selector: setCollectionMintMode(bool) 00018e84 + function setCollectionMintMode(bool mode) external; +} + +interface UniqueRefungible is Dummy, ERC165, Collection {} --- a/tests/src/eth/api/UniqueRefungibleToken.sol +++ b/tests/src/eth/api/UniqueRefungibleToken.sol @@ -24,24 +24,45 @@ // Selector: 942e8b22 interface ERC20 is Dummy, ERC165, ERC20Events { + // @return the name of the token. + // // Selector: name() 06fdde03 function name() external view returns (string memory); + // @return the symbol of the token. + // // Selector: symbol() 95d89b41 function symbol() external view returns (string memory); + // @dev Total number of tokens in existence + // // Selector: totalSupply() 18160ddd function totalSupply() external view returns (uint256); + // @dev Not supported + // // Selector: decimals() 313ce567 function decimals() external view returns (uint8); + // @dev Gets the balance of the specified address. + // @param owner The address to query the balance of. + // @return An uint256 representing the amount owned by the passed address. + // // Selector: balanceOf(address) 70a08231 function balanceOf(address owner) external view returns (uint256); + // @dev Transfer token for a specified address + // @param to The address to transfer to. + // @param amount The amount to be transferred. + // // Selector: transfer(address,uint256) a9059cbb function transfer(address to, uint256 amount) external returns (bool); + // @dev Transfer tokens from one address to another + // @param from address The address which you want to send tokens from + // @param to address The address which you want to transfer to + // @param amount uint256 the amount of tokens to be transferred + // // Selector: transferFrom(address,address,uint256) 23b872dd function transferFrom( address from, @@ -49,9 +70,22 @@ uint256 amount ) external returns (bool); + // @dev Approve the passed address to spend the specified amount of tokens on behalf of `msg.sender`. + // Beware that changing an allowance with this method brings the risk that someone may use both the old + // and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this + // race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards: + // https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + // @param spender The address which will spend the funds. + // @param amount The amount of tokens to be spent. + // // Selector: approve(address,uint256) 095ea7b3 function approve(address spender, uint256 amount) external returns (bool); + // @dev Function to check the amount of tokens that an owner allowed to a spender. + // @param owner address The address which owns the funds. + // @param spender address The address which will spend the funds. + // @return A uint256 specifying the amount of tokens still available for the spender. + // // Selector: allowance(address,address) dd62ed3e function allowance(address owner, address spender) external @@ -61,9 +95,18 @@ // Selector: ab8deb37 interface ERC20UniqueExtensions is Dummy, ERC165 { + // @dev Function that burns an amount of the token of a given account, + // deducting from the sender's allowance for said account. + // @param from The account whose tokens will be burnt. + // @param amount The amount that will be burnt. + // // Selector: burnFrom(address,uint256) 79cc6790 function burnFrom(address from, uint256 amount) external returns (bool); + // @dev Function that changes total amount of the tokens. + // Throws if `msg.sender` doesn't owns all of the tokens. + // @param amount New total amount of the tokens. + // // Selector: repartition(uint256) d2418ca7 function repartition(uint256 amount) external returns (bool); } --- a/tests/src/eth/collectionHelpersAbi.json +++ b/tests/src/eth/collectionHelpersAbi.json @@ -22,6 +22,18 @@ "inputs": [ { "internalType": "string", "name": "name", "type": "string" }, { "internalType": "string", "name": "description", "type": "string" }, + { "internalType": "string", "name": "tokenPrefix", "type": "string" }, + { "internalType": "string", "name": "baseUri", "type": "string" } + ], + "name": "createERC721MetadataCompatibleCollection", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "string", "name": "name", "type": "string" }, + { "internalType": "string", "name": "description", "type": "string" }, { "internalType": "string", "name": "tokenPrefix", "type": "string" } ], "name": "createNonfungibleCollection", @@ -31,6 +43,17 @@ }, { "inputs": [ + { "internalType": "string", "name": "name", "type": "string" }, + { "internalType": "string", "name": "description", "type": "string" }, + { "internalType": "string", "name": "tokenPrefix", "type": "string" } + ], + "name": "createRefungibleCollection", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ { "internalType": "address", "name": "collectionAddress", --- a/tests/src/eth/createCollection.test.ts +++ /dev/null @@ -1,230 +0,0 @@ -// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. -// This file is part of Unique Network. - -// Unique Network is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Unique Network is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Unique Network. If not, see . - -import {evmToAddress} from '@polkadot/util-crypto'; -import {expect} from 'chai'; -import {getCreatedCollectionCount, getDetailedCollectionInfo} from '../util/helpers'; -import { - evmCollectionHelpers, - collectionIdToAddress, - createEthAccount, - createEthAccountWithBalance, - evmCollection, - itWeb3, - getCollectionAddressFromResult, -} from './util/helpers'; - -describe('Create collection from EVM', () => { - // itWeb3('Create collection', async ({api, web3, privateKeyWrapper}) => { - // const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - // const collectionHelper = evmCollectionHelpers(web3, owner); - // const collectionName = 'CollectionEVM'; - // const description = 'Some description'; - // const tokenPrefix = 'token prefix'; - - // const collectionCountBefore = await getCreatedCollectionCount(api); - // const result = await collectionHelper.methods - // .createNonfungibleCollection(collectionName, description, tokenPrefix) - // .send(); - // const collectionCountAfter = await getCreatedCollectionCount(api); - - // const {collectionId, collection} = await getCollectionAddressFromResult(api, result); - // expect(collectionCountAfter - collectionCountBefore).to.be.eq(1); - // expect(collectionId).to.be.eq(collectionCountAfter); - // expect(collection.name.map(v => String.fromCharCode(v.toNumber())).join('')).to.be.eq(collectionName); - // expect(collection.description.map(v => String.fromCharCode(v.toNumber())).join('')).to.be.eq(description); - // expect(collection.tokenPrefix.toHuman()).to.be.eq(tokenPrefix); - // }); - - // itWeb3('Check collection address exist', async ({api, web3, privateKeyWrapper}) => { - // const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - // const collectionHelpers = evmCollectionHelpers(web3, owner); - - // const expectedCollectionId = await getCreatedCollectionCount(api) + 1; - // const expectedCollectionAddress = collectionIdToAddress(expectedCollectionId); - // expect(await collectionHelpers.methods - // .isCollectionExist(expectedCollectionAddress) - // .call()).to.be.false; - - // await collectionHelpers.methods - // .createNonfungibleCollection('A', 'A', 'A') - // .send(); - - // expect(await collectionHelpers.methods - // .isCollectionExist(expectedCollectionAddress) - // .call()).to.be.true; - // }); - - itWeb3('Set sponsorship', async ({api, web3, privateKeyWrapper}) => { - const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - const collectionHelpers = evmCollectionHelpers(web3, owner); - let result = await collectionHelpers.methods.createNonfungibleCollection('Sponsor collection', '1', '1').send(); - const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result); - const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - const collectionEvm = evmCollection(web3, owner, collectionIdAddress); - result = await collectionEvm.methods.setCollectionSponsor(sponsor).send(); - let collectionSub = (await getDetailedCollectionInfo(api, collectionId))!; - expect(collectionSub.sponsorship.isUnconfirmed).to.be.true; - const ss58Format = (api.registry.getChainProperties())!.toJSON().ss58Format; - expect(collectionSub.sponsorship.asUnconfirmed.toHuman()).to.be.eq(evmToAddress(sponsor, Number(ss58Format))); - await expect(collectionEvm.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('caller is not set as sponsor'); - const sponsorCollection = evmCollection(web3, sponsor, collectionIdAddress); - await sponsorCollection.methods.confirmCollectionSponsorship().send(); - collectionSub = (await getDetailedCollectionInfo(api, collectionId))!; - expect(collectionSub.sponsorship.isConfirmed).to.be.true; - expect(collectionSub.sponsorship.asConfirmed.toHuman()).to.be.eq(evmToAddress(sponsor, Number(ss58Format))); - }); - - itWeb3('Set limits', async ({api, web3, privateKeyWrapper}) => { - const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - const collectionHelpers = evmCollectionHelpers(web3, owner); - const result = await collectionHelpers.methods.createNonfungibleCollection('Const collection', '5', '5').send(); - const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result); - const limits = { - accountTokenOwnershipLimit: 1000, - sponsoredDataSize: 1024, - sponsoredDataRateLimit: 30, - tokenLimit: 1000000, - sponsorTransferTimeout: 6, - sponsorApproveTimeout: 6, - ownerCanTransfer: false, - ownerCanDestroy: false, - transfersEnabled: false, - }; - - const collectionEvm = evmCollection(web3, owner, collectionIdAddress); - await collectionEvm.methods['setCollectionLimit(string,uint32)']('accountTokenOwnershipLimit', limits.accountTokenOwnershipLimit).send(); - await collectionEvm.methods['setCollectionLimit(string,uint32)']('sponsoredDataSize', limits.sponsoredDataSize).send(); - await collectionEvm.methods['setCollectionLimit(string,uint32)']('sponsoredDataRateLimit', limits.sponsoredDataRateLimit).send(); - await collectionEvm.methods['setCollectionLimit(string,uint32)']('tokenLimit', limits.tokenLimit).send(); - await collectionEvm.methods['setCollectionLimit(string,uint32)']('sponsorTransferTimeout', limits.sponsorTransferTimeout).send(); - await collectionEvm.methods['setCollectionLimit(string,uint32)']('sponsorApproveTimeout', limits.sponsorApproveTimeout).send(); - await collectionEvm.methods['setCollectionLimit(string,bool)']('ownerCanTransfer', limits.ownerCanTransfer).send(); - await collectionEvm.methods['setCollectionLimit(string,bool)']('ownerCanDestroy', limits.ownerCanDestroy).send(); - await collectionEvm.methods['setCollectionLimit(string,bool)']('transfersEnabled', limits.transfersEnabled).send(); - - const collectionSub = (await getDetailedCollectionInfo(api, collectionId))!; - expect(collectionSub.limits.accountTokenOwnershipLimit.unwrap().toNumber()).to.be.eq(limits.accountTokenOwnershipLimit); - expect(collectionSub.limits.sponsoredDataSize.unwrap().toNumber()).to.be.eq(limits.sponsoredDataSize); - expect(collectionSub.limits.sponsoredDataRateLimit.unwrap().asBlocks.toNumber()).to.be.eq(limits.sponsoredDataRateLimit); - expect(collectionSub.limits.tokenLimit.unwrap().toNumber()).to.be.eq(limits.tokenLimit); - expect(collectionSub.limits.sponsorTransferTimeout.unwrap().toNumber()).to.be.eq(limits.sponsorTransferTimeout); - expect(collectionSub.limits.sponsorApproveTimeout.unwrap().toNumber()).to.be.eq(limits.sponsorApproveTimeout); - expect(collectionSub.limits.ownerCanTransfer.toHuman()).to.be.eq(limits.ownerCanTransfer); - expect(collectionSub.limits.ownerCanDestroy.toHuman()).to.be.eq(limits.ownerCanDestroy); - expect(collectionSub.limits.transfersEnabled.toHuman()).to.be.eq(limits.transfersEnabled); - }); - - itWeb3('Collection address exist', async ({api, web3, privateKeyWrapper}) => { - const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - const collectionAddressForNonexistentCollection = '0x17C4E6453CC49AAAAEACA894E6D9683E00112233'; - const collectionHelpers = evmCollectionHelpers(web3, owner); - expect(await collectionHelpers.methods - .isCollectionExist(collectionAddressForNonexistentCollection).call()) - .to.be.false; - - const result = await collectionHelpers.methods.createNonfungibleCollection('Collection address exist', '7', '7').send(); - const {collectionIdAddress} = await getCollectionAddressFromResult(api, result); - expect(await collectionHelpers.methods - .isCollectionExist(collectionIdAddress).call()) - .to.be.true; - }); -}); - -describe('(!negative tests!) Create collection from EVM', () => { - itWeb3('(!negative test!) Create collection (bad lengths)', async ({api, web3, privateKeyWrapper}) => { - const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - const helper = evmCollectionHelpers(web3, owner); - { - const MAX_NAME_LENGHT = 64; - const collectionName = 'A'.repeat(MAX_NAME_LENGHT + 1); - const description = 'A'; - const tokenPrefix = 'A'; - - await expect(helper.methods - .createNonfungibleCollection(collectionName, description, tokenPrefix) - .call()).to.be.rejectedWith('name is too long. Max length is ' + MAX_NAME_LENGHT); - - } - { - const MAX_DESCRIPTION_LENGHT = 256; - const collectionName = 'A'; - const description = 'A'.repeat(MAX_DESCRIPTION_LENGHT + 1); - const tokenPrefix = 'A'; - await expect(helper.methods - .createNonfungibleCollection(collectionName, description, tokenPrefix) - .call()).to.be.rejectedWith('description is too long. Max length is ' + MAX_DESCRIPTION_LENGHT); - } - { - const MAX_TOKEN_PREFIX_LENGHT = 16; - const collectionName = 'A'; - const description = 'A'; - const tokenPrefix = 'A'.repeat(MAX_TOKEN_PREFIX_LENGHT + 1); - await expect(helper.methods - .createNonfungibleCollection(collectionName, description, tokenPrefix) - .call()).to.be.rejectedWith('token_prefix is too long. Max length is ' + MAX_TOKEN_PREFIX_LENGHT); - } - }); - - itWeb3('(!negative test!) Create collection (no funds)', async ({web3}) => { - const owner = await createEthAccount(web3); - const helper = evmCollectionHelpers(web3, owner); - const collectionName = 'A'; - const description = 'A'; - const tokenPrefix = 'A'; - - await expect(helper.methods - .createNonfungibleCollection(collectionName, description, tokenPrefix) - .call()).to.be.rejectedWith('NotSufficientFounds'); - }); - - itWeb3('(!negative test!) Check owner', async ({api, web3, privateKeyWrapper}) => { - const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - const notOwner = await createEthAccount(web3); - const collectionHelpers = evmCollectionHelpers(web3, owner); - const result = await collectionHelpers.methods.createNonfungibleCollection('A', 'A', 'A').send(); - const {collectionIdAddress} = await getCollectionAddressFromResult(api, result); - const contractEvmFromNotOwner = evmCollection(web3, notOwner, collectionIdAddress); - const EXPECTED_ERROR = 'NoPermission'; - { - const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - await expect(contractEvmFromNotOwner.methods - .setCollectionSponsor(sponsor) - .call()).to.be.rejectedWith(EXPECTED_ERROR); - - const sponsorCollection = evmCollection(web3, sponsor, collectionIdAddress); - await expect(sponsorCollection.methods - .confirmCollectionSponsorship() - .call()).to.be.rejectedWith('caller is not set as sponsor'); - } - { - await expect(contractEvmFromNotOwner.methods - .setCollectionLimit('account_token_ownership_limit', '1000') - .call()).to.be.rejectedWith(EXPECTED_ERROR); - } - }); - - itWeb3('(!negative test!) Set limits', async ({api, web3, privateKeyWrapper}) => { - const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - const collectionHelpers = evmCollectionHelpers(web3, owner); - const result = await collectionHelpers.methods.createNonfungibleCollection('Schema collection', 'A', 'A').send(); - const {collectionIdAddress} = await getCollectionAddressFromResult(api, result); - const collectionEvm = evmCollection(web3, owner, collectionIdAddress); - await expect(collectionEvm.methods - .setCollectionLimit('badLimit', 'true') - .call()).to.be.rejectedWith('unknown boolean limit "badLimit"'); - }); -}); \ No newline at end of file --- /dev/null +++ b/tests/src/eth/createNFTCollection.test.ts @@ -0,0 +1,231 @@ +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// This file is part of Unique Network. + +// Unique Network is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Unique Network is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Unique Network. If not, see . + +import {evmToAddress} from '@polkadot/util-crypto'; +import {expect} from 'chai'; +import {getCreatedCollectionCount, getDetailedCollectionInfo} from '../util/helpers'; +import { + evmCollectionHelpers, + collectionIdToAddress, + createEthAccount, + createEthAccountWithBalance, + evmCollection, + itWeb3, + getCollectionAddressFromResult, +} from './util/helpers'; + +describe('Create NFT collection from EVM', () => { + itWeb3('Create collection', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const collectionHelper = evmCollectionHelpers(web3, owner); + const collectionName = 'CollectionEVM'; + const description = 'Some description'; + const tokenPrefix = 'token prefix'; + + const collectionCountBefore = await getCreatedCollectionCount(api); + const result = await collectionHelper.methods + .createNonfungibleCollection(collectionName, description, tokenPrefix) + .send(); + const collectionCountAfter = await getCreatedCollectionCount(api); + + const {collectionId, collection} = await getCollectionAddressFromResult(api, result); + expect(collectionCountAfter - collectionCountBefore).to.be.eq(1); + expect(collectionId).to.be.eq(collectionCountAfter); + expect(collection.name.map(v => String.fromCharCode(v.toNumber())).join('')).to.be.eq(collectionName); + expect(collection.description.map(v => String.fromCharCode(v.toNumber())).join('')).to.be.eq(description); + expect(collection.tokenPrefix.toHuman()).to.be.eq(tokenPrefix); + expect(collection.mode.isNft).to.be.true; + }); + + itWeb3('Check collection address exist', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const collectionHelpers = evmCollectionHelpers(web3, owner); + + const expectedCollectionId = await getCreatedCollectionCount(api) + 1; + const expectedCollectionAddress = collectionIdToAddress(expectedCollectionId); + expect(await collectionHelpers.methods + .isCollectionExist(expectedCollectionAddress) + .call()).to.be.false; + + await collectionHelpers.methods + .createNonfungibleCollection('A', 'A', 'A') + .send(); + + expect(await collectionHelpers.methods + .isCollectionExist(expectedCollectionAddress) + .call()).to.be.true; + }); + + itWeb3('Set sponsorship', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const collectionHelpers = evmCollectionHelpers(web3, owner); + let result = await collectionHelpers.methods.createNonfungibleCollection('Sponsor collection', '1', '1').send(); + const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const collectionEvm = evmCollection(web3, owner, collectionIdAddress); + result = await collectionEvm.methods.setCollectionSponsor(sponsor).send(); + let collectionSub = (await getDetailedCollectionInfo(api, collectionId))!; + expect(collectionSub.sponsorship.isUnconfirmed).to.be.true; + const ss58Format = (api.registry.getChainProperties())!.toJSON().ss58Format; + expect(collectionSub.sponsorship.asUnconfirmed.toHuman()).to.be.eq(evmToAddress(sponsor, Number(ss58Format))); + await expect(collectionEvm.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('caller is not set as sponsor'); + const sponsorCollection = evmCollection(web3, sponsor, collectionIdAddress); + await sponsorCollection.methods.confirmCollectionSponsorship().send(); + collectionSub = (await getDetailedCollectionInfo(api, collectionId))!; + expect(collectionSub.sponsorship.isConfirmed).to.be.true; + expect(collectionSub.sponsorship.asConfirmed.toHuman()).to.be.eq(evmToAddress(sponsor, Number(ss58Format))); + }); + + itWeb3('Set limits', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const collectionHelpers = evmCollectionHelpers(web3, owner); + const result = await collectionHelpers.methods.createNonfungibleCollection('Const collection', '5', '5').send(); + const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result); + const limits = { + accountTokenOwnershipLimit: 1000, + sponsoredDataSize: 1024, + sponsoredDataRateLimit: 30, + tokenLimit: 1000000, + sponsorTransferTimeout: 6, + sponsorApproveTimeout: 6, + ownerCanTransfer: false, + ownerCanDestroy: false, + transfersEnabled: false, + }; + + const collectionEvm = evmCollection(web3, owner, collectionIdAddress); + await collectionEvm.methods['setCollectionLimit(string,uint32)']('accountTokenOwnershipLimit', limits.accountTokenOwnershipLimit).send(); + await collectionEvm.methods['setCollectionLimit(string,uint32)']('sponsoredDataSize', limits.sponsoredDataSize).send(); + await collectionEvm.methods['setCollectionLimit(string,uint32)']('sponsoredDataRateLimit', limits.sponsoredDataRateLimit).send(); + await collectionEvm.methods['setCollectionLimit(string,uint32)']('tokenLimit', limits.tokenLimit).send(); + await collectionEvm.methods['setCollectionLimit(string,uint32)']('sponsorTransferTimeout', limits.sponsorTransferTimeout).send(); + await collectionEvm.methods['setCollectionLimit(string,uint32)']('sponsorApproveTimeout', limits.sponsorApproveTimeout).send(); + await collectionEvm.methods['setCollectionLimit(string,bool)']('ownerCanTransfer', limits.ownerCanTransfer).send(); + await collectionEvm.methods['setCollectionLimit(string,bool)']('ownerCanDestroy', limits.ownerCanDestroy).send(); + await collectionEvm.methods['setCollectionLimit(string,bool)']('transfersEnabled', limits.transfersEnabled).send(); + + const collectionSub = (await getDetailedCollectionInfo(api, collectionId))!; + expect(collectionSub.limits.accountTokenOwnershipLimit.unwrap().toNumber()).to.be.eq(limits.accountTokenOwnershipLimit); + expect(collectionSub.limits.sponsoredDataSize.unwrap().toNumber()).to.be.eq(limits.sponsoredDataSize); + expect(collectionSub.limits.sponsoredDataRateLimit.unwrap().asBlocks.toNumber()).to.be.eq(limits.sponsoredDataRateLimit); + expect(collectionSub.limits.tokenLimit.unwrap().toNumber()).to.be.eq(limits.tokenLimit); + expect(collectionSub.limits.sponsorTransferTimeout.unwrap().toNumber()).to.be.eq(limits.sponsorTransferTimeout); + expect(collectionSub.limits.sponsorApproveTimeout.unwrap().toNumber()).to.be.eq(limits.sponsorApproveTimeout); + expect(collectionSub.limits.ownerCanTransfer.toHuman()).to.be.eq(limits.ownerCanTransfer); + expect(collectionSub.limits.ownerCanDestroy.toHuman()).to.be.eq(limits.ownerCanDestroy); + expect(collectionSub.limits.transfersEnabled.toHuman()).to.be.eq(limits.transfersEnabled); + }); + + itWeb3('Collection address exist', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const collectionAddressForNonexistentCollection = '0x17C4E6453CC49AAAAEACA894E6D9683E00112233'; + const collectionHelpers = evmCollectionHelpers(web3, owner); + expect(await collectionHelpers.methods + .isCollectionExist(collectionAddressForNonexistentCollection).call()) + .to.be.false; + + const result = await collectionHelpers.methods.createNonfungibleCollection('Collection address exist', '7', '7').send(); + const {collectionIdAddress} = await getCollectionAddressFromResult(api, result); + expect(await collectionHelpers.methods + .isCollectionExist(collectionIdAddress).call()) + .to.be.true; + }); +}); + +describe('(!negative tests!) Create NFT collection from EVM', () => { + itWeb3('(!negative test!) Create collection (bad lengths)', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const helper = evmCollectionHelpers(web3, owner); + { + const MAX_NAME_LENGHT = 64; + const collectionName = 'A'.repeat(MAX_NAME_LENGHT + 1); + const description = 'A'; + const tokenPrefix = 'A'; + + await expect(helper.methods + .createNonfungibleCollection(collectionName, description, tokenPrefix) + .call()).to.be.rejectedWith('name is too long. Max length is ' + MAX_NAME_LENGHT); + + } + { + const MAX_DESCRIPTION_LENGHT = 256; + const collectionName = 'A'; + const description = 'A'.repeat(MAX_DESCRIPTION_LENGHT + 1); + const tokenPrefix = 'A'; + await expect(helper.methods + .createNonfungibleCollection(collectionName, description, tokenPrefix) + .call()).to.be.rejectedWith('description is too long. Max length is ' + MAX_DESCRIPTION_LENGHT); + } + { + const MAX_TOKEN_PREFIX_LENGHT = 16; + const collectionName = 'A'; + const description = 'A'; + const tokenPrefix = 'A'.repeat(MAX_TOKEN_PREFIX_LENGHT + 1); + await expect(helper.methods + .createNonfungibleCollection(collectionName, description, tokenPrefix) + .call()).to.be.rejectedWith('token_prefix is too long. Max length is ' + MAX_TOKEN_PREFIX_LENGHT); + } + }); + + itWeb3('(!negative test!) Create collection (no funds)', async ({web3}) => { + const owner = await createEthAccount(web3); + const helper = evmCollectionHelpers(web3, owner); + const collectionName = 'A'; + const description = 'A'; + const tokenPrefix = 'A'; + + await expect(helper.methods + .createNonfungibleCollection(collectionName, description, tokenPrefix) + .call()).to.be.rejectedWith('NotSufficientFounds'); + }); + + itWeb3('(!negative test!) Check owner', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const notOwner = await createEthAccount(web3); + const collectionHelpers = evmCollectionHelpers(web3, owner); + const result = await collectionHelpers.methods.createNonfungibleCollection('A', 'A', 'A').send(); + const {collectionIdAddress} = await getCollectionAddressFromResult(api, result); + const contractEvmFromNotOwner = evmCollection(web3, notOwner, collectionIdAddress); + const EXPECTED_ERROR = 'NoPermission'; + { + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + await expect(contractEvmFromNotOwner.methods + .setCollectionSponsor(sponsor) + .call()).to.be.rejectedWith(EXPECTED_ERROR); + + const sponsorCollection = evmCollection(web3, sponsor, collectionIdAddress); + await expect(sponsorCollection.methods + .confirmCollectionSponsorship() + .call()).to.be.rejectedWith('caller is not set as sponsor'); + } + { + await expect(contractEvmFromNotOwner.methods + .setCollectionLimit('account_token_ownership_limit', '1000') + .call()).to.be.rejectedWith(EXPECTED_ERROR); + } + }); + + itWeb3('(!negative test!) Set limits', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const collectionHelpers = evmCollectionHelpers(web3, owner); + const result = await collectionHelpers.methods.createNonfungibleCollection('Schema collection', 'A', 'A').send(); + const {collectionIdAddress} = await getCollectionAddressFromResult(api, result); + const collectionEvm = evmCollection(web3, owner, collectionIdAddress); + await expect(collectionEvm.methods + .setCollectionLimit('badLimit', 'true') + .call()).to.be.rejectedWith('unknown boolean limit "badLimit"'); + }); +}); \ No newline at end of file --- /dev/null +++ b/tests/src/eth/createRFTCollection.test.ts @@ -0,0 +1,231 @@ +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// This file is part of Unique Network. + +// Unique Network is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Unique Network is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Unique Network. If not, see . + +import {evmToAddress} from '@polkadot/util-crypto'; +import {expect} from 'chai'; +import {getCreatedCollectionCount, getDetailedCollectionInfo} from '../util/helpers'; +import { + evmCollectionHelpers, + collectionIdToAddress, + createEthAccount, + createEthAccountWithBalance, + evmCollection, + itWeb3, + getCollectionAddressFromResult, +} from './util/helpers'; + +describe('Create RFT collection from EVM', () => { + itWeb3('Create collection', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const collectionHelper = evmCollectionHelpers(web3, owner); + const collectionName = 'CollectionEVM'; + const description = 'Some description'; + const tokenPrefix = 'token prefix'; + + const collectionCountBefore = await getCreatedCollectionCount(api); + const result = await collectionHelper.methods + .createRefungibleCollection(collectionName, description, tokenPrefix) + .send(); + const collectionCountAfter = await getCreatedCollectionCount(api); + + const {collectionId, collection} = await getCollectionAddressFromResult(api, result); + expect(collectionCountAfter - collectionCountBefore).to.be.eq(1); + expect(collectionId).to.be.eq(collectionCountAfter); + expect(collection.name.map(v => String.fromCharCode(v.toNumber())).join('')).to.be.eq(collectionName); + expect(collection.description.map(v => String.fromCharCode(v.toNumber())).join('')).to.be.eq(description); + expect(collection.tokenPrefix.toHuman()).to.be.eq(tokenPrefix); + expect(collection.mode.isReFungible).to.be.true; + }); + + itWeb3('Check collection address exist', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const collectionHelpers = evmCollectionHelpers(web3, owner); + + const expectedCollectionId = await getCreatedCollectionCount(api) + 1; + const expectedCollectionAddress = collectionIdToAddress(expectedCollectionId); + expect(await collectionHelpers.methods + .isCollectionExist(expectedCollectionAddress) + .call()).to.be.false; + + await collectionHelpers.methods + .createRefungibleCollection('A', 'A', 'A') + .send(); + + expect(await collectionHelpers.methods + .isCollectionExist(expectedCollectionAddress) + .call()).to.be.true; + }); + + itWeb3('Set sponsorship', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const collectionHelpers = evmCollectionHelpers(web3, owner); + let result = await collectionHelpers.methods.createRefungibleCollection('Sponsor collection', '1', '1').send(); + const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const collectionEvm = evmCollection(web3, owner, collectionIdAddress, {type: 'ReFungible'}); + result = await collectionEvm.methods.setCollectionSponsor(sponsor).send(); + let collectionSub = (await getDetailedCollectionInfo(api, collectionId))!; + expect(collectionSub.sponsorship.isUnconfirmed).to.be.true; + const ss58Format = (api.registry.getChainProperties())!.toJSON().ss58Format; + expect(collectionSub.sponsorship.asUnconfirmed.toHuman()).to.be.eq(evmToAddress(sponsor, Number(ss58Format))); + await expect(collectionEvm.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('caller is not set as sponsor'); + const sponsorCollection = evmCollection(web3, sponsor, collectionIdAddress); + await sponsorCollection.methods.confirmCollectionSponsorship().send(); + collectionSub = (await getDetailedCollectionInfo(api, collectionId))!; + expect(collectionSub.sponsorship.isConfirmed).to.be.true; + expect(collectionSub.sponsorship.asConfirmed.toHuman()).to.be.eq(evmToAddress(sponsor, Number(ss58Format))); + }); + + itWeb3('Set limits', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const collectionHelpers = evmCollectionHelpers(web3, owner); + const result = await collectionHelpers.methods.createRefungibleCollection('Const collection', '5', '5').send(); + const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result); + const limits = { + accountTokenOwnershipLimit: 1000, + sponsoredDataSize: 1024, + sponsoredDataRateLimit: 30, + tokenLimit: 1000000, + sponsorTransferTimeout: 6, + sponsorApproveTimeout: 6, + ownerCanTransfer: false, + ownerCanDestroy: false, + transfersEnabled: false, + }; + + const collectionEvm = evmCollection(web3, owner, collectionIdAddress, {type: 'ReFungible'}); + await collectionEvm.methods['setCollectionLimit(string,uint32)']('accountTokenOwnershipLimit', limits.accountTokenOwnershipLimit).send(); + await collectionEvm.methods['setCollectionLimit(string,uint32)']('sponsoredDataSize', limits.sponsoredDataSize).send(); + await collectionEvm.methods['setCollectionLimit(string,uint32)']('sponsoredDataRateLimit', limits.sponsoredDataRateLimit).send(); + await collectionEvm.methods['setCollectionLimit(string,uint32)']('tokenLimit', limits.tokenLimit).send(); + await collectionEvm.methods['setCollectionLimit(string,uint32)']('sponsorTransferTimeout', limits.sponsorTransferTimeout).send(); + await collectionEvm.methods['setCollectionLimit(string,uint32)']('sponsorApproveTimeout', limits.sponsorApproveTimeout).send(); + await collectionEvm.methods['setCollectionLimit(string,bool)']('ownerCanTransfer', limits.ownerCanTransfer).send(); + await collectionEvm.methods['setCollectionLimit(string,bool)']('ownerCanDestroy', limits.ownerCanDestroy).send(); + await collectionEvm.methods['setCollectionLimit(string,bool)']('transfersEnabled', limits.transfersEnabled).send(); + + const collectionSub = (await getDetailedCollectionInfo(api, collectionId))!; + expect(collectionSub.limits.accountTokenOwnershipLimit.unwrap().toNumber()).to.be.eq(limits.accountTokenOwnershipLimit); + expect(collectionSub.limits.sponsoredDataSize.unwrap().toNumber()).to.be.eq(limits.sponsoredDataSize); + expect(collectionSub.limits.sponsoredDataRateLimit.unwrap().asBlocks.toNumber()).to.be.eq(limits.sponsoredDataRateLimit); + expect(collectionSub.limits.tokenLimit.unwrap().toNumber()).to.be.eq(limits.tokenLimit); + expect(collectionSub.limits.sponsorTransferTimeout.unwrap().toNumber()).to.be.eq(limits.sponsorTransferTimeout); + expect(collectionSub.limits.sponsorApproveTimeout.unwrap().toNumber()).to.be.eq(limits.sponsorApproveTimeout); + expect(collectionSub.limits.ownerCanTransfer.toHuman()).to.be.eq(limits.ownerCanTransfer); + expect(collectionSub.limits.ownerCanDestroy.toHuman()).to.be.eq(limits.ownerCanDestroy); + expect(collectionSub.limits.transfersEnabled.toHuman()).to.be.eq(limits.transfersEnabled); + }); + + itWeb3('Collection address exist', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const collectionAddressForNonexistentCollection = '0x17C4E6453CC49AAAAEACA894E6D9683E00112233'; + const collectionHelpers = evmCollectionHelpers(web3, owner); + expect(await collectionHelpers.methods + .isCollectionExist(collectionAddressForNonexistentCollection).call()) + .to.be.false; + + const result = await collectionHelpers.methods.createRefungibleCollection('Collection address exist', '7', '7').send(); + const {collectionIdAddress} = await getCollectionAddressFromResult(api, result); + expect(await collectionHelpers.methods + .isCollectionExist(collectionIdAddress).call()) + .to.be.true; + }); +}); + +describe('(!negative tests!) Create RFT collection from EVM', () => { + itWeb3('(!negative test!) Create collection (bad lengths)', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const helper = evmCollectionHelpers(web3, owner); + { + const MAX_NAME_LENGHT = 64; + const collectionName = 'A'.repeat(MAX_NAME_LENGHT + 1); + const description = 'A'; + const tokenPrefix = 'A'; + + await expect(helper.methods + .createRefungibleCollection(collectionName, description, tokenPrefix) + .call()).to.be.rejectedWith('name is too long. Max length is ' + MAX_NAME_LENGHT); + + } + { + const MAX_DESCRIPTION_LENGHT = 256; + const collectionName = 'A'; + const description = 'A'.repeat(MAX_DESCRIPTION_LENGHT + 1); + const tokenPrefix = 'A'; + await expect(helper.methods + .createRefungibleCollection(collectionName, description, tokenPrefix) + .call()).to.be.rejectedWith('description is too long. Max length is ' + MAX_DESCRIPTION_LENGHT); + } + { + const MAX_TOKEN_PREFIX_LENGHT = 16; + const collectionName = 'A'; + const description = 'A'; + const tokenPrefix = 'A'.repeat(MAX_TOKEN_PREFIX_LENGHT + 1); + await expect(helper.methods + .createRefungibleCollection(collectionName, description, tokenPrefix) + .call()).to.be.rejectedWith('token_prefix is too long. Max length is ' + MAX_TOKEN_PREFIX_LENGHT); + } + }); + + itWeb3('(!negative test!) Create collection (no funds)', async ({web3}) => { + const owner = await createEthAccount(web3); + const helper = evmCollectionHelpers(web3, owner); + const collectionName = 'A'; + const description = 'A'; + const tokenPrefix = 'A'; + + await expect(helper.methods + .createRefungibleCollection(collectionName, description, tokenPrefix) + .call()).to.be.rejectedWith('NotSufficientFounds'); + }); + + itWeb3('(!negative test!) Check owner', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const notOwner = await createEthAccount(web3); + const collectionHelpers = evmCollectionHelpers(web3, owner); + const result = await collectionHelpers.methods.createRefungibleCollection('A', 'A', 'A').send(); + const {collectionIdAddress} = await getCollectionAddressFromResult(api, result); + const contractEvmFromNotOwner = evmCollection(web3, notOwner, collectionIdAddress, {type: 'ReFungible'}); + const EXPECTED_ERROR = 'NoPermission'; + { + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + await expect(contractEvmFromNotOwner.methods + .setCollectionSponsor(sponsor) + .call()).to.be.rejectedWith(EXPECTED_ERROR); + + const sponsorCollection = evmCollection(web3, sponsor, collectionIdAddress); + await expect(sponsorCollection.methods + .confirmCollectionSponsorship() + .call()).to.be.rejectedWith('caller is not set as sponsor'); + } + { + await expect(contractEvmFromNotOwner.methods + .setCollectionLimit('account_token_ownership_limit', '1000') + .call()).to.be.rejectedWith(EXPECTED_ERROR); + } + }); + + itWeb3('(!negative test!) Set limits', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const collectionHelpers = evmCollectionHelpers(web3, owner); + const result = await collectionHelpers.methods.createRefungibleCollection('Schema collection', 'A', 'A').send(); + const {collectionIdAddress} = await getCollectionAddressFromResult(api, result); + const collectionEvm = evmCollection(web3, owner, collectionIdAddress, {type: 'ReFungible'}); + await expect(collectionEvm.methods + .setCollectionLimit('badLimit', 'true') + .call()).to.be.rejectedWith('unknown boolean limit "badLimit"'); + }); +}); \ No newline at end of file --- a/tests/src/eth/nonFungible.test.ts +++ b/tests/src/eth/nonFungible.test.ts @@ -72,6 +72,147 @@ }); }); +describe('Check ERC721 token URI for NFT', () => { + itWeb3('Empty tokenURI', async ({web3, api, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const helper = evmCollectionHelpers(web3, owner); + let result = await helper.methods.createERC721MetadataCompatibleCollection('Mint collection', '1', '1', '').send(); + const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result); + const receiver = createEthAccount(web3); + const contract = evmCollection(web3, owner, collectionIdAddress); + + const nextTokenId = await contract.methods.nextTokenId().call(); + expect(nextTokenId).to.be.equal('1'); + result = await contract.methods.mint( + receiver, + nextTokenId, + ).send(); + + const events = normalizeEvents(result.events); + const address = collectionIdToAddress(collectionId); + + expect(events).to.be.deep.equal([ + { + address, + event: 'Transfer', + args: { + from: '0x0000000000000000000000000000000000000000', + to: receiver, + tokenId: nextTokenId, + }, + }, + ]); + + expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal(''); + }); + + itWeb3('TokenURI from url', async ({web3, api, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const helper = evmCollectionHelpers(web3, owner); + let result = await helper.methods.createERC721MetadataCompatibleCollection('Mint collection', '1', '1', 'BaseURI_').send(); + const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result); + const receiver = createEthAccount(web3); + const contract = evmCollection(web3, owner, collectionIdAddress); + + const nextTokenId = await contract.methods.nextTokenId().call(); + expect(nextTokenId).to.be.equal('1'); + result = await contract.methods.mint( + receiver, + nextTokenId, + ).send(); + + // Set URL + await contract.methods.setProperty(nextTokenId, 'url', Buffer.from('Token URI')).send(); + + const events = normalizeEvents(result.events); + const address = collectionIdToAddress(collectionId); + + expect(events).to.be.deep.equal([ + { + address, + event: 'Transfer', + args: { + from: '0x0000000000000000000000000000000000000000', + to: receiver, + tokenId: nextTokenId, + }, + }, + ]); + + expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('Token URI'); + }); + + itWeb3('TokenURI from baseURI + tokenId', async ({web3, api, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const helper = evmCollectionHelpers(web3, owner); + let result = await helper.methods.createERC721MetadataCompatibleCollection('Mint collection', '1', '1', 'BaseURI_').send(); + const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result); + const receiver = createEthAccount(web3); + const contract = evmCollection(web3, owner, collectionIdAddress); + + const nextTokenId = await contract.methods.nextTokenId().call(); + expect(nextTokenId).to.be.equal('1'); + result = await contract.methods.mint( + receiver, + nextTokenId, + ).send(); + + const events = normalizeEvents(result.events); + const address = collectionIdToAddress(collectionId); + + expect(events).to.be.deep.equal([ + { + address, + event: 'Transfer', + args: { + from: '0x0000000000000000000000000000000000000000', + to: receiver, + tokenId: nextTokenId, + }, + }, + ]); + + expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('BaseURI_' + nextTokenId); + }); + + itWeb3('TokenURI from baseURI + suffix', async ({web3, api, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const helper = evmCollectionHelpers(web3, owner); + let result = await helper.methods.createERC721MetadataCompatibleCollection('Mint collection', '1', '1', 'BaseURI_').send(); + const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result); + const receiver = createEthAccount(web3); + const contract = evmCollection(web3, owner, collectionIdAddress); + + const nextTokenId = await contract.methods.nextTokenId().call(); + expect(nextTokenId).to.be.equal('1'); + result = await contract.methods.mint( + receiver, + nextTokenId, + ).send(); + + // Set suffix + const suffix = '/some/suffix'; + await contract.methods.setProperty(nextTokenId, 'suffix', Buffer.from(suffix)).send(); + + const events = normalizeEvents(result.events); + const address = collectionIdToAddress(collectionId); + + expect(events).to.be.deep.equal([ + { + address, + event: 'Transfer', + args: { + from: '0x0000000000000000000000000000000000000000', + to: receiver, + tokenId: nextTokenId, + }, + }, + ]); + + expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('BaseURI_' + suffix); + }); +}); + describe('NFT: Plain calls', () => { itWeb3('Can perform mint()', async ({web3, api, privateKeyWrapper}) => { const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); --- a/tests/src/eth/reFungibleToken.test.ts +++ b/tests/src/eth/reFungibleToken.test.ts @@ -15,7 +15,7 @@ // along with Unique Network. If not, see . import {approve, createCollection, createRefungibleToken, transfer, transferFrom, UNIQUE} from '../util/helpers'; -import {createEthAccount, createEthAccountWithBalance, GAS_ARGS, itWeb3, normalizeEvents, recordEthFee, recordEvents, subToEth, tokenIdToAddress, transferBalanceToEth} from './util/helpers'; +import {collectionIdToAddress, createEthAccount, createEthAccountWithBalance, evmCollection, evmCollectionHelpers, GAS_ARGS, getCollectionAddressFromResult, itWeb3, normalizeEvents, recordEthFee, recordEvents, subToEth, tokenIdToAddress, transferBalanceToEth} from './util/helpers'; import reFungibleTokenAbi from './reFungibleTokenAbi.json'; import chai from 'chai'; @@ -73,6 +73,148 @@ }); }); +// FIXME: Need erc721 for ReFubgible. +describe.skip('Check ERC721 token URI for ReFungible', () => { + itWeb3('Empty tokenURI', async ({web3, api, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const helper = evmCollectionHelpers(web3, owner); + let result = await helper.methods.createERC721MetadataCompatibleCollection('Mint collection', '1', '1', '').send(); + const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result); + const receiver = createEthAccount(web3); + const contract = evmCollection(web3, owner, collectionIdAddress, {type: 'ReFungible'}); + + const nextTokenId = await contract.methods.nextTokenId().call(); + expect(nextTokenId).to.be.equal('1'); + result = await contract.methods.mint( + receiver, + nextTokenId, + ).send(); + + const events = normalizeEvents(result.events); + const address = collectionIdToAddress(collectionId); + + expect(events).to.be.deep.equal([ + { + address, + event: 'Transfer', + args: { + from: '0x0000000000000000000000000000000000000000', + to: receiver, + tokenId: nextTokenId, + }, + }, + ]); + + expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal(''); + }); + + itWeb3('TokenURI from url', async ({web3, api, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const helper = evmCollectionHelpers(web3, owner); + let result = await helper.methods.createERC721MetadataCompatibleCollection('Mint collection', '1', '1', 'BaseURI_').send(); + const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result); + const receiver = createEthAccount(web3); + const contract = evmCollection(web3, owner, collectionIdAddress, {type: 'ReFungible'}); + + const nextTokenId = await contract.methods.nextTokenId().call(); + expect(nextTokenId).to.be.equal('1'); + result = await contract.methods.mint( + receiver, + nextTokenId, + ).send(); + + // Set URL + await contract.methods.setProperty(nextTokenId, 'url', Buffer.from('Token URI')).send(); + + const events = normalizeEvents(result.events); + const address = collectionIdToAddress(collectionId); + + expect(events).to.be.deep.equal([ + { + address, + event: 'Transfer', + args: { + from: '0x0000000000000000000000000000000000000000', + to: receiver, + tokenId: nextTokenId, + }, + }, + ]); + + expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('Token URI'); + }); + + itWeb3('TokenURI from baseURI + tokenId', async ({web3, api, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const helper = evmCollectionHelpers(web3, owner); + let result = await helper.methods.createERC721MetadataCompatibleCollection('Mint collection', '1', '1', 'BaseURI_').send(); + const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result); + const receiver = createEthAccount(web3); + const contract = evmCollection(web3, owner, collectionIdAddress, {type: 'ReFungible'}); + + const nextTokenId = await contract.methods.nextTokenId().call(); + expect(nextTokenId).to.be.equal('1'); + result = await contract.methods.mint( + receiver, + nextTokenId, + ).send(); + + const events = normalizeEvents(result.events); + const address = collectionIdToAddress(collectionId); + + expect(events).to.be.deep.equal([ + { + address, + event: 'Transfer', + args: { + from: '0x0000000000000000000000000000000000000000', + to: receiver, + tokenId: nextTokenId, + }, + }, + ]); + + expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('BaseURI_' + nextTokenId); + }); + + itWeb3('TokenURI from baseURI + suffix', async ({web3, api, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const helper = evmCollectionHelpers(web3, owner); + let result = await helper.methods.createERC721MetadataCompatibleCollection('Mint collection', '1', '1', 'BaseURI_').send(); + const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result); + const receiver = createEthAccount(web3); + const contract = evmCollection(web3, owner, collectionIdAddress, {type: 'ReFungible'}); + + const nextTokenId = await contract.methods.nextTokenId().call(); + expect(nextTokenId).to.be.equal('1'); + result = await contract.methods.mint( + receiver, + nextTokenId, + ).send(); + + // Set suffix + const suffix = '/some/suffix'; + await contract.methods.setProperty(nextTokenId, 'suffix', Buffer.from(suffix)).send(); + + const events = normalizeEvents(result.events); + const address = collectionIdToAddress(collectionId); + + expect(events).to.be.deep.equal([ + { + address, + event: 'Transfer', + args: { + from: '0x0000000000000000000000000000000000000000', + to: receiver, + tokenId: nextTokenId, + }, + }, + ]); + + expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('BaseURI_' + suffix); + }); +}); + describe('Refungible: Plain calls', () => { itWeb3('Can perform approve()', async ({web3, api, privateKeyWrapper}) => { const alice = privateKeyWrapper('//Alice'); --- /dev/null +++ b/tests/src/eth/refungibleAbi.json @@ -0,0 +1,167 @@ +[ + { + "inputs": [ + { "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "addCollectionAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "newAdmin", "type": "uint256" } + ], + "name": "addCollectionAdminSubstrate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "user", "type": "address" } + ], + "name": "addToCollectionAllowList", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "string", "name": "key", "type": "string" }], + "name": "collectionProperty", + "outputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "confirmCollectionSponsorship", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "contractAddress", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "string", "name": "key", "type": "string" }], + "name": "deleteCollectionProperty", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "admin", "type": "address" } + ], + "name": "removeCollectionAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "admin", "type": "uint256" } + ], + "name": "removeCollectionAdminSubstrate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "user", "type": "address" } + ], + "name": "removeFromCollectionAllowList", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint8", "name": "mode", "type": "uint8" }], + "name": "setCollectionAccess", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "string", "name": "limit", "type": "string" }, + { "internalType": "uint32", "name": "value", "type": "uint32" } + ], + "name": "setCollectionLimit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "string", "name": "limit", "type": "string" }, + { "internalType": "bool", "name": "value", "type": "bool" } + ], + "name": "setCollectionLimit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "bool", "name": "mode", "type": "bool" }], + "name": "setCollectionMintMode", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "bool", "name": "enable", "type": "bool" }], + "name": "setCollectionNesting", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bool", "name": "enable", "type": "bool" }, + { + "internalType": "address[]", + "name": "collections", + "type": "address[]" + } + ], + "name": "setCollectionNesting", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "string", "name": "key", "type": "string" }, + { "internalType": "bytes", "name": "value", "type": "bytes" } + ], + "name": "setCollectionProperty", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "sponsor", "type": "address" } + ], + "name": "setCollectionSponsor", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes4", "name": "interfaceID", "type": "bytes4" } + ], + "name": "supportsInterface", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + } +] --- a/tests/src/eth/util/helpers.ts +++ b/tests/src/eth/util/helpers.ts @@ -25,12 +25,13 @@ import Web3 from 'web3'; import config from '../../config'; import getBalance from '../../substrate/get-balance'; -import privateKey from '../../substrate/privateKey'; import usingApi, {submitTransactionAsync} from '../../substrate/substrate-api'; import waitNewBlocks from '../../substrate/wait-new-blocks'; -import {CrossAccountId, getDetailedCollectionInfo, getGenericResult, UNIQUE} from '../../util/helpers'; +import {CollectionMode, CrossAccountId, getDetailedCollectionInfo, getGenericResult, UNIQUE} from '../../util/helpers'; import collectionHelpersAbi from '../collectionHelpersAbi.json'; +import fungibleAbi from '../fungibleAbi.json'; import nonFungibleAbi from '../nonFungibleAbi.json'; +import refungibleAbi from '../refungibleAbi.json'; import contractHelpersAbi from './contractHelpersAbi.json'; export const GAS_ARGS = {gas: 2500000}; @@ -307,8 +308,26 @@ * @param caller - eth address * @returns */ -export function evmCollection(web3: Web3, caller: string, collection: string) { - return new web3.eth.Contract(nonFungibleAbi as any, collection, {from: caller, ...GAS_ARGS}); +export function evmCollection(web3: Web3, caller: string, collection: string, mode: CollectionMode = {type: 'NFT'}) { + let abi; + switch (mode.type) { + case 'Fungible': + abi = fungibleAbi; + break; + + case 'NFT': + abi = nonFungibleAbi; + break; + + case 'ReFungible': + abi = refungibleAbi; + break; + + default: + throw 'Bad collection mode'; + } + const contract = new web3.eth.Contract(abi as any, collection, {from: caller, ...GAS_ARGS}); + return contract; } /**