From 2e3c4de7682fcf7242c2a287412f3d1caad7c7b0 Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Tue, 18 Oct 2022 14:54:23 +0000 Subject: [PATCH] Merge pull request #647 from UniqueNetwork/feature/supports-interface-for-erc721-metadata --- --- a/.maintain/scripts/generate_sol.sh +++ b/.maintain/scripts/generate_sol.sh @@ -11,4 +11,6 @@ formatted=$(mktemp) prettier --config $PRETTIER_CONFIG $raw > $formatted +sed -i -E -e "s/.+\/\/ FORMATTING: FORCE NEWLINE//g" $formatted + mv $formatted $OUTPUT --- a/crates/evm-coder/procedural/src/solidity_interface.rs +++ b/crates/evm-coder/procedural/src/solidity_interface.rs @@ -291,22 +291,40 @@ struct MethodInfo { rename_selector: Option, + hide: bool, } impl Parse for MethodInfo { fn parse(input: ParseStream) -> syn::Result { let mut rename_selector = None; - let lookahead = input.lookahead1(); - if lookahead.peek(kw::rename_selector) { - let k = input.parse::()?; - input.parse::()?; - if rename_selector - .replace(input.parse::()?.value()) - .is_some() - { - return Err(syn::Error::new(k.span(), "rename_selector is already set")); + let mut hide = false; + while !input.is_empty() { + let lookahead = input.lookahead1(); + if lookahead.peek(kw::rename_selector) { + let k = input.parse::()?; + input.parse::()?; + if rename_selector + .replace(input.parse::()?.value()) + .is_some() + { + return Err(syn::Error::new(k.span(), "rename_selector is already set")); + } + } else if lookahead.peek(kw::hide) { + input.parse::()?; + hide = true; + } else { + return Err(lookahead.error()); + } + + if input.peek(Token![,]) { + input.parse::()?; + } else if !input.is_empty() { + return Err(syn::Error::new(input.span(), "expected end")); } } - Ok(Self { rename_selector }) + Ok(Self { + rename_selector, + hide, + }) } } @@ -548,6 +566,7 @@ syn::custom_keyword!(expect_selector); syn::custom_keyword!(rename_selector); + syn::custom_keyword!(hide); } /// Rust methods are parsed into this structure when Solidity code is generated @@ -558,6 +577,7 @@ screaming_name: Ident, selector_str: String, selector: u32, + hide: bool, args: Vec, has_normal_args: bool, has_value_args: bool, @@ -570,6 +590,7 @@ fn try_from(value: &ImplItemMethod) -> syn::Result { let mut info = MethodInfo { rename_selector: None, + hide: false, }; let mut docs = Vec::new(); let mut weight = None; @@ -667,6 +688,7 @@ screaming_name: snake_ident_to_screaming(ident), selector_str, selector, + hide: info.hide, args, has_normal_args, has_value_args, @@ -826,12 +848,14 @@ let docs = &self.docs; let selector_str = &self.selector_str; let selector = self.selector; + let hide = self.hide; let is_payable = self.has_value_args; quote! { SolidityFunction { docs: &[#(#docs),*], selector_str: #selector_str, selector: #selector, + hide: #hide, name: #camel_name, mutability: #mutability, is_payable: #is_payable, --- a/crates/evm-coder/src/solidity.rs +++ b/crates/evm-coder/src/solidity.rs @@ -225,7 +225,7 @@ pub trait SolidityArguments { fn solidity_name(&self, writer: &mut impl fmt::Write, tc: &TypeCollector) -> fmt::Result; - fn solidity_get(&self, writer: &mut impl fmt::Write) -> fmt::Result; + fn solidity_get(&self, prefix: &str, writer: &mut impl fmt::Write) -> fmt::Result; fn solidity_default(&self, writer: &mut impl fmt::Write, tc: &TypeCollector) -> fmt::Result; fn is_empty(&self) -> bool { self.len() == 0 @@ -248,7 +248,7 @@ Ok(()) } } - fn solidity_get(&self, _writer: &mut impl fmt::Write) -> fmt::Result { + fn solidity_get(&self, _prefix: &str, _writer: &mut impl fmt::Write) -> fmt::Result { Ok(()) } fn solidity_default(&self, writer: &mut impl fmt::Write, tc: &TypeCollector) -> fmt::Result { @@ -283,8 +283,8 @@ Ok(()) } } - fn solidity_get(&self, writer: &mut impl fmt::Write) -> fmt::Result { - writeln!(writer, "\t\t{};", self.0) + fn solidity_get(&self, prefix: &str, writer: &mut impl fmt::Write) -> fmt::Result { + writeln!(writer, "\t{prefix}\t{};", self.0) } fn solidity_default(&self, writer: &mut impl fmt::Write, tc: &TypeCollector) -> fmt::Result { T::solidity_default(writer, tc) @@ -318,8 +318,8 @@ Ok(()) } } - fn solidity_get(&self, writer: &mut impl fmt::Write) -> fmt::Result { - writeln!(writer, "\t\t{};", self.1) + fn solidity_get(&self, prefix: &str, writer: &mut impl fmt::Write) -> fmt::Result { + writeln!(writer, "\t{prefix}\t{};", self.1) } fn solidity_default(&self, writer: &mut impl fmt::Write, tc: &TypeCollector) -> fmt::Result { T::solidity_default(writer, tc) @@ -337,7 +337,7 @@ fn solidity_name(&self, _writer: &mut impl fmt::Write, _tc: &TypeCollector) -> fmt::Result { Ok(()) } - fn solidity_get(&self, _writer: &mut impl fmt::Write) -> fmt::Result { + fn solidity_get(&self, _prefix: &str, _writer: &mut impl fmt::Write) -> fmt::Result { Ok(()) } fn solidity_default(&self, _writer: &mut impl fmt::Write, _tc: &TypeCollector) -> fmt::Result { @@ -365,9 +365,9 @@ )* ); Ok(()) } - fn solidity_get(&self, writer: &mut impl fmt::Write) -> fmt::Result { + fn solidity_get(&self, prefix: &str, writer: &mut impl fmt::Write) -> fmt::Result { for_tuples!( #( - Tuple.solidity_get(writer)?; + Tuple.solidity_get(prefix, writer)?; )* ); Ok(()) } @@ -418,6 +418,7 @@ pub docs: &'static [&'static str], pub selector_str: &'static str, pub selector: u32, + pub hide: bool, pub name: &'static str, pub args: A, pub result: R, @@ -431,16 +432,21 @@ writer: &mut impl fmt::Write, tc: &TypeCollector, ) -> fmt::Result { + let hide_comment = self.hide.then(|| "// ").unwrap_or(""); for doc in self.docs { - writeln!(writer, "\t///{}", doc)?; + writeln!(writer, "\t{hide_comment}///{}", doc)?; } writeln!( writer, - "\t/// @dev EVM selector for this function is: 0x{:0>8x},", + "\t{hide_comment}/// @dev EVM selector for this function is: 0x{:0>8x},", self.selector )?; - writeln!(writer, "\t/// or in textual repr: {}", self.selector_str)?; - write!(writer, "\tfunction {}(", self.name)?; + writeln!( + writer, + "\t{hide_comment}/// or in textual repr: {}", + self.selector_str + )?; + write!(writer, "\t{hide_comment}function {}(", self.name)?; self.args.solidity_name(writer, tc)?; write!(writer, ")")?; if is_impl { @@ -463,22 +469,25 @@ } if is_impl { writeln!(writer, " {{")?; - writeln!(writer, "\t\trequire(false, stub_error);")?; - self.args.solidity_get(writer)?; + writeln!(writer, "\t{hide_comment}\trequire(false, stub_error);")?; + self.args.solidity_get(hide_comment, writer)?; match &self.mutability { SolidityMutability::Pure => {} - SolidityMutability::View => writeln!(writer, "\t\tdummy;")?, - SolidityMutability::Mutable => writeln!(writer, "\t\tdummy = 0;")?, + SolidityMutability::View => writeln!(writer, "\t{hide_comment}\tdummy;")?, + SolidityMutability::Mutable => writeln!(writer, "\t{hide_comment}\tdummy = 0;")?, } if !self.result.is_empty() { - write!(writer, "\t\treturn ")?; + write!(writer, "\t{hide_comment}\treturn ")?; self.result.solidity_default(writer, tc)?; writeln!(writer, ";")?; } - writeln!(writer, "\t}}")?; + writeln!(writer, "\t{hide_comment}}}")?; } else { writeln!(writer, ";")?; } + if self.hide { + writeln!(writer, "// FORMATTING: FORCE NEWLINE")?; + } Ok(()) } } --- a/pallets/common/src/dispatch.rs +++ b/pallets/common/src/dispatch.rs @@ -9,7 +9,7 @@ traits::Get, }; use sp_runtime::DispatchError; -use up_data_structs::{CollectionId, CreateCollectionData}; +use up_data_structs::{CollectionId, CreateCollectionData, CollectionFlags}; use crate::{pallet::Config, CommonCollectionOperations, CollectionHandle}; @@ -80,6 +80,7 @@ sender: T::CrossAccountId, payer: T::CrossAccountId, data: CreateCollectionData, + flags: CollectionFlags, ) -> Result; /// Delete the collection. --- a/pallets/common/src/erc.rs +++ b/pallets/common/src/erc.rs @@ -592,6 +592,7 @@ /// /// @dev Owner can be changed only by current owner /// @param newOwner new owner account + #[solidity(rename_selector = "changeCollectionOwner")] fn set_owner(&mut self, caller: caller, new_owner: address) -> Result { self.consume_store_writes(1)?; @@ -659,11 +660,6 @@ /// 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 { @@ -672,30 +668,17 @@ /// Key "url". pub fn url() -> up_data_structs::PropertyKey { - property_key_from_bytes(b"url").expect(EXPECT_CONVERT_ERROR) + property_key_from_bytes(b"URI").expect(EXPECT_CONVERT_ERROR) } /// Key "suffix". pub fn suffix() -> up_data_structs::PropertyKey { - property_key_from_bytes(b"suffix").expect(EXPECT_CONVERT_ERROR) + property_key_from_bytes(b"URISuffix").expect(EXPECT_CONVERT_ERROR) } /// Key "parentNft". pub fn parent_nft() -> up_data_structs::PropertyKey { property_key_from_bytes(b"parentNft").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) } } --- a/pallets/common/src/lib.rs +++ b/pallets/common/src/lib.rs @@ -71,6 +71,7 @@ Collection, RpcCollection, CollectionFlags, + RpcCollectionFlags, CollectionId, CreateItemData, MAX_TOKEN_PREFIX_LENGTH, @@ -824,7 +825,11 @@ token_property_permissions, properties, read_only: flags.external, - foreign: flags.foreign, + + flags: RpcCollectionFlags { + foreign: flags.foreign, + erc721metadata: flags.erc721metadata, + }, }) } } --- a/pallets/fungible/src/lib.rs +++ b/pallets/fungible/src/lib.rs @@ -212,8 +212,9 @@ owner: T::CrossAccountId, payer: T::CrossAccountId, data: CreateCollectionData, + flags: CollectionFlags, ) -> Result { - >::init_collection(owner, payer, data, CollectionFlags::default()) + >::init_collection(owner, payer, data, flags) } /// Initializes the collection with ForeignCollection flag. Returns [CollectionId] on success, [DispatchError] otherwise. --- a/pallets/fungible/src/stubs/UniqueFungible.sol +++ b/pallets/fungible/src/stubs/UniqueFungible.sol @@ -18,7 +18,7 @@ } /// @title A contract that allows you to work with collections. -/// @dev the ERC-165 identifier for this interface is 0x3e1e8083 +/// @dev the ERC-165 identifier for this interface is 0x62e22290 contract Collection is Dummy, ERC165 { /// Set collection property. /// @@ -296,9 +296,9 @@ /// /// @dev Owner can be changed only by current owner /// @param newOwner new owner account - /// @dev EVM selector for this function is: 0x13af4035, - /// or in textual repr: setOwner(address) - function setOwner(address newOwner) public { + /// @dev EVM selector for this function is: 0x4f53e226, + /// or in textual repr: changeCollectionOwner(address) + function changeCollectionOwner(address newOwner) public { require(false, stub_error); newOwner; dummy = 0; --- a/pallets/nonfungible/src/benchmarking.rs +++ b/pallets/nonfungible/src/benchmarking.rs @@ -55,7 +55,7 @@ owner, CollectionMode::NFT, |owner: T::CrossAccountId, data| { - >::init_collection(owner.clone(), owner, data, true) + >::init_collection(owner.clone(), owner, data, Default::default()) }, NonfungibleHandle::cast, ) --- a/pallets/nonfungible/src/erc.rs +++ b/pallets/nonfungible/src/erc.rs @@ -33,16 +33,12 @@ use pallet_evm_coder_substrate::dispatch_to_evm; use sp_std::vec::Vec; use pallet_common::{ - erc::{ - CommonEvmHandler, PrecompileResult, CollectionCall, - static_property::{key, value as property_value}, - }, + erc::{CommonEvmHandler, PrecompileResult, CollectionCall, static_property::key}, 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, @@ -194,7 +190,7 @@ } #[derive(ToLog)] -pub enum ERC721MintableEvents { +pub enum ERC721UniqueMintableEvents { #[allow(dead_code)] MintingFinished {}, } @@ -204,15 +200,17 @@ #[solidity_interface(name = ERC721Metadata, expect_selector = 0x5b5e139f)] impl NonfungibleHandle { /// @notice A descriptive name for a collection of NFTs in this contract - fn name(&self) -> Result { - Ok(decode_utf16(self.name.iter().copied()) - .map(|r| r.unwrap_or(REPLACEMENT_CHARACTER)) - .collect::()) + /// @dev real implementation of this function lies in `ERC721UniqueExtensions` + #[solidity(hide, rename_selector = "name")] + fn name_proxy(&self) -> Result { + self.name() } /// @notice An abbreviated name for NFTs in this contract - fn symbol(&self) -> Result { - Ok(string::from_utf8_lossy(&self.token_prefix).into()) + /// @dev real implementation of this function lies in `ERC721UniqueExtensions` + #[solidity(hide, rename_selector = "symbol")] + fn symbol_proxy(&self) -> Result { + self.symbol() } /// @notice A distinct Uniform Resource Identifier (URI) for a given asset. @@ -228,35 +226,38 @@ fn token_uri(&self, token_id: uint256) -> Result { 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); + match get_token_property(self, token_id_u32, &key::url()).as_deref() { + Err(_) | Ok("") => (), + Ok(url) => { + return Ok(url.into()); } - } else if !is_erc721_metadata_compatible::(self.id) { - return Err("tokenURI not set".into()); - } + }; - if let Some(base_uri) = + let 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| { + .map(BoundedVec::into_inner) + .map(string::from_utf8) + .transpose() + .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()); - } - } - return Ok(base_uri + token_id.to_string().as_str()); + let base_uri = match base_uri.as_deref() { + None | Some("") => { + return Ok("".into()); } - } + Some(base_uri) => base_uri.into(), + }; - Ok("".into()) + Ok( + match get_token_property(self, token_id_u32, &key::suffix()).as_deref() { + Err(_) | Ok("") => base_uri, + Ok(suffix) => base_uri + suffix, + }, + ) } } @@ -427,19 +428,33 @@ } /// @title ERC721 minting logic. -#[solidity_interface(name = ERC721Mintable, events(ERC721MintableEvents))] +#[solidity_interface(name = ERC721UniqueMintable, events(ERC721UniqueMintableEvents))] impl NonfungibleHandle { fn minting_finished(&self) -> Result { Ok(false) } /// @notice Function to mint token. + /// @param to The new owner + /// @return uint256 The id of the newly minted token + #[weight(>::create_item())] + fn mint(&mut self, caller: caller, to: address) -> Result { + let token_id: uint256 = >::get(self.id) + .checked_add(1) + .ok_or("item id overflow")? + .into(); + self.mint_check_id(caller, to, token_id)?; + Ok(token_id) + } + + /// @notice Function to mint token. /// @dev `tokenId` should be obtained with `nextTokenId` method, /// unlike standard, you can't specify it manually /// @param to The new owner /// @param tokenId ID of the minted NFT + #[solidity(hide, rename_selector = "mint")] #[weight(>::create_item())] - fn mint(&mut self, caller: caller, to: address, token_id: uint256) -> Result { + fn mint_check_id(&mut self, caller: caller, to: address, token_id: uint256) -> Result { let caller = T::CrossAccountId::from_eth(caller); let to = T::CrossAccountId::from_eth(to); let token_id: u32 = token_id.try_into()?; @@ -470,14 +485,34 @@ } /// @notice Function to mint token with the given tokenUri. + /// @param to The new owner + /// @param tokenUri Token URI that would be stored in the NFT properties + /// @return uint256 The id of the newly minted token + #[solidity(rename_selector = "mintWithTokenURI")] + #[weight(>::create_item())] + fn mint_with_token_uri( + &mut self, + caller: caller, + to: address, + token_uri: string, + ) -> Result { + let token_id: uint256 = >::get(self.id) + .checked_add(1) + .ok_or("item id overflow")? + .into(); + self.mint_with_token_uri_check_id(caller, to, token_id, token_uri)?; + Ok(token_id) + } + + /// @notice Function to mint token with the given tokenUri. /// @dev `tokenId` should be obtained with `nextTokenId` method, /// unlike standard, you can't specify it manually /// @param to The new owner /// @param tokenId ID of the minted NFT /// @param tokenUri Token URI that would be stored in the NFT properties - #[solidity(rename_selector = "mintWithTokenURI")] + #[solidity(hide, rename_selector = "mintWithTokenURI")] #[weight(>::create_item())] - fn mint_with_token_uri( + fn mint_with_token_uri_check_id( &mut self, caller: caller, to: address, @@ -550,17 +585,6 @@ 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, @@ -575,21 +599,23 @@ Error::Revert(alloc::format!("No permission for key {}", key)) })?; Ok(a) -} - -fn has_token_permission(collection_id: CollectionId, key: &PropertyKey) -> bool { - if let Ok(token_property_permissions) = - CollectionPropertyPermissions::::try_get(collection_id) - { - return token_property_permissions.contains_key(key); - } - - false } /// @title Unique extensions for ERC721. #[solidity_interface(name = ERC721UniqueExtensions)] impl NonfungibleHandle { + /// @notice A descriptive name for a collection of NFTs in this contract + fn name(&self) -> Result { + Ok(decode_utf16(self.name.iter().copied()) + .map(|r| r.unwrap_or(REPLACEMENT_CHARACTER)) + .collect::()) + } + + /// @notice An abbreviated name for NFTs in this contract + fn symbol(&self) -> Result { + Ok(string::from_utf8_lossy(&self.token_prefix).into()) + } + /// @notice Transfer ownership of an NFT /// @dev Throws unless `msg.sender` is the current owner. Throws if `to` /// is the zero address. Throws if `tokenId` is not a valid NFT. @@ -642,6 +668,7 @@ /// should be obtained with `nextTokenId` method /// @param to The new owner /// @param tokenIds IDs of the minted NFTs + // #[solidity(hide)] #[weight(>::create_multiple_items(token_ids.len() as u32))] fn mint_bulk(&mut self, caller: caller, to: address, token_ids: Vec) -> Result { let caller = T::CrossAccountId::from_eth(caller); @@ -678,7 +705,7 @@ /// numbers and first number should be obtained with `nextTokenId` method /// @param to The new owner /// @param tokens array of pairs of token ID and token URI for minted tokens - #[solidity(rename_selector = "mintBulkWithTokenURI")] + #[solidity(/*hide,*/ rename_selector = "mintBulkWithTokenURI")] #[weight(>::create_multiple_items(tokens.len() as u32))] fn mint_bulk_with_token_uri( &mut self, @@ -731,11 +758,11 @@ name = UniqueNFT, is( ERC721, - ERC721Metadata, ERC721Enumerable, ERC721UniqueExtensions, - ERC721Mintable, + ERC721UniqueMintable, ERC721Burnable, + ERC721Metadata(if(this.flags.erc721metadata)), Collection(via(common_mut returns CollectionHandle)), TokenProperties, ) --- a/pallets/nonfungible/src/lib.rs +++ b/pallets/nonfungible/src/lib.rs @@ -295,6 +295,7 @@ &mut self.0 } } + impl WithRecorder for NonfungibleHandle { fn recorder(&self) -> &SubstrateRecorder { self.0.recorder() @@ -407,17 +408,9 @@ owner: T::CrossAccountId, payer: T::CrossAccountId, data: CreateCollectionData, - is_external: bool, + flags: CollectionFlags, ) -> Result { - >::init_collection( - owner, - payer, - data, - CollectionFlags { - external: is_external, - ..Default::default() - }, - ) + >::init_collection(owner, payer, data, flags) } /// Destroy NFT collection --- a/pallets/nonfungible/src/stubs/UniqueNFT.sol +++ b/pallets/nonfungible/src/stubs/UniqueNFT.sol @@ -91,7 +91,7 @@ } /// @title A contract that allows you to work with collections. -/// @dev the ERC-165 identifier for this interface is 0x3e1e8083 +/// @dev the ERC-165 identifier for this interface is 0x62e22290 contract Collection is Dummy, ERC165 { /// Set collection property. /// @@ -369,9 +369,9 @@ /// /// @dev Owner can be changed only by current owner /// @param newOwner new owner account - /// @dev EVM selector for this function is: 0x13af4035, - /// or in textual repr: setOwner(address) - function setOwner(address newOwner) public { + /// @dev EVM selector for this function is: 0x4f53e226, + /// or in textual repr: changeCollectionOwner(address) + function changeCollectionOwner(address newOwner) public { require(false, stub_error); newOwner; dummy = 0; @@ -384,6 +384,49 @@ uint256 field_1; } +/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension +/// @dev See https://eips.ethereum.org/EIPS/eip-721 +/// @dev the ERC-165 identifier for this interface is 0x5b5e139f +contract ERC721Metadata is Dummy, ERC165 { + // /// @notice A descriptive name for a collection of NFTs in this contract + // /// @dev real implementation of this function lies in `ERC721UniqueExtensions` + // /// @dev EVM selector for this function is: 0x06fdde03, + // /// or in textual repr: name() + // function name() public view returns (string memory) { + // require(false, stub_error); + // dummy; + // return ""; + // } + + // /// @notice An abbreviated name for NFTs in this contract + // /// @dev real implementation of this function lies in `ERC721UniqueExtensions` + // /// @dev EVM selector for this function is: 0x95d89b41, + // /// or in textual repr: symbol() + // function symbol() public view returns (string memory) { + // require(false, stub_error); + // dummy; + // return ""; + // } + + /// @notice A distinct Uniform Resource Identifier (URI) for a given asset. + /// + /// @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 + /// @dev EVM selector for this function is: 0xc87b56dd, + /// or in textual repr: tokenURI(uint256) + function tokenURI(uint256 tokenId) public view returns (string memory) { + require(false, stub_error); + tokenId; + dummy; + return ""; + } +} + /// @title ERC721 Token that can be irreversibly burned (destroyed). /// @dev the ERC-165 identifier for this interface is 0x42966c68 contract ERC721Burnable is Dummy, ERC165 { @@ -401,13 +444,13 @@ } /// @dev inlined interface -contract ERC721MintableEvents { +contract ERC721UniqueMintableEvents { event MintingFinished(); } /// @title ERC721 minting logic. -/// @dev the ERC-165 identifier for this interface is 0x68ccfe89 -contract ERC721Mintable is Dummy, ERC165, ERC721MintableEvents { +/// @dev the ERC-165 identifier for this interface is 0x476ff149 +contract ERC721UniqueMintable is Dummy, ERC165, ERC721UniqueMintableEvents { /// @dev EVM selector for this function is: 0x05d2035b, /// or in textual repr: mintingFinished() function mintingFinished() public view returns (bool) { @@ -417,41 +460,63 @@ } /// @notice Function to mint token. - /// @dev `tokenId` should be obtained with `nextTokenId` method, - /// unlike standard, you can't specify it manually /// @param to The new owner - /// @param tokenId ID of the minted NFT - /// @dev EVM selector for this function is: 0x40c10f19, - /// or in textual repr: mint(address,uint256) - function mint(address to, uint256 tokenId) public returns (bool) { + /// @return uint256 The id of the newly minted token + /// @dev EVM selector for this function is: 0x6a627842, + /// or in textual repr: mint(address) + function mint(address to) public returns (uint256) { require(false, stub_error); to; - tokenId; dummy = 0; - return false; + return 0; } + // /// @notice Function to mint token. + // /// @dev `tokenId` should be obtained with `nextTokenId` method, + // /// unlike standard, you can't specify it manually + // /// @param to The new owner + // /// @param tokenId ID of the minted NFT + // /// @dev EVM selector for this function is: 0x40c10f19, + // /// or in textual repr: mint(address,uint256) + // function mint(address to, uint256 tokenId) public returns (bool) { + // require(false, stub_error); + // to; + // tokenId; + // dummy = 0; + // return false; + // } + /// @notice Function to mint token with the given tokenUri. - /// @dev `tokenId` should be obtained with `nextTokenId` method, - /// unlike standard, you can't specify it manually /// @param to The new owner - /// @param tokenId ID of the minted NFT /// @param tokenUri Token URI that would be stored in the NFT properties - /// @dev EVM selector for this function is: 0x50bb4e7f, - /// or in textual repr: mintWithTokenURI(address,uint256,string) - function mintWithTokenURI( - address to, - uint256 tokenId, - string memory tokenUri - ) public returns (bool) { + /// @return uint256 The id of the newly minted token + /// @dev EVM selector for this function is: 0x45c17782, + /// or in textual repr: mintWithTokenURI(address,string) + function mintWithTokenURI(address to, string memory tokenUri) public returns (uint256) { require(false, stub_error); to; - tokenId; tokenUri; dummy = 0; - return false; + return 0; } + // /// @notice Function to mint token with the given tokenUri. + // /// @dev `tokenId` should be obtained with `nextTokenId` method, + // /// unlike standard, you can't specify it manually + // /// @param to The new owner + // /// @param tokenId ID of the minted NFT + // /// @param tokenUri Token URI that would be stored in the NFT properties + // /// @dev EVM selector for this function is: 0x50bb4e7f, + // /// or in textual repr: mintWithTokenURI(address,uint256,string) + // function mintWithTokenURI(address to, uint256 tokenId, string memory tokenUri) public returns (bool) { + // require(false, stub_error); + // to; + // tokenId; + // tokenUri; + // dummy = 0; + // return false; + // } + /// @dev Not implemented /// @dev EVM selector for this function is: 0x7d64bcb4, /// or in textual repr: finishMinting() @@ -463,8 +528,26 @@ } /// @title Unique extensions for ERC721. -/// @dev the ERC-165 identifier for this interface is 0xd74d154f +/// @dev the ERC-165 identifier for this interface is 0x4468500d contract ERC721UniqueExtensions is Dummy, ERC165 { + /// @notice A descriptive name for a collection of NFTs in this contract + /// @dev EVM selector for this function is: 0x06fdde03, + /// or in textual repr: name() + function name() public view returns (string memory) { + require(false, stub_error); + dummy; + return ""; + } + + /// @notice An abbreviated name for NFTs in this contract + /// @dev EVM selector for this function is: 0x95d89b41, + /// or in textual repr: symbol() + function symbol() public view returns (string memory) { + require(false, stub_error); + dummy; + return ""; + } + /// @notice Transfer ownership of an NFT /// @dev Throws unless `msg.sender` is the current owner. Throws if `to` /// is the zero address. Throws if `tokenId` is not a valid NFT. @@ -525,7 +608,7 @@ /// @param tokens array of pairs of token ID and token URI for minted tokens /// @dev EVM selector for this function is: 0x36543006, /// or in textual repr: mintBulkWithTokenURI(address,(uint256,string)[]) - function mintBulkWithTokenURI(address to, Tuple8[] memory tokens) public returns (bool) { + function mintBulkWithTokenURI(address to, Tuple6[] memory tokens) public returns (bool) { require(false, stub_error); to; tokens; @@ -535,7 +618,7 @@ } /// @dev anonymous struct -struct Tuple8 { +struct Tuple6 { uint256 field_0; string field_1; } @@ -579,48 +662,7 @@ return 0; } } - -/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension -/// @dev See https://eips.ethereum.org/EIPS/eip-721 -/// @dev the ERC-165 identifier for this interface is 0x5b5e139f -contract ERC721Metadata is Dummy, ERC165 { - /// @notice A descriptive name for a collection of NFTs in this contract - /// @dev EVM selector for this function is: 0x06fdde03, - /// or in textual repr: name() - function name() public view returns (string memory) { - require(false, stub_error); - dummy; - return ""; - } - - /// @notice An abbreviated name for NFTs in this contract - /// @dev EVM selector for this function is: 0x95d89b41, - /// or in textual repr: symbol() - function symbol() public view returns (string memory) { - require(false, stub_error); - dummy; - return ""; - } - /// @notice A distinct Uniform Resource Identifier (URI) for a given asset. - /// - /// @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 - /// @dev EVM selector for this function is: 0xc87b56dd, - /// or in textual repr: tokenURI(uint256) - function tokenURI(uint256 tokenId) public view returns (string memory) { - require(false, stub_error); - tokenId; - dummy; - return ""; - } -} - /// @dev inlined interface contract ERC721Events { event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); @@ -766,11 +808,11 @@ Dummy, ERC165, ERC721, - ERC721Metadata, ERC721Enumerable, ERC721UniqueExtensions, - ERC721Mintable, + ERC721UniqueMintable, ERC721Burnable, + ERC721Metadata, Collection, TokenProperties {} --- a/pallets/proxy-rmrk-core/src/lib.rs +++ b/pallets/proxy-rmrk-core/src/lib.rs @@ -1448,7 +1448,15 @@ data: CreateCollectionData, properties: impl Iterator, ) -> Result { - let collection_id = >::init_collection(sender.clone(), sender, data, true); + let collection_id = >::init_collection( + sender.clone(), + sender, + data, + up_data_structs::CollectionFlags { + external: true, + ..Default::default() + }, + ); if let Err(DispatchError::Arithmetic(_)) = &collection_id { return Err(>::NoAvailableCollectionId.into()); --- a/pallets/proxy-rmrk-equip/src/lib.rs +++ b/pallets/proxy-rmrk-equip/src/lib.rs @@ -254,7 +254,10 @@ cross_sender.clone(), cross_sender.clone(), data, - true, + up_data_structs::CollectionFlags { + external: true, + ..Default::default() + }, ); if let Err(DispatchError::Arithmetic(_)) = &collection_id_res { --- a/pallets/refungible/src/erc.rs +++ b/pallets/refungible/src/erc.rs @@ -21,19 +21,15 @@ extern crate alloc; -use alloc::string::ToString; use core::{ char::{REPLACEMENT_CHARACTER, decode_utf16}, convert::TryInto, }; use evm_coder::{ToLog, execution::*, generate_stubgen, solidity, solidity_interface, types::*, weight}; -use frame_support::BoundedBTreeMap; +use frame_support::{BoundedBTreeMap, BoundedVec}; use pallet_common::{ CollectionHandle, CollectionPropertyPermissions, - erc::{ - CommonEvmHandler, CollectionCall, - static_property::{key, value as property_value}, - }, + erc::{CommonEvmHandler, CollectionCall, static_property::key}, }; use pallet_evm::{account::CrossAccountId, PrecompileHandle}; use pallet_evm_coder_substrate::{call, dispatch_to_evm}; @@ -191,7 +187,7 @@ } #[derive(ToLog)] -pub enum ERC721MintableEvents { +pub enum ERC721UniqueMintableEvents { /// @dev Not supported #[allow(dead_code)] MintingFinished {}, @@ -199,16 +195,18 @@ #[solidity_interface(name = ERC721Metadata)] impl RefungibleHandle { - /// @notice A descriptive name for a collection of RFTs in this contract - fn name(&self) -> Result { - Ok(decode_utf16(self.name.iter().copied()) - .map(|r| r.unwrap_or(REPLACEMENT_CHARACTER)) - .collect::()) + /// @notice A descriptive name for a collection of NFTs in this contract + /// @dev real implementation of this function lies in `ERC721UniqueExtensions` + #[solidity(hide, rename_selector = "name")] + fn name_proxy(&self) -> Result { + self.name() } - /// @notice An abbreviated name for RFTs in this contract - fn symbol(&self) -> Result { - Ok(string::from_utf8_lossy(&self.token_prefix).into()) + /// @notice An abbreviated name for NFTs in this contract + /// @dev real implementation of this function lies in `ERC721UniqueExtensions` + #[solidity(hide, rename_selector = "symbol")] + fn symbol_proxy(&self) -> Result { + self.symbol() } /// @notice A distinct Uniform Resource Identifier (URI) for a given asset. @@ -224,35 +222,38 @@ fn token_uri(&self, token_id: uint256) -> Result { 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); + match get_token_property(self, token_id_u32, &key::url()).as_deref() { + Err(_) | Ok("") => (), + Ok(url) => { + return Ok(url.into()); } - } else if !is_erc721_metadata_compatible::(self.id) { - return Err("tokenURI not set".into()); - } + }; - if let Some(base_uri) = + let 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| { + .map(BoundedVec::into_inner) + .map(string::from_utf8) + .transpose() + .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()); - } - } - return Ok(base_uri + token_id.to_string().as_str()); + let base_uri = match base_uri.as_deref() { + None | Some("") => { + return Ok("".into()); } - } + Some(base_uri) => base_uri.into(), + }; - Ok("".into()) + Ok( + match get_token_property(self, token_id_u32, &key::suffix()).as_deref() { + Err(_) | Ok("") => base_uri, + Ok(suffix) => base_uri + suffix, + }, + ) } } @@ -448,19 +449,33 @@ } /// @title ERC721 minting logic. -#[solidity_interface(name = ERC721Mintable, events(ERC721MintableEvents))] +#[solidity_interface(name = ERC721UniqueMintable, events(ERC721UniqueMintableEvents))] impl RefungibleHandle { fn minting_finished(&self) -> Result { Ok(false) } /// @notice Function to mint token. + /// @param to The new owner + /// @return uint256 The id of the newly minted token + #[weight(>::create_item())] + fn mint(&mut self, caller: caller, to: address) -> Result { + let token_id: uint256 = >::get(self.id) + .checked_add(1) + .ok_or("item id overflow")? + .into(); + self.mint_check_id(caller, to, token_id)?; + Ok(token_id) + } + + /// @notice Function to mint token. /// @dev `tokenId` should be obtained with `nextTokenId` method, /// unlike standard, you can't specify it manually /// @param to The new owner /// @param tokenId ID of the minted RFT + #[solidity(hide, rename_selector = "mint")] #[weight(>::create_item())] - fn mint(&mut self, caller: caller, to: address, token_id: uint256) -> Result { + fn mint_check_id(&mut self, caller: caller, to: address, token_id: uint256) -> Result { let caller = T::CrossAccountId::from_eth(caller); let to = T::CrossAccountId::from_eth(to); let token_id: u32 = token_id.try_into()?; @@ -496,14 +511,34 @@ } /// @notice Function to mint token with the given tokenUri. + /// @param to The new owner + /// @param tokenUri Token URI that would be stored in the NFT properties + /// @return uint256 The id of the newly minted token + #[solidity(rename_selector = "mintWithTokenURI")] + #[weight(>::create_item())] + fn mint_with_token_uri( + &mut self, + caller: caller, + to: address, + token_uri: string, + ) -> Result { + let token_id: uint256 = >::get(self.id) + .checked_add(1) + .ok_or("item id overflow")? + .into(); + self.mint_with_token_uri_check_id(caller, to, token_id, token_uri)?; + Ok(token_id) + } + + /// @notice Function to mint token with the given tokenUri. /// @dev `tokenId` should be obtained with `nextTokenId` method, /// unlike standard, you can't specify it manually /// @param to The new owner /// @param tokenId ID of the minted RFT /// @param tokenUri Token URI that would be stored in the RFT properties - #[solidity(rename_selector = "mintWithTokenURI")] + #[solidity(hide, rename_selector = "mintWithTokenURI")] #[weight(>::create_item())] - fn mint_with_token_uri( + fn mint_with_token_uri_check_id( &mut self, caller: caller, to: address, @@ -578,17 +613,6 @@ 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, @@ -608,6 +632,18 @@ /// @title Unique extensions for ERC721. #[solidity_interface(name = ERC721UniqueExtensions)] impl RefungibleHandle { + /// @notice A descriptive name for a collection of NFTs in this contract + fn name(&self) -> Result { + Ok(decode_utf16(self.name.iter().copied()) + .map(|r| r.unwrap_or(REPLACEMENT_CHARACTER)) + .collect::()) + } + + /// @notice An abbreviated name for NFTs in this contract + fn symbol(&self) -> Result { + Ok(string::from_utf8_lossy(&self.token_prefix).into()) + } + /// @notice Transfer ownership of an RFT /// @dev Throws unless `msg.sender` is the current owner. Throws if `to` /// is the zero address. Throws if `tokenId` is not a valid RFT. @@ -669,6 +705,7 @@ /// should be obtained with `nextTokenId` method /// @param to The new owner /// @param tokenIds IDs of the minted RFTs + // #[solidity(hide)] #[weight(>::create_multiple_items(token_ids.len() as u32))] fn mint_bulk(&mut self, caller: caller, to: address, token_ids: Vec) -> Result { let caller = T::CrossAccountId::from_eth(caller); @@ -711,7 +748,7 @@ /// numbers and first number should be obtained with `nextTokenId` method /// @param to The new owner /// @param tokens array of pairs of token ID and token URI for minted tokens - #[solidity(rename_selector = "mintBulkWithTokenURI")] + #[solidity(/*hide,*/ rename_selector = "mintBulkWithTokenURI")] #[weight(>::create_multiple_items(tokens.len() as u32))] fn mint_bulk_with_token_uri( &mut self, @@ -780,11 +817,11 @@ name = UniqueRefungible, is( ERC721, - ERC721Metadata, ERC721Enumerable, ERC721UniqueExtensions, - ERC721Mintable, + ERC721UniqueMintable, ERC721Burnable, + ERC721Metadata(if(this.flags.erc721metadata)), Collection(via(common_mut returns CollectionHandle)), TokenProperties, ) --- a/pallets/refungible/src/lib.rs +++ b/pallets/refungible/src/lib.rs @@ -92,9 +92,11 @@ use codec::{Encode, Decode, MaxEncodedLen}; use core::ops::Deref; +use derivative::Derivative; use evm_coder::ToLog; use frame_support::{ - BoundedVec, ensure, fail, storage::with_transaction, transactional, pallet_prelude::ConstU32, + BoundedBTreeMap, BoundedVec, ensure, fail, storage::with_transaction, transactional, + pallet_prelude::ConstU32, }; use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm}; use pallet_evm_coder_substrate::WithRecorder; @@ -113,8 +115,6 @@ MAX_REFUNGIBLE_PIECES, Property, PropertyKey, PropertyKeyPermission, PropertyPermission, PropertyScope, PropertyValue, TokenId, TrySetProperty, }; -use frame_support::BoundedBTreeMap; -use derivative::Derivative; pub use pallet::*; #[cfg(feature = "runtime-benchmarks")] @@ -371,8 +371,9 @@ owner: T::CrossAccountId, payer: T::CrossAccountId, data: CreateCollectionData, + flags: CollectionFlags, ) -> Result { - >::init_collection(owner, payer, data, CollectionFlags::default()) + >::init_collection(owner, payer, data, flags) } /// Destroy RFT collection --- a/pallets/refungible/src/stubs/UniqueRefungible.sol +++ b/pallets/refungible/src/stubs/UniqueRefungible.sol @@ -91,7 +91,7 @@ } /// @title A contract that allows you to work with collections. -/// @dev the ERC-165 identifier for this interface is 0x3e1e8083 +/// @dev the ERC-165 identifier for this interface is 0x62e22290 contract Collection is Dummy, ERC165 { /// Set collection property. /// @@ -369,9 +369,9 @@ /// /// @dev Owner can be changed only by current owner /// @param newOwner new owner account - /// @dev EVM selector for this function is: 0x13af4035, - /// or in textual repr: setOwner(address) - function setOwner(address newOwner) public { + /// @dev EVM selector for this function is: 0x4f53e226, + /// or in textual repr: changeCollectionOwner(address) + function changeCollectionOwner(address newOwner) public { require(false, stub_error); newOwner; dummy = 0; @@ -384,6 +384,47 @@ uint256 field_1; } +/// @dev the ERC-165 identifier for this interface is 0x5b5e139f +contract ERC721Metadata is Dummy, ERC165 { + // /// @notice A descriptive name for a collection of NFTs in this contract + // /// @dev real implementation of this function lies in `ERC721UniqueExtensions` + // /// @dev EVM selector for this function is: 0x06fdde03, + // /// or in textual repr: name() + // function name() public view returns (string memory) { + // require(false, stub_error); + // dummy; + // return ""; + // } + + // /// @notice An abbreviated name for NFTs in this contract + // /// @dev real implementation of this function lies in `ERC721UniqueExtensions` + // /// @dev EVM selector for this function is: 0x95d89b41, + // /// or in textual repr: symbol() + // function symbol() public view returns (string memory) { + // require(false, stub_error); + // dummy; + // return ""; + // } + + /// @notice A distinct Uniform Resource Identifier (URI) for a given asset. + /// + /// @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 + /// @dev EVM selector for this function is: 0xc87b56dd, + /// or in textual repr: tokenURI(uint256) + function tokenURI(uint256 tokenId) public view returns (string memory) { + require(false, stub_error); + tokenId; + dummy; + return ""; + } +} + /// @title ERC721 Token that can be irreversibly burned (destroyed). /// @dev the ERC-165 identifier for this interface is 0x42966c68 contract ERC721Burnable is Dummy, ERC165 { @@ -401,13 +442,13 @@ } /// @dev inlined interface -contract ERC721MintableEvents { +contract ERC721UniqueMintableEvents { event MintingFinished(); } /// @title ERC721 minting logic. -/// @dev the ERC-165 identifier for this interface is 0x68ccfe89 -contract ERC721Mintable is Dummy, ERC165, ERC721MintableEvents { +/// @dev the ERC-165 identifier for this interface is 0x476ff149 +contract ERC721UniqueMintable is Dummy, ERC165, ERC721UniqueMintableEvents { /// @dev EVM selector for this function is: 0x05d2035b, /// or in textual repr: mintingFinished() function mintingFinished() public view returns (bool) { @@ -417,41 +458,63 @@ } /// @notice Function to mint token. - /// @dev `tokenId` should be obtained with `nextTokenId` method, - /// unlike standard, you can't specify it manually /// @param to The new owner - /// @param tokenId ID of the minted RFT - /// @dev EVM selector for this function is: 0x40c10f19, - /// or in textual repr: mint(address,uint256) - function mint(address to, uint256 tokenId) public returns (bool) { + /// @return uint256 The id of the newly minted token + /// @dev EVM selector for this function is: 0x6a627842, + /// or in textual repr: mint(address) + function mint(address to) public returns (uint256) { require(false, stub_error); to; - tokenId; dummy = 0; - return false; + return 0; } + // /// @notice Function to mint token. + // /// @dev `tokenId` should be obtained with `nextTokenId` method, + // /// unlike standard, you can't specify it manually + // /// @param to The new owner + // /// @param tokenId ID of the minted RFT + // /// @dev EVM selector for this function is: 0x40c10f19, + // /// or in textual repr: mint(address,uint256) + // function mint(address to, uint256 tokenId) public returns (bool) { + // require(false, stub_error); + // to; + // tokenId; + // dummy = 0; + // return false; + // } + /// @notice Function to mint token with the given tokenUri. - /// @dev `tokenId` should be obtained with `nextTokenId` method, - /// unlike standard, you can't specify it manually /// @param to The new owner - /// @param tokenId ID of the minted RFT - /// @param tokenUri Token URI that would be stored in the RFT properties - /// @dev EVM selector for this function is: 0x50bb4e7f, - /// or in textual repr: mintWithTokenURI(address,uint256,string) - function mintWithTokenURI( - address to, - uint256 tokenId, - string memory tokenUri - ) public returns (bool) { + /// @param tokenUri Token URI that would be stored in the NFT properties + /// @return uint256 The id of the newly minted token + /// @dev EVM selector for this function is: 0x45c17782, + /// or in textual repr: mintWithTokenURI(address,string) + function mintWithTokenURI(address to, string memory tokenUri) public returns (uint256) { require(false, stub_error); to; - tokenId; tokenUri; dummy = 0; - return false; + return 0; } + // /// @notice Function to mint token with the given tokenUri. + // /// @dev `tokenId` should be obtained with `nextTokenId` method, + // /// unlike standard, you can't specify it manually + // /// @param to The new owner + // /// @param tokenId ID of the minted RFT + // /// @param tokenUri Token URI that would be stored in the RFT properties + // /// @dev EVM selector for this function is: 0x50bb4e7f, + // /// or in textual repr: mintWithTokenURI(address,uint256,string) + // function mintWithTokenURI(address to, uint256 tokenId, string memory tokenUri) public returns (bool) { + // require(false, stub_error); + // to; + // tokenId; + // tokenUri; + // dummy = 0; + // return false; + // } + /// @dev Not implemented /// @dev EVM selector for this function is: 0x7d64bcb4, /// or in textual repr: finishMinting() @@ -463,8 +526,26 @@ } /// @title Unique extensions for ERC721. -/// @dev the ERC-165 identifier for this interface is 0x7c3bef89 +/// @dev the ERC-165 identifier for this interface is 0xef1eaacb contract ERC721UniqueExtensions is Dummy, ERC165 { + /// @notice A descriptive name for a collection of NFTs in this contract + /// @dev EVM selector for this function is: 0x06fdde03, + /// or in textual repr: name() + function name() public view returns (string memory) { + require(false, stub_error); + dummy; + return ""; + } + + /// @notice An abbreviated name for NFTs in this contract + /// @dev EVM selector for this function is: 0x95d89b41, + /// or in textual repr: symbol() + function symbol() public view returns (string memory) { + require(false, stub_error); + dummy; + return ""; + } + /// @notice Transfer ownership of an RFT /// @dev Throws unless `msg.sender` is the current owner. Throws if `to` /// is the zero address. Throws if `tokenId` is not a valid RFT. @@ -527,7 +608,7 @@ /// @param tokens array of pairs of token ID and token URI for minted tokens /// @dev EVM selector for this function is: 0x36543006, /// or in textual repr: mintBulkWithTokenURI(address,(uint256,string)[]) - function mintBulkWithTokenURI(address to, Tuple8[] memory tokens) public returns (bool) { + function mintBulkWithTokenURI(address to, Tuple6[] memory tokens) public returns (bool) { require(false, stub_error); to; tokens; @@ -549,7 +630,7 @@ } /// @dev anonymous struct -struct Tuple8 { +struct Tuple6 { uint256 field_0; string field_1; } @@ -591,46 +672,7 @@ require(false, stub_error); dummy; return 0; - } -} - -/// @dev the ERC-165 identifier for this interface is 0x5b5e139f -contract ERC721Metadata is Dummy, ERC165 { - /// @notice A descriptive name for a collection of RFTs in this contract - /// @dev EVM selector for this function is: 0x06fdde03, - /// or in textual repr: name() - function name() public view returns (string memory) { - require(false, stub_error); - dummy; - return ""; - } - - /// @notice An abbreviated name for RFTs in this contract - /// @dev EVM selector for this function is: 0x95d89b41, - /// or in textual repr: symbol() - function symbol() public view returns (string memory) { - require(false, stub_error); - dummy; - return ""; } - - /// @notice A distinct Uniform Resource Identifier (URI) for a given asset. - /// - /// @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 - /// @dev EVM selector for this function is: 0xc87b56dd, - /// or in textual repr: tokenURI(uint256) - function tokenURI(uint256 tokenId) public view returns (string memory) { - require(false, stub_error); - tokenId; - dummy; - return ""; - } } /// @dev inlined interface @@ -776,11 +818,11 @@ Dummy, ERC165, ERC721, - ERC721Metadata, ERC721Enumerable, ERC721UniqueExtensions, - ERC721Mintable, + ERC721UniqueMintable, ERC721Burnable, + ERC721Metadata, Collection, TokenProperties {} --- a/pallets/unique/src/eth/mod.rs +++ b/pallets/unique/src/eth/mod.rs @@ -25,14 +25,16 @@ dispatch::CollectionDispatch, erc::{ CollectionHelpersEvents, - static_property::{key, value as property_value}, + static_property::{key}, }, + Pallet as PalletCommon, }; use pallet_evm_coder_substrate::{dispatch_to_evm, SubstrateRecorder, WithRecorder}; use pallet_evm::{account::CrossAccountId, OnMethodCall, PrecompileHandle, PrecompileResult}; +use sp_std::vec; use up_data_structs::{ CollectionName, CollectionDescription, CollectionTokenPrefix, CreateCollectionData, - CollectionMode, PropertyValue, + CollectionMode, PropertyValue, CollectionFlags, }; use crate::{Config, SelfWeightOf, weights::WeightInfo}; @@ -57,13 +59,11 @@ 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 @@ -81,75 +81,7 @@ 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) + Ok((caller, name, description, token_prefix)) } fn create_refungible_collection_internal< @@ -160,26 +92,27 @@ name: string, description: string, token_prefix: string, - base_uri: string, - add_properties: bool, ) -> Result
{ - let (caller, name, description, token_prefix, base_uri_value) = - convert_data::(caller, name, description, token_prefix, base_uri)?; - let data = make_data::( + let (caller, name, description, token_prefix) = + convert_data::(caller, name, description, token_prefix)?; + let data = CreateCollectionData { name, - CollectionMode::ReFungible, + mode: CollectionMode::ReFungible, description, token_prefix, - base_uri_value, - add_properties, - )?; + ..Default::default() + }; check_sent_amount_equals_collection_creation_price::(value)?; let collection_helpers_address = T::CrossAccountId::from_eth(::ContractAddress::get()); - let collection_id = - T::CollectionDispatch::create(caller.clone(), collection_helpers_address, data) - .map_err(pallet_evm_coder_substrate::dispatch_to_evm::)?; + let collection_id = T::CollectionDispatch::create( + caller.clone(), + collection_helpers_address, + data, + Default::default(), + ) + .map_err(pallet_evm_coder_substrate::dispatch_to_evm::)?; let address = pallet_common::eth::collection_id_to_address(collection_id); Ok(address) } @@ -212,7 +145,8 @@ /// @param tokenPrefix Token prefix to represent the collection tokens in UI and user applications /// @return address Address of the newly created collection #[weight(>::create_collection())] - fn create_nonfungible_collection( + #[solidity(rename_selector = "createNFTCollection")] + fn create_nft_collection( &mut self, caller: caller, value: value, @@ -220,60 +154,51 @@ description: string, token_prefix: string, ) -> Result
{ - let (caller, name, description, token_prefix, _base_uri_value) = - convert_data::(caller, name, description, token_prefix, "".into())?; - let data = make_data::( + let (caller, name, description, token_prefix) = + convert_data::(caller, name, description, token_prefix)?; + let data = CreateCollectionData { name, - CollectionMode::NFT, + mode: CollectionMode::NFT, description, token_prefix, - Default::default(), - false, - )?; + ..Default::default() + }; check_sent_amount_equals_collection_creation_price::(value)?; let collection_helpers_address = T::CrossAccountId::from_eth(::ContractAddress::get()); - let collection_id = T::CollectionDispatch::create(caller, collection_helpers_address, data) - .map_err(dispatch_to_evm::)?; + let collection_id = T::CollectionDispatch::create( + caller, + collection_helpers_address, + data, + Default::default(), + ) + .map_err(dispatch_to_evm::)?; let address = pallet_common::eth::collection_id_to_address(collection_id); Ok(address) } - + /// Create an NFT collection + /// @param name Name of the collection + /// @param description Informative description of the collection + /// @param tokenPrefix Token prefix to represent the collection tokens in UI and user applications + /// @return address Address of the newly created collection #[weight(>::create_collection())] - #[solidity(rename_selector = "createERC721MetadataCompatibleCollection")] - fn create_nonfungible_collection_with_properties( + #[deprecated(note = "mathod was renamed to `create_nft_collection`, prefer it instead")] + #[solidity(hide)] + fn create_nonfungible_collection( &mut self, caller: caller, value: value, 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, - )?; - check_sent_amount_equals_collection_creation_price::(value)?; - let collection_helpers_address = - T::CrossAccountId::from_eth(::ContractAddress::get()); - let collection_id = T::CollectionDispatch::create(caller, collection_helpers_address, data) - .map_err(pallet_evm_coder_substrate::dispatch_to_evm::)?; - - let address = pallet_common::eth::collection_id_to_address(collection_id); - Ok(address) + self.create_nft_collection(caller, value, name, description, token_prefix) } #[weight(>::create_collection())] #[solidity(rename_selector = "createRFTCollection")] - fn create_refungible_collection( + fn create_rft_collection( &mut self, caller: caller, value: value, @@ -281,37 +206,94 @@ description: string, token_prefix: string, ) -> Result
{ - create_refungible_collection_internal::( - caller, - value, - name, - description, - token_prefix, - Default::default(), - false, - ) + create_refungible_collection_internal::(caller, value, name, description, token_prefix) } - #[weight(>::create_collection())] - #[solidity(rename_selector = "createERC721MetadataCompatibleRFTCollection")] - fn create_refungible_collection_with_properties( + #[solidity(rename_selector = "makeCollectionERC721MetadataCompatible")] + fn make_collection_metadata_compatible( &mut self, caller: caller, - value: value, - name: string, - description: string, - token_prefix: string, + collection: address, base_uri: string, - ) -> Result
{ - create_refungible_collection_internal::( - caller, - value, - name, - description, - token_prefix, - base_uri, - true, - ) + ) -> Result<()> { + let caller = T::CrossAccountId::from_eth(caller); + let collection = + pallet_common::eth::map_eth_to_id(&collection).ok_or("not a collection address")?; + let mut collection = + >::new(collection).ok_or("collection not found")?; + + if !matches!( + collection.mode, + CollectionMode::NFT | CollectionMode::ReFungible + ) { + return Err("target collection should be either NFT or Refungible".into()); + } + + self.recorder().consume_sstore()?; + collection + .check_is_owner_or_admin(&caller) + .map_err(dispatch_to_evm::)?; + + if collection.flags.erc721metadata { + return Err("target collection is already Erc721Metadata compatible".into()); + } + collection.flags.erc721metadata = true; + + let all_permissions = >::get(collection.id); + if all_permissions.get(&key::url()).is_none() { + self.recorder().consume_sstore()?; + >::set_property_permission( + &collection, + &caller, + up_data_structs::PropertyKeyPermission { + key: key::url(), + permission: up_data_structs::PropertyPermission { + mutable: true, + collection_admin: true, + token_owner: false, + }, + }, + ) + .map_err(dispatch_to_evm::)?; + } + if all_permissions.get(&key::suffix()).is_none() { + self.recorder().consume_sstore()?; + >::set_property_permission( + &collection, + &caller, + up_data_structs::PropertyKeyPermission { + key: key::suffix(), + permission: up_data_structs::PropertyPermission { + mutable: true, + collection_admin: true, + token_owner: false, + }, + }, + ) + .map_err(dispatch_to_evm::)?; + } + + let all_properties = >::get(collection.id); + if all_properties.get(&key::base_uri()).is_none() && !base_uri.is_empty() { + self.recorder().consume_sstore()?; + >::set_collection_properties( + &collection, + &caller, + vec![up_data_structs::Property { + key: key::base_uri(), + value: base_uri + .into_bytes() + .try_into() + .map_err(|_| "base uri is too large")?, + }], + ) + .map_err(dispatch_to_evm::)?; + } + + self.recorder().consume_sstore()?; + collection.save().map_err(dispatch_to_evm::)?; + + Ok(()) } /// Check if a collection exists --- a/pallets/unique/src/eth/stubs/CollectionHelpers.sol +++ b/pallets/unique/src/eth/stubs/CollectionHelpers.sol @@ -23,16 +23,16 @@ } /// @title Contract, which allows users to operate with collections -/// @dev the ERC-165 identifier for this interface is 0x5ad4f440 +/// @dev the ERC-165 identifier for this interface is 0x58918631 contract CollectionHelpers is Dummy, ERC165, CollectionHelpersEvents { /// Create an NFT collection /// @param name Name of the collection /// @param description Informative description of the collection /// @param tokenPrefix Token prefix to represent the collection tokens in UI and user applications /// @return address Address of the newly created collection - /// @dev EVM selector for this function is: 0xe34a6844, - /// or in textual repr: createNonfungibleCollection(string,string,string) - function createNonfungibleCollection( + /// @dev EVM selector for this function is: 0x844af658, + /// or in textual repr: createNFTCollection(string,string,string) + function createNFTCollection( string memory name, string memory description, string memory tokenPrefix @@ -45,22 +45,21 @@ return 0x0000000000000000000000000000000000000000; } - /// @dev EVM selector for this function is: 0xa634a5f9, - /// or in textual repr: createERC721MetadataCompatibleCollection(string,string,string,string) - function createERC721MetadataCompatibleCollection( - string memory name, - string memory description, - string memory tokenPrefix, - string memory baseUri - ) public payable returns (address) { - require(false, stub_error); - name; - description; - tokenPrefix; - baseUri; - dummy = 0; - return 0x0000000000000000000000000000000000000000; - } + // /// Create an NFT collection + // /// @param name Name of the collection + // /// @param description Informative description of the collection + // /// @param tokenPrefix Token prefix to represent the collection tokens in UI and user applications + // /// @return address Address of the newly created collection + // /// @dev EVM selector for this function is: 0xe34a6844, + // /// or in textual repr: createNonfungibleCollection(string,string,string) + // function createNonfungibleCollection(string memory name, string memory description, string memory tokenPrefix) public payable returns (address) { + // require(false, stub_error); + // name; + // description; + // tokenPrefix; + // dummy = 0; + // return 0x0000000000000000000000000000000000000000; + // } /// @dev EVM selector for this function is: 0xab173450, /// or in textual repr: createRFTCollection(string,string,string) @@ -77,21 +76,13 @@ return 0x0000000000000000000000000000000000000000; } - /// @dev EVM selector for this function is: 0xa5596388, - /// or in textual repr: createERC721MetadataCompatibleRFTCollection(string,string,string,string) - function createERC721MetadataCompatibleRFTCollection( - string memory name, - string memory description, - string memory tokenPrefix, - string memory baseUri - ) public payable returns (address) { + /// @dev EVM selector for this function is: 0x85624258, + /// or in textual repr: makeCollectionERC721MetadataCompatible(address,string) + function makeCollectionERC721MetadataCompatible(address collection, string memory baseUri) public { require(false, stub_error); - name; - description; - tokenPrefix; + collection; baseUri; dummy = 0; - return 0x0000000000000000000000000000000000000000; } /// Check if a collection exists --- a/pallets/unique/src/lib.rs +++ b/pallets/unique/src/lib.rs @@ -345,7 +345,7 @@ // ========= let sender = T::CrossAccountId::from_sub(sender); - let _id = T::CollectionDispatch::create(sender.clone(), sender, data)?; + let _id = T::CollectionDispatch::create(sender.clone(), sender, data, Default::default())?; Ok(()) } --- a/primitives/data-structs/src/lib.rs +++ b/primitives/data-structs/src/lib.rs @@ -365,11 +365,14 @@ /// Tokens in foreign collections can be transferred, but not burnt #[bondrewd(bits = "0..1")] pub foreign: bool, + /// Supports ERC721Metadata + #[bondrewd(bits = "1..2")] + pub erc721metadata: bool, /// External collections can't be managed using `unique` api #[bondrewd(bits = "7..8")] pub external: bool, - #[bondrewd(reserve, bits = "1..7")] + #[bondrewd(reserve, bits = "2..7")] pub reserved: u8, } bondrewd_codec!(CollectionFlags); @@ -434,6 +437,15 @@ pub meta_update_permission: MetaUpdatePermission, } +#[derive(Encode, Decode, Clone, PartialEq, TypeInfo)] +#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))] +pub struct RpcCollectionFlags { + /// Is collection is foreign. + pub foreign: bool, + /// Collection supports ERC721Metadata. + pub erc721metadata: bool, +} + /// Collection parameters, used in RPC calls (see [`Collection`] for the storage version). #[derive(Encode, Decode, Clone, PartialEq, TypeInfo)] #[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))] @@ -471,8 +483,8 @@ /// Is collection read only. pub read_only: bool, - /// Is collection is foreign. - pub foreign: bool, + /// Extra collection flags + pub flags: RpcCollectionFlags, } /// Data used for create collection. --- a/runtime/common/dispatch.rs +++ b/runtime/common/dispatch.rs @@ -31,7 +31,7 @@ }; use up_data_structs::{ CollectionMode, CreateCollectionData, MAX_DECIMAL_POINTS, mapping::TokenAddressMapping, - CollectionId, + CollectionId, CollectionFlags, }; #[cfg(not(feature = "refungible"))] @@ -57,10 +57,11 @@ sender: T::CrossAccountId, payer: T::CrossAccountId, data: CreateCollectionData, + flags: CollectionFlags, ) -> Result { let id = match data.mode { CollectionMode::NFT => { - >::init_collection(sender, payer, data, false)? + >::init_collection(sender, payer, data, flags)? } CollectionMode::Fungible(decimal_points) => { // check params @@ -68,11 +69,13 @@ decimal_points <= MAX_DECIMAL_POINTS, pallet_unique::Error::::CollectionDecimalPointLimitExceeded ); - >::init_collection(sender, payer, data)? + >::init_collection(sender, payer, data, flags)? } #[cfg(feature = "refungible")] - CollectionMode::ReFungible => >::init_collection(sender, payer, data)?, + CollectionMode::ReFungible => { + >::init_collection(sender, payer, data, flags)? + } #[cfg(not(feature = "refungible"))] CollectionMode::ReFungible => return unsupported!(T), --- a/runtime/common/ethereum/sponsoring.rs +++ b/runtime/common/ethereum/sponsoring.rs @@ -24,7 +24,7 @@ use pallet_nonfungible::{ Config as NonfungibleConfig, erc::{ - UniqueNFTCall, ERC721UniqueExtensionsCall, ERC721MintableCall, ERC721Call, + UniqueNFTCall, ERC721UniqueExtensionsCall, ERC721UniqueMintableCall, ERC721Call, TokenPropertiesCall, }, }; @@ -82,18 +82,17 @@ let token_id: TokenId = token_id.try_into().ok()?; withdraw_transfer::(&collection, &who, &token_id).map(|()| sponsor) } - UniqueNFTCall::ERC721Mintable( - ERC721MintableCall::Mint { token_id, .. } - | ERC721MintableCall::MintWithTokenUri { token_id, .. }, - ) => { - let _token_id: TokenId = token_id.try_into().ok()?; - withdraw_create_item::( - &collection, - &who, - &CreateItemData::NFT(CreateNftData::default()), - ) - .map(|()| sponsor) - } + UniqueNFTCall::ERC721UniqueMintable( + ERC721UniqueMintableCall::Mint { .. } + | ERC721UniqueMintableCall::MintCheckId { .. } + | ERC721UniqueMintableCall::MintWithTokenUri { .. } + | ERC721UniqueMintableCall::MintWithTokenUriCheckId { .. }, + ) => withdraw_create_item::( + &collection, + &who, + &CreateItemData::NFT(CreateNftData::default()), + ) + .map(|()| sponsor), UniqueNFTCall::ERC721(ERC721Call::TransferFrom { token_id, from, .. }) => { let token_id: TokenId = token_id.try_into().ok()?; let from = T::CrossAccountId::from_eth(from); --- a/tests/src/eth/allowlist.test.ts +++ b/tests/src/eth/allowlist.test.ts @@ -78,7 +78,7 @@ const owner = await helper.eth.createAccountWithBalance(donor); const user = helper.eth.createAccount(); - const {collectionAddress} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C'); + const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C'); const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); expect(await collectionEvm.methods.allowed(user).call({from: owner})).to.be.false; @@ -94,7 +94,7 @@ // const owner = await helper.eth.createAccountWithBalance(donor); // const user = donor; - // const {collectionAddress, collectionId} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C'); + // const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C'); // const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); // expect(await helper.collection.allowed(collectionId, {Substrate: user.address})).to.be.false; @@ -110,7 +110,7 @@ const notOwner = await helper.eth.createAccountWithBalance(donor); const user = helper.eth.createAccount(); - const {collectionAddress} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C'); + const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C'); const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); expect(await collectionEvm.methods.allowed(user).call({from: owner})).to.be.false; @@ -129,7 +129,7 @@ // const notOwner = await helper.eth.createAccountWithBalance(donor); // const user = donor; - // const {collectionAddress, collectionId} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C'); + // const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C'); // const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); // expect(await helper.collection.allowed(collectionId, {Substrate: user.address})).to.be.false; --- a/tests/src/eth/api/CollectionHelpers.sol +++ b/tests/src/eth/api/CollectionHelpers.sol @@ -18,29 +18,29 @@ } /// @title Contract, which allows users to operate with collections -/// @dev the ERC-165 identifier for this interface is 0x5ad4f440 +/// @dev the ERC-165 identifier for this interface is 0x58918631 interface CollectionHelpers is Dummy, ERC165, CollectionHelpersEvents { /// Create an NFT collection /// @param name Name of the collection /// @param description Informative description of the collection /// @param tokenPrefix Token prefix to represent the collection tokens in UI and user applications /// @return address Address of the newly created collection - /// @dev EVM selector for this function is: 0xe34a6844, - /// or in textual repr: createNonfungibleCollection(string,string,string) - function createNonfungibleCollection( + /// @dev EVM selector for this function is: 0x844af658, + /// or in textual repr: createNFTCollection(string,string,string) + function createNFTCollection( string memory name, string memory description, string memory tokenPrefix ) external payable returns (address); - /// @dev EVM selector for this function is: 0xa634a5f9, - /// or in textual repr: createERC721MetadataCompatibleCollection(string,string,string,string) - function createERC721MetadataCompatibleCollection( - string memory name, - string memory description, - string memory tokenPrefix, - string memory baseUri - ) external payable returns (address); + // /// Create an NFT collection + // /// @param name Name of the collection + // /// @param description Informative description of the collection + // /// @param tokenPrefix Token prefix to represent the collection tokens in UI and user applications + // /// @return address Address of the newly created collection + // /// @dev EVM selector for this function is: 0xe34a6844, + // /// or in textual repr: createNonfungibleCollection(string,string,string) + // function createNonfungibleCollection(string memory name, string memory description, string memory tokenPrefix) external payable returns (address); /// @dev EVM selector for this function is: 0xab173450, /// or in textual repr: createRFTCollection(string,string,string) @@ -50,14 +50,9 @@ string memory tokenPrefix ) external payable returns (address); - /// @dev EVM selector for this function is: 0xa5596388, - /// or in textual repr: createERC721MetadataCompatibleRFTCollection(string,string,string,string) - function createERC721MetadataCompatibleRFTCollection( - string memory name, - string memory description, - string memory tokenPrefix, - string memory baseUri - ) external payable returns (address); + /// @dev EVM selector for this function is: 0x85624258, + /// or in textual repr: makeCollectionERC721MetadataCompatible(address,string) + function makeCollectionERC721MetadataCompatible(address collection, string memory baseUri) external; /// Check if a collection exists /// @param collectionAddress Address of the collection in question --- a/tests/src/eth/api/UniqueFungible.sol +++ b/tests/src/eth/api/UniqueFungible.sol @@ -13,7 +13,7 @@ } /// @title A contract that allows you to work with collections. -/// @dev the ERC-165 identifier for this interface is 0x3e1e8083 +/// @dev the ERC-165 identifier for this interface is 0x62e22290 interface Collection is Dummy, ERC165 { /// Set collection property. /// @@ -194,9 +194,9 @@ /// /// @dev Owner can be changed only by current owner /// @param newOwner new owner account - /// @dev EVM selector for this function is: 0x13af4035, - /// or in textual repr: setOwner(address) - function setOwner(address newOwner) external; + /// @dev EVM selector for this function is: 0x4f53e226, + /// or in textual repr: changeCollectionOwner(address) + function changeCollectionOwner(address newOwner) external; } /// @dev the ERC-165 identifier for this interface is 0x63034ac5 --- a/tests/src/eth/api/UniqueNFT.sol +++ b/tests/src/eth/api/UniqueNFT.sol @@ -62,7 +62,7 @@ } /// @title A contract that allows you to work with collections. -/// @dev the ERC-165 identifier for this interface is 0x3e1e8083 +/// @dev the ERC-165 identifier for this interface is 0x62e22290 interface Collection is Dummy, ERC165 { /// Set collection property. /// @@ -243,9 +243,9 @@ /// /// @dev Owner can be changed only by current owner /// @param newOwner new owner account - /// @dev EVM selector for this function is: 0x13af4035, - /// or in textual repr: setOwner(address) - function setOwner(address newOwner) external; + /// @dev EVM selector for this function is: 0x4f53e226, + /// or in textual repr: changeCollectionOwner(address) + function changeCollectionOwner(address newOwner) external; } /// @dev anonymous struct @@ -254,6 +254,36 @@ uint256 field_1; } +/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension +/// @dev See https://eips.ethereum.org/EIPS/eip-721 +/// @dev the ERC-165 identifier for this interface is 0x5b5e139f +interface ERC721Metadata is Dummy, ERC165 { + // /// @notice A descriptive name for a collection of NFTs in this contract + // /// @dev real implementation of this function lies in `ERC721UniqueExtensions` + // /// @dev EVM selector for this function is: 0x06fdde03, + // /// or in textual repr: name() + // function name() external view returns (string memory); + + // /// @notice An abbreviated name for NFTs in this contract + // /// @dev real implementation of this function lies in `ERC721UniqueExtensions` + // /// @dev EVM selector for this function is: 0x95d89b41, + // /// or in textual repr: symbol() + // function symbol() external view returns (string memory); + + /// @notice A distinct Uniform Resource Identifier (URI) for a given asset. + /// + /// @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 + /// @dev EVM selector for this function is: 0xc87b56dd, + /// or in textual repr: tokenURI(uint256) + function tokenURI(uint256 tokenId) external view returns (string memory); +} + /// @title ERC721 Token that can be irreversibly burned (destroyed). /// @dev the ERC-165 identifier for this interface is 0x42966c68 interface ERC721Burnable is Dummy, ERC165 { @@ -267,39 +297,50 @@ } /// @dev inlined interface -interface ERC721MintableEvents { +interface ERC721UniqueMintableEvents { event MintingFinished(); } /// @title ERC721 minting logic. -/// @dev the ERC-165 identifier for this interface is 0x68ccfe89 -interface ERC721Mintable is Dummy, ERC165, ERC721MintableEvents { +/// @dev the ERC-165 identifier for this interface is 0x476ff149 +interface ERC721UniqueMintable is Dummy, ERC165, ERC721UniqueMintableEvents { /// @dev EVM selector for this function is: 0x05d2035b, /// or in textual repr: mintingFinished() function mintingFinished() external view returns (bool); /// @notice Function to mint token. - /// @dev `tokenId` should be obtained with `nextTokenId` method, - /// unlike standard, you can't specify it manually /// @param to The new owner - /// @param tokenId ID of the minted NFT - /// @dev EVM selector for this function is: 0x40c10f19, - /// or in textual repr: mint(address,uint256) - function mint(address to, uint256 tokenId) external returns (bool); + /// @return uint256 The id of the newly minted token + /// @dev EVM selector for this function is: 0x6a627842, + /// or in textual repr: mint(address) + function mint(address to) external returns (uint256); + + // /// @notice Function to mint token. + // /// @dev `tokenId` should be obtained with `nextTokenId` method, + // /// unlike standard, you can't specify it manually + // /// @param to The new owner + // /// @param tokenId ID of the minted NFT + // /// @dev EVM selector for this function is: 0x40c10f19, + // /// or in textual repr: mint(address,uint256) + // function mint(address to, uint256 tokenId) external returns (bool); /// @notice Function to mint token with the given tokenUri. - /// @dev `tokenId` should be obtained with `nextTokenId` method, - /// unlike standard, you can't specify it manually /// @param to The new owner - /// @param tokenId ID of the minted NFT /// @param tokenUri Token URI that would be stored in the NFT properties - /// @dev EVM selector for this function is: 0x50bb4e7f, - /// or in textual repr: mintWithTokenURI(address,uint256,string) - function mintWithTokenURI( - address to, - uint256 tokenId, - string memory tokenUri - ) external returns (bool); + /// @return uint256 The id of the newly minted token + /// @dev EVM selector for this function is: 0x45c17782, + /// or in textual repr: mintWithTokenURI(address,string) + function mintWithTokenURI(address to, string memory tokenUri) external returns (uint256); + + // /// @notice Function to mint token with the given tokenUri. + // /// @dev `tokenId` should be obtained with `nextTokenId` method, + // /// unlike standard, you can't specify it manually + // /// @param to The new owner + // /// @param tokenId ID of the minted NFT + // /// @param tokenUri Token URI that would be stored in the NFT properties + // /// @dev EVM selector for this function is: 0x50bb4e7f, + // /// or in textual repr: mintWithTokenURI(address,uint256,string) + // function mintWithTokenURI(address to, uint256 tokenId, string memory tokenUri) external returns (bool); /// @dev Not implemented /// @dev EVM selector for this function is: 0x7d64bcb4, @@ -308,8 +349,18 @@ } /// @title Unique extensions for ERC721. -/// @dev the ERC-165 identifier for this interface is 0xd74d154f +/// @dev the ERC-165 identifier for this interface is 0x4468500d interface ERC721UniqueExtensions is Dummy, ERC165 { + /// @notice A descriptive name for a collection of NFTs in this contract + /// @dev EVM selector for this function is: 0x06fdde03, + /// or in textual repr: name() + function name() external view returns (string memory); + + /// @notice An abbreviated name for NFTs in this contract + /// @dev EVM selector for this function is: 0x95d89b41, + /// or in textual repr: symbol() + function symbol() external view returns (string memory); + /// @notice Transfer ownership of an NFT /// @dev Throws unless `msg.sender` is the current owner. Throws if `to` /// is the zero address. Throws if `tokenId` is not a valid NFT. @@ -350,11 +401,11 @@ /// @param tokens array of pairs of token ID and token URI for minted tokens /// @dev EVM selector for this function is: 0x36543006, /// or in textual repr: mintBulkWithTokenURI(address,(uint256,string)[]) - function mintBulkWithTokenURI(address to, Tuple8[] memory tokens) external returns (bool); + function mintBulkWithTokenURI(address to, Tuple6[] memory tokens) external returns (bool); } /// @dev anonymous struct -struct Tuple8 { +struct Tuple6 { uint256 field_0; string field_1; } @@ -384,34 +435,6 @@ function totalSupply() external view returns (uint256); } -/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension -/// @dev See https://eips.ethereum.org/EIPS/eip-721 -/// @dev the ERC-165 identifier for this interface is 0x5b5e139f -interface ERC721Metadata is Dummy, ERC165 { - /// @notice A descriptive name for a collection of NFTs in this contract - /// @dev EVM selector for this function is: 0x06fdde03, - /// or in textual repr: name() - function name() external view returns (string memory); - - /// @notice An abbreviated name for NFTs in this contract - /// @dev EVM selector for this function is: 0x95d89b41, - /// or in textual repr: symbol() - function symbol() external view returns (string memory); - - /// @notice A distinct Uniform Resource Identifier (URI) for a given asset. - /// - /// @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 - /// @dev EVM selector for this function is: 0xc87b56dd, - /// or in textual repr: tokenURI(uint256) - function tokenURI(uint256 tokenId) external view returns (string memory); -} - /// @dev inlined interface interface ERC721Events { event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); @@ -507,11 +530,11 @@ Dummy, ERC165, ERC721, - ERC721Metadata, ERC721Enumerable, ERC721UniqueExtensions, - ERC721Mintable, + ERC721UniqueMintable, ERC721Burnable, + ERC721Metadata, Collection, TokenProperties {} --- a/tests/src/eth/api/UniqueRFT.sol +++ /dev/null @@ -1,163 +0,0 @@ -// 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 {} --- a/tests/src/eth/api/UniqueRefungible.sol +++ b/tests/src/eth/api/UniqueRefungible.sol @@ -62,7 +62,7 @@ } /// @title A contract that allows you to work with collections. -/// @dev the ERC-165 identifier for this interface is 0x3e1e8083 +/// @dev the ERC-165 identifier for this interface is 0x62e22290 interface Collection is Dummy, ERC165 { /// Set collection property. /// @@ -243,9 +243,9 @@ /// /// @dev Owner can be changed only by current owner /// @param newOwner new owner account - /// @dev EVM selector for this function is: 0x13af4035, - /// or in textual repr: setOwner(address) - function setOwner(address newOwner) external; + /// @dev EVM selector for this function is: 0x4f53e226, + /// or in textual repr: changeCollectionOwner(address) + function changeCollectionOwner(address newOwner) external; } /// @dev anonymous struct @@ -254,6 +254,34 @@ uint256 field_1; } +/// @dev the ERC-165 identifier for this interface is 0x5b5e139f +interface ERC721Metadata is Dummy, ERC165 { + // /// @notice A descriptive name for a collection of NFTs in this contract + // /// @dev real implementation of this function lies in `ERC721UniqueExtensions` + // /// @dev EVM selector for this function is: 0x06fdde03, + // /// or in textual repr: name() + // function name() external view returns (string memory); + + // /// @notice An abbreviated name for NFTs in this contract + // /// @dev real implementation of this function lies in `ERC721UniqueExtensions` + // /// @dev EVM selector for this function is: 0x95d89b41, + // /// or in textual repr: symbol() + // function symbol() external view returns (string memory); + + /// @notice A distinct Uniform Resource Identifier (URI) for a given asset. + /// + /// @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 + /// @dev EVM selector for this function is: 0xc87b56dd, + /// or in textual repr: tokenURI(uint256) + function tokenURI(uint256 tokenId) external view returns (string memory); +} + /// @title ERC721 Token that can be irreversibly burned (destroyed). /// @dev the ERC-165 identifier for this interface is 0x42966c68 interface ERC721Burnable is Dummy, ERC165 { @@ -267,40 +295,51 @@ } /// @dev inlined interface -interface ERC721MintableEvents { +interface ERC721UniqueMintableEvents { event MintingFinished(); } /// @title ERC721 minting logic. -/// @dev the ERC-165 identifier for this interface is 0x68ccfe89 -interface ERC721Mintable is Dummy, ERC165, ERC721MintableEvents { +/// @dev the ERC-165 identifier for this interface is 0x476ff149 +interface ERC721UniqueMintable is Dummy, ERC165, ERC721UniqueMintableEvents { /// @dev EVM selector for this function is: 0x05d2035b, /// or in textual repr: mintingFinished() function mintingFinished() external view returns (bool); /// @notice Function to mint token. - /// @dev `tokenId` should be obtained with `nextTokenId` method, - /// unlike standard, you can't specify it manually /// @param to The new owner - /// @param tokenId ID of the minted RFT - /// @dev EVM selector for this function is: 0x40c10f19, - /// or in textual repr: mint(address,uint256) - function mint(address to, uint256 tokenId) external returns (bool); + /// @return uint256 The id of the newly minted token + /// @dev EVM selector for this function is: 0x6a627842, + /// or in textual repr: mint(address) + function mint(address to) external returns (uint256); + + // /// @notice Function to mint token. + // /// @dev `tokenId` should be obtained with `nextTokenId` method, + // /// unlike standard, you can't specify it manually + // /// @param to The new owner + // /// @param tokenId ID of the minted RFT + // /// @dev EVM selector for this function is: 0x40c10f19, + // /// or in textual repr: mint(address,uint256) + // function mint(address to, uint256 tokenId) external returns (bool); /// @notice Function to mint token with the given tokenUri. - /// @dev `tokenId` should be obtained with `nextTokenId` method, - /// unlike standard, you can't specify it manually /// @param to The new owner - /// @param tokenId ID of the minted RFT - /// @param tokenUri Token URI that would be stored in the RFT properties - /// @dev EVM selector for this function is: 0x50bb4e7f, - /// or in textual repr: mintWithTokenURI(address,uint256,string) - function mintWithTokenURI( - address to, - uint256 tokenId, - string memory tokenUri - ) external returns (bool); + /// @param tokenUri Token URI that would be stored in the NFT properties + /// @return uint256 The id of the newly minted token + /// @dev EVM selector for this function is: 0x45c17782, + /// or in textual repr: mintWithTokenURI(address,string) + function mintWithTokenURI(address to, string memory tokenUri) external returns (uint256); + // /// @notice Function to mint token with the given tokenUri. + // /// @dev `tokenId` should be obtained with `nextTokenId` method, + // /// unlike standard, you can't specify it manually + // /// @param to The new owner + // /// @param tokenId ID of the minted RFT + // /// @param tokenUri Token URI that would be stored in the RFT properties + // /// @dev EVM selector for this function is: 0x50bb4e7f, + // /// or in textual repr: mintWithTokenURI(address,uint256,string) + // function mintWithTokenURI(address to, uint256 tokenId, string memory tokenUri) external returns (bool); + /// @dev Not implemented /// @dev EVM selector for this function is: 0x7d64bcb4, /// or in textual repr: finishMinting() @@ -308,8 +347,18 @@ } /// @title Unique extensions for ERC721. -/// @dev the ERC-165 identifier for this interface is 0x7c3bef89 +/// @dev the ERC-165 identifier for this interface is 0xef1eaacb interface ERC721UniqueExtensions is Dummy, ERC165 { + /// @notice A descriptive name for a collection of NFTs in this contract + /// @dev EVM selector for this function is: 0x06fdde03, + /// or in textual repr: name() + function name() external view returns (string memory); + + /// @notice An abbreviated name for NFTs in this contract + /// @dev EVM selector for this function is: 0x95d89b41, + /// or in textual repr: symbol() + function symbol() external view returns (string memory); + /// @notice Transfer ownership of an RFT /// @dev Throws unless `msg.sender` is the current owner. Throws if `to` /// is the zero address. Throws if `tokenId` is not a valid RFT. @@ -352,7 +401,7 @@ /// @param tokens array of pairs of token ID and token URI for minted tokens /// @dev EVM selector for this function is: 0x36543006, /// or in textual repr: mintBulkWithTokenURI(address,(uint256,string)[]) - function mintBulkWithTokenURI(address to, Tuple8[] memory tokens) external returns (bool); + function mintBulkWithTokenURI(address to, Tuple6[] memory tokens) external returns (bool); /// Returns EVM address for refungible token /// @@ -363,7 +412,7 @@ } /// @dev anonymous struct -struct Tuple8 { +struct Tuple6 { uint256 field_0; string field_1; } @@ -393,32 +442,6 @@ function totalSupply() external view returns (uint256); } -/// @dev the ERC-165 identifier for this interface is 0x5b5e139f -interface ERC721Metadata is Dummy, ERC165 { - /// @notice A descriptive name for a collection of RFTs in this contract - /// @dev EVM selector for this function is: 0x06fdde03, - /// or in textual repr: name() - function name() external view returns (string memory); - - /// @notice An abbreviated name for RFTs in this contract - /// @dev EVM selector for this function is: 0x95d89b41, - /// or in textual repr: symbol() - function symbol() external view returns (string memory); - - /// @notice A distinct Uniform Resource Identifier (URI) for a given asset. - /// - /// @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 - /// @dev EVM selector for this function is: 0xc87b56dd, - /// or in textual repr: tokenURI(uint256) - function tokenURI(uint256 tokenId) external view returns (string memory); -} - /// @dev inlined interface interface ERC721Events { event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); @@ -512,11 +535,11 @@ Dummy, ERC165, ERC721, - ERC721Metadata, ERC721Enumerable, ERC721UniqueExtensions, - ERC721Mintable, + ERC721UniqueMintable, ERC721Burnable, + ERC721Metadata, Collection, TokenProperties {} --- a/tests/src/eth/base.test.ts +++ b/tests/src/eth/base.test.ts @@ -23,7 +23,7 @@ describe('Contract calls', () => { let donor: IKeyringPair; - before(async function() { + before(async function () { await usingEthPlaygrounds(async (_helper, privateKey) => { donor = await privateKey({filename: __filename}); }); @@ -40,7 +40,12 @@ itEth('Balance transfer fee is less than 0.2 UNQ', async ({helper}) => { const userA = await helper.eth.createAccountWithBalance(donor); const userB = helper.eth.createAccount(); - const cost = await helper.eth.calculateFee({Ethereum: userA}, () => helper.getWeb3().eth.sendTransaction({from: userA, to: userB, value: '1000000', gas: helper.eth.DEFAULT_GAS})); + const cost = await helper.eth.calculateFee({Ethereum: userA}, () => helper.getWeb3().eth.sendTransaction({ + from: userA, + to: userB, + value: '1000000', + gas: helper.eth.DEFAULT_GAS + })); const balanceB = await helper.balance.getEthereum(userB); expect(cost - balanceB < BigInt(0.2 * Number(helper.balance.getOneTokenNominal()))).to.be.true; }); @@ -69,51 +74,59 @@ describe('ERC165 tests', async () => { // https://eips.ethereum.org/EIPS/eip-165 - let collection: number; + let erc721MetadataCompatibleNftCollectionId: number; + let simpleNftCollectionId: number; let minter: string; - function contract(helper: EthUniqueHelper): Contract { - return helper.ethNativeContract.collection(helper.ethAddress.fromCollectionId(collection), 'nft', minter); + const BASE_URI = 'base/'; + + async function checkInterface(helper: EthUniqueHelper, interfaceId: string, simpleResult: boolean, compatibleResult: boolean) { + const simple = helper.ethNativeContract.collection(helper.ethAddress.fromCollectionId(simpleNftCollectionId), 'nft', minter); + const compatible = helper.ethNativeContract.collection(helper.ethAddress.fromCollectionId(erc721MetadataCompatibleNftCollectionId), 'nft', minter); + + expect(await simple.methods.supportsInterface(interfaceId).call()).to.equal(simpleResult, `empty (not ERC721Metadata compatible) NFT collection returns not ${simpleResult}`); + expect(await compatible.methods.supportsInterface(interfaceId).call()).to.equal(compatibleResult, `ERC721Metadata compatible NFT collection returns not ${compatibleResult}`); } before(async () => { await usingEthPlaygrounds(async (helper, privateKey) => { const donor = await privateKey({filename: __filename}); const [alice] = await helper.arrange.createAccounts([10n], donor); - ({collectionId: collection} = await helper.nft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'})); + ({collectionId: simpleNftCollectionId} = await helper.nft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'})); minter = helper.eth.createAccount(); + ({collectionId: erc721MetadataCompatibleNftCollectionId} = await helper.eth.createERC721MetadataCompatibleNFTCollection(minter, 'n', 'd', 'p', BASE_URI)); }); }); - itEth('interfaceID == 0xffffffff always false', async ({helper}) => { - expect(await contract(helper).methods.supportsInterface('0xffffffff').call()).to.be.false; + itEth('nonexistent interfaceID - 0xffffffff - always false', async ({helper}) => { + await checkInterface(helper, '0xffffffff', false, false); }); - itEth('ERC721 support', async ({helper}) => { - expect(await contract(helper).methods.supportsInterface('0x780e9d63').call()).to.be.true; + itEth('ERC721 - 0x780e9d63 - support', async ({helper}) => { + await checkInterface(helper, '0x780e9d63', true, true); }); - itEth('ERC721Metadata support', async ({helper}) => { - expect(await contract(helper).methods.supportsInterface('0x5b5e139f').call()).to.be.true; + itEth('ERC721Metadata - 0x5b5e139f - support', async ({helper}) => { + await checkInterface(helper, '0x5b5e139f', false, true); }); - itEth('ERC721Mintable support', async ({helper}) => { - expect(await contract(helper).methods.supportsInterface('0x68ccfe89').call()).to.be.true; + itEth('ERC721UniqueMintable - 0x476ff149 - support', async ({helper}) => { + await checkInterface(helper, '0x476ff149', true, true); }); - itEth('ERC721Enumerable support', async ({helper}) => { - expect(await contract(helper).methods.supportsInterface('0x780e9d63').call()).to.be.true; + itEth('ERC721Enumerable - 0x780e9d63 - support', async ({helper}) => { + await checkInterface(helper, '0x780e9d63', true, true); }); - itEth('ERC721UniqueExtensions support', async ({helper}) => { - expect(await contract(helper).methods.supportsInterface('0xd74d154f').call()).to.be.true; + itEth('ERC721UniqueExtensions - 0x4468500d - support', async ({helper}) => { + await checkInterface(helper, '0x4468500d', true, true); }); - itEth('ERC721Burnable support', async ({helper}) => { - expect(await contract(helper).methods.supportsInterface('0x42966c68').call()).to.be.true; + itEth('ERC721Burnable - 0x42966c68 - support', async ({helper}) => { + await checkInterface(helper, '0x42966c68', true, true); }); - itEth('ERC165 support', async ({helper}) => { - expect(await contract(helper).methods.supportsInterface('0x01ffc9a7').call()).to.be.true; + itEth('ERC165 - 0x01ffc9a7 - support', async ({helper}) => { + await checkInterface(helper, '0x01ffc9a7', true, true); }); }); --- a/tests/src/eth/collectionAdmin.test.ts +++ b/tests/src/eth/collectionAdmin.test.ts @@ -38,7 +38,7 @@ itEth('Add admin by owner', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); - const {collectionAddress, collectionId} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C'); + const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C'); const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); const newAdmin = helper.eth.createAccount(); @@ -51,7 +51,7 @@ itEth.skip('Add substrate admin by owner', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); - const {collectionAddress, collectionId} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C'); + const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C'); const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); const [newAdmin] = await helper.arrange.createAccounts([10n], donor); @@ -64,7 +64,7 @@ itEth('Verify owner or admin', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); - const {collectionAddress} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C'); + const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C'); const newAdmin = helper.eth.createAccount(); const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); @@ -75,7 +75,7 @@ itEth('(!negative tests!) Add admin by ADMIN is not allowed', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); - const {collectionAddress, collectionId} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C'); + const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C'); const admin = await helper.eth.createAccountWithBalance(donor); const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); @@ -93,7 +93,7 @@ itEth('(!negative tests!) Add admin by USER is not allowed', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); - const {collectionAddress, collectionId} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C'); + const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C'); const notAdmin = await helper.eth.createAccountWithBalance(donor); const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); @@ -108,7 +108,7 @@ itEth.skip('(!negative tests!) Add substrate admin by ADMIN is not allowed', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); - const {collectionAddress, collectionId} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C'); + const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C'); const admin = await helper.eth.createAccountWithBalance(donor); const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); @@ -126,7 +126,7 @@ itEth.skip('(!negative tests!) Add substrate admin by USER is not allowed', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); - const {collectionAddress, collectionId} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C'); + const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C'); const notAdmin0 = await helper.eth.createAccountWithBalance(donor); const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); @@ -150,7 +150,7 @@ itEth('Remove admin by owner', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); - const {collectionAddress, collectionId} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C'); + const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C'); const newAdmin = helper.eth.createAccount(); const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); @@ -170,7 +170,7 @@ itEth.skip('Remove substrate admin by owner', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); - const {collectionAddress, collectionId} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C'); + const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C'); const [newAdmin] = await helper.arrange.createAccounts([10n], donor); const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); @@ -188,7 +188,7 @@ itEth('(!negative tests!) Remove admin by ADMIN is not allowed', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); - const {collectionAddress, collectionId} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C'); + const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C'); const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); @@ -210,7 +210,7 @@ itEth('(!negative tests!) Remove admin by USER is not allowed', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); - const {collectionAddress, collectionId} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C'); + const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C'); const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); @@ -230,7 +230,7 @@ itEth.skip('(!negative tests!) Remove substrate admin by ADMIN is not allowed', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); - const {collectionAddress, collectionId} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C'); + const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C'); const [adminSub] = await helper.arrange.createAccounts([10n], donor); const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); @@ -250,7 +250,7 @@ itEth.skip('(!negative tests!) Remove substrate admin by USER is not allowed', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); - const {collectionAddress, collectionId} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C'); + const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C'); const [adminSub] = await helper.arrange.createAccounts([10n], donor); const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); @@ -279,10 +279,10 @@ itEth('Change owner', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); const newOwner = await helper.eth.createAccountWithBalance(donor); - const {collectionAddress} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C'); + const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C'); const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); - await collectionEvm.methods.setOwner(newOwner).send(); + await collectionEvm.methods.changeCollectionOwner(newOwner).send(); expect(await collectionEvm.methods.isOwnerOrAdmin(owner).call()).to.be.false; expect(await collectionEvm.methods.isOwnerOrAdmin(newOwner).call()).to.be.true; @@ -291,9 +291,9 @@ itEth('change owner call fee', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); const newOwner = await helper.eth.createAccountWithBalance(donor); - const {collectionAddress} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C'); + const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C'); const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); - const cost = await recordEthFee(helper, owner, () => collectionEvm.methods.setOwner(newOwner).send()); + const cost = await recordEthFee(helper, owner, () => collectionEvm.methods.changeCollectionOwner(newOwner).send()); expect(cost < BigInt(0.2 * Number(helper.balance.getOneTokenNominal()))); expect(cost > 0); }); @@ -301,10 +301,10 @@ itEth('(!negative tests!) call setOwner by non owner', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); const newOwner = await helper.eth.createAccountWithBalance(donor); - const {collectionAddress} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C'); + const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C'); const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); - await expect(collectionEvm.methods.setOwner(newOwner).send({from: newOwner})).to.be.rejected; + await expect(collectionEvm.methods.changeCollectionOwner(newOwner).send({from: newOwner})).to.be.rejected; expect(await collectionEvm.methods.isOwnerOrAdmin(newOwner).call()).to.be.false; }); }); @@ -321,7 +321,7 @@ itEth.skip('Change owner', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); const [newOwner] = await helper.arrange.createAccounts([10n], donor); - const {collectionAddress} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C'); + const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C'); const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); expect(await collectionEvm.methods.isOwnerOrAdmin(owner).call()).to.be.true; @@ -336,7 +336,7 @@ itEth.skip('change owner call fee', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); const [newOwner] = await helper.arrange.createAccounts([10n], donor); - const {collectionAddress} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C'); + const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C'); const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); const cost = await recordEthFee(helper, owner, () => collectionEvm.methods.setOwnerSubstrate(newOwner.addressRaw).send()); @@ -348,7 +348,7 @@ const owner = await helper.eth.createAccountWithBalance(donor); const otherReceiver = await helper.eth.createAccountWithBalance(donor); const [newOwner] = await helper.arrange.createAccounts([10n], donor); - const {collectionAddress} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C'); + const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C'); const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); await expect(collectionEvm.methods.setOwnerSubstrate(newOwner.addressRaw).send({from: otherReceiver})).to.be.rejected; --- a/tests/src/eth/collectionHelpersAbi.json +++ b/tests/src/eth/collectionHelpersAbi.json @@ -29,33 +29,9 @@ "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": "payable", - "type": "function" - }, - { - "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": "createERC721MetadataCompatibleRFTCollection", - "outputs": [{ "internalType": "address", "name": "", "type": "address" }], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "string", "name": "name", "type": "string" }, - { "internalType": "string", "name": "description", "type": "string" }, { "internalType": "string", "name": "tokenPrefix", "type": "string" } ], - "name": "createNonfungibleCollection", + "name": "createNFTCollection", "outputs": [{ "internalType": "address", "name": "", "type": "address" }], "stateMutability": "payable", "type": "function" @@ -86,6 +62,16 @@ }, { "inputs": [ + { "internalType": "address", "name": "collection", "type": "address" }, + { "internalType": "string", "name": "baseUri", "type": "string" } + ], + "name": "makeCollectionERC721MetadataCompatible", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ { "internalType": "bytes4", "name": "interfaceID", "type": "bytes4" } ], "name": "supportsInterface", --- a/tests/src/eth/collectionProperties.test.ts +++ b/tests/src/eth/collectionProperties.test.ts @@ -14,8 +14,11 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . -import {itEth, usingEthPlaygrounds, expect} from './util'; +import {itEth, usingEthPlaygrounds, expect, EthUniqueHelper} from './util'; +import {Pallets} from '../util'; +import {IProperty, ITokenPropertyPermission} from '../util/playgrounds/types'; import {IKeyringPair} from '@polkadot/types/types'; +import {Contract} from 'web3-eth-contract'; describe('EVM collection properties', () => { let donor: IKeyringPair; @@ -30,7 +33,7 @@ itEth('Can be set', async({helper}) => { const caller = await helper.eth.createAccountWithBalance(donor); - const collection = await helper.nft.mintCollection(alice, {name: 'name', description: 'test', tokenPrefix: 'test'}); + const collection = await helper.nft.mintCollection(alice, {name: 'name', description: 'test', tokenPrefix: 'test', properties: []}); await collection.addAdmin(alice, {Ethereum: caller}); const address = helper.ethAddress.fromCollectionId(collection.collectionId); @@ -70,3 +73,92 @@ expect(value).to.equal(helper.getWeb3().utils.toHex('testValue')); }); }); + +describe('Supports ERC721Metadata', () => { + let donor: IKeyringPair; + + before(async function() { + await usingEthPlaygrounds(async (_helper, privateKey) => { + donor = await privateKey({filename: __filename}); + }); + }); + + const checkERC721Metadata = async (helper: EthUniqueHelper, mode: 'nft' | 'rft') => { + const caller = await helper.eth.createAccountWithBalance(donor); + const bruh = await helper.eth.createAccountWithBalance(donor); + + const BASE_URI = 'base/' + const SUFFIX = 'suffix1' + const URI = 'uri1' + + const collectionHelpers = helper.ethNativeContract.collectionHelpers(caller); + const creatorMethod = mode === 'rft' ? 'createRFTCollection' : 'createNFTCollection' + + const {collectionId, collectionAddress} = await helper.eth[creatorMethod](caller, 'n', 'd', 'p') + + const contract = helper.ethNativeContract.collectionById(collectionId, mode, caller); + await contract.methods.addCollectionAdmin(bruh).send(); // to check that admin will work too + + const collection1 = await helper.nft.getCollectionObject(collectionId); + const data1 = await collection1.getData() + expect(data1?.raw.flags.erc721metadata).to.be.false; + expect(await contract.methods.supportsInterface('0x5b5e139f').call()).to.be.false; + + await collectionHelpers.methods.makeCollectionERC721MetadataCompatible(collectionAddress, BASE_URI) + .send({from: bruh}); + + expect(await contract.methods.supportsInterface('0x5b5e139f').call()).to.be.true; + + const collection2 = await helper.nft.getCollectionObject(collectionId); + const data2 = await collection2.getData() + expect(data2?.raw.flags.erc721metadata).to.be.true; + + const TPPs = data2?.raw.tokenPropertyPermissions + expect(TPPs?.length).to.equal(2); + + expect(TPPs.find((tpp: ITokenPropertyPermission) => { + return tpp.key === "URI" && tpp.permission.mutable && tpp.permission.collectionAdmin && !tpp.permission.tokenOwner + })).to.be.not.null + + expect(TPPs.find((tpp: ITokenPropertyPermission) => { + return tpp.key === "URISuffix" && tpp.permission.mutable && tpp.permission.collectionAdmin && !tpp.permission.tokenOwner + })).to.be.not.null + + expect(data2?.raw.properties?.find((property: IProperty) => { + return property.key === "baseURI" && property.value === BASE_URI + })).to.be.not.null + + const token1Result = await contract.methods.mint(bruh).send(); + const tokenId1 = token1Result.events.Transfer.returnValues.tokenId; + + expect(await contract.methods.tokenURI(tokenId1).call()).to.equal(BASE_URI); + + await contract.methods.setProperty(tokenId1, "URISuffix", Buffer.from(SUFFIX)).send(); + expect(await contract.methods.tokenURI(tokenId1).call()).to.equal(BASE_URI + SUFFIX); + + await contract.methods.setProperty(tokenId1, "URI", Buffer.from(URI)).send(); + expect(await contract.methods.tokenURI(tokenId1).call()).to.equal(URI); + + await contract.methods.deleteProperty(tokenId1, "URI").send(); + expect(await contract.methods.tokenURI(tokenId1).call()).to.equal(BASE_URI + SUFFIX); + + const token2Result = await contract.methods.mintWithTokenURI(bruh, URI).send(); + const tokenId2 = token2Result.events.Transfer.returnValues.tokenId; + + expect(await contract.methods.tokenURI(tokenId2).call()).to.equal(URI); + + await contract.methods.deleteProperty(tokenId2, "URI").send(); + expect(await contract.methods.tokenURI(tokenId2).call()).to.equal(BASE_URI); + + await contract.methods.setProperty(tokenId2, "URISuffix", Buffer.from(SUFFIX)).send(); + expect(await contract.methods.tokenURI(tokenId2).call()).to.equal(BASE_URI + SUFFIX); + } + + itEth('ERC721Metadata property can be set for NFT collection', async({helper}) => { + await checkERC721Metadata(helper, 'nft'); + }); + + itEth.ifWithPallets('ERC721Metadata property can be set for RFT collection', [Pallets.ReFungible], async({helper}) => { + await checkERC721Metadata(helper, 'rft'); + }); +}); --- a/tests/src/eth/collectionSponsoring.test.ts +++ b/tests/src/eth/collectionSponsoring.test.ts @@ -44,9 +44,8 @@ await collection.addToAllowList(alice, {Ethereum: minter}); - const nextTokenId = await contract.methods.nextTokenId().call(); - expect(nextTokenId).to.equal('1'); - const result = await contract.methods.mint(minter, nextTokenId).send(); + const result = await contract.methods.mint(minter).send(); + const events = helper.eth.normalizeEvents(result.events); expect(events).to.be.deep.equal([ { @@ -55,7 +54,7 @@ args: { from: '0x0000000000000000000000000000000000000000', to: minter, - tokenId: nextTokenId, + tokenId: '1', }, }, ]); @@ -65,7 +64,7 @@ // itWeb3('Set substrate sponsor', 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(); + // let result = await collectionHelpers.methods.createNFTCollection('Sponsor collection', '1', '1').send(); // const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result); // const sponsor = privateKeyWrapper('//Alice'); // const collectionEvm = evmCollection(web3, owner, collectionIdAddress); @@ -73,11 +72,11 @@ // expect(await collectionEvm.methods.hasCollectionPendingSponsor().call({from: owner})).to.be.false; // result = await collectionEvm.methods.setCollectionSponsorSubstrate(sponsor.addressRaw).send({from: owner}); // expect(await collectionEvm.methods.hasCollectionPendingSponsor().call({from: owner})).to.be.true; - + // const confirmTx = await api.tx.unique.confirmSponsorship(collectionId); // await submitTransactionAsync(sponsor, confirmTx); // expect(await collectionEvm.methods.hasCollectionPendingSponsor().call({from: owner})).to.be.false; - + // const sponsorTuple = await collectionEvm.methods.collectionSponsor().call({from: owner}); // expect(bigIntToSub(api, BigInt(sponsorTuple[1]))).to.be.eq(sponsor.address); // }); @@ -86,7 +85,7 @@ const owner = await helper.eth.createAccountWithBalance(donor); const collectionHelpers = helper.ethNativeContract.collectionHelpers(owner); - let result = await collectionHelpers.methods.createNonfungibleCollection('Sponsor collection', '1', '1').send({value: Number(2n * nominal)}); + let result = await collectionHelpers.methods.createNFTCollection('Sponsor collection', '1', '1').send({value: Number(2n * nominal)}); const collectionIdAddress = helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId); const sponsor = await helper.eth.createAccountWithBalance(donor); const collectionEvm = helper.ethNativeContract.collection(collectionIdAddress, 'nft', owner); @@ -94,28 +93,26 @@ expect(await collectionEvm.methods.hasCollectionPendingSponsor().call({from: owner})).to.be.false; result = await collectionEvm.methods.setCollectionSponsor(sponsor).send({from: owner}); expect(await collectionEvm.methods.hasCollectionPendingSponsor().call({from: owner})).to.be.true; - + await collectionEvm.methods.confirmCollectionSponsorship().send({from: sponsor}); expect(await collectionEvm.methods.hasCollectionPendingSponsor().call({from: owner})).to.be.false; - + await collectionEvm.methods.removeCollectionSponsor().send({from: owner}); - + const sponsorTuple = await collectionEvm.methods.collectionSponsor().call({from: owner}); expect(sponsorTuple.field_0).to.be.eq('0x0000000000000000000000000000000000000000'); }); itEth('Sponsoring collection from evm address via access list', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); - const collectionHelpers = helper.ethNativeContract.collectionHelpers(owner); - let result = await collectionHelpers.methods.createNonfungibleCollection('Sponsor collection', '1', '1').send({value: Number(2n * nominal)}); - const collectionIdAddress = helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId); - const collectionId = helper.ethAddress.extractCollectionId(collectionIdAddress); + const {collectionId, collectionAddress} = await helper.eth.createERC721MetadataCompatibleNFTCollection(owner, 'Sponsor collection', '1', '1', ''); + const collection = helper.nft.getCollectionObject(collectionId); const sponsor = await helper.eth.createAccountWithBalance(donor); - const collectionEvm = helper.ethNativeContract.collection(collectionIdAddress, 'nft', owner); + const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); - result = await collectionEvm.methods.setCollectionSponsor(sponsor).send({from: owner}); + await collectionEvm.methods.setCollectionSponsor(sponsor).send({from: owner}); let collectionData = (await collection.getData())!; expect(collectionData.raw.sponsorship.Unconfirmed).to.be.eq(helper.address.ethToSubstrate(sponsor, true)); await expect(collectionEvm.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('caller is not set as sponsor'); @@ -144,23 +141,17 @@ const sponsorBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsor)); { - const nextTokenId = await collectionEvm.methods.nextTokenId().call(); - expect(nextTokenId).to.be.equal('1'); - const result = await collectionEvm.methods.mintWithTokenURI( - user, - nextTokenId, - 'Test URI', - ).send({from: user}); + const result = await collectionEvm.methods.mintWithTokenURI(user, 'Test URI').send({from: user}); const events = helper.eth.normalizeEvents(result.events); expect(events).to.be.deep.equal([ { - address: collectionIdAddress, + address: collectionAddress, event: 'Transfer', args: { from: '0x0000000000000000000000000000000000000000', to: user, - tokenId: nextTokenId, + tokenId: '1', }, }, ]); @@ -178,16 +169,16 @@ // itWeb3('Sponsoring collection from substrate address via access list', async ({api, web3, privateKeyWrapper}) => { // const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); // const collectionHelpers = evmCollectionHelpers(web3, owner); - // const result = await collectionHelpers.methods.createNonfungibleCollection('Sponsor collection', '1', '1').send(); + // const result = await collectionHelpers.methods.createERC721MetadataCompatibleNFTCollection('Sponsor collection', '1', '1', '').send(); // const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result); // const sponsor = privateKeyWrapper('//Alice'); // const collectionEvm = evmCollection(web3, owner, collectionIdAddress); // await collectionEvm.methods.setCollectionSponsorSubstrate(sponsor.addressRaw).send({from: owner}); - + // const confirmTx = await api.tx.unique.confirmSponsorship(collectionId); // await submitTransactionAsync(sponsor, confirmTx); - + // const user = createEthAccount(web3); // const nextTokenId = await collectionEvm.methods.nextTokenId().call(); // expect(nextTokenId).to.be.equal('1'); @@ -232,39 +223,32 @@ itEth('Check that transaction via EVM spend money from sponsor address', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); - const collectionHelpers = helper.ethNativeContract.collectionHelpers(owner); - let result = await collectionHelpers.methods.createNonfungibleCollection('Sponsor collection', '1', '1').send({value: Number(2n * nominal)}); - const collectionIdAddress = helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId); - const collectionId = helper.ethAddress.extractCollectionId(collectionIdAddress); + const {collectionAddress, collectionId} = await helper.eth.createERC721MetadataCompatibleNFTCollection(owner,'Sponsor collection', '1', '1', ''); const collection = helper.nft.getCollectionObject(collectionId); const sponsor = await helper.eth.createAccountWithBalance(donor); - const collectionEvm = helper.ethNativeContract.collection(collectionIdAddress, 'nft', owner); + const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); - result = await collectionEvm.methods.setCollectionSponsor(sponsor).send(); + await collectionEvm.methods.setCollectionSponsor(sponsor).send(); let collectionData = (await collection.getData())!; expect(collectionData.raw.sponsorship.Unconfirmed).to.be.eq(helper.address.ethToSubstrate(sponsor, true)); await expect(collectionEvm.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('caller is not set as sponsor'); - const sponsorCollection = helper.ethNativeContract.collection(collectionIdAddress, 'nft', sponsor); + const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'nft', sponsor); await sponsorCollection.methods.confirmCollectionSponsorship().send(); collectionData = (await collection.getData())!; expect(collectionData.raw.sponsorship.Confirmed).to.be.eq(helper.address.ethToSubstrate(sponsor, true)); const user = helper.eth.createAccount(); await collectionEvm.methods.addCollectionAdmin(user).send(); - + const ownerBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner)); const sponsorBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsor)); - const userCollectionEvm = helper.ethNativeContract.collection(collectionIdAddress, 'nft', user); - const nextTokenId = await userCollectionEvm.methods.nextTokenId().call(); - expect(nextTokenId).to.be.equal('1'); - result = await userCollectionEvm.methods.mintWithTokenURI( - user, - nextTokenId, - 'Test URI', - ).send(); + const userCollectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', user); + + let result = await userCollectionEvm.methods.mintWithTokenURI(user, 'Test URI',).send(); + const tokenId = result.events.Transfer.returnValues.tokenId; const events = helper.eth.normalizeEvents(result.events); const address = helper.ethAddress.fromCollectionId(collectionId); @@ -276,12 +260,12 @@ args: { from: '0x0000000000000000000000000000000000000000', to: user, - tokenId: nextTokenId, + tokenId: '1', }, }, ]); - expect(await userCollectionEvm.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI'); - + expect(await userCollectionEvm.methods.tokenURI(tokenId).call()).to.be.equal('Test URI'); + const ownerBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner)); expect(ownerBalanceAfter).to.be.eq(ownerBalanceBefore); const sponsorBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsor)); --- a/tests/src/eth/createNFTCollection.test.ts +++ b/tests/src/eth/createNFTCollection.test.ts @@ -35,13 +35,49 @@ const description = 'Some description'; const prefix = 'token prefix'; - const {collectionId} = await helper.eth.createNonfungibleCollection(owner, name, description, prefix); + const {collectionId} = await helper.eth.createNFTCollection(owner, name, description, prefix); const data = (await helper.rft.getData(collectionId))!; + const collection = helper.nft.getCollectionObject(collectionId); + + expect(data.name).to.be.eq(name); + expect(data.description).to.be.eq(description); + expect(data.raw.tokenPrefix).to.be.eq(prefix); + expect(data.raw.mode).to.be.eq('NFT'); + + const options = await collection.getOptions(); + + expect(options.tokenPropertyPermissions).to.be.empty; + }); + + itEth('Create collection with properties', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + + const name = 'CollectionEVM'; + const description = 'Some description'; + const prefix = 'token prefix'; + const baseUri = 'BaseURI'; + + const {collectionId} = await helper.eth.createERC721MetadataCompatibleNFTCollection(owner, name, description, prefix, baseUri); + + const collection = helper.nft.getCollectionObject(collectionId); + const data = (await collection.getData())!; expect(data.name).to.be.eq(name); expect(data.description).to.be.eq(description); expect(data.raw.tokenPrefix).to.be.eq(prefix); expect(data.raw.mode).to.be.eq('NFT'); + + const options = await collection.getOptions(); + expect(options.tokenPropertyPermissions).to.be.deep.equal([ + { + key: 'URI', + permission: {mutable: true, collectionAdmin: true, tokenOwner: false}, + }, + { + key: 'URISuffix', + permission: {mutable: true, collectionAdmin: true, tokenOwner: false}, + }, + ]); }); // this test will occasionally fail when in async environment. @@ -57,7 +93,7 @@ .call()).to.be.false; await collectionHelpers.methods - .createNonfungibleCollection('A', 'A', 'A') + .createNFTCollection('A', 'A', 'A') .send({value: Number(2n * helper.balance.getOneTokenNominal())}); expect(await collectionHelpers.methods @@ -69,7 +105,7 @@ const owner = await helper.eth.createAccountWithBalance(donor); const sponsor = await helper.eth.createAccountWithBalance(donor); const ss58Format = helper.chain.getChainProperties().ss58Format; - const {collectionId, collectionAddress} = await helper.eth.createNonfungibleCollection(owner, 'Sponsor', 'absolutely anything', 'ROC'); + const {collectionId, collectionAddress} = await helper.eth.createNFTCollection(owner, 'Sponsor', 'absolutely anything', 'ROC'); const collection = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); await collection.methods.setCollectionSponsor(sponsor).send(); @@ -88,7 +124,7 @@ itEth('Set limits', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); - const {collectionId, collectionAddress} = await helper.eth.createNonfungibleCollection(owner, 'Limits', 'absolutely anything', 'FLO'); + const {collectionId, collectionAddress} = await helper.eth.createNFTCollection(owner, 'Limits', 'absolutely anything', 'FLO'); const limits = { accountTokenOwnershipLimit: 1000, sponsoredDataSize: 1024, @@ -131,7 +167,7 @@ .methods.isCollectionExist(collectionAddressForNonexistentCollection).call()) .to.be.false; - const {collectionAddress} = await helper.eth.createNonfungibleCollection(owner, 'Exister', 'absolutely anything', 'EVC'); + const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'Exister', 'absolutely anything', 'EVC'); expect(await helper.ethNativeContract.collectionHelpers(collectionAddress) .methods.isCollectionExist(collectionAddress).call()) .to.be.true; @@ -159,7 +195,7 @@ const tokenPrefix = 'A'; await expect(collectionHelper.methods - .createNonfungibleCollection(collectionName, description, tokenPrefix) + .createNFTCollection(collectionName, description, tokenPrefix) .call({value: Number(2n * nominal)})).to.be.rejectedWith('name is too long. Max length is ' + MAX_NAME_LENGTH); } @@ -169,7 +205,7 @@ const description = 'A'.repeat(MAX_DESCRIPTION_LENGTH + 1); const tokenPrefix = 'A'; await expect(collectionHelper.methods - .createNonfungibleCollection(collectionName, description, tokenPrefix) + .createNFTCollection(collectionName, description, tokenPrefix) .call({value: Number(2n * nominal)})).to.be.rejectedWith('description is too long. Max length is ' + MAX_DESCRIPTION_LENGTH); } { @@ -178,7 +214,7 @@ const description = 'A'; const tokenPrefix = 'A'.repeat(MAX_TOKEN_PREFIX_LENGTH + 1); await expect(collectionHelper.methods - .createNonfungibleCollection(collectionName, description, tokenPrefix) + .createNFTCollection(collectionName, description, tokenPrefix) .call({value: Number(2n * nominal)})).to.be.rejectedWith('token_prefix is too long. Max length is ' + MAX_TOKEN_PREFIX_LENGTH); } }); @@ -187,14 +223,14 @@ const owner = await helper.eth.createAccountWithBalance(donor); const collectionHelper = helper.ethNativeContract.collectionHelpers(owner); await expect(collectionHelper.methods - .createNonfungibleCollection('Peasantry', 'absolutely anything', 'CVE') + .createNFTCollection('Peasantry', 'absolutely anything', 'CVE') .call({value: Number(1n * nominal)})).to.be.rejectedWith('Sent amount not equals to collection creation price (2000000000000000000)'); }); itEth('(!negative test!) Check owner', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); const malfeasant = helper.eth.createAccount(); - const {collectionAddress} = await helper.eth.createNonfungibleCollection(owner, 'Transgressed', 'absolutely anything', 'COR'); + const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'Transgressed', 'absolutely anything', 'COR'); const malfeasantCollection = helper.ethNativeContract.collection(collectionAddress, 'nft', malfeasant); const EXPECTED_ERROR = 'NoPermission'; { @@ -217,10 +253,10 @@ itEth('(!negative test!) Set limits', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); - const {collectionAddress} = await helper.eth.createNonfungibleCollection(owner, 'Limits', 'absolutely anything', 'OLF'); + const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'Limits', 'absolutely anything', 'OLF'); const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); 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/createRFTCollection.test.ts +++ b/tests/src/eth/createRFTCollection.test.ts @@ -37,13 +37,51 @@ const description = 'Some description'; const prefix = 'token prefix'; - const {collectionId} = await helper.eth.createRefungibleCollection(owner, name, description, prefix); + const {collectionId} = await helper.eth.createRFTCollection(owner, name, description, prefix); const data = (await helper.rft.getData(collectionId))!; + const collection = helper.rft.getCollectionObject(collectionId); + + expect(data.name).to.be.eq(name); + expect(data.description).to.be.eq(description); + expect(data.raw.tokenPrefix).to.be.eq(prefix); + expect(data.raw.mode).to.be.eq('ReFungible'); + + const options = await collection.getOptions(); + + expect(options.tokenPropertyPermissions).to.be.empty; + }); + + + + itEth('Create collection with properties', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + + const name = 'CollectionEVM'; + const description = 'Some description'; + const prefix = 'token prefix'; + const baseUri = 'BaseURI'; + + const {collectionId} = await helper.eth.createERC721MetadataCompatibleRFTCollection(owner, name, description, prefix, baseUri); + const collection = helper.rft.getCollectionObject(collectionId); + const data = (await collection.getData())!; + expect(data.name).to.be.eq(name); expect(data.description).to.be.eq(description); expect(data.raw.tokenPrefix).to.be.eq(prefix); expect(data.raw.mode).to.be.eq('ReFungible'); + + const options = await collection.getOptions(); + expect(options.tokenPropertyPermissions).to.be.deep.equal([ + { + key: 'URI', + permission: {mutable: true, collectionAdmin: true, tokenOwner: false}, + }, + { + key: 'URISuffix', + permission: {mutable: true, collectionAdmin: true, tokenOwner: false}, + }, + ]); }); // this test will occasionally fail when in async environment. @@ -71,7 +109,7 @@ const owner = await helper.eth.createAccountWithBalance(donor); const sponsor = await helper.eth.createAccountWithBalance(donor); const ss58Format = helper.chain.getChainProperties().ss58Format; - const {collectionId, collectionAddress} = await helper.eth.createRefungibleCollection(owner, 'Sponsor', 'absolutely anything', 'ENVY'); + const {collectionId, collectionAddress} = await helper.eth.createRFTCollection(owner, 'Sponsor', 'absolutely anything', 'ENVY'); const collection = helper.ethNativeContract.collection(collectionAddress, 'rft', owner); await collection.methods.setCollectionSponsor(sponsor).send(); @@ -90,7 +128,7 @@ itEth('Set limits', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); - const {collectionId, collectionAddress} = await helper.eth.createRefungibleCollection(owner, 'Limits', 'absolutely anything', 'INSI'); + const {collectionId, collectionAddress} = await helper.eth.createRFTCollection(owner, 'Limits', 'absolutely anything', 'INSI'); const limits = { accountTokenOwnershipLimit: 1000, sponsoredDataSize: 1024, @@ -133,7 +171,7 @@ .methods.isCollectionExist(collectionAddressForNonexistentCollection).call()) .to.be.false; - const {collectionAddress} = await helper.eth.createRefungibleCollection(owner, 'Exister', 'absolutely anything', 'WIWT'); + const {collectionAddress} = await helper.eth.createRFTCollection(owner, 'Exister', 'absolutely anything', 'WIWT'); expect(await helper.ethNativeContract.collectionHelpers(collectionAddress) .methods.isCollectionExist(collectionAddress).call()) .to.be.true; @@ -196,7 +234,7 @@ itEth('(!negative test!) Check owner', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); const peasant = helper.eth.createAccount(); - const {collectionAddress} = await helper.eth.createRefungibleCollection(owner, 'Transgressed', 'absolutely anything', 'YVNE'); + const {collectionAddress} = await helper.eth.createRFTCollection(owner, 'Transgressed', 'absolutely anything', 'YVNE'); const peasantCollection = helper.ethNativeContract.collection(collectionAddress, 'rft', peasant); const EXPECTED_ERROR = 'NoPermission'; { @@ -219,7 +257,7 @@ itEth('(!negative test!) Set limits', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); - const {collectionAddress} = await helper.eth.createRefungibleCollection(owner, 'Limits', 'absolutely anything', 'ISNI'); + const {collectionAddress} = await helper.eth.createRFTCollection(owner, 'Limits', 'absolutely anything', 'ISNI'); const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'rft', owner); await expect(collectionEvm.methods .setCollectionLimit('badLimit', 'true') --- a/tests/src/eth/evmCoder.test.ts +++ b/tests/src/eth/evmCoder.test.ts @@ -65,7 +65,7 @@ itEth('Call non-existing function', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); - const collection = await helper.eth.createNonfungibleCollection(owner, 'EVMCODER', '', 'TEST'); + const collection = await helper.eth.createNFTCollection(owner, 'EVMCODER', '', 'TEST'); const contract = await helper.ethContract.deployByCode(owner, 'Test', getContractSource(collection.collectionAddress, '0x1bfed5D614b886b9Ab2eA4CBAc22A96B7EC29c9c')); const testContract = await helper.ethContract.deployByCode(owner, 'Test', getContractSource(collection.collectionAddress, contract.options.address)); { --- a/tests/src/eth/fractionalizer/Fractionalizer.sol +++ b/tests/src/eth/fractionalizer/Fractionalizer.sol @@ -124,8 +124,7 @@ address rftTokenAddress; UniqueRefungibleToken rftTokenContract; if (nft2rftMapping[_collection][_token] == 0) { - rftTokenId = rftCollectionContract.nextTokenId(); - rftCollectionContract.mint(address(this), rftTokenId); + rftTokenId = rftCollectionContract.mint(address(this)); rftTokenAddress = rftCollectionContract.tokenContractAddress(rftTokenId); nft2rftMapping[_collection][_token] = rftTokenId; rft2nftMapping[rftTokenAddress] = Token(_collection, _token); --- a/tests/src/eth/fractionalizer/fractionalizer.test.ts +++ b/tests/src/eth/fractionalizer/fractionalizer.test.ts @@ -62,10 +62,10 @@ const mintRFTToken = async (helper: EthUniqueHelper, owner: string, fractionalizer: Contract, amount: bigint): Promise<{ nftCollectionAddress: string, nftTokenId: number, rftTokenAddress: string }> => { - const nftCollection = await helper.eth.createNonfungibleCollection(owner, 'nft', 'NFT collection', 'NFT'); + const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT'); const nftContract = helper.ethNativeContract.collection(nftCollection.collectionAddress, 'nft', owner); - const nftTokenId = await nftContract.methods.nextTokenId().call(); - await nftContract.methods.mint(owner, nftTokenId).send({from: owner}); + const mintResult = await nftContract.methods.mint(owner).send({from: owner}); + const nftTokenId = mintResult.events.Transfer.returnValues.tokenId; await fractionalizer.methods.setNftCollectionIsAllowed(nftCollection.collectionAddress, true).send({from: owner}); await nftContract.methods.approve(fractionalizer.options.address, nftTokenId).send({from: owner}); @@ -92,7 +92,7 @@ itEth('Set RFT collection', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor, 10n); const fractionalizer = await deployContract(helper, owner); - const rftCollection = await helper.eth.createRefungibleCollection(owner, 'rft', 'RFT collection', 'RFT'); + const rftCollection = await helper.eth.createRFTCollection(owner, 'rft', 'RFT collection', 'RFT'); const rftContract = helper.ethNativeContract.collection(rftCollection.collectionAddress, 'rft', owner); await rftContract.methods.addCollectionAdmin(fractionalizer.options.address).send({from: owner}); @@ -121,7 +121,7 @@ itEth('Set Allowlist', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor, 20n); const {contract: fractionalizer} = await initContract(helper, owner); - const nftCollection = await helper.eth.createNonfungibleCollection(owner, 'nft', 'NFT collection', 'NFT'); + const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT'); const result1 = await fractionalizer.methods.setNftCollectionIsAllowed(nftCollection.collectionAddress, true).send({from: owner}); expect(result1.events).to.be.like({ @@ -146,10 +146,10 @@ itEth('NFT to RFT', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor, 20n); - const nftCollection = await helper.eth.createNonfungibleCollection(owner, 'nft', 'NFT collection', 'NFT'); + const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT'); const nftContract = helper.ethNativeContract.collection(nftCollection.collectionAddress, 'nft', owner); - const nftTokenId = await nftContract.methods.nextTokenId().call(); - await nftContract.methods.mint(owner, nftTokenId).send({from: owner}); + const mintResult = await nftContract.methods.mint(owner).send({from: owner}); + const nftTokenId = mintResult.events.Transfer.returnValues.tokenId; const {contract: fractionalizer} = await initContract(helper, owner); @@ -231,7 +231,7 @@ itEth('call setRFTCollection twice', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor, 20n); - const rftCollection = await helper.eth.createRefungibleCollection(owner, 'rft', 'RFT collection', 'RFT'); + const rftCollection = await helper.eth.createRFTCollection(owner, 'rft', 'RFT collection', 'RFT'); const refungibleContract = helper.ethNativeContract.collection(rftCollection.collectionAddress, 'rft', owner); const fractionalizer = await deployContract(helper, owner); @@ -244,7 +244,7 @@ itEth('call setRFTCollection with NFT collection', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor, 20n); - const nftCollection = await helper.eth.createNonfungibleCollection(owner, 'nft', 'NFT collection', 'NFT'); + const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT'); const nftContract = helper.ethNativeContract.collection(nftCollection.collectionAddress, 'nft', owner); const fractionalizer = await deployContract(helper, owner); @@ -257,7 +257,7 @@ itEth('call setRFTCollection while not collection admin', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor, 20n); const fractionalizer = await deployContract(helper, owner); - const rftCollection = await helper.eth.createRefungibleCollection(owner, 'rft', 'RFT collection', 'RFT'); + const rftCollection = await helper.eth.createRFTCollection(owner, 'rft', 'RFT collection', 'RFT'); await expect(fractionalizer.methods.setRFTCollection(rftCollection.collectionAddress).call()) .to.be.rejectedWith(/Fractionalizer contract should be an admin of the collection$/g); @@ -278,10 +278,10 @@ itEth('call nft2rft without setting RFT collection for contract', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor, 20n); - const nftCollection = await helper.eth.createNonfungibleCollection(owner, 'nft', 'NFT collection', 'NFT'); + const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT'); const nftContract = helper.ethNativeContract.collection(nftCollection.collectionAddress, 'nft', owner); - const nftTokenId = await nftContract.methods.nextTokenId().call(); - await nftContract.methods.mint(owner, nftTokenId).send({from: owner}); + const mintResult = await nftContract.methods.mint(owner).send({from: owner}); + const nftTokenId = mintResult.events.Transfer.returnValues.tokenId; const fractionalizer = await deployContract(helper, owner); @@ -293,10 +293,10 @@ const owner = await helper.eth.createAccountWithBalance(donor, 20n); const nftOwner = await helper.eth.createAccountWithBalance(donor, 10n); - const nftCollection = await helper.eth.createNonfungibleCollection(owner, 'nft', 'NFT collection', 'NFT'); + const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT'); const nftContract = helper.ethNativeContract.collection(nftCollection.collectionAddress, 'nft', owner); - const nftTokenId = await nftContract.methods.nextTokenId().call(); - await nftContract.methods.mint(owner, nftTokenId).send({from: owner}); + const mintResult = await nftContract.methods.mint(owner).send({from: owner}); + const nftTokenId = mintResult.events.Transfer.returnValues.tokenId; await nftContract.methods.transfer(nftOwner, 1).send({from: owner}); @@ -310,10 +310,10 @@ itEth('call nft2rft while not in list of allowed accounts', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor, 20n); - const nftCollection = await helper.eth.createNonfungibleCollection(owner, 'nft', 'NFT collection', 'NFT'); + const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT'); const nftContract = helper.ethNativeContract.collection(nftCollection.collectionAddress, 'nft', owner); - const nftTokenId = await nftContract.methods.nextTokenId().call(); - await nftContract.methods.mint(owner, nftTokenId).send({from: owner}); + const mintResult = await nftContract.methods.mint(owner).send({from: owner}); + const nftTokenId = mintResult.events.Transfer.returnValues.tokenId; const {contract: fractionalizer} = await initContract(helper, owner); @@ -325,10 +325,10 @@ itEth('call nft2rft while fractionalizer doesnt have approval for nft token', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor, 20n); - const nftCollection = await helper.eth.createNonfungibleCollection(owner, 'nft', 'NFT collection', 'NFT'); + const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT'); const nftContract = helper.ethNativeContract.collection(nftCollection.collectionAddress, 'nft', owner); - const nftTokenId = await nftContract.methods.nextTokenId().call(); - await nftContract.methods.mint(owner, nftTokenId).send({from: owner}); + const mintResult = await nftContract.methods.mint(owner).send({from: owner}); + const nftTokenId = mintResult.events.Transfer.returnValues.tokenId; const {contract: fractionalizer} = await initContract(helper, owner); @@ -341,11 +341,11 @@ const owner = await helper.eth.createAccountWithBalance(donor, 20n); const fractionalizer = await deployContract(helper, owner); - const rftCollection = await helper.eth.createRefungibleCollection(owner, 'rft', 'RFT collection', 'RFT'); + const rftCollection = await helper.eth.createRFTCollection(owner, 'rft', 'RFT collection', 'RFT'); const refungibleContract = helper.ethNativeContract.collection(rftCollection.collectionAddress, 'rft', owner); - const rftTokenId = await refungibleContract.methods.nextTokenId().call(); - await refungibleContract.methods.mint(owner, rftTokenId).send({from: owner}); - + const mintResult = await refungibleContract.methods.mint(owner).send({from: owner}); + const rftTokenId = mintResult.events.Transfer.returnValues.tokenId; + await expect(fractionalizer.methods.rft2nft(rftCollection.collectionAddress, rftTokenId).call({from: owner})) .to.be.rejectedWith(/RFT collection is not set$/g); }); @@ -354,18 +354,18 @@ const owner = await helper.eth.createAccountWithBalance(donor, 20n); const {contract: fractionalizer} = await initContract(helper, owner); - const rftCollection = await helper.eth.createRefungibleCollection(owner, 'rft', 'RFT collection', 'RFT'); + const rftCollection = await helper.eth.createRFTCollection(owner, 'rft', 'RFT collection', 'RFT'); const refungibleContract = helper.ethNativeContract.collection(rftCollection.collectionAddress, 'rft', owner); - const rftTokenId = await refungibleContract.methods.nextTokenId().call(); - await refungibleContract.methods.mint(owner, rftTokenId).send({from: owner}); - + const mintResult = await refungibleContract.methods.mint(owner).send({from: owner}); + const rftTokenId = mintResult.events.Transfer.returnValues.tokenId; + await expect(fractionalizer.methods.rft2nft(rftCollection.collectionAddress, rftTokenId).call()) .to.be.rejectedWith(/Wrong RFT collection$/g); }); itEth('call rft2nft for RFT token that was not minted by fractionalizer contract', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor, 20n); - const rftCollection = await helper.eth.createRefungibleCollection(owner, 'rft', 'RFT collection', 'RFT'); + const rftCollection = await helper.eth.createRFTCollection(owner, 'rft', 'RFT collection', 'RFT'); const refungibleContract = helper.ethNativeContract.collection(rftCollection.collectionAddress, 'rft', owner); const fractionalizer = await deployContract(helper, owner); @@ -373,9 +373,9 @@ await refungibleContract.methods.addCollectionAdmin(fractionalizer.options.address).send({from: owner}); await fractionalizer.methods.setRFTCollection(rftCollection.collectionAddress).send({from: owner}); - const rftTokenId = await refungibleContract.methods.nextTokenId().call(); - await refungibleContract.methods.mint(owner, rftTokenId).send({from: owner}); - + const mintResult = await refungibleContract.methods.mint(owner).send({from: owner}); + const rftTokenId = mintResult.events.Transfer.returnValues.tokenId; + await expect(fractionalizer.methods.rft2nft(rftCollection.collectionAddress, rftTokenId).call()) .to.be.rejectedWith(/No corresponding NFT token found$/g); }); @@ -386,7 +386,7 @@ const {contract: fractionalizer, rftCollectionAddress} = await initContract(helper, owner); const {rftTokenAddress} = await mintRFTToken(helper, owner, fractionalizer, 100n); - + const {tokenId} = helper.ethAddress.extractTokenId(rftTokenAddress); const refungibleTokenContract = helper.ethNativeContract.rftToken(rftTokenAddress, owner); await refungibleTokenContract.methods.transfer(receiver, 50).send({from: owner}); @@ -420,7 +420,7 @@ await expect(fractionalizer.methods.nft2rft(nftCollectionAddress, nftToken.tokenId, 100).call()) .to.be.rejectedWith(/TransferNotAllowed$/g); }); - + itEth('fractionalize NFT with RFT transfers disallowed', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor, 20n); @@ -432,10 +432,10 @@ await fractionalizer.methods.setRFTCollection(rftCollectionAddress).send({from: owner}); await helper.executeExtrinsic(donor, 'api.tx.unique.setTransfersEnabledFlag', [rftCollection.collectionId, false], true); - const nftCollection = await helper.eth.createNonfungibleCollection(owner, 'nft', 'NFT collection', 'NFT'); + const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT'); const nftContract = helper.ethNativeContract.collection(nftCollection.collectionAddress, 'nft', owner); - const nftTokenId = await nftContract.methods.nextTokenId().call(); - await nftContract.methods.mint(owner, nftTokenId).send({from: owner}); + const mintResult = await nftContract.methods.mint(owner).send({from: owner}); + const nftTokenId = mintResult.events.Transfer.returnValues.tokenId; await fractionalizer.methods.setNftCollectionIsAllowed(nftCollection.collectionAddress, true).send({from: owner}); await nftContract.methods.approve(fractionalizer.options.address, nftTokenId).send({from: owner}); --- a/tests/src/eth/fungibleAbi.json +++ b/tests/src/eth/fungibleAbi.json @@ -116,6 +116,15 @@ "type": "function" }, { + "inputs": [ + { "internalType": "address", "name": "newOwner", "type": "address" } + ], + "name": "changeCollectionOwner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { "inputs": [], "name": "collectionOwner", "outputs": [ @@ -329,15 +338,6 @@ { "internalType": "address", "name": "sponsor", "type": "address" } ], "name": "setCollectionSponsor", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "newOwner", "type": "address" } - ], - "name": "setOwner", "outputs": [], "stateMutability": "nonpayable", "type": "function" --- a/tests/src/eth/nesting/nest.test.ts +++ b/tests/src/eth/nesting/nest.test.ts @@ -7,7 +7,7 @@ helper: EthUniqueHelper, owner: string, ): Promise<{ collectionId: number, collectionAddress: string, contract: Contract }> => { - const {collectionAddress, collectionId} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C'); + const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C'); const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); await contract.methods.setCollectionNesting(true).send({from: owner}); @@ -29,74 +29,53 @@ itEth('NFT: allows an Owner to nest/unnest their token', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); const {collectionId, contract} = await createNestingCollection(helper, owner); - - // Create a token to be nested - const targetNFTTokenId = await contract.methods.nextTokenId().call(); - await contract.methods.mint( - owner, - targetNFTTokenId, - ).send({from: owner}); - + + // Create a token to be nested to + const mintingTargetNFTTokenIdResult = await contract.methods.mint(owner).send({from: owner}); + const targetNFTTokenId = mintingTargetNFTTokenIdResult.events.Transfer.returnValues.tokenId; const targetNftTokenAddress = helper.ethAddress.fromTokenId(collectionId, targetNFTTokenId); - + // Create a nested token - const firstTokenId = await contract.methods.nextTokenId().call(); - await contract.methods.mint( - targetNftTokenAddress, - firstTokenId, - ).send({from: owner}); - + const mintingFirstTokenIdResult = await contract.methods.mint(targetNftTokenAddress).send({from: owner}); + const firstTokenId = mintingFirstTokenIdResult.events.Transfer.returnValues.tokenId; expect(await contract.methods.ownerOf(firstTokenId).call()).to.be.equal(targetNftTokenAddress); - + // Create a token to be nested and nest - const secondTokenId = await contract.methods.nextTokenId().call(); - await contract.methods.mint( - owner, - secondTokenId, - ).send({from: owner}); - + const mintingSecondTokenIdResult = await contract.methods.mint(owner).send({from: owner}); + const secondTokenId = mintingSecondTokenIdResult.events.Transfer.returnValues.tokenId; + await contract.methods.transfer(targetNftTokenAddress, secondTokenId).send({from: owner}); - expect(await contract.methods.ownerOf(secondTokenId).call()).to.be.equal(targetNftTokenAddress); - + // Unnest token back await contract.methods.transferFrom(targetNftTokenAddress, owner, secondTokenId).send({from: owner}); expect(await contract.methods.ownerOf(secondTokenId).call()).to.be.equal(owner); }); - + itEth('NFT: allows an Owner to nest/unnest their token (Restricted nesting)', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); - + const {collectionId: collectionIdA, collectionAddress: collectionAddressA, contract: contractA} = await createNestingCollection(helper, owner); const {collectionAddress: collectionAddressB, contract: contractB} = await createNestingCollection(helper, owner); await contractA.methods.setCollectionNesting(true, [collectionAddressA, collectionAddressB]).send({from: owner}); - + // Create a token to nest into - const targetNftTokenId = await contractA.methods.nextTokenId().call(); - await contractA.methods.mint( - owner, - targetNftTokenId, - ).send({from: owner}); + const mintingtargetNftTokenIdResult = await contractA.methods.mint(owner).send({from: owner}); + const targetNftTokenId = mintingtargetNftTokenIdResult.events.Transfer.returnValues.tokenId; const nftTokenAddressA1 = helper.ethAddress.fromTokenId(collectionIdA, targetNftTokenId); - + // Create a token for nesting in the same collection as the target - const nftTokenIdA = await contractA.methods.nextTokenId().call(); - await contractA.methods.mint( - owner, - nftTokenIdA, - ).send({from: owner}); - + const mintingTokenIdAResult = await contractA.methods.mint(owner).send({from: owner}); + const nftTokenIdA = mintingTokenIdAResult.events.Transfer.returnValues.tokenId; + // Create a token for nesting in a different collection - const nftTokenIdB = await contractB.methods.nextTokenId().call(); - await contractB.methods.mint( - owner, - nftTokenIdB, - ).send({from: owner}); - + const mintingTokenIdBResult = await contractB.methods.mint(owner).send({from: owner}); + const nftTokenIdB = mintingTokenIdBResult.events.Transfer.returnValues.tokenId; + // Nest await contractA.methods.transfer(nftTokenAddressA1, nftTokenIdA).send({from: owner}); expect(await contractA.methods.ownerOf(nftTokenIdA).call()).to.be.equal(nftTokenAddressA1); - + await contractB.methods.transfer(nftTokenAddressA1, nftTokenIdB).send({from: owner}); expect(await contractB.methods.ownerOf(nftTokenIdB).call()).to.be.equal(nftTokenAddressA1); }); @@ -105,112 +84,88 @@ describe('Negative Test: EVM Nesting', async() => { itEth('NFT: disallows to nest token if nesting is disabled', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); - + const {collectionId, contract} = await createNestingCollection(helper, owner); await contract.methods.setCollectionNesting(false).send({from: owner}); - + // Create a token to nest into - const targetNftTokenId = await contract.methods.nextTokenId().call(); - await contract.methods.mint( - owner, - targetNftTokenId, - ).send({from: owner}); - - const targetNftTokenAddress = helper.ethAddress.fromTokenId(collectionId, targetNftTokenId); - + const mintingTargetTokenIdResult = await contract.methods.mint(owner).send({from: owner}); + const targetTokenId = mintingTargetTokenIdResult.events.Transfer.returnValues.tokenId; + const targetNftTokenAddress = helper.ethAddress.fromTokenId(collectionId, targetTokenId); + // Create a token to nest - const nftTokenId = await contract.methods.nextTokenId().call(); - await contract.methods.mint( - owner, - nftTokenId, - ).send({from: owner}); - + const mintingNftTokenIdResult = await contract.methods.mint(owner).send({from: owner}); + const nftTokenId = mintingNftTokenIdResult.events.Transfer.returnValues.tokenId; + // Try to nest await expect(contract.methods .transfer(targetNftTokenAddress, nftTokenId) .call({from: owner})).to.be.rejectedWith('UserIsNotAllowedToNest'); }); - + itEth('NFT: disallows a non-Owner to nest someone else\'s token', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); const malignant = await helper.eth.createAccountWithBalance(donor); - + const {collectionId, contract} = await createNestingCollection(helper, owner); - + // Mint a token - const targetTokenId = await contract.methods.nextTokenId().call(); - await contract.methods.mint( - owner, - targetTokenId, - ).send({from: owner}); + const mintingTargetTokenIdResult = await contract.methods.mint(owner).send({from: owner}); + const targetTokenId = mintingTargetTokenIdResult.events.Transfer.returnValues.tokenId; const targetTokenAddress = helper.ethAddress.fromTokenId(collectionId, targetTokenId); - + // Mint a token belonging to a different account - const tokenId = await contract.methods.nextTokenId().call(); - await contract.methods.mint( - malignant, - tokenId, - ).send({from: owner}); - + const mintingTokenIdResult = await contract.methods.mint(malignant).send({from: owner}); + const tokenId = mintingTokenIdResult.events.Transfer.returnValues.tokenId; + // Try to nest one token in another as a non-owner account await expect(contract.methods .transfer(targetTokenAddress, tokenId) .call({from: malignant})).to.be.rejectedWith('UserIsNotAllowedToNest'); }); - + itEth('NFT: disallows a non-Owner to nest someone else\'s token (Restricted nesting)', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); const malignant = await helper.eth.createAccountWithBalance(donor); - + const {collectionId: collectionIdA, collectionAddress: collectionAddressA, contract: contractA} = await createNestingCollection(helper, owner); const {collectionAddress: collectionAddressB, contract: contractB} = await createNestingCollection(helper, owner); - + await contractA.methods.setCollectionNesting(true, [collectionAddressA, collectionAddressB]).send({from: owner}); - + // Create a token in one collection - const nftTokenIdA = await contractA.methods.nextTokenId().call(); - await contractA.methods.mint( - owner, - nftTokenIdA, - ).send({from: owner}); + const mintingTokenIdAResult = await contractA.methods.mint(owner).send({from: owner}); + const nftTokenIdA = mintingTokenIdAResult.events.Transfer.returnValues.tokenId; const nftTokenAddressA = helper.ethAddress.fromTokenId(collectionIdA, nftTokenIdA); - - // Create a token in another collection belonging to someone else - const nftTokenIdB = await contractB.methods.nextTokenId().call(); - await contractB.methods.mint( - malignant, - nftTokenIdB, - ).send({from: owner}); - + + // Create a token in another collection + const mintingTokenIdBResult = await contractB.methods.mint(malignant).send({from: owner}); + const nftTokenIdB = mintingTokenIdBResult.events.Transfer.returnValues.tokenId; + // Try to drag someone else's token into the other collection and nest await expect(contractB.methods .transfer(nftTokenAddressA, nftTokenIdB) .call({from: malignant})).to.be.rejectedWith('UserIsNotAllowedToNest'); }); - + itEth('NFT: disallows to nest token in an unlisted collection', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); - + const {collectionId: collectionIdA, collectionAddress: collectionAddressA, contract: contractA} = await createNestingCollection(helper, owner); const {contract: contractB} = await createNestingCollection(helper, owner); - + await contractA.methods.setCollectionNesting(true, [collectionAddressA]).send({from: owner}); - + // Create a token in one collection - const nftTokenIdA = await contractA.methods.nextTokenId().call(); - await contractA.methods.mint( - owner, - nftTokenIdA, - ).send({from: owner}); + const mintingTokenIdAResult = await contractA.methods.mint(owner).send({from: owner}); + const nftTokenIdA = mintingTokenIdAResult.events.Transfer.returnValues.tokenId; const nftTokenAddressA = helper.ethAddress.fromTokenId(collectionIdA, nftTokenIdA); - + // Create a token in another collection - const nftTokenIdB = await contractB.methods.nextTokenId().call(); - await contractB.methods.mint( - owner, - nftTokenIdB, - ).send({from: owner}); - + const mintingTokenIdBResult = await contractB.methods.mint(owner).send({from: owner}); + const nftTokenIdB = mintingTokenIdBResult.events.Transfer.returnValues.tokenId; + + // Try to nest into a token in the other collection, disallowed in the first await expect(contractB.methods .transfer(nftTokenAddressA, nftTokenIdB) --- a/tests/src/eth/nonFungible.test.ts +++ b/tests/src/eth/nonFungible.test.ts @@ -29,7 +29,7 @@ [alice] = await helper.arrange.createAccounts([10n], donor); }); }); - + itEth('totalSupply', async ({helper}) => { const collection = await helper.nft.mintCollection(alice, {}); await collection.mintToken(alice); @@ -68,6 +68,16 @@ expect(owner).to.equal(caller); }); + + itEth('name/symbol is available regardless of ERC721Metadata support', async ({helper}) => { + const collection = await helper.nft.mintCollection(alice, {name: 'test', tokenPrefix: 'TEST'}); + const caller = helper.eth.createAccount(); + + const contract = helper.ethNativeContract.collectionById(collection.collectionId, 'nft', caller); + + expect(await contract.methods.name().call()).to.equal('test'); + expect(await contract.methods.symbol().call()).to.equal('TEST'); + }); }); describe('Check ERC721 token URI for NFT', () => { @@ -79,34 +89,29 @@ }); }); - async function setup(helper: EthUniqueHelper, tokenPrefix: string, propertyKey?: string, propertyValue?: string): Promise<{contract: Contract, nextTokenId: string}> { + async function setup(helper: EthUniqueHelper, baseUri: string, propertyKey?: string, propertyValue?: string): Promise<{contract: Contract, nextTokenId: string}> { const owner = await helper.eth.createAccountWithBalance(donor); const receiver = helper.eth.createAccount(); - const collectionHelper = helper.ethNativeContract.collectionHelpers(owner); - let result = await collectionHelper.methods.createERC721MetadataCompatibleCollection('Mint collection', 'a', 'b', tokenPrefix).send({value: Number(2n * helper.balance.getOneTokenNominal())}); - const collectionAddress = helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId); + const {collectionAddress} = await helper.eth.createERC721MetadataCompatibleNFTCollection(owner, 'Mint collection', 'a', 'b', baseUri); const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); - - const nextTokenId = await contract.methods.nextTokenId().call(); - expect(nextTokenId).to.be.equal('1'); - result = await contract.methods.mint( - receiver, - nextTokenId, - ).send(); + const result = await contract.methods.mint(receiver).send(); + const tokenId = result.events.Transfer.returnValues.tokenId; + expect(tokenId).to.be.equal('1'); + if (propertyKey && propertyValue) { // Set URL or suffix - await contract.methods.setProperty(nextTokenId, propertyKey, Buffer.from(propertyValue)).send(); + await contract.methods.setProperty(tokenId, propertyKey, Buffer.from(propertyValue)).send(); } const event = result.events.Transfer; expect(event.address).to.be.equal(collectionAddress); expect(event.returnValues.from).to.be.equal('0x0000000000000000000000000000000000000000'); expect(event.returnValues.to).to.be.equal(receiver); - expect(event.returnValues.tokenId).to.be.equal(nextTokenId); + expect(event.returnValues.tokenId).to.be.equal(tokenId); - return {contract, nextTokenId}; + return {contract, nextTokenId: tokenId}; } itEth('Empty tokenURI', async ({helper}) => { @@ -115,18 +120,18 @@ }); itEth('TokenURI from url', async ({helper}) => { - const {contract, nextTokenId} = await setup(helper, 'BaseURI_', 'url', 'Token URI'); + const {contract, nextTokenId} = await setup(helper, 'BaseURI_', 'URI', 'Token URI'); expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('Token URI'); }); - itEth('TokenURI from baseURI + tokenId', async ({helper}) => { + itEth('TokenURI from baseURI', async ({helper}) => { const {contract, nextTokenId} = await setup(helper, 'BaseURI_'); - expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('BaseURI_' + nextTokenId); + expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('BaseURI_'); }); itEth('TokenURI from baseURI + suffix', async ({helper}) => { const suffix = '/some/suffix'; - const {contract, nextTokenId} = await setup(helper, 'BaseURI_', 'suffix', suffix); + const {contract, nextTokenId} = await setup(helper, 'BaseURI_', 'URISuffix', suffix); expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('BaseURI_' + suffix); }); }); @@ -146,24 +151,19 @@ const owner = await helper.eth.createAccountWithBalance(donor); const receiver = helper.eth.createAccount(); - const {collectionAddress} = await helper.eth.createNonfungibleCollection(owner, 'Minty', '6', '6'); + const {collectionAddress} = await helper.eth.createERC721MetadataCompatibleNFTCollection(owner, 'Mint collection', '6', '6', ''); const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); - const nextTokenId = await contract.methods.nextTokenId().call(); - expect(nextTokenId).to.be.equal('1'); - const result = await contract.methods.mintWithTokenURI( - receiver, - nextTokenId, - 'Test URI', - ).send(); + const result = await contract.methods.mintWithTokenURI(receiver, 'Test URI').send(); + const tokenId = result.events.Transfer.returnValues.tokenId; + expect(tokenId).to.be.equal('1'); const event = result.events.Transfer; expect(event.address).to.be.equal(collectionAddress); expect(event.returnValues.from).to.be.equal('0x0000000000000000000000000000000000000000'); expect(event.returnValues.to).to.be.equal(receiver); - expect(event.returnValues.tokenId).to.be.equal(nextTokenId); - expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI'); + expect(await contract.methods.tokenURI(tokenId).call()).to.be.equal('Test URI'); // TODO: this wont work right now, need release 919000 first // await helper.methods.setOffchainSchema(collectionIdAddress, 'https://offchain-service.local/token-info/{id}').send(); @@ -216,7 +216,7 @@ { const result = await contract.methods.burn(tokenId).send({from: caller}); - + const event = result.events.Transfer; expect(event.address).to.be.equal(collectionAddress); expect(event.returnValues.from).to.be.equal(caller); @@ -322,7 +322,7 @@ [alice] = await helper.arrange.createAccounts([10n], donor); }); }); - + itEth('approve() call fee is less than 0.2UNQ', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); const spender = helper.eth.createAccount(); @@ -403,7 +403,7 @@ const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId); const contract = helper.ethNativeContract.collection(collectionAddress, 'nft'); - + const events: any = []; contract.events.allEvents((_: any, event: any) => { events.push(event); @@ -428,7 +428,7 @@ const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId); const contract = helper.ethNativeContract.collection(collectionAddress, 'nft'); - + const events: any = []; contract.events.allEvents((_: any, event: any) => { events.push(event); @@ -455,13 +455,14 @@ const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId); const contract = helper.ethNativeContract.collection(collectionAddress, 'nft'); - + const events: any = []; contract.events.allEvents((_: any, event: any) => { events.push(event); }); await token.transferFrom(bob, {Substrate: alice.address}, {Ethereum: receiver}); + if (events.length == 0) await helper.wait.newBlocks(1); const event = events[0]; @@ -479,13 +480,14 @@ const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId); const contract = helper.ethNativeContract.collection(collectionAddress, 'nft'); - + const events: any = []; contract.events.allEvents((_: any, event: any) => { events.push(event); }); await token.transfer(alice, {Ethereum: receiver}); + if (events.length == 0) await helper.wait.newBlocks(1); const event = events[0]; @@ -509,7 +511,23 @@ itEth('Returns collection name', async ({helper}) => { const caller = await helper.eth.createAccountWithBalance(donor); - const collection = await helper.nft.mintCollection(alice, {name: 'oh River', tokenPrefix: 'CHANGE'}); + const tokenPropertyPermissions = [{ + key: 'URI', + permission: { + mutable: true, + collectionAdmin: true, + tokenOwner: false, + }, + }]; + const collection = await helper.nft.mintCollection( + alice, + { + name: 'oh River', + tokenPrefix: 'CHANGE', + properties: [{key: 'ERC721Metadata', value: '1'}], + tokenPropertyPermissions, + }, + ); const contract = helper.ethNativeContract.collectionById(collection.collectionId, 'nft', caller); const name = await contract.methods.name().call(); @@ -518,10 +536,26 @@ itEth('Returns symbol name', async ({helper}) => { const caller = await helper.eth.createAccountWithBalance(donor); - const collection = await helper.nft.mintCollection(alice, {name: 'oh River', tokenPrefix: 'CHANGE'}); + const tokenPropertyPermissions = [{ + key: 'URI', + permission: { + mutable: true, + collectionAdmin: true, + tokenOwner: false, + }, + }]; + const collection = await helper.nft.mintCollection( + alice, + { + name: 'oh River', + tokenPrefix: 'CHANGE', + properties: [{key: 'ERC721Metadata', value: '1'}], + tokenPropertyPermissions, + }, + ); const contract = helper.ethNativeContract.collectionById(collection.collectionId, 'nft', caller); const symbol = await contract.methods.symbol().call(); expect(symbol).to.equal('CHANGE'); }); -}); \ No newline at end of file +}); --- a/tests/src/eth/nonFungibleAbi.json +++ b/tests/src/eth/nonFungibleAbi.json @@ -146,6 +146,15 @@ "type": "function" }, { + "inputs": [ + { "internalType": "address", "name": "newOwner", "type": "address" } + ], + "name": "changeCollectionOwner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { "inputs": [], "name": "collectionOwner", "outputs": [ @@ -260,12 +269,9 @@ "type": "function" }, { - "inputs": [ - { "internalType": "address", "name": "to", "type": "address" }, - { "internalType": "uint256", "name": "tokenId", "type": "uint256" } - ], + "inputs": [{ "internalType": "address", "name": "to", "type": "address" }], "name": "mint", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "nonpayable", "type": "function" }, @@ -287,7 +293,7 @@ { "internalType": "uint256", "name": "field_0", "type": "uint256" }, { "internalType": "string", "name": "field_1", "type": "string" } ], - "internalType": "struct Tuple8[]", + "internalType": "struct Tuple6[]", "name": "tokens", "type": "tuple[]" } @@ -300,11 +306,10 @@ { "inputs": [ { "internalType": "address", "name": "to", "type": "address" }, - { "internalType": "uint256", "name": "tokenId", "type": "uint256" }, { "internalType": "string", "name": "tokenUri", "type": "string" } ], "name": "mintWithTokenURI", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "nonpayable", "type": "function" }, @@ -476,15 +481,6 @@ { "internalType": "address", "name": "sponsor", "type": "address" } ], "name": "setCollectionSponsor", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "newOwner", "type": "address" } - ], - "name": "setOwner", "outputs": [], "stateMutability": "nonpayable", "type": "function" --- a/tests/src/eth/payable.test.ts +++ b/tests/src/eth/payable.test.ts @@ -118,7 +118,7 @@ const deployer = await helper.eth.createAccountWithBalance(donor); const caller = await helper.eth.createAccountWithBalance(donor); const contract = await helper.eth.deployFlipper(deployer); - + const initialCallerBalance = await helper.balance.getEthereum(caller); await contract.methods.flip().send({from: caller}); const finalCallerBalance = await helper.balance.getEthereum(caller); @@ -129,7 +129,7 @@ const deployer = await helper.eth.createAccountWithBalance(donor); const caller = await helper.eth.createAccountWithBalance(donor); const contract = await deployProxyContract(helper, deployer); - + const initialCallerBalance = await helper.balance.getEthereum(caller); const initialContractBalance = await helper.balance.getEthereum(contract.options.address); await contract.methods.flip().send({from: caller}); @@ -138,7 +138,7 @@ expect(finalCallerBalance < initialCallerBalance).to.be.true; expect(finalContractBalance == initialContractBalance).to.be.true; }); - + itEth('Fee for nested calls to native methods is withdrawn from the user', async({helper}) => { const CONTRACT_BALANCE = 2n * helper.balance.getOneTokenNominal(); @@ -146,7 +146,7 @@ const caller = await helper.eth.createAccountWithBalance(donor); const contract = await deployProxyContract(helper, deployer); - const collectionAddress = (await contract.methods.createNonfungibleCollection().send({from: caller, value: Number(CONTRACT_BALANCE)})).events.CollectionCreated.returnValues.collection; + const collectionAddress = (await contract.methods.createNFTCollection().send({from: caller, value: Number(CONTRACT_BALANCE)})).events.CollectionCreated.returnValues.collection; const initialCallerBalance = await helper.balance.getEthereum(caller); const initialContractBalance = await helper.balance.getEthereum(contract.options.address); await contract.methods.mintNftToken(collectionAddress).send({from: caller}); @@ -155,7 +155,7 @@ expect(finalCallerBalance < initialCallerBalance).to.be.true; expect(finalContractBalance == initialContractBalance).to.be.true; }); - + itEth('Fee for nested calls to create*Collection methods is withdrawn from the user and from the contract', async({helper}) => { const CONTRACT_BALANCE = 2n * helper.balance.getOneTokenNominal(); const deployer = await helper.eth.createAccountWithBalance(donor); @@ -164,7 +164,7 @@ const initialCallerBalance = await helper.balance.getEthereum(caller); const initialContractBalance = await helper.balance.getEthereum(contract.options.address); - await contract.methods.createNonfungibleCollection().send({from: caller, value: Number(CONTRACT_BALANCE)}); + await contract.methods.createNFTCollection().send({from: caller, value: Number(CONTRACT_BALANCE)}); const finalCallerBalance = await helper.balance.getEthereum(caller); const finalContractBalance = await helper.balance.getEthereum(contract.options.address); expect(finalCallerBalance < initialCallerBalance).to.be.true; @@ -176,9 +176,9 @@ const BIG_FEE = 3n * helper.balance.getOneTokenNominal(); const caller = await helper.eth.createAccountWithBalance(donor); const collectionHelper = helper.ethNativeContract.collectionHelpers(caller); - - await expect(collectionHelper.methods.createNonfungibleCollection('A', 'B', 'C').call({value: Number(SMALL_FEE)})).to.be.rejectedWith('Sent amount not equals to collection creation price (2000000000000000000)'); - await expect(collectionHelper.methods.createNonfungibleCollection('A', 'B', 'C').call({value: Number(BIG_FEE)})).to.be.rejectedWith('Sent amount not equals to collection creation price (2000000000000000000)'); + + await expect(collectionHelper.methods.createNFTCollection('A', 'B', 'C').call({value: Number(SMALL_FEE)})).to.be.rejectedWith('Sent amount not equals to collection creation price (2000000000000000000)'); + await expect(collectionHelper.methods.createNFTCollection('A', 'B', 'C').call({value: Number(BIG_FEE)})).to.be.rejectedWith('Sent amount not equals to collection creation price (2000000000000000000)'); }); itEth('Negative test: call createRFTCollection with wrong fee', async({helper}) => { @@ -186,7 +186,7 @@ const BIG_FEE = 3n * helper.balance.getOneTokenNominal(); const caller = await helper.eth.createAccountWithBalance(donor); const collectionHelper = helper.ethNativeContract.collectionHelpers(caller); - + await expect(collectionHelper.methods.createRFTCollection('A', 'B', 'C').call({value: Number(SMALL_FEE)})).to.be.rejectedWith('Sent amount not equals to collection creation price (2000000000000000000)'); await expect(collectionHelper.methods.createRFTCollection('A', 'B', 'C').call({value: Number(BIG_FEE)})).to.be.rejectedWith('Sent amount not equals to collection creation price (2000000000000000000)'); }); @@ -227,16 +227,15 @@ InnerContract(innerContract).flip(); } - function createNonfungibleCollection() external payable { + function createNFTCollection() external payable { address collectionHelpers = 0x6C4E9fE1AE37a41E93CEE429e8E1881aBdcbb54F; - address nftCollection = CollectionHelpers(collectionHelpers).createNonfungibleCollection{value: msg.value}("A", "B", "C"); + address nftCollection = CollectionHelpers(collectionHelpers).createNFTCollection{value: msg.value}("A", "B", "C"); emit CollectionCreated(nftCollection); } function mintNftToken(address collectionAddress) external { UniqueNFT collection = UniqueNFT(collectionAddress); - uint256 tokenId = collection.nextTokenId(); - collection.mint(msg.sender, tokenId); + uint256 tokenId = collection.mint(msg.sender); emit TokenMinted(tokenId); } --- a/tests/src/eth/proxy/UniqueNFTProxy.sol +++ b/tests/src/eth/proxy/UniqueNFTProxy.sol @@ -120,20 +120,19 @@ return proxied.mintingFinished(); } - function mint(address to, uint256 tokenId) + function mint(address to) external override - returns (bool) + returns (uint256) { - return proxied.mint(to, tokenId); + return proxied.mint(to); } function mintWithTokenURI( address to, - uint256 tokenId, string memory tokenUri - ) external override returns (bool) { - return proxied.mintWithTokenURI(to, tokenId, tokenUri); + ) external override returns (uint256) { + return proxied.mintWithTokenURI(to, tokenUri); } function finishMinting() external override returns (bool) { @@ -169,7 +168,7 @@ return proxied.mintBulk(to, tokenIds); } - function mintBulkWithTokenURI(address to, Tuple0[] memory tokens) + function mintBulkWithTokenURI(address to, Tuple6[] memory tokens) external override returns (bool) --- a/tests/src/eth/proxy/nonFungibleProxy.test.ts +++ b/tests/src/eth/proxy/nonFungibleProxy.test.ts @@ -101,7 +101,7 @@ itEth('Can perform mint()', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); - const {collectionAddress} = await helper.eth.createNonfungibleCollection(owner, 'A', 'A', 'A'); + const {collectionAddress} = await helper.eth.createERC721MetadataCompatibleNFTCollection(owner, 'A', 'A', 'A', ''); const caller = await helper.eth.createAccountWithBalance(donor); const receiver = helper.eth.createAccount(); @@ -111,13 +111,11 @@ await collectionEvmOwned.methods.addCollectionAdmin(contract.options.address).send(); { - const nextTokenId = await contract.methods.nextTokenId().call(); - expect(nextTokenId).to.be.equal('1'); - const result = await contract.methods.mintWithTokenURI( - receiver, - nextTokenId, - 'Test URI', - ).send({from: caller}); + const nextTokenId = await contract.methods.nextTokenId().call() + const result = await contract.methods.mintWithTokenURI(receiver, nextTokenId, 'Test URI').send({from: caller}); + const tokenId = result.events.Transfer.returnValues.tokenId; + expect(tokenId).to.be.equal('1'); + const events = helper.eth.normalizeEvents(result.events); events[0].address = events[0].address.toLocaleLowerCase(); @@ -128,12 +126,12 @@ args: { from: '0x0000000000000000000000000000000000000000', to: receiver, - tokenId: nextTokenId, + tokenId, }, }, ]); - expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI'); + expect(await contract.methods.tokenURI(tokenId).call()).to.be.equal('Test URI'); } }); --- a/tests/src/eth/reFungible.test.ts +++ b/tests/src/eth/reFungible.test.ts @@ -31,31 +31,23 @@ itEth('totalSupply', async ({helper}) => { const caller = await helper.eth.createAccountWithBalance(donor); - const {collectionAddress} = await helper.eth.createRefungibleCollection(caller, 'TotalSupply', '6', '6'); + const {collectionAddress} = await helper.eth.createRFTCollection(caller, 'TotalSupply', '6', '6'); const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', caller); - const nextTokenId = await contract.methods.nextTokenId().call(); - await contract.methods.mint(caller, nextTokenId).send(); + + await contract.methods.mint(caller).send(); + const totalSupply = await contract.methods.totalSupply().call(); expect(totalSupply).to.equal('1'); }); itEth('balanceOf', async ({helper}) => { const caller = await helper.eth.createAccountWithBalance(donor); - const {collectionAddress} = await helper.eth.createRefungibleCollection(caller, 'BalanceOf', '6', '6'); + const {collectionAddress} = await helper.eth.createRFTCollection(caller, 'BalanceOf', '6', '6'); const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', caller); - { - const nextTokenId = await contract.methods.nextTokenId().call(); - await contract.methods.mint(caller, nextTokenId).send(); - } - { - const nextTokenId = await contract.methods.nextTokenId().call(); - await contract.methods.mint(caller, nextTokenId).send(); - } - { - const nextTokenId = await contract.methods.nextTokenId().call(); - await contract.methods.mint(caller, nextTokenId).send(); - } + await contract.methods.mint(caller).send(); + await contract.methods.mint(caller).send(); + await contract.methods.mint(caller).send(); const balance = await contract.methods.balanceOf(caller).call(); expect(balance).to.equal('3'); @@ -63,11 +55,11 @@ itEth('ownerOf', async ({helper}) => { const caller = await helper.eth.createAccountWithBalance(donor); - const {collectionAddress} = await helper.eth.createRefungibleCollection(caller, 'OwnerOf', '6', '6'); + const {collectionAddress} = await helper.eth.createRFTCollection(caller, 'OwnerOf', '6', '6'); const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', caller); - const tokenId = await contract.methods.nextTokenId().call(); - await contract.methods.mint(caller, tokenId).send(); + const result = await contract.methods.mint(caller).send(); + const tokenId = result.events.Transfer.returnValues.tokenId; const owner = await contract.methods.ownerOf(tokenId).call(); expect(owner).to.equal(caller); @@ -76,11 +68,11 @@ itEth('ownerOf after burn', async ({helper}) => { const caller = await helper.eth.createAccountWithBalance(donor); const receiver = helper.eth.createAccount(); - const {collectionId, collectionAddress} = await helper.eth.createRefungibleCollection(caller, 'OwnerOf-AfterBurn', '6', '6'); + const {collectionId, collectionAddress} = await helper.eth.createRFTCollection(caller, 'OwnerOf-AfterBurn', '6', '6'); const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', caller); - const tokenId = await contract.methods.nextTokenId().call(); - await contract.methods.mint(caller, tokenId).send(); + const result = await contract.methods.mint(caller).send(); + const tokenId = result.events.Transfer.returnValues.tokenId; const tokenContract = helper.ethNativeContract.rftTokenById(collectionId, tokenId, caller); await tokenContract.methods.repartition(2).send(); @@ -95,11 +87,11 @@ itEth('ownerOf for partial ownership', async ({helper}) => { const caller = await helper.eth.createAccountWithBalance(donor); const receiver = helper.eth.createAccount(); - const {collectionId, collectionAddress} = await helper.eth.createRefungibleCollection(caller, 'Partial-OwnerOf', '6', '6'); + const {collectionId, collectionAddress} = await helper.eth.createRFTCollection(caller, 'Partial-OwnerOf', '6', '6'); const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', caller); - const tokenId = await contract.methods.nextTokenId().call(); - await contract.methods.mint(caller, tokenId).send(); + const result = await contract.methods.mint(caller).send(); + const tokenId = result.events.Transfer.returnValues.tokenId; const tokenContract = helper.ethNativeContract.rftTokenById(collectionId, tokenId, caller); await tokenContract.methods.repartition(2).send(); @@ -124,30 +116,25 @@ itEth('Can perform mint()', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); const receiver = helper.eth.createAccount(); - const {collectionAddress} = await helper.eth.createRefungibleCollection(owner, 'Minty', '6', '6'); + const {collectionAddress} = await helper.eth.createERC721MetadataCompatibleRFTCollection(owner, 'Minty', '6', '6', ''); const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', owner); - - const nextTokenId = await contract.methods.nextTokenId().call(); - expect(nextTokenId).to.be.equal('1'); - const result = await contract.methods.mintWithTokenURI( - receiver, - nextTokenId, - 'Test URI', - ).send(); + const result = await contract.methods.mintWithTokenURI(receiver, 'Test URI').send(); + const event = result.events.Transfer; expect(event.address).to.equal(collectionAddress); expect(event.returnValues.from).to.equal('0x0000000000000000000000000000000000000000'); expect(event.returnValues.to).to.equal(receiver); - expect(event.returnValues.tokenId).to.equal(nextTokenId); + const tokenId = event.returnValues.tokenId; + expect(tokenId).to.be.equal('1'); - expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI'); + expect(await contract.methods.tokenURI(tokenId).call()).to.be.equal('Test URI'); }); itEth('Can perform mintBulk()', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); const receiver = helper.eth.createAccount(); - const {collectionAddress} = await helper.eth.createRefungibleCollection(owner, 'MintBulky', '6', '6'); + const {collectionAddress} = await helper.eth.createERC721MetadataCompatibleRFTCollection(owner, 'MintBulky', '6', '6', ''); const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', owner); { @@ -179,11 +166,11 @@ itEth('Can perform burn()', async ({helper}) => { const caller = await helper.eth.createAccountWithBalance(donor); - const {collectionAddress} = await helper.eth.createRefungibleCollection(caller, 'Burny', '6', '6'); + const {collectionAddress} = await helper.eth.createRFTCollection(caller, 'Burny', '6', '6'); const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', caller); - const tokenId = await contract.methods.nextTokenId().call(); - await contract.methods.mint(caller, tokenId).send(); + const result = await contract.methods.mint(caller).send(); + const tokenId = result.events.Transfer.returnValues.tokenId; { const result = await contract.methods.burn(tokenId).send(); const event = result.events.Transfer; @@ -197,12 +184,13 @@ itEth('Can perform transferFrom()', async ({helper}) => { const caller = await helper.eth.createAccountWithBalance(donor); const receiver = helper.eth.createAccount(); - const {collectionId, collectionAddress} = await helper.eth.createRefungibleCollection(caller, 'TransferFromy', '6', '6'); + const {collectionId, collectionAddress} = await helper.eth.createRFTCollection(caller, 'TransferFromy', '6', '6'); const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', caller); - const tokenId = await contract.methods.nextTokenId().call(); + const result = await contract.methods.mint(caller).send(); + const tokenId = result.events.Transfer.returnValues.tokenId; + const tokenAddress = helper.ethAddress.fromTokenId(collectionId, tokenId); - await contract.methods.mint(caller, tokenId).send(); const tokenContract = helper.ethNativeContract.rftToken(tokenAddress, caller); await tokenContract.methods.repartition(15).send(); @@ -241,15 +229,15 @@ itEth('Can perform transfer()', async ({helper}) => { const caller = await helper.eth.createAccountWithBalance(donor); const receiver = helper.eth.createAccount(); - const {collectionAddress} = await helper.eth.createRefungibleCollection(caller, 'Transferry', '6', '6'); + const {collectionAddress} = await helper.eth.createRFTCollection(caller, 'Transferry', '6', '6'); const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', caller); - const tokenId = await contract.methods.nextTokenId().call(); - await contract.methods.mint(caller, tokenId).send(); + const result = await contract.methods.mint(caller).send(); + const tokenId = result.events.Transfer.returnValues.tokenId; { const result = await contract.methods.transfer(receiver, tokenId).send(); - + const event = result.events.Transfer; expect(event.address).to.equal(collectionAddress); expect(event.returnValues.from).to.equal(caller); @@ -271,11 +259,11 @@ itEth('transfer event on transfer from partial ownership to full ownership', async ({helper}) => { const caller = await helper.eth.createAccountWithBalance(donor); const receiver = helper.eth.createAccount(); - const {collectionId, collectionAddress} = await helper.eth.createRefungibleCollection(caller, 'Transferry-Partial-to-Full', '6', '6'); + const {collectionId, collectionAddress} = await helper.eth.createRFTCollection(caller, 'Transferry-Partial-to-Full', '6', '6'); const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', caller); - const tokenId = await contract.methods.nextTokenId().call(); - await contract.methods.mint(caller, tokenId).send(); + const result = await contract.methods.mint(caller).send(); + const tokenId = result.events.Transfer.returnValues.tokenId; const tokenContract = helper.ethNativeContract.rftTokenById(collectionId, tokenId, caller); @@ -300,11 +288,11 @@ itEth('transfer event on transfer from full ownership to partial ownership', async ({helper}) => { const caller = await helper.eth.createAccountWithBalance(donor); const receiver = helper.eth.createAccount(); - const {collectionId, collectionAddress} = await helper.eth.createRefungibleCollection(caller, 'Transferry-Full-to-Partial', '6', '6'); + const {collectionId, collectionAddress} = await helper.eth.createRFTCollection(caller, 'Transferry-Full-to-Partial', '6', '6'); const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', caller); - const tokenId = await contract.methods.nextTokenId().call(); - await contract.methods.mint(caller, tokenId).send(); + const result = await contract.methods.mint(caller).send(); + const tokenId = result.events.Transfer.returnValues.tokenId; const tokenContract = helper.ethNativeContract.rftTokenById(collectionId, tokenId, caller); @@ -340,11 +328,11 @@ itEth('transferFrom() call fee is less than 0.2UNQ', async ({helper}) => { const caller = await helper.eth.createAccountWithBalance(donor); const receiver = helper.eth.createAccount(); - const {collectionAddress} = await helper.eth.createRefungibleCollection(caller, 'Feeful-Transfer-From', '6', '6'); + const {collectionAddress} = await helper.eth.createRFTCollection(caller, 'Feeful-Transfer-From', '6', '6'); const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', caller); - const tokenId = await contract.methods.nextTokenId().call(); - await contract.methods.mint(caller, tokenId).send(); + const result = await contract.methods.mint(caller).send(); + const tokenId = result.events.Transfer.returnValues.tokenId; const cost = await helper.eth.recordCallFee(caller, () => contract.methods.transferFrom(caller, receiver, tokenId).send()); expect(cost < BigInt(0.2 * Number(helper.balance.getOneTokenNominal()))); @@ -354,11 +342,11 @@ itEth('transfer() call fee is less than 0.2UNQ', async ({helper}) => { const caller = await helper.eth.createAccountWithBalance(donor); const receiver = helper.eth.createAccount(); - const {collectionAddress} = await helper.eth.createRefungibleCollection(caller, 'Feeful-Transfer', '6', '6'); + const {collectionAddress} = await helper.eth.createRFTCollection(caller, 'Feeful-Transfer', '6', '6'); const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', caller); - const tokenId = await contract.methods.nextTokenId().call(); - await contract.methods.mint(caller, tokenId).send(); + const result = await contract.methods.mint(caller).send(); + const tokenId = result.events.Transfer.returnValues.tokenId; const cost = await helper.eth.recordCallFee(caller, () => contract.methods.transfer(receiver, tokenId).send()); expect(cost < BigInt(0.2 * Number(helper.balance.getOneTokenNominal()))); @@ -381,8 +369,24 @@ itEth('Returns collection name', async ({helper}) => { const caller = helper.eth.createAccount(); - const collection = await helper.rft.mintCollection(alice, {name: 'Leviathan', tokenPrefix: '11'}); - + const tokenPropertyPermissions = [{ + key: 'URI', + permission: { + mutable: true, + collectionAdmin: true, + tokenOwner: false, + }, + }]; + const collection = await helper.rft.mintCollection( + alice, + { + name: 'Leviathan', + tokenPrefix: '11', + properties: [{key: 'ERC721Metadata', value: '1'}], + tokenPropertyPermissions, + }, + ); + const contract = helper.ethNativeContract.collectionById(collection.collectionId, 'rft', caller); const name = await contract.methods.name().call(); expect(name).to.equal('Leviathan'); @@ -390,8 +394,25 @@ itEth('Returns symbol name', async ({helper}) => { const caller = await helper.eth.createAccountWithBalance(donor); - const {collectionAddress} = await helper.eth.createRefungibleCollection(caller, 'Leviathan', '', '12'); - const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', caller); + const tokenPropertyPermissions = [{ + key: 'URI', + permission: { + mutable: true, + collectionAdmin: true, + tokenOwner: false, + }, + }]; + const {collectionId} = await helper.rft.mintCollection( + alice, + { + name: 'Leviathan', + tokenPrefix: '12', + properties: [{key: 'ERC721Metadata', value: '1'}], + tokenPropertyPermissions, + }, + ); + + const contract = helper.ethNativeContract.collectionById(collectionId, 'rft', caller); const symbol = await contract.methods.symbol().call(); expect(symbol).to.equal('12'); }); --- a/tests/src/eth/reFungibleAbi.json +++ b/tests/src/eth/reFungibleAbi.json @@ -146,6 +146,15 @@ "type": "function" }, { + "inputs": [ + { "internalType": "address", "name": "newOwner", "type": "address" } + ], + "name": "changeCollectionOwner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { "inputs": [], "name": "collectionOwner", "outputs": [ @@ -260,12 +269,9 @@ "type": "function" }, { - "inputs": [ - { "internalType": "address", "name": "to", "type": "address" }, - { "internalType": "uint256", "name": "tokenId", "type": "uint256" } - ], + "inputs": [{ "internalType": "address", "name": "to", "type": "address" }], "name": "mint", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "nonpayable", "type": "function" }, @@ -287,7 +293,7 @@ { "internalType": "uint256", "name": "field_0", "type": "uint256" }, { "internalType": "string", "name": "field_1", "type": "string" } ], - "internalType": "struct Tuple8[]", + "internalType": "struct Tuple6[]", "name": "tokens", "type": "tuple[]" } @@ -300,11 +306,10 @@ { "inputs": [ { "internalType": "address", "name": "to", "type": "address" }, - { "internalType": "uint256", "name": "tokenId", "type": "uint256" }, { "internalType": "string", "name": "tokenUri", "type": "string" } ], "name": "mintWithTokenURI", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "nonpayable", "type": "function" }, @@ -476,15 +481,6 @@ { "internalType": "address", "name": "sponsor", "type": "address" } ], "name": "setCollectionSponsor", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "newOwner", "type": "address" } - ], - "name": "setOwner", "outputs": [], "stateMutability": "nonpayable", "type": "function" --- a/tests/src/eth/reFungibleToken.test.ts +++ b/tests/src/eth/reFungibleToken.test.ts @@ -76,34 +76,28 @@ }); }); - async function setup(helper: EthUniqueHelper, tokenPrefix: string, propertyKey?: string, propertyValue?: string): Promise<{contract: Contract, nextTokenId: string}> { + async function setup(helper: EthUniqueHelper, baseUri: string, propertyKey?: string, propertyValue?: string): Promise<{contract: Contract, nextTokenId: string}> { const owner = await helper.eth.createAccountWithBalance(donor); const receiver = helper.eth.createAccount(); - const collectionHelper = helper.ethNativeContract.collectionHelpers(owner); - let result = await collectionHelper.methods.createERC721MetadataCompatibleCollection('Mint collection', 'a', 'b', tokenPrefix).send({value: Number(2n * helper.balance.getOneTokenNominal())}); - const collectionAddress = helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId); + const {collectionAddress} = await helper.eth.createERC721MetadataCompatibleRFTCollection(owner, 'Mint collection', 'a', 'b', baseUri); const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', owner); - - const nextTokenId = await contract.methods.nextTokenId().call(); - expect(nextTokenId).to.be.equal('1'); - result = await contract.methods.mint( - receiver, - nextTokenId, - ).send(); - if (propertyKey && propertyValue) { - // Set URL or suffix - await contract.methods.setProperty(nextTokenId, propertyKey, Buffer.from(propertyValue)).send(); - } + const result = await contract.methods.mint(receiver).send(); const event = result.events.Transfer; + const tokenId = event.returnValues.tokenId; + expect(tokenId).to.be.equal('1'); expect(event.address).to.be.equal(collectionAddress); expect(event.returnValues.from).to.be.equal('0x0000000000000000000000000000000000000000'); expect(event.returnValues.to).to.be.equal(receiver); - expect(event.returnValues.tokenId).to.be.equal(nextTokenId); - return {contract, nextTokenId}; + if (propertyKey && propertyValue) { + // Set URL or suffix + await contract.methods.setProperty(tokenId, propertyKey, Buffer.from(propertyValue)).send(); + } + + return {contract, nextTokenId: tokenId}; } itEth('Empty tokenURI', async ({helper}) => { @@ -112,18 +106,18 @@ }); itEth('TokenURI from url', async ({helper}) => { - const {contract, nextTokenId} = await setup(helper, 'BaseURI_', 'url', 'Token URI'); + const {contract, nextTokenId} = await setup(helper, 'BaseURI_', 'URI', 'Token URI'); expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('Token URI'); }); - itEth('TokenURI from baseURI + tokenId', async ({helper}) => { + itEth('TokenURI from baseURI', async ({helper}) => { const {contract, nextTokenId} = await setup(helper, 'BaseURI_'); - expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('BaseURI_' + nextTokenId); + expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('BaseURI_'); }); itEth('TokenURI from baseURI + suffix', async ({helper}) => { const suffix = '/some/suffix'; - const {contract, nextTokenId} = await setup(helper, 'BaseURI_', 'suffix', suffix); + const {contract, nextTokenId} = await setup(helper, 'BaseURI_', 'URISuffix', suffix); expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('BaseURI_' + suffix); }); }); @@ -294,11 +288,11 @@ itEth('Receiving Transfer event on burning into full ownership', async ({helper}) => { const caller = await helper.eth.createAccountWithBalance(donor); const receiver = await helper.eth.createAccountWithBalance(donor); - const {collectionId, collectionAddress} = await helper.eth.createRefungibleCollection(caller, 'Devastation', '6', '6'); + const {collectionId, collectionAddress} = await helper.eth.createRFTCollection(caller, 'Devastation', '6', '6'); const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', caller); - const tokenId = await contract.methods.nextTokenId().call(); - await contract.methods.mint(caller, tokenId).send(); + const result = await contract.methods.mint(caller).send(); + const tokenId = result.events.Transfer.returnValues.tokenId; const tokenAddress = helper.ethAddress.fromTokenId(collectionId, tokenId); const tokenContract = helper.ethNativeContract.rftToken(tokenAddress, caller); @@ -484,11 +478,12 @@ itEth('Default parent token address and id', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); - const {collectionId, collectionAddress} = await helper.eth.createRefungibleCollection(owner, 'Sands', '', 'GRAIN'); + const {collectionId, collectionAddress} = await helper.eth.createRFTCollection(owner, 'Sands', '', 'GRAIN'); const collectionContract = helper.ethNativeContract.collection(collectionAddress, 'rft', owner); - - const tokenId = await collectionContract.methods.nextTokenId().call(); - await collectionContract.methods.mint(owner, tokenId).send(); + + const result = await collectionContract.methods.mint(owner).send(); + const tokenId = result.events.Transfer.returnValues.tokenId; + const tokenAddress = helper.ethAddress.fromTokenId(collectionId, tokenId); const tokenContract = helper.ethNativeContract.rftToken(tokenAddress, owner); --- a/tests/src/eth/util/playgrounds/unique.dev.ts +++ b/tests/src/eth/util/playgrounds/unique.dev.ts @@ -43,12 +43,12 @@ if(!imports) return function(path: string) { return {error: `File not found: ${path}`}; }; - + const knownImports = {} as {[key: string]: string}; for(const imp of imports) { knownImports[imp.solPath] = (await readFile(imp.fsPath)).toString(); } - + return function(path: string) { if(path in knownImports) return {contents: knownImports[path]}; return {error: `File not found: ${path}`}; @@ -71,7 +71,7 @@ }, }, }), {import: await this.findImports(imports)})).contracts[`${name}.sol`][name]; - + return { abi: out.abi, object: '0x' + out.evm.bytecode.object, @@ -94,7 +94,7 @@ } } - + class NativeContractGroup extends EthGroupBase { contractHelpers(caller: string): Contract { @@ -145,14 +145,14 @@ async createAccountWithBalance(donor: IKeyringPair, amount=100n) { const account = this.createAccount(); await this.transferBalanceFromSubstrate(donor, account, amount); - + return account; } async transferBalanceFromSubstrate(donor: IKeyringPair, recepient: string, amount=100n, inTokens=true) { return await this.helper.balance.transferToSubstrate(donor, evmToAddress(recepient), amount * (inTokens ? this.helper.balance.getOneTokenNominal() : 1n)); } - + async getCollectionCreationFee(signer: string) { const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer); return await collectionHelper.methods.collectionCreationFee().call(); @@ -174,22 +174,32 @@ return await this.helper.callRpc('api.rpc.eth.call', [{from: signer, to: contractAddress, data: abi}]); } - async createNonfungibleCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{collectionId: number, collectionAddress: string}> { + async createNFTCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{collectionId: number, collectionAddress: string}> { const collectionCreationPrice = this.helper.balance.getCollectionCreationPrice(); const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer); - - const result = await collectionHelper.methods.createNonfungibleCollection(name, description, tokenPrefix).send({value: Number(collectionCreationPrice)}); + const result = await collectionHelper.methods.createNFTCollection(name, description, tokenPrefix).send({value: Number(collectionCreationPrice)}); + const collectionAddress = this.helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId); const collectionId = this.helper.ethAddress.extractCollectionId(collectionAddress); return {collectionId, collectionAddress}; } - async createRefungibleCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{collectionId: number, collectionAddress: string}> { + async createERC721MetadataCompatibleNFTCollection(signer: string, name: string, description: string, tokenPrefix: string, baseUri: string): Promise<{collectionId: number, collectionAddress: string}> { + const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer); + + const {collectionId, collectionAddress} = await this.createNFTCollection(signer, name, description, tokenPrefix) + + await collectionHelper.methods.makeCollectionERC721MetadataCompatible(collectionAddress, baseUri).send(); + + return {collectionId, collectionAddress}; + } + + async createRFTCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{collectionId: number, collectionAddress: string}> { const collectionCreationPrice = this.helper.balance.getCollectionCreationPrice(); const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer); - + const result = await collectionHelper.methods.createRFTCollection(name, description, tokenPrefix).send({value: Number(collectionCreationPrice)}); const collectionAddress = this.helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId); @@ -198,6 +208,16 @@ return {collectionId, collectionAddress}; } + async createERC721MetadataCompatibleRFTCollection(signer: string, name: string, description: string, tokenPrefix: string, baseUri: string): Promise<{collectionId: number, collectionAddress: string}> { + const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer); + + const {collectionId, collectionAddress} = await this.createRFTCollection(signer, name, description, tokenPrefix) + + await collectionHelper.methods.makeCollectionERC721MetadataCompatible(collectionAddress, baseUri).send(); + + return {collectionId, collectionAddress}; + } + async deployCollectorContract(signer: string): Promise { return await this.helper.ethContract.deployByCode(signer, 'Collector', ` // SPDX-License-Identifier: UNLICENSED @@ -288,7 +308,7 @@ }; return await this.helper.arrange.calculcateFee(address, wrappedCode); } -} +} class EthAddressGroup extends EthGroupBase { extractCollectionId(address: string): number { @@ -319,8 +339,8 @@ normalizeAddress(address: string): string { return '0x' + address.substring(address.length - 40); } -} - +} + export type EthUniqueHelperConstructor = new (...args: any[]) => EthUniqueHelper; export class EthUniqueHelper extends DevUniqueHelper { @@ -373,4 +393,3 @@ return newHelper; } } - \ No newline at end of file --- a/tests/src/util/playgrounds/unique.ts +++ b/tests/src/util/playgrounds/unique.ts @@ -1026,6 +1026,10 @@ return (await this.helper.callRpc('api.rpc.unique.collectionProperties', [collectionId, propertyKeys])).toHuman(); } + async getCollectionOptions(collectionId: number) { + return (await this.helper.callRpc('api.rpc.unique.collectionById', [collectionId])).toHuman(); + } + /** * Deletes onchain properties from the collection. * @@ -2839,6 +2843,10 @@ return await this.helper.collection.getTokenNextSponsored(this.collectionId, tokenId, addressObj); } + async getOptions() { + return await this.helper.collection.getCollectionOptions(this.collectionId); + } + async setSponsor(signer: TSigner, sponsorAddress: TSubstrateAccount) { return await this.helper.collection.setSponsor(signer, this.collectionId, sponsorAddress); } -- gitstuff