--- a/Cargo.lock +++ b/Cargo.lock @@ -1077,13 +1077,13 @@ [[package]] name = "console" -version = "0.15.1" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89eab4d20ce20cea182308bca13088fecea9c05f6776cf287205d41a0ed3c847" +checksum = "c050367d967ced717c04b65d8c619d863ef9292ce0c5760028655a2fb298718c" dependencies = [ "encode_unicode", + "lazy_static", "libc", - "once_cell", "terminal_size", "winapi", ] @@ -1871,9 +1871,9 @@ [[package]] name = "cxx" -version = "1.0.79" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f83d0ebf42c6eafb8d7c52f7e5f2d3003b89c7aa4fd2b79229209459a849af8" +checksum = "19f39818dcfc97d45b03953c1292efc4e80954e1583c4aa770bac1383e2310a4" dependencies = [ "cc", "cxxbridge-flags", @@ -1883,9 +1883,9 @@ [[package]] name = "cxx-build" -version = "1.0.79" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07d050484b55975889284352b0ffc2ecbda25c0c55978017c132b29ba0818a86" +checksum = "3e580d70777c116df50c390d1211993f62d40302881e54d4b79727acb83d0199" dependencies = [ "cc", "codespan-reporting", @@ -1898,15 +1898,15 @@ [[package]] name = "cxxbridge-flags" -version = "1.0.79" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d2199b00553eda8012dfec8d3b1c75fce747cf27c169a270b3b99e3448ab78" +checksum = "56a46460b88d1cec95112c8c363f0e2c39afdb237f60583b0b36343bf627ea9c" [[package]] name = "cxxbridge-macro" -version = "1.0.79" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb67a6de1f602736dd7eaead0080cf3435df806c61b24b13328db128c58868f" +checksum = "747b608fecf06b0d72d440f27acc99288207324b793be2c17991839f3d4995ea" dependencies = [ "proc-macro2", "quote", @@ -3561,9 +3561,9 @@ [[package]] name = "iana-time-zone-haiku" -version = "0.1.1" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +checksum = "fde6edd6cef363e9359ed3c98ba64590ba9eecba2293eb5a723ab32aee8926aa" dependencies = [ "cxx", "cxx-build", @@ -10774,9 +10774,9 @@ [[package]] name = "serde_json" -version = "1.0.87" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45" +checksum = "41feea4228a6f1cd09ec7a3593a682276702cd67b5273544757dae23c096f074" dependencies = [ "itoa", "ryu", @@ -12055,9 +12055,9 @@ [[package]] name = "syn" -version = "1.0.103" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" +checksum = "3fcd952facd492f9be3ef0d0b7032a6e442ee9b361d4acc2b1d0c4aaa5f613a1" dependencies = [ "proc-macro2", "quote", --- a/pallets/common/src/erc.rs +++ b/pallets/common/src/erc.rs @@ -34,8 +34,8 @@ use crate::{ Pallet, CollectionHandle, Config, CollectionProperties, SelfWeightOf, eth::{ - convert_cross_account_to_uint256, convert_uint256_to_cross_account, - convert_cross_account_to_tuple, + convert_cross_account_to_uint256, convert_cross_account_to_tuple, + convert_tuple_to_cross_account, }, weights::WeightInfo, }; @@ -139,26 +139,25 @@ save(self) } - // TODO: Temprorary off. Need refactor - // /// Set the substrate sponsor of the collection. - // /// - // /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. - // /// - // /// @param sponsor Substrate address of the sponsor from whose account funds will be debited for operations with the contract. - // fn set_collection_sponsor_substrate( - // &mut self, - // caller: caller, - // sponsor: uint256, - // ) -> Result { - // self.consume_store_reads_and_writes(1, 1)?; + /// Set the sponsor of the collection. + /// + /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. + /// + /// @param sponsor Cross account address of the sponsor from whose account funds will be debited for operations with the contract. + fn set_collection_sponsor_cross( + &mut self, + caller: caller, + sponsor: (address, uint256), + ) -> Result { + self.consume_store_reads_and_writes(1, 1)?; - // check_is_owner_or_admin(caller, self)?; + check_is_owner_or_admin(caller, self)?; - // let sponsor = convert_uint256_to_cross_account::(sponsor); - // self.set_sponsor(sponsor.as_sub().clone()) - // .map_err(dispatch_to_evm::)?; - // save(self) - // } + let sponsor = convert_tuple_to_cross_account::(sponsor)?; + self.set_sponsor(sponsor.as_sub().clone()) + .map_err(dispatch_to_evm::)?; + save(self) + } /// Whether there is a pending sponsor. fn has_collection_pending_sponsor(&self) -> Result { @@ -300,37 +299,35 @@ Ok(crate::eth::collection_id_to_address(self.id)) } - // TODO: Temprorary off. Need refactor - // /// Add collection admin by substrate address. - // /// @param newAdmin Substrate administrator address. - // fn add_collection_admin_substrate( - // &mut self, - // caller: caller, - // new_admin: uint256, - // ) -> Result { - // self.consume_store_writes(2)?; + /// Add collection admin. + /// @param newAdmin Cross account administrator address. + fn add_collection_admin_cross( + &mut self, + caller: caller, + new_admin: (address, uint256), + ) -> Result { + self.consume_store_writes(2)?; - // let caller = T::CrossAccountId::from_eth(caller); - // let new_admin = convert_uint256_to_cross_account::(new_admin); - // >::toggle_admin(self, &caller, &new_admin, true).map_err(dispatch_to_evm::)?; - // Ok(()) - // } + let caller = T::CrossAccountId::from_eth(caller); + let new_admin = convert_tuple_to_cross_account::(new_admin)?; + >::toggle_admin(self, &caller, &new_admin, true).map_err(dispatch_to_evm::)?; + Ok(()) + } - // TODO: Temprorary off. Need refactor - // /// Remove collection admin by substrate address. - // /// @param admin Substrate administrator address. - // fn remove_collection_admin_substrate( - // &mut self, - // caller: caller, - // admin: uint256, - // ) -> Result { - // self.consume_store_writes(2)?; + /// Remove collection admin. + /// @param admin Cross account administrator address. + fn remove_collection_admin_cross( + &mut self, + caller: caller, + admin: (address, uint256), + ) -> Result { + self.consume_store_writes(2)?; - // let caller = T::CrossAccountId::from_eth(caller); - // let admin = convert_uint256_to_cross_account::(admin); - // >::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::)?; - // Ok(()) - // } + let caller = T::CrossAccountId::from_eth(caller); + let admin = convert_tuple_to_cross_account::(admin)?; + >::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::)?; + Ok(()) + } /// Add collection admin. /// @param newAdmin Address of the added administrator. @@ -479,22 +476,21 @@ Ok(()) } - // TODO: Temprorary off. Need refactor - // /// Add substrate user to allowed list. - // /// - // /// @param user User substrate address. - // fn add_to_collection_allow_list_substrate( - // &mut self, - // caller: caller, - // user: uint256, - // ) -> Result { - // self.consume_store_writes(1)?; + /// Add user to allowed list. + /// + /// @param user User cross account address. + fn add_to_collection_allow_list_cross( + &mut self, + caller: caller, + user: (address, uint256), + ) -> Result { + self.consume_store_writes(1)?; - // let caller = T::CrossAccountId::from_eth(caller); - // let user = convert_uint256_to_cross_account::(user); - // Pallet::::toggle_allowlist(self, &caller, &user, true).map_err(dispatch_to_evm::)?; - // Ok(()) - // } + let caller = T::CrossAccountId::from_eth(caller); + let user = convert_tuple_to_cross_account::(user)?; + Pallet::::toggle_allowlist(self, &caller, &user, true).map_err(dispatch_to_evm::)?; + Ok(()) + } /// Remove the user from the allowed list. /// @@ -508,22 +504,21 @@ Ok(()) } - // TODO: Temprorary off. Need refactor - // /// Remove substrate user from allowed list. - // /// - // /// @param user User substrate address. - // fn remove_from_collection_allow_list_substrate( - // &mut self, - // caller: caller, - // user: uint256, - // ) -> Result { - // self.consume_store_writes(1)?; + /// Remove user from allowed list. + /// + /// @param user User cross account address. + fn remove_from_collection_allow_list_cross( + &mut self, + caller: caller, + user: (address, uint256), + ) -> Result { + self.consume_store_writes(1)?; - // let caller = T::CrossAccountId::from_eth(caller); - // let user = convert_uint256_to_cross_account::(user); - // Pallet::::toggle_allowlist(self, &caller, &user, false).map_err(dispatch_to_evm::)?; - // Ok(()) - // } + let caller = T::CrossAccountId::from_eth(caller); + let user = convert_tuple_to_cross_account::(user)?; + Pallet::::toggle_allowlist(self, &caller, &user, false).map_err(dispatch_to_evm::)?; + Ok(()) + } /// Switch permission for minting. /// @@ -556,15 +551,14 @@ Ok(self.is_owner_or_admin(&user)) } - // TODO: Temprorary off. Need refactor - // /// Check that substrate account is the owner or admin of the collection - // /// - // /// @param user account to verify - // /// @return "true" if account is the owner or admin - // fn is_owner_or_admin_substrate(&self, user: uint256) -> Result { - // let user = convert_uint256_to_cross_account::(user); - // Ok(self.is_owner_or_admin(&user)) - // } + /// Check that account is the owner or admin of the collection + /// + /// @param user User cross account to verify + /// @return "true" if account is the owner or admin + fn is_owner_or_admin_cross(&self, user: (address, uint256)) -> Result { + let user = convert_tuple_to_cross_account::(user)?; + Ok(self.is_owner_or_admin(&user)) + } /// Returns collection type /// @@ -580,7 +574,7 @@ /// Get collection owner. /// - /// @return Tuple with sponsor address and his substrate mirror. + /// @return Tuble with sponsor address and his substrate mirror. /// If address is canonical then substrate mirror is zero and vice versa. fn collection_owner(&self) -> Result<(address, uint256)> { Ok(convert_cross_account_to_tuple::( @@ -602,20 +596,6 @@ .map_err(dispatch_to_evm::) } - // TODO: Temprorary off. Need refactor - // /// Changes collection owner to another substrate account - // /// - // /// @dev Owner can be changed only by current owner - // /// @param newOwner new owner substrate account - // fn set_owner_substrate(&mut self, caller: caller, new_owner: uint256) -> Result { - // self.consume_store_writes(1)?; - - // let caller = T::CrossAccountId::from_eth(caller); - // let new_owner = convert_uint256_to_cross_account::(new_owner); - // self.set_owner_internal(caller, new_owner) - // .map_err(dispatch_to_evm::) - // } - /// Get collection administrators /// /// @return Vector of tuples with admins address and his substrate mirror. @@ -626,6 +606,19 @@ .collect(); Ok(result) } + + /// Changes collection owner to another account + /// + /// @dev Owner can be changed only by current owner + /// @param newOwner new owner cross account + fn set_owner_cross(&mut self, caller: caller, new_owner: (address, uint256)) -> Result { + self.consume_store_writes(1)?; + + let caller = T::CrossAccountId::from_eth(caller); + let new_owner = convert_tuple_to_cross_account::(new_owner)?; + self.set_owner_internal(caller, new_owner) + .map_err(dispatch_to_evm::) + } } /// ### Note --- a/pallets/common/src/eth.rs +++ b/pallets/common/src/eth.rs @@ -85,3 +85,27 @@ (eth, Default::default()) } } + +/// Convert tuple `(address, uint256)` to `CrossAccountId`. +/// +/// If `address` in the tuple has *default* value, then the canonical form is substrate, +/// if `uint256` has *default* value, then the ethereum form is canonical, +/// if both values are *default* or *non default*, then this is considered an invalid address and `Error` is returned. +pub fn convert_tuple_to_cross_account( + eth_cross_account_id: (address, uint256), +) -> evm_coder::execution::Result +where + T::AccountId: From<[u8; 32]>, +{ + if eth_cross_account_id == Default::default() { + Err("All fields of cross account is zeroed".into()) + } else if eth_cross_account_id.0 == Default::default() { + Ok(convert_uint256_to_cross_account::( + eth_cross_account_id.1, + )) + } else if eth_cross_account_id.1 == Default::default() { + Ok(T::CrossAccountId::from_eth(eth_cross_account_id.0)) + } else { + Err("All fields of cross account is non zeroed".into()) + } +} --- a/pallets/fungible/src/erc.rs +++ b/pallets/fungible/src/erc.rs @@ -19,6 +19,7 @@ use core::char::{REPLACEMENT_CHARACTER, decode_utf16}; use core::convert::TryInto; use evm_coder::{ToLog, execution::*, generate_stubgen, solidity_interface, types::*, weight}; +use pallet_common::eth::convert_tuple_to_cross_account; use up_data_structs::CollectionMode; use pallet_common::erc::{CommonEvmHandler, PrecompileResult}; use sp_std::vec::Vec; @@ -149,7 +150,26 @@ } #[solidity_interface(name = ERC20UniqueExtensions)] -impl FungibleHandle { +impl FungibleHandle +where + T::AccountId: From<[u8; 32]>, +{ + #[weight(>::approve())] + fn approve_cross( + &mut self, + caller: caller, + spender: (address, uint256), + amount: uint256, + ) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + let spender = convert_tuple_to_cross_account::(spender)?; + let amount = amount.try_into().map_err(|_| "amount overflow")?; + + >::set_allowance(self, &caller, &spender, amount) + .map_err(dispatch_to_evm::)?; + Ok(true) + } + /// Burn tokens from account /// @dev Function that burns an `amount` of the tokens of a given account, /// deducting from the sender's allowance for said account. @@ -169,6 +189,30 @@ Ok(true) } + /// Burn tokens from account + /// @dev Function that burns an `amount` of the tokens of a given account, + /// deducting from the sender's allowance for said account. + /// @param from The account whose tokens will be burnt. + /// @param amount The amount that will be burnt. + #[weight(>::burn_from())] + fn burn_from_cross( + &mut self, + caller: caller, + from: (address, uint256), + amount: uint256, + ) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + let from = convert_tuple_to_cross_account::(from)?; + let amount = amount.try_into().map_err(|_| "amount overflow")?; + let budget = self + .recorder + .weight_calls_budget(>::find_parent()); + + >::burn_from(self, &caller, &from, amount, &budget) + .map_err(dispatch_to_evm::)?; + Ok(true) + } + /// Mint tokens for multiple accounts. /// @param amounts array of pairs of account address and amount #[weight(>::create_multiple_items_ex(amounts.len() as u32))] @@ -191,6 +235,27 @@ .map_err(dispatch_to_evm::)?; Ok(true) } + + #[weight(>::transfer_from())] + fn transfer_from_cross( + &mut self, + caller: caller, + from: (address, uint256), + to: (address, uint256), + amount: uint256, + ) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + let from = convert_tuple_to_cross_account::(from)?; + let to = convert_tuple_to_cross_account::(to)?; + let amount = amount.try_into().map_err(|_| "amount overflow")?; + let budget = self + .recorder + .weight_calls_budget(>::find_parent()); + + >::transfer_from(self, &caller, &from, &to, amount, &budget) + .map_err(dispatch_to_evm::)?; + Ok(true) + } } #[solidity_interface( --- 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 0x3af103fb +/// @dev the ERC-165 identifier for this interface is 0x25d897dc contract Collection is Dummy, ERC165 { /// Set collection property. /// @@ -72,6 +72,19 @@ dummy = 0; } + /// Set the sponsor of the collection. + /// + /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. + /// + /// @param sponsor Cross account address of the sponsor from whose account funds will be debited for operations with the contract. + /// @dev EVM selector for this function is: 0x84a1d5a8, + /// or in textual repr: setCollectionSponsorCross((address,uint256)) + function setCollectionSponsorCross(Tuple6 memory sponsor) public { + require(false, stub_error); + sponsor; + dummy = 0; + } + /// Whether there is a pending sponsor. /// @dev EVM selector for this function is: 0x058ac185, /// or in textual repr: hasCollectionPendingSponsor() @@ -155,6 +168,26 @@ } /// Add collection admin. + /// @param newAdmin Cross account administrator address. + /// @dev EVM selector for this function is: 0x859aa7d6, + /// or in textual repr: addCollectionAdminCross((address,uint256)) + function addCollectionAdminCross(Tuple6 memory newAdmin) public { + require(false, stub_error); + newAdmin; + dummy = 0; + } + + /// Remove collection admin. + /// @param admin Cross account administrator address. + /// @dev EVM selector for this function is: 0x6c0cd173, + /// or in textual repr: removeCollectionAdminCross((address,uint256)) + function removeCollectionAdminCross(Tuple6 memory admin) public { + require(false, stub_error); + admin; + dummy = 0; + } + + /// Add collection admin. /// @param newAdmin Address of the added administrator. /// @dev EVM selector for this function is: 0x92e462c7, /// or in textual repr: addCollectionAdmin(address) @@ -234,6 +267,17 @@ dummy = 0; } + /// Add user to allowed list. + /// + /// @param user User cross account address. + /// @dev EVM selector for this function is: 0xa0184a3a, + /// or in textual repr: addToCollectionAllowListCross((address,uint256)) + function addToCollectionAllowListCross(Tuple6 memory user) public { + require(false, stub_error); + user; + dummy = 0; + } + /// Remove the user from the allowed list. /// /// @param user Address of a removed user. @@ -245,6 +289,17 @@ dummy = 0; } + /// Remove user from allowed list. + /// + /// @param user User cross account address. + /// @dev EVM selector for this function is: 0x09ba452a, + /// or in textual repr: removeFromCollectionAllowListCross((address,uint256)) + function removeFromCollectionAllowListCross(Tuple6 memory user) public { + require(false, stub_error); + user; + dummy = 0; + } + /// Switch permission for minting. /// /// @param mode Enable if "true". @@ -269,6 +324,19 @@ return false; } + /// Check that account is the owner or admin of the collection + /// + /// @param user User cross account to verify + /// @return "true" if account is the owner or admin + /// @dev EVM selector for this function is: 0x3e75a905, + /// or in textual repr: isOwnerOrAdminCross((address,uint256)) + function isOwnerOrAdminCross(Tuple6 memory user) public view returns (bool) { + require(false, stub_error); + user; + dummy; + return false; + } + /// Returns collection type /// /// @return `Fungible` or `NFT` or `ReFungible` @@ -282,7 +350,7 @@ /// Get collection owner. /// - /// @return Tuple with sponsor address and his substrate mirror. + /// @return Tuble with sponsor address and his substrate mirror. /// If address is canonical then substrate mirror is zero and vice versa. /// @dev EVM selector for this function is: 0xdf727d3b, /// or in textual repr: collectionOwner() @@ -315,10 +383,32 @@ dummy; return new Tuple6[](0); } + + /// Changes collection owner to another account + /// + /// @dev Owner can be changed only by current owner + /// @param newOwner new owner cross account + /// @dev EVM selector for this function is: 0xe5c9913f, + /// or in textual repr: setOwnerCross((address,uint256)) + function setOwnerCross(Tuple6 memory newOwner) public { + require(false, stub_error); + newOwner; + dummy = 0; + } } -/// @dev the ERC-165 identifier for this interface is 0x63034ac5 +/// @dev the ERC-165 identifier for this interface is 0x032e5926 contract ERC20UniqueExtensions is Dummy, ERC165 { + /// @dev EVM selector for this function is: 0x0ecd0ab0, + /// or in textual repr: approveCross((address,uint256),uint256) + function approveCross(Tuple6 memory spender, uint256 amount) public returns (bool) { + require(false, stub_error); + spender; + amount; + dummy = 0; + return false; + } + /// Burn tokens from account /// @dev Function that burns an `amount` of the tokens of a given account, /// deducting from the sender's allowance for said account. @@ -334,6 +424,21 @@ return false; } + /// Burn tokens from account + /// @dev Function that burns an `amount` of the tokens of a given account, + /// deducting from the sender's allowance for said account. + /// @param from The account whose tokens will be burnt. + /// @param amount The amount that will be burnt. + /// @dev EVM selector for this function is: 0xbb2f5a58, + /// or in textual repr: burnFromCross((address,uint256),uint256) + function burnFromCross(Tuple6 memory from, uint256 amount) public returns (bool) { + require(false, stub_error); + from; + amount; + dummy = 0; + return false; + } + /// Mint tokens for multiple accounts. /// @param amounts array of pairs of account address and amount /// @dev EVM selector for this function is: 0x1acf2d55, @@ -344,6 +449,21 @@ dummy = 0; return false; } + + /// @dev EVM selector for this function is: 0xd5cf430b, + /// or in textual repr: transferFromCross((address,uint256),(address,uint256),uint256) + function transferFromCross( + Tuple6 memory from, + Tuple6 memory to, + uint256 amount + ) public returns (bool) { + require(false, stub_error); + from; + to; + amount; + dummy = 0; + return false; + } } /// @dev anonymous struct --- a/pallets/nonfungible/src/erc.rs +++ b/pallets/nonfungible/src/erc.rs @@ -35,6 +35,7 @@ use pallet_common::{ erc::{CommonEvmHandler, PrecompileResult, CollectionCall, static_property::key}, CollectionHandle, CollectionPropertyPermissions, + eth::convert_tuple_to_cross_account, }; use pallet_evm::{account::CrossAccountId, PrecompileHandle}; use pallet_evm_coder_substrate::call; @@ -198,7 +199,10 @@ /// @title ERC-721 Non-Fungible Token Standard, optional metadata extension /// @dev See https://eips.ethereum.org/EIPS/eip-721 #[solidity_interface(name = ERC721Metadata, expect_selector = 0x5b5e139f)] -impl NonfungibleHandle { +impl NonfungibleHandle +where + T::AccountId: From<[u8; 32]>, +{ /// @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")] @@ -603,7 +607,10 @@ /// @title Unique extensions for ERC721. #[solidity_interface(name = ERC721UniqueExtensions)] -impl NonfungibleHandle { +impl NonfungibleHandle +where + T::AccountId: From<[u8; 32]>, +{ /// @notice A descriptive name for a collection of NFTs in this contract fn name(&self) -> Result { Ok(decode_utf16(self.name.iter().copied()) @@ -616,6 +623,28 @@ Ok(string::from_utf8_lossy(&self.token_prefix).into()) } + /// @notice Set or reaffirm the approved address for an NFT + /// @dev The zero address indicates there is no approved address. + /// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized + /// operator of the current owner. + /// @param approved The new substrate address approved NFT controller + /// @param tokenId The NFT to approve + #[weight(>::approve())] + fn approve_cross( + &mut self, + caller: caller, + approved: (address, uint256), + token_id: uint256, + ) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + let approved = convert_tuple_to_cross_account::(approved)?; + let token = token_id.try_into()?; + + >::set_allowance(self, &caller, token, Some(&approved)) + .map_err(dispatch_to_evm::)?; + Ok(()) + } + /// @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. @@ -634,6 +663,32 @@ Ok(()) } + /// @notice Transfer ownership of an NFT from cross account address to cross account address + /// @dev Throws unless `msg.sender` is the current owner. Throws if `to` + /// is the zero address. Throws if `tokenId` is not a valid NFT. + /// @param from Cross acccount address of current owner + /// @param to Cross acccount address of new owner + /// @param tokenId The NFT to transfer + #[weight(>::transfer())] + fn transfer_from_cross( + &mut self, + caller: caller, + from: (address, uint256), + to: (address, uint256), + token_id: uint256, + ) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + let from = convert_tuple_to_cross_account::(from)?; + let to = convert_tuple_to_cross_account::(to)?; + let token_id = token_id.try_into()?; + let budget = self + .recorder + .weight_calls_budget(>::find_parent()); + Pallet::::transfer_from(self, &caller, &from, &to, token_id, &budget) + .map_err(dispatch_to_evm::)?; + Ok(()) + } + /// @notice Burns a specific ERC721 token. /// @dev Throws unless `msg.sender` is the current owner or an authorized /// operator for this NFT. Throws if `from` is not the current owner. Throws @@ -654,6 +709,31 @@ Ok(()) } + /// @notice Burns a specific ERC721 token. + /// @dev Throws unless `msg.sender` is the current owner or an authorized + /// operator for this NFT. Throws if `from` is not the current owner. Throws + /// if `to` is the zero address. Throws if `tokenId` is not a valid NFT. + /// @param from The current owner of the NFT + /// @param tokenId The NFT to transfer + #[weight(>::burn_from())] + fn burn_from_cross( + &mut self, + caller: caller, + from: (address, uint256), + token_id: uint256, + ) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + let from = convert_tuple_to_cross_account::(from)?; + let token = token_id.try_into()?; + let budget = self + .recorder + .weight_calls_budget(>::find_parent()); + + >::burn_from(self, &caller, &from, token, &budget) + .map_err(dispatch_to_evm::)?; + Ok(()) + } + /// @notice Returns next free NFT ID. fn next_token_id(&self) -> Result { self.consume_store_reads(1)?; --- 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 0x3af103fb +/// @dev the ERC-165 identifier for this interface is 0x25d897dc contract Collection is Dummy, ERC165 { /// Set collection property. /// @@ -145,6 +145,19 @@ dummy = 0; } + /// Set the sponsor of the collection. + /// + /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. + /// + /// @param sponsor Cross account address of the sponsor from whose account funds will be debited for operations with the contract. + /// @dev EVM selector for this function is: 0x84a1d5a8, + /// or in textual repr: setCollectionSponsorCross((address,uint256)) + function setCollectionSponsorCross(Tuple6 memory sponsor) public { + require(false, stub_error); + sponsor; + dummy = 0; + } + /// Whether there is a pending sponsor. /// @dev EVM selector for this function is: 0x058ac185, /// or in textual repr: hasCollectionPendingSponsor() @@ -177,10 +190,10 @@ /// @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw. /// @dev EVM selector for this function is: 0x6ec0a9f1, /// or in textual repr: collectionSponsor() - function collectionSponsor() public view returns (Tuple17 memory) { + function collectionSponsor() public view returns (Tuple6 memory) { require(false, stub_error); dummy; - return Tuple17(0x0000000000000000000000000000000000000000, 0); + return Tuple6(0x0000000000000000000000000000000000000000, 0); } /// Set limits for the collection. @@ -228,6 +241,26 @@ } /// Add collection admin. + /// @param newAdmin Cross account administrator address. + /// @dev EVM selector for this function is: 0x859aa7d6, + /// or in textual repr: addCollectionAdminCross((address,uint256)) + function addCollectionAdminCross(Tuple6 memory newAdmin) public { + require(false, stub_error); + newAdmin; + dummy = 0; + } + + /// Remove collection admin. + /// @param admin Cross account administrator address. + /// @dev EVM selector for this function is: 0x6c0cd173, + /// or in textual repr: removeCollectionAdminCross((address,uint256)) + function removeCollectionAdminCross(Tuple6 memory admin) public { + require(false, stub_error); + admin; + dummy = 0; + } + + /// Add collection admin. /// @param newAdmin Address of the added administrator. /// @dev EVM selector for this function is: 0x92e462c7, /// or in textual repr: addCollectionAdmin(address) @@ -307,6 +340,17 @@ dummy = 0; } + /// Add user to allowed list. + /// + /// @param user User cross account address. + /// @dev EVM selector for this function is: 0xa0184a3a, + /// or in textual repr: addToCollectionAllowListCross((address,uint256)) + function addToCollectionAllowListCross(Tuple6 memory user) public { + require(false, stub_error); + user; + dummy = 0; + } + /// Remove the user from the allowed list. /// /// @param user Address of a removed user. @@ -318,6 +362,17 @@ dummy = 0; } + /// Remove user from allowed list. + /// + /// @param user User cross account address. + /// @dev EVM selector for this function is: 0x09ba452a, + /// or in textual repr: removeFromCollectionAllowListCross((address,uint256)) + function removeFromCollectionAllowListCross(Tuple6 memory user) public { + require(false, stub_error); + user; + dummy = 0; + } + /// Switch permission for minting. /// /// @param mode Enable if "true". @@ -342,6 +397,19 @@ return false; } + /// Check that account is the owner or admin of the collection + /// + /// @param user User cross account to verify + /// @return "true" if account is the owner or admin + /// @dev EVM selector for this function is: 0x3e75a905, + /// or in textual repr: isOwnerOrAdminCross((address,uint256)) + function isOwnerOrAdminCross(Tuple6 memory user) public view returns (bool) { + require(false, stub_error); + user; + dummy; + return false; + } + /// Returns collection type /// /// @return `Fungible` or `NFT` or `ReFungible` @@ -355,14 +423,14 @@ /// Get collection owner. /// - /// @return Tuple with sponsor address and his substrate mirror. + /// @return Tuble with sponsor address and his substrate mirror. /// If address is canonical then substrate mirror is zero and vice versa. /// @dev EVM selector for this function is: 0xdf727d3b, /// or in textual repr: collectionOwner() - function collectionOwner() public view returns (Tuple17 memory) { + function collectionOwner() public view returns (Tuple6 memory) { require(false, stub_error); dummy; - return Tuple17(0x0000000000000000000000000000000000000000, 0); + return Tuple6(0x0000000000000000000000000000000000000000, 0); } /// Changes collection owner to another account @@ -383,17 +451,23 @@ /// If address is canonical then substrate mirror is zero and vice versa. /// @dev EVM selector for this function is: 0x5813216b, /// or in textual repr: collectionAdmins() - function collectionAdmins() public view returns (Tuple17[] memory) { + function collectionAdmins() public view returns (Tuple6[] memory) { require(false, stub_error); dummy; - return new Tuple17[](0); + return new Tuple6[](0); } -} -/// @dev anonymous struct -struct Tuple17 { - address field_0; - uint256 field_1; + /// Changes collection owner to another account + /// + /// @dev Owner can be changed only by current owner + /// @param newOwner new owner cross account + /// @dev EVM selector for this function is: 0xe5c9913f, + /// or in textual repr: setOwnerCross((address,uint256)) + function setOwnerCross(Tuple6 memory newOwner) public { + require(false, stub_error); + newOwner; + dummy = 0; + } } /// @title ERC-721 Non-Fungible Token Standard, optional metadata extension @@ -540,7 +614,7 @@ } /// @title Unique extensions for ERC721. -/// @dev the ERC-165 identifier for this interface is 0x4468500d +/// @dev the ERC-165 identifier for this interface is 0x244543ee 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, @@ -560,6 +634,21 @@ return ""; } + /// @notice Set or reaffirm the approved address for an NFT + /// @dev The zero address indicates there is no approved address. + /// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized + /// operator of the current owner. + /// @param approved The new substrate address approved NFT controller + /// @param tokenId The NFT to approve + /// @dev EVM selector for this function is: 0x0ecd0ab0, + /// or in textual repr: approveCross((address,uint256),uint256) + function approveCross(Tuple6 memory approved, uint256 tokenId) public { + require(false, stub_error); + approved; + tokenId; + dummy = 0; + } + /// @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. @@ -574,6 +663,26 @@ dummy = 0; } + /// @notice Transfer ownership of an NFT from cross account address to cross account address + /// @dev Throws unless `msg.sender` is the current owner. Throws if `to` + /// is the zero address. Throws if `tokenId` is not a valid NFT. + /// @param from Cross acccount address of current owner + /// @param to Cross acccount address of new owner + /// @param tokenId The NFT to transfer + /// @dev EVM selector for this function is: 0xd5cf430b, + /// or in textual repr: transferFromCross((address,uint256),(address,uint256),uint256) + function transferFromCross( + Tuple6 memory from, + Tuple6 memory to, + uint256 tokenId + ) public { + require(false, stub_error); + from; + to; + tokenId; + dummy = 0; + } + /// @notice Burns a specific ERC721 token. /// @dev Throws unless `msg.sender` is the current owner or an authorized /// operator for this NFT. Throws if `from` is not the current owner. Throws @@ -589,6 +698,21 @@ dummy = 0; } + /// @notice Burns a specific ERC721 token. + /// @dev Throws unless `msg.sender` is the current owner or an authorized + /// operator for this NFT. Throws if `from` is not the current owner. Throws + /// if `to` is the zero address. Throws if `tokenId` is not a valid NFT. + /// @param from The current owner of the NFT + /// @param tokenId The NFT to transfer + /// @dev EVM selector for this function is: 0xbb2f5a58, + /// or in textual repr: burnFromCross((address,uint256),uint256) + function burnFromCross(Tuple6 memory from, uint256 tokenId) public { + require(false, stub_error); + from; + tokenId; + dummy = 0; + } + /// @notice Returns next free NFT ID. /// @dev EVM selector for this function is: 0x75794a3c, /// or in textual repr: nextTokenId() @@ -619,7 +743,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, Tuple6[] memory tokens) public returns (bool) { + // function mintBulkWithTokenURI(address to, Tuple8[] memory tokens) public returns (bool) { // require(false, stub_error); // to; // tokens; @@ -630,11 +754,17 @@ } /// @dev anonymous struct -struct Tuple6 { +struct Tuple8 { uint256 field_0; string field_1; } +/// @dev anonymous struct +struct Tuple6 { + address field_0; + uint256 field_1; +} + /// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension /// @dev See https://eips.ethereum.org/EIPS/eip-721 /// @dev the ERC-165 identifier for this interface is 0x780e9d63 --- a/pallets/refungible/src/erc.rs +++ b/pallets/refungible/src/erc.rs @@ -30,6 +30,7 @@ use pallet_common::{ CollectionHandle, CollectionPropertyPermissions, erc::{CommonEvmHandler, CollectionCall, static_property::key}, + eth::convert_tuple_to_cross_account, }; use pallet_evm::{account::CrossAccountId, PrecompileHandle}; use pallet_evm_coder_substrate::{call, dispatch_to_evm}; @@ -194,7 +195,10 @@ } #[solidity_interface(name = ERC721Metadata)] -impl RefungibleHandle { +impl RefungibleHandle +where + T::AccountId: From<[u8; 32]>, +{ /// @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")] @@ -631,7 +635,10 @@ /// @title Unique extensions for ERC721. #[solidity_interface(name = ERC721UniqueExtensions)] -impl RefungibleHandle { +impl RefungibleHandle +where + T::AccountId: From<[u8; 32]>, +{ /// @notice A descriptive name for a collection of NFTs in this contract fn name(&self) -> Result { Ok(decode_utf16(self.name.iter().copied()) @@ -659,14 +666,44 @@ .recorder .weight_calls_budget(>::find_parent()); - let balance = balance(&self, token, &caller)?; - ensure_single_owner(&self, token, balance)?; + let balance = balance(self, token, &caller)?; + ensure_single_owner(self, token, balance)?; >::transfer(self, &caller, &to, token, balance, &budget) .map_err(dispatch_to_evm::)?; Ok(()) } + /// @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. + /// Throws if RFT pieces have multiple owners. + /// @param to The new owner + /// @param tokenId The RFT to transfer + #[weight(>::transfer_creating_removing())] + fn transfer_from_cross( + &mut self, + caller: caller, + from: (address, uint256), + to: (address, uint256), + token_id: uint256, + ) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + let from = convert_tuple_to_cross_account::(from)?; + let to = convert_tuple_to_cross_account::(to)?; + let token_id = token_id.try_into()?; + let budget = self + .recorder + .weight_calls_budget(>::find_parent()); + + let balance = balance(self, token_id, &from)?; + ensure_single_owner(self, token_id, balance)?; + + Pallet::::transfer_from(self, &caller, &from, &to, token_id, balance, &budget) + .map_err(dispatch_to_evm::)?; + Ok(()) + } + /// @notice Burns a specific ERC721 token. /// @dev Throws unless `msg.sender` is the current owner or an authorized /// operator for this RFT. Throws if `from` is not the current owner. Throws @@ -683,8 +720,37 @@ .recorder .weight_calls_budget(>::find_parent()); - let balance = balance(&self, token, &caller)?; - ensure_single_owner(&self, token, balance)?; + let balance = balance(self, token, &from)?; + ensure_single_owner(self, token, balance)?; + + >::burn_from(self, &caller, &from, token, balance, &budget) + .map_err(dispatch_to_evm::)?; + Ok(()) + } + + /// @notice Burns a specific ERC721 token. + /// @dev Throws unless `msg.sender` is the current owner or an authorized + /// operator for this RFT. Throws if `from` is not the current owner. Throws + /// if `to` is the zero address. Throws if `tokenId` is not a valid RFT. + /// Throws if RFT pieces have multiple owners. + /// @param from The current owner of the RFT + /// @param tokenId The RFT to transfer + #[weight(>::burn_from())] + fn burn_from_cross( + &mut self, + caller: caller, + from: (address, uint256), + token_id: uint256, + ) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + let from = convert_tuple_to_cross_account::(from)?; + let token = token_id.try_into()?; + let budget = self + .recorder + .weight_calls_budget(>::find_parent()); + + let balance = balance(self, token, &from)?; + ensure_single_owner(self, token, balance)?; >::burn_from(self, &caller, &from, token, balance, &budget) .map_err(dispatch_to_evm::)?; --- 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 0x3af103fb +/// @dev the ERC-165 identifier for this interface is 0x25d897dc contract Collection is Dummy, ERC165 { /// Set collection property. /// @@ -145,6 +145,19 @@ dummy = 0; } + /// Set the sponsor of the collection. + /// + /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. + /// + /// @param sponsor Cross account address of the sponsor from whose account funds will be debited for operations with the contract. + /// @dev EVM selector for this function is: 0x84a1d5a8, + /// or in textual repr: setCollectionSponsorCross((address,uint256)) + function setCollectionSponsorCross(Tuple6 memory sponsor) public { + require(false, stub_error); + sponsor; + dummy = 0; + } + /// Whether there is a pending sponsor. /// @dev EVM selector for this function is: 0x058ac185, /// or in textual repr: hasCollectionPendingSponsor() @@ -177,10 +190,10 @@ /// @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw. /// @dev EVM selector for this function is: 0x6ec0a9f1, /// or in textual repr: collectionSponsor() - function collectionSponsor() public view returns (Tuple17 memory) { + function collectionSponsor() public view returns (Tuple6 memory) { require(false, stub_error); dummy; - return Tuple17(0x0000000000000000000000000000000000000000, 0); + return Tuple6(0x0000000000000000000000000000000000000000, 0); } /// Set limits for the collection. @@ -228,6 +241,26 @@ } /// Add collection admin. + /// @param newAdmin Cross account administrator address. + /// @dev EVM selector for this function is: 0x859aa7d6, + /// or in textual repr: addCollectionAdminCross((address,uint256)) + function addCollectionAdminCross(Tuple6 memory newAdmin) public { + require(false, stub_error); + newAdmin; + dummy = 0; + } + + /// Remove collection admin. + /// @param admin Cross account administrator address. + /// @dev EVM selector for this function is: 0x6c0cd173, + /// or in textual repr: removeCollectionAdminCross((address,uint256)) + function removeCollectionAdminCross(Tuple6 memory admin) public { + require(false, stub_error); + admin; + dummy = 0; + } + + /// Add collection admin. /// @param newAdmin Address of the added administrator. /// @dev EVM selector for this function is: 0x92e462c7, /// or in textual repr: addCollectionAdmin(address) @@ -307,6 +340,17 @@ dummy = 0; } + /// Add user to allowed list. + /// + /// @param user User cross account address. + /// @dev EVM selector for this function is: 0xa0184a3a, + /// or in textual repr: addToCollectionAllowListCross((address,uint256)) + function addToCollectionAllowListCross(Tuple6 memory user) public { + require(false, stub_error); + user; + dummy = 0; + } + /// Remove the user from the allowed list. /// /// @param user Address of a removed user. @@ -318,6 +362,17 @@ dummy = 0; } + /// Remove user from allowed list. + /// + /// @param user User cross account address. + /// @dev EVM selector for this function is: 0x09ba452a, + /// or in textual repr: removeFromCollectionAllowListCross((address,uint256)) + function removeFromCollectionAllowListCross(Tuple6 memory user) public { + require(false, stub_error); + user; + dummy = 0; + } + /// Switch permission for minting. /// /// @param mode Enable if "true". @@ -342,6 +397,19 @@ return false; } + /// Check that account is the owner or admin of the collection + /// + /// @param user User cross account to verify + /// @return "true" if account is the owner or admin + /// @dev EVM selector for this function is: 0x3e75a905, + /// or in textual repr: isOwnerOrAdminCross((address,uint256)) + function isOwnerOrAdminCross(Tuple6 memory user) public view returns (bool) { + require(false, stub_error); + user; + dummy; + return false; + } + /// Returns collection type /// /// @return `Fungible` or `NFT` or `ReFungible` @@ -355,14 +423,14 @@ /// Get collection owner. /// - /// @return Tuple with sponsor address and his substrate mirror. + /// @return Tuble with sponsor address and his substrate mirror. /// If address is canonical then substrate mirror is zero and vice versa. /// @dev EVM selector for this function is: 0xdf727d3b, /// or in textual repr: collectionOwner() - function collectionOwner() public view returns (Tuple17 memory) { + function collectionOwner() public view returns (Tuple6 memory) { require(false, stub_error); dummy; - return Tuple17(0x0000000000000000000000000000000000000000, 0); + return Tuple6(0x0000000000000000000000000000000000000000, 0); } /// Changes collection owner to another account @@ -383,17 +451,23 @@ /// If address is canonical then substrate mirror is zero and vice versa. /// @dev EVM selector for this function is: 0x5813216b, /// or in textual repr: collectionAdmins() - function collectionAdmins() public view returns (Tuple17[] memory) { + function collectionAdmins() public view returns (Tuple6[] memory) { require(false, stub_error); dummy; - return new Tuple17[](0); + return new Tuple6[](0); } -} -/// @dev anonymous struct -struct Tuple17 { - address field_0; - uint256 field_1; + /// Changes collection owner to another account + /// + /// @dev Owner can be changed only by current owner + /// @param newOwner new owner cross account + /// @dev EVM selector for this function is: 0xe5c9913f, + /// or in textual repr: setOwnerCross((address,uint256)) + function setOwnerCross(Tuple6 memory newOwner) public { + require(false, stub_error); + newOwner; + dummy = 0; + } } /// @dev the ERC-165 identifier for this interface is 0x5b5e139f @@ -538,7 +612,7 @@ } /// @title Unique extensions for ERC721. -/// @dev the ERC-165 identifier for this interface is 0xef1eaacb +/// @dev the ERC-165 identifier for this interface is 0x81feb398 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, @@ -573,6 +647,26 @@ dummy = 0; } + /// @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. + /// Throws if RFT pieces have multiple owners. + /// @param to The new owner + /// @param tokenId The RFT to transfer + /// @dev EVM selector for this function is: 0xd5cf430b, + /// or in textual repr: transferFromCross((address,uint256),(address,uint256),uint256) + function transferFromCross( + Tuple6 memory from, + Tuple6 memory to, + uint256 tokenId + ) public { + require(false, stub_error); + from; + to; + tokenId; + dummy = 0; + } + /// @notice Burns a specific ERC721 token. /// @dev Throws unless `msg.sender` is the current owner or an authorized /// operator for this RFT. Throws if `from` is not the current owner. Throws @@ -589,6 +683,22 @@ dummy = 0; } + /// @notice Burns a specific ERC721 token. + /// @dev Throws unless `msg.sender` is the current owner or an authorized + /// operator for this RFT. Throws if `from` is not the current owner. Throws + /// if `to` is the zero address. Throws if `tokenId` is not a valid RFT. + /// Throws if RFT pieces have multiple owners. + /// @param from The current owner of the RFT + /// @param tokenId The RFT to transfer + /// @dev EVM selector for this function is: 0xbb2f5a58, + /// or in textual repr: burnFromCross((address,uint256),uint256) + function burnFromCross(Tuple6 memory from, uint256 tokenId) public { + require(false, stub_error); + from; + tokenId; + dummy = 0; + } + /// @notice Returns next free RFT ID. /// @dev EVM selector for this function is: 0x75794a3c, /// or in textual repr: nextTokenId() @@ -620,7 +730,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, Tuple6[] memory tokens) public returns (bool) { + // function mintBulkWithTokenURI(address to, Tuple8[] memory tokens) public returns (bool) { // require(false, stub_error); // to; // tokens; @@ -642,11 +752,17 @@ } /// @dev anonymous struct -struct Tuple6 { +struct Tuple8 { uint256 field_0; string field_1; } +/// @dev anonymous struct +struct Tuple6 { + address field_0; + uint256 field_1; +} + /// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension /// @dev See https://eips.ethereum.org/EIPS/eip-721 /// @dev the ERC-165 identifier for this interface is 0x780e9d63 --- a/tests/src/eth/allowlist.test.ts +++ b/tests/src/eth/allowlist.test.ts @@ -89,22 +89,22 @@ expect(await collectionEvm.methods.allowed(user).call({from: owner})).to.be.false; }); - // TODO: Temprorary off. Need refactor - // itEth('Collection allowlist can be added and removed by [sub] address', async ({helper}) => { - // const owner = await helper.eth.createAccountWithBalance(donor); - // const user = donor; - - // 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; - // await collectionEvm.methods.addToCollectionAllowListSubstrate(user.addressRaw).send({from: owner}); - // expect(await helper.collection.allowed(collectionId, {Substrate: user.address})).to.be.true; + //itEth('Collection allowlist can be added and removed by [cross] address', async ({helper}) => { + // const owner = await helper.eth.createAccountWithBalance(donor); + // const user = donor; + // + // const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C'); + // const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); + // const userCross = helper.ethCrossAccount.fromKeyringPair(user); + // + // expect(await helper.collection.allowed(collectionId, {Substrate: user.address})).to.be.false; + // await collectionEvm.methods.addToCollectionAllowListCross(userCross).send({from: owner}); + // expect(await helper.collection.allowed(collectionId, {Substrate: user.address})).to.be.true; + // + // await collectionEvm.methods.removeFromCollectionAllowListCross(userCross).send({from: owner}); + // expect(await helper.collection.allowed(collectionId, {Substrate: user.address})).to.be.false; + //}); - // await collectionEvm.methods.removeFromCollectionAllowListSubstrate(user.addressRaw).send({from: owner}); - // expect(await helper.collection.allowed(collectionId, {Substrate: user.address})).to.be.false; - // }); - itEth('Collection allowlist can not be add and remove [eth] address by not owner', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); const notOwner = await helper.eth.createAccountWithBalance(donor); @@ -117,28 +117,27 @@ await expect(collectionEvm.methods.addToCollectionAllowList(user).call({from: notOwner})).to.be.rejectedWith('NoPermission'); expect(await collectionEvm.methods.allowed(user).call({from: owner})).to.be.false; await collectionEvm.methods.addToCollectionAllowList(user).send({from: owner}); - expect(await collectionEvm.methods.allowed(user).call({from: owner})).to.be.true; await expect(collectionEvm.methods.removeFromCollectionAllowList(user).call({from: notOwner})).to.be.rejectedWith('NoPermission'); expect(await collectionEvm.methods.allowed(user).call({from: owner})).to.be.true; }); - // TODO: Temprorary off. Need refactor - // itEth('Collection allowlist can not be add and remove [sub] address by not owner', async ({helper}) => { - // const owner = await helper.eth.createAccountWithBalance(donor); - // const notOwner = await helper.eth.createAccountWithBalance(donor); - // const user = donor; - - // 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; - // await expect(collectionEvm.methods.addToCollectionAllowListSubstrate(user.addressRaw).call({from: notOwner})).to.be.rejectedWith('NoPermission'); - // expect(await helper.collection.allowed(collectionId, {Substrate: user.address})).to.be.false; - // await collectionEvm.methods.addToCollectionAllowListSubstrate(user.addressRaw).send({from: owner}); - - // expect(await helper.collection.allowed(collectionId, {Substrate: user.address})).to.be.true; - // await expect(collectionEvm.methods.removeFromCollectionAllowListSubstrate(user.addressRaw).call({from: notOwner})).to.be.rejectedWith('NoPermission'); - // expect(await helper.collection.allowed(collectionId, {Substrate: user.address})).to.be.true; - // }); + //itEth('Collection allowlist can not be add and remove [cross] address by not owner', async ({helper}) => { + // const owner = await helper.eth.createAccountWithBalance(donor); + // const notOwner = await helper.eth.createAccountWithBalance(donor); + // const user = donor; + // + // 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; + // const userCross = helper.ethCrossAccount.fromKeyringPair(user); + // await expect(collectionEvm.methods.addToCollectionAllowListCross(userCross).call({from: notOwner})).to.be.rejectedWith('NoPermission'); + // expect(await helper.collection.allowed(collectionId, {Substrate: user.address})).to.be.false; + // await collectionEvm.methods.addToCollectionAllowListCross(userCross).send({from: owner}); + // + // expect(await helper.collection.allowed(collectionId, {Substrate: user.address})).to.be.true; + // await expect(collectionEvm.methods.removeFromCollectionAllowListCross(userCross).call({from: notOwner})).to.be.rejectedWith('NoPermission'); + // expect(await helper.collection.allowed(collectionId, {Substrate: user.address})).to.be.true; + //}); }); --- 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 0x3af103fb +/// @dev the ERC-165 identifier for this interface is 0x25d897dc interface Collection is Dummy, ERC165 { /// Set collection property. /// @@ -49,6 +49,15 @@ /// or in textual repr: setCollectionSponsor(address) function setCollectionSponsor(address sponsor) external; + /// Set the sponsor of the collection. + /// + /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. + /// + /// @param sponsor Cross account address of the sponsor from whose account funds will be debited for operations with the contract. + /// @dev EVM selector for this function is: 0x84a1d5a8, + /// or in textual repr: setCollectionSponsorCross((address,uint256)) + function setCollectionSponsorCross(Tuple6 memory sponsor) external; + /// Whether there is a pending sponsor. /// @dev EVM selector for this function is: 0x058ac185, /// or in textual repr: hasCollectionPendingSponsor() @@ -104,6 +113,18 @@ function contractAddress() external view returns (address); /// Add collection admin. + /// @param newAdmin Cross account administrator address. + /// @dev EVM selector for this function is: 0x859aa7d6, + /// or in textual repr: addCollectionAdminCross((address,uint256)) + function addCollectionAdminCross(Tuple6 memory newAdmin) external; + + /// Remove collection admin. + /// @param admin Cross account administrator address. + /// @dev EVM selector for this function is: 0x6c0cd173, + /// or in textual repr: removeCollectionAdminCross((address,uint256)) + function removeCollectionAdminCross(Tuple6 memory admin) external; + + /// Add collection admin. /// @param newAdmin Address of the added administrator. /// @dev EVM selector for this function is: 0x92e462c7, /// or in textual repr: addCollectionAdmin(address) @@ -153,6 +174,13 @@ /// or in textual repr: addToCollectionAllowList(address) function addToCollectionAllowList(address user) external; + /// Add user to allowed list. + /// + /// @param user User cross account address. + /// @dev EVM selector for this function is: 0xa0184a3a, + /// or in textual repr: addToCollectionAllowListCross((address,uint256)) + function addToCollectionAllowListCross(Tuple6 memory user) external; + /// Remove the user from the allowed list. /// /// @param user Address of a removed user. @@ -160,6 +188,13 @@ /// or in textual repr: removeFromCollectionAllowList(address) function removeFromCollectionAllowList(address user) external; + /// Remove user from allowed list. + /// + /// @param user User cross account address. + /// @dev EVM selector for this function is: 0x09ba452a, + /// or in textual repr: removeFromCollectionAllowListCross((address,uint256)) + function removeFromCollectionAllowListCross(Tuple6 memory user) external; + /// Switch permission for minting. /// /// @param mode Enable if "true". @@ -175,6 +210,14 @@ /// or in textual repr: isOwnerOrAdmin(address) function isOwnerOrAdmin(address user) external view returns (bool); + /// Check that account is the owner or admin of the collection + /// + /// @param user User cross account to verify + /// @return "true" if account is the owner or admin + /// @dev EVM selector for this function is: 0x3e75a905, + /// or in textual repr: isOwnerOrAdminCross((address,uint256)) + function isOwnerOrAdminCross(Tuple6 memory user) external view returns (bool); + /// Returns collection type /// /// @return `Fungible` or `NFT` or `ReFungible` @@ -184,7 +227,7 @@ /// Get collection owner. /// - /// @return Tuple with sponsor address and his substrate mirror. + /// @return Tuble with sponsor address and his substrate mirror. /// If address is canonical then substrate mirror is zero and vice versa. /// @dev EVM selector for this function is: 0xdf727d3b, /// or in textual repr: collectionOwner() @@ -205,10 +248,22 @@ /// @dev EVM selector for this function is: 0x5813216b, /// or in textual repr: collectionAdmins() function collectionAdmins() external view returns (Tuple6[] memory); + + /// Changes collection owner to another account + /// + /// @dev Owner can be changed only by current owner + /// @param newOwner new owner cross account + /// @dev EVM selector for this function is: 0xe5c9913f, + /// or in textual repr: setOwnerCross((address,uint256)) + function setOwnerCross(Tuple6 memory newOwner) external; } -/// @dev the ERC-165 identifier for this interface is 0x63034ac5 +/// @dev the ERC-165 identifier for this interface is 0x032e5926 interface ERC20UniqueExtensions is Dummy, ERC165 { + /// @dev EVM selector for this function is: 0x0ecd0ab0, + /// or in textual repr: approveCross((address,uint256),uint256) + function approveCross(Tuple6 memory spender, uint256 amount) external returns (bool); + /// Burn tokens from account /// @dev Function that burns an `amount` of the tokens of a given account, /// deducting from the sender's allowance for said account. @@ -218,11 +273,28 @@ /// or in textual repr: burnFrom(address,uint256) function burnFrom(address from, uint256 amount) external returns (bool); + /// Burn tokens from account + /// @dev Function that burns an `amount` of the tokens of a given account, + /// deducting from the sender's allowance for said account. + /// @param from The account whose tokens will be burnt. + /// @param amount The amount that will be burnt. + /// @dev EVM selector for this function is: 0xbb2f5a58, + /// or in textual repr: burnFromCross((address,uint256),uint256) + function burnFromCross(Tuple6 memory from, uint256 amount) external returns (bool); + /// Mint tokens for multiple accounts. /// @param amounts array of pairs of account address and amount /// @dev EVM selector for this function is: 0x1acf2d55, /// or in textual repr: mintBulk((address,uint256)[]) function mintBulk(Tuple6[] memory amounts) external returns (bool); + + /// @dev EVM selector for this function is: 0xd5cf430b, + /// or in textual repr: transferFromCross((address,uint256),(address,uint256),uint256) + function transferFromCross( + Tuple6 memory from, + Tuple6 memory to, + uint256 amount + ) external returns (bool); } /// @dev anonymous struct --- 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 0x3af103fb +/// @dev the ERC-165 identifier for this interface is 0x25d897dc interface Collection is Dummy, ERC165 { /// Set collection property. /// @@ -98,6 +98,15 @@ /// or in textual repr: setCollectionSponsor(address) function setCollectionSponsor(address sponsor) external; + /// Set the sponsor of the collection. + /// + /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. + /// + /// @param sponsor Cross account address of the sponsor from whose account funds will be debited for operations with the contract. + /// @dev EVM selector for this function is: 0x84a1d5a8, + /// or in textual repr: setCollectionSponsorCross((address,uint256)) + function setCollectionSponsorCross(Tuple6 memory sponsor) external; + /// Whether there is a pending sponsor. /// @dev EVM selector for this function is: 0x058ac185, /// or in textual repr: hasCollectionPendingSponsor() @@ -120,7 +129,7 @@ /// @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw. /// @dev EVM selector for this function is: 0x6ec0a9f1, /// or in textual repr: collectionSponsor() - function collectionSponsor() external view returns (Tuple17 memory); + function collectionSponsor() external view returns (Tuple6 memory); /// Set limits for the collection. /// @dev Throws error if limit not found. @@ -153,6 +162,18 @@ function contractAddress() external view returns (address); /// Add collection admin. + /// @param newAdmin Cross account administrator address. + /// @dev EVM selector for this function is: 0x859aa7d6, + /// or in textual repr: addCollectionAdminCross((address,uint256)) + function addCollectionAdminCross(Tuple6 memory newAdmin) external; + + /// Remove collection admin. + /// @param admin Cross account administrator address. + /// @dev EVM selector for this function is: 0x6c0cd173, + /// or in textual repr: removeCollectionAdminCross((address,uint256)) + function removeCollectionAdminCross(Tuple6 memory admin) external; + + /// Add collection admin. /// @param newAdmin Address of the added administrator. /// @dev EVM selector for this function is: 0x92e462c7, /// or in textual repr: addCollectionAdmin(address) @@ -202,6 +223,13 @@ /// or in textual repr: addToCollectionAllowList(address) function addToCollectionAllowList(address user) external; + /// Add user to allowed list. + /// + /// @param user User cross account address. + /// @dev EVM selector for this function is: 0xa0184a3a, + /// or in textual repr: addToCollectionAllowListCross((address,uint256)) + function addToCollectionAllowListCross(Tuple6 memory user) external; + /// Remove the user from the allowed list. /// /// @param user Address of a removed user. @@ -209,6 +237,13 @@ /// or in textual repr: removeFromCollectionAllowList(address) function removeFromCollectionAllowList(address user) external; + /// Remove user from allowed list. + /// + /// @param user User cross account address. + /// @dev EVM selector for this function is: 0x09ba452a, + /// or in textual repr: removeFromCollectionAllowListCross((address,uint256)) + function removeFromCollectionAllowListCross(Tuple6 memory user) external; + /// Switch permission for minting. /// /// @param mode Enable if "true". @@ -224,6 +259,14 @@ /// or in textual repr: isOwnerOrAdmin(address) function isOwnerOrAdmin(address user) external view returns (bool); + /// Check that account is the owner or admin of the collection + /// + /// @param user User cross account to verify + /// @return "true" if account is the owner or admin + /// @dev EVM selector for this function is: 0x3e75a905, + /// or in textual repr: isOwnerOrAdminCross((address,uint256)) + function isOwnerOrAdminCross(Tuple6 memory user) external view returns (bool); + /// Returns collection type /// /// @return `Fungible` or `NFT` or `ReFungible` @@ -233,11 +276,11 @@ /// Get collection owner. /// - /// @return Tuple with sponsor address and his substrate mirror. + /// @return Tuble with sponsor address and his substrate mirror. /// If address is canonical then substrate mirror is zero and vice versa. /// @dev EVM selector for this function is: 0xdf727d3b, /// or in textual repr: collectionOwner() - function collectionOwner() external view returns (Tuple17 memory); + function collectionOwner() external view returns (Tuple6 memory); /// Changes collection owner to another account /// @@ -253,13 +296,15 @@ /// If address is canonical then substrate mirror is zero and vice versa. /// @dev EVM selector for this function is: 0x5813216b, /// or in textual repr: collectionAdmins() - function collectionAdmins() external view returns (Tuple17[] memory); -} + function collectionAdmins() external view returns (Tuple6[] memory); -/// @dev anonymous struct -struct Tuple17 { - address field_0; - uint256 field_1; + /// Changes collection owner to another account + /// + /// @dev Owner can be changed only by current owner + /// @param newOwner new owner cross account + /// @dev EVM selector for this function is: 0xe5c9913f, + /// or in textual repr: setOwnerCross((address,uint256)) + function setOwnerCross(Tuple6 memory newOwner) external; } /// @title ERC-721 Non-Fungible Token Standard, optional metadata extension @@ -357,7 +402,7 @@ } /// @title Unique extensions for ERC721. -/// @dev the ERC-165 identifier for this interface is 0x4468500d +/// @dev the ERC-165 identifier for this interface is 0x244543ee 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, @@ -369,6 +414,16 @@ /// or in textual repr: symbol() function symbol() external view returns (string memory); + /// @notice Set or reaffirm the approved address for an NFT + /// @dev The zero address indicates there is no approved address. + /// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized + /// operator of the current owner. + /// @param approved The new substrate address approved NFT controller + /// @param tokenId The NFT to approve + /// @dev EVM selector for this function is: 0x0ecd0ab0, + /// or in textual repr: approveCross((address,uint256),uint256) + function approveCross(Tuple6 memory approved, uint256 tokenId) external; + /// @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. @@ -378,6 +433,20 @@ /// or in textual repr: transfer(address,uint256) function transfer(address to, uint256 tokenId) external; + /// @notice Transfer ownership of an NFT from cross account address to cross account address + /// @dev Throws unless `msg.sender` is the current owner. Throws if `to` + /// is the zero address. Throws if `tokenId` is not a valid NFT. + /// @param from Cross acccount address of current owner + /// @param to Cross acccount address of new owner + /// @param tokenId The NFT to transfer + /// @dev EVM selector for this function is: 0xd5cf430b, + /// or in textual repr: transferFromCross((address,uint256),(address,uint256),uint256) + function transferFromCross( + Tuple6 memory from, + Tuple6 memory to, + uint256 tokenId + ) external; + /// @notice Burns a specific ERC721 token. /// @dev Throws unless `msg.sender` is the current owner or an authorized /// operator for this NFT. Throws if `from` is not the current owner. Throws @@ -388,6 +457,16 @@ /// or in textual repr: burnFrom(address,uint256) function burnFrom(address from, uint256 tokenId) external; + /// @notice Burns a specific ERC721 token. + /// @dev Throws unless `msg.sender` is the current owner or an authorized + /// operator for this NFT. Throws if `from` is not the current owner. Throws + /// if `to` is the zero address. Throws if `tokenId` is not a valid NFT. + /// @param from The current owner of the NFT + /// @param tokenId The NFT to transfer + /// @dev EVM selector for this function is: 0xbb2f5a58, + /// or in textual repr: burnFromCross((address,uint256),uint256) + function burnFromCross(Tuple6 memory from, uint256 tokenId) external; + /// @notice Returns next free NFT ID. /// @dev EVM selector for this function is: 0x75794a3c, /// or in textual repr: nextTokenId() @@ -408,16 +487,22 @@ // /// @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, Tuple6[] memory tokens) external returns (bool); + // function mintBulkWithTokenURI(address to, Tuple8[] memory tokens) external returns (bool); } /// @dev anonymous struct -struct Tuple6 { +struct Tuple8 { uint256 field_0; string field_1; } +/// @dev anonymous struct +struct Tuple6 { + address field_0; + uint256 field_1; +} + /// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension /// @dev See https://eips.ethereum.org/EIPS/eip-721 /// @dev the ERC-165 identifier for this interface is 0x780e9d63 --- 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 0x3af103fb +/// @dev the ERC-165 identifier for this interface is 0x25d897dc interface Collection is Dummy, ERC165 { /// Set collection property. /// @@ -98,6 +98,15 @@ /// or in textual repr: setCollectionSponsor(address) function setCollectionSponsor(address sponsor) external; + /// Set the sponsor of the collection. + /// + /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. + /// + /// @param sponsor Cross account address of the sponsor from whose account funds will be debited for operations with the contract. + /// @dev EVM selector for this function is: 0x84a1d5a8, + /// or in textual repr: setCollectionSponsorCross((address,uint256)) + function setCollectionSponsorCross(Tuple6 memory sponsor) external; + /// Whether there is a pending sponsor. /// @dev EVM selector for this function is: 0x058ac185, /// or in textual repr: hasCollectionPendingSponsor() @@ -120,7 +129,7 @@ /// @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw. /// @dev EVM selector for this function is: 0x6ec0a9f1, /// or in textual repr: collectionSponsor() - function collectionSponsor() external view returns (Tuple17 memory); + function collectionSponsor() external view returns (Tuple6 memory); /// Set limits for the collection. /// @dev Throws error if limit not found. @@ -153,6 +162,18 @@ function contractAddress() external view returns (address); /// Add collection admin. + /// @param newAdmin Cross account administrator address. + /// @dev EVM selector for this function is: 0x859aa7d6, + /// or in textual repr: addCollectionAdminCross((address,uint256)) + function addCollectionAdminCross(Tuple6 memory newAdmin) external; + + /// Remove collection admin. + /// @param admin Cross account administrator address. + /// @dev EVM selector for this function is: 0x6c0cd173, + /// or in textual repr: removeCollectionAdminCross((address,uint256)) + function removeCollectionAdminCross(Tuple6 memory admin) external; + + /// Add collection admin. /// @param newAdmin Address of the added administrator. /// @dev EVM selector for this function is: 0x92e462c7, /// or in textual repr: addCollectionAdmin(address) @@ -202,6 +223,13 @@ /// or in textual repr: addToCollectionAllowList(address) function addToCollectionAllowList(address user) external; + /// Add user to allowed list. + /// + /// @param user User cross account address. + /// @dev EVM selector for this function is: 0xa0184a3a, + /// or in textual repr: addToCollectionAllowListCross((address,uint256)) + function addToCollectionAllowListCross(Tuple6 memory user) external; + /// Remove the user from the allowed list. /// /// @param user Address of a removed user. @@ -209,6 +237,13 @@ /// or in textual repr: removeFromCollectionAllowList(address) function removeFromCollectionAllowList(address user) external; + /// Remove user from allowed list. + /// + /// @param user User cross account address. + /// @dev EVM selector for this function is: 0x09ba452a, + /// or in textual repr: removeFromCollectionAllowListCross((address,uint256)) + function removeFromCollectionAllowListCross(Tuple6 memory user) external; + /// Switch permission for minting. /// /// @param mode Enable if "true". @@ -224,6 +259,14 @@ /// or in textual repr: isOwnerOrAdmin(address) function isOwnerOrAdmin(address user) external view returns (bool); + /// Check that account is the owner or admin of the collection + /// + /// @param user User cross account to verify + /// @return "true" if account is the owner or admin + /// @dev EVM selector for this function is: 0x3e75a905, + /// or in textual repr: isOwnerOrAdminCross((address,uint256)) + function isOwnerOrAdminCross(Tuple6 memory user) external view returns (bool); + /// Returns collection type /// /// @return `Fungible` or `NFT` or `ReFungible` @@ -233,11 +276,11 @@ /// Get collection owner. /// - /// @return Tuple with sponsor address and his substrate mirror. + /// @return Tuble with sponsor address and his substrate mirror. /// If address is canonical then substrate mirror is zero and vice versa. /// @dev EVM selector for this function is: 0xdf727d3b, /// or in textual repr: collectionOwner() - function collectionOwner() external view returns (Tuple17 memory); + function collectionOwner() external view returns (Tuple6 memory); /// Changes collection owner to another account /// @@ -253,13 +296,15 @@ /// If address is canonical then substrate mirror is zero and vice versa. /// @dev EVM selector for this function is: 0x5813216b, /// or in textual repr: collectionAdmins() - function collectionAdmins() external view returns (Tuple17[] memory); -} + function collectionAdmins() external view returns (Tuple6[] memory); -/// @dev anonymous struct -struct Tuple17 { - address field_0; - uint256 field_1; + /// Changes collection owner to another account + /// + /// @dev Owner can be changed only by current owner + /// @param newOwner new owner cross account + /// @dev EVM selector for this function is: 0xe5c9913f, + /// or in textual repr: setOwnerCross((address,uint256)) + function setOwnerCross(Tuple6 memory newOwner) external; } /// @dev the ERC-165 identifier for this interface is 0x5b5e139f @@ -355,7 +400,7 @@ } /// @title Unique extensions for ERC721. -/// @dev the ERC-165 identifier for this interface is 0xef1eaacb +/// @dev the ERC-165 identifier for this interface is 0x81feb398 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, @@ -377,6 +422,20 @@ /// or in textual repr: transfer(address,uint256) function transfer(address to, uint256 tokenId) external; + /// @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. + /// Throws if RFT pieces have multiple owners. + /// @param to The new owner + /// @param tokenId The RFT to transfer + /// @dev EVM selector for this function is: 0xd5cf430b, + /// or in textual repr: transferFromCross((address,uint256),(address,uint256),uint256) + function transferFromCross( + Tuple6 memory from, + Tuple6 memory to, + uint256 tokenId + ) external; + /// @notice Burns a specific ERC721 token. /// @dev Throws unless `msg.sender` is the current owner or an authorized /// operator for this RFT. Throws if `from` is not the current owner. Throws @@ -388,6 +447,17 @@ /// or in textual repr: burnFrom(address,uint256) function burnFrom(address from, uint256 tokenId) external; + /// @notice Burns a specific ERC721 token. + /// @dev Throws unless `msg.sender` is the current owner or an authorized + /// operator for this RFT. Throws if `from` is not the current owner. Throws + /// if `to` is the zero address. Throws if `tokenId` is not a valid RFT. + /// Throws if RFT pieces have multiple owners. + /// @param from The current owner of the RFT + /// @param tokenId The RFT to transfer + /// @dev EVM selector for this function is: 0xbb2f5a58, + /// or in textual repr: burnFromCross((address,uint256),uint256) + function burnFromCross(Tuple6 memory from, uint256 tokenId) external; + /// @notice Returns next free RFT ID. /// @dev EVM selector for this function is: 0x75794a3c, /// or in textual repr: nextTokenId() @@ -409,7 +479,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, Tuple6[] memory tokens) external returns (bool); + // function mintBulkWithTokenURI(address to, Tuple8[] memory tokens) external returns (bool); /// Returns EVM address for refungible token /// @@ -420,11 +490,17 @@ } /// @dev anonymous struct -struct Tuple6 { +struct Tuple8 { uint256 field_0; string field_1; } +/// @dev anonymous struct +struct Tuple6 { + address field_0; + uint256 field_1; +} + /// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension /// @dev See https://eips.ethereum.org/EIPS/eip-721 /// @dev the ERC-165 identifier for this interface is 0x780e9d63 --- a/tests/src/eth/base.test.ts +++ b/tests/src/eth/base.test.ts @@ -116,8 +116,8 @@ await checkInterface(helper, '0x780e9d63', true, true); }); - itEth('ERC721UniqueExtensions - 0x4468500d - support', async ({helper}) => { - await checkInterface(helper, '0x4468500d', true, true); + itEth('ERC721UniqueExtensions - 0x244543ee - support', async ({helper}) => { + await checkInterface(helper, '0x244543ee', true, true); }); itEth('ERC721Burnable - 0x42966c68 - support', async ({helper}) => { --- a/tests/src/eth/collectionAdmin.test.ts +++ b/tests/src/eth/collectionAdmin.test.ts @@ -50,17 +50,18 @@ .to.be.eq(newAdmin.toLocaleLowerCase()); }); - itEth.skip('Add substrate admin by owner', async ({helper}) => { + itEth('Add cross account admin by owner', async ({helper, privateKey}) => { const owner = await helper.eth.createAccountWithBalance(donor); + 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); - await collectionEvm.methods.addCollectionAdminSubstrate(newAdmin.addressRaw).send(); + + const newAdmin = await privateKey('//Bob'); + const newAdminCross = helper.ethCrossAccount.fromKeyringPair(newAdmin); + await collectionEvm.methods.addCollectionAdminCross(newAdminCross).send(); - const adminList = await helper.callRpc('api.rpc.unique.adminlist', [collectionId]); - expect(adminList[0].asSubstrate.toString().toLocaleLowerCase()) - .to.be.eq(newAdmin.address.toLocaleLowerCase()); + const adminList = await helper.collection.getAdmins(collectionId); + expect(adminList).to.be.like([{Substrate: newAdmin.address}]); }); itEth('Verify owner or admin', async ({helper}) => { --- a/tests/src/eth/fractionalizer/Fractionalizer.sol +++ b/tests/src/eth/fractionalizer/Fractionalizer.sol @@ -84,7 +84,11 @@ ) external payable onlyOwner { require(rftCollection == address(0), "RFT collection is already set"); address collectionHelpers = 0x6C4E9fE1AE37a41E93CEE429e8E1881aBdcbb54F; - rftCollection = CollectionHelpers(collectionHelpers).createRFTCollection{value: msg.value}(_name, _description, _tokenPrefix); + rftCollection = CollectionHelpers(collectionHelpers).createRFTCollection{value: msg.value}( + _name, + _description, + _tokenPrefix + ); emit RFTCollectionSet(rftCollection); } --- a/tests/src/eth/fungible.test.ts +++ b/tests/src/eth/fungible.test.ts @@ -148,6 +148,49 @@ } }); + itEth('Can perform burnFromCross()', async ({helper, privateKey}) => { + const alice = await privateKey('//Alice'); + const sender = await helper.eth.createAccountWithBalance(alice, 100n); + + const collection = await helper.ft.mintCollection(alice, {name: 'A', description: 'B', tokenPrefix: 'C'}, 0); + + await collection.mint(alice, 200n, {Substrate: alice.address}); + await collection.approveTokens(alice, {Ethereum: sender}, 100n); + + const address = helper.ethAddress.fromCollectionId(collection.collectionId); + const contract = helper.ethNativeContract.collection(address, 'ft'); + + const fromBalanceBefore = await collection.getBalance({Substrate: alice.address}); + + const ownerCross = helper.ethCrossAccount.fromKeyringPair(alice); + const result = await contract.methods.burnFromCross(ownerCross, 49).send({from: sender}); + const events = result.events; + + expect(events).to.be.like({ + Transfer: { + address: helper.ethAddress.fromCollectionId(collection.collectionId), + event: 'Transfer', + returnValues: { + from: helper.address.substrateToEth(alice.address), + to: '0x0000000000000000000000000000000000000000', + value: '49', + }, + }, + Approval: { + address: helper.ethAddress.fromCollectionId(collection.collectionId), + returnValues: { + owner: helper.address.substrateToEth(alice.address), + spender: sender, + value: '51', + }, + event: 'Approval', + }, + }); + + const fromBalanceAfter = await collection.getBalance({Substrate: alice.address}); + expect(fromBalanceBefore - fromBalanceAfter).to.be.eq(49n); + }); + itEth('Can perform transferFrom()', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); const spender = await helper.eth.createAccountWithBalance(donor); @@ -216,6 +259,54 @@ expect(+balance).to.equal(50); } }); + + itEth('Can perform transferFromCross()', async ({helper, privateKey}) => { + const alice = await privateKey('//Alice'); + const sender = await helper.eth.createAccountWithBalance(alice, 100n); + + const collection = await helper.ft.mintCollection(alice, {name: 'A', description: 'B', tokenPrefix: 'C'}, 0); + + const receiver = helper.eth.createAccount(); + + await collection.mint(alice, 200n, {Substrate: alice.address}); + await collection.approveTokens(alice, {Ethereum: sender}, 100n); + + const address = helper.ethAddress.fromCollectionId(collection.collectionId); + const contract = helper.ethNativeContract.collection(address, 'ft'); + + const from = helper.ethCrossAccount.fromKeyringPair(alice); + const to = helper.ethCrossAccount.fromAddress(receiver); + + const fromBalanceBefore = await collection.getBalance({Substrate: alice.address}); + const toBalanceBefore = await collection.getBalance({Ethereum: receiver}); + + const result = await contract.methods.transferFromCross(from, to, 51).send({from: sender}); + + expect(result.events).to.be.like({ + Transfer: { + address, + event: 'Transfer', + returnValues: { + from: helper.address.substrateToEth(alice.address), + to: receiver, + value: '51', + }, + }, + Approval: { + address, + event: 'Approval', + returnValues: { + owner: helper.address.substrateToEth(alice.address), + spender: sender, + value: '49', + }, + }}); + + const fromBalanceAfter = await collection.getBalance({Substrate: alice.address}); + expect(fromBalanceBefore - fromBalanceAfter).to.be.eq(51n); + const toBalanceAfter = await collection.getBalance({Ethereum: receiver}); + expect(toBalanceAfter - toBalanceBefore).to.be.eq(51n); + }); }); describe('Fungible: Fees', () => { @@ -362,4 +453,44 @@ expect(event.returnValues.to).to.be.equal(receiver); expect(event.returnValues.value).to.be.equal('51'); }); + + itEth('Events emitted for transferFromCross()', async ({helper, privateKey}) => { + const alice = await privateKey('//Alice'); + const sender = await helper.eth.createAccountWithBalance(alice, 100n); + + const collection = await helper.ft.mintCollection(alice, {name: 'A', description: 'B', tokenPrefix: 'C'}, 0); + + const receiver = helper.eth.createAccount(); + + await collection.mint(alice, 200n, {Substrate: alice.address}); + await collection.approveTokens(alice, {Ethereum: sender}, 100n); + + const address = helper.ethAddress.fromCollectionId(collection.collectionId); + const contract = helper.ethNativeContract.collection(address, 'ft'); + + const from = helper.ethCrossAccount.fromKeyringPair(alice); + const to = helper.ethCrossAccount.fromAddress(receiver); + + const result = await contract.methods.transferFromCross(from, to, 51).send({from: sender}); + + expect(result.events).to.be.like({ + Transfer: { + address, + event: 'Transfer', + returnValues: { + from: helper.address.substrateToEth(alice.address), + to: receiver, + value: '51', + }, + }, + Approval: { + address, + event: 'Approval', + returnValues: { + owner: helper.address.substrateToEth(alice.address), + spender: sender, + value: '49', + }, + }}); + }); }); --- a/tests/src/eth/fungibleAbi.json +++ b/tests/src/eth/fungibleAbi.json @@ -60,6 +60,23 @@ }, { "inputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6", + "name": "newAdmin", + "type": "tuple" + } + ], + "name": "addCollectionAdminCross", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ { "internalType": "address", "name": "user", "type": "address" } ], "name": "addToCollectionAllowList", @@ -69,6 +86,23 @@ }, { "inputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6", + "name": "user", + "type": "tuple" + } + ], + "name": "addToCollectionAllowListCross", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ { "internalType": "address", "name": "owner", "type": "address" }, { "internalType": "address", "name": "spender", "type": "address" } ], @@ -98,6 +132,24 @@ }, { "inputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6", + "name": "spender", + "type": "tuple" + }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "approveCross", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ { "internalType": "address", "name": "owner", "type": "address" } ], "name": "balanceOf", @@ -117,6 +169,24 @@ }, { "inputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6", + "name": "from", + "type": "tuple" + }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "burnFromCross", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ { "internalType": "address", "name": "newOwner", "type": "address" } ], "name": "changeCollectionOwner", @@ -228,6 +298,23 @@ }, { "inputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6", + "name": "user", + "type": "tuple" + } + ], + "name": "isOwnerOrAdminCross", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ { "internalType": "address", "name": "to", "type": "address" }, { "internalType": "uint256", "name": "amount", "type": "uint256" } ], @@ -270,6 +357,23 @@ "type": "function" }, { + "inputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6", + "name": "admin", + "type": "tuple" + } + ], + "name": "removeCollectionAdminCross", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { "inputs": [], "name": "removeCollectionSponsor", "outputs": [], @@ -286,6 +390,23 @@ "type": "function" }, { + "inputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6", + "name": "user", + "type": "tuple" + } + ], + "name": "removeFromCollectionAllowListCross", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { "inputs": [{ "internalType": "uint8", "name": "mode", "type": "uint8" }], "name": "setCollectionAccess", "outputs": [], @@ -361,6 +482,40 @@ }, { "inputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6", + "name": "sponsor", + "type": "tuple" + } + ], + "name": "setCollectionSponsorCross", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6", + "name": "newOwner", + "type": "tuple" + } + ], + "name": "setOwnerCross", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ { "internalType": "bytes4", "name": "interfaceID", "type": "bytes4" } ], "name": "supportsInterface", @@ -404,6 +559,33 @@ "type": "function" }, { + "inputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6", + "name": "from", + "type": "tuple" + }, + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6", + "name": "to", + "type": "tuple" + }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "transferFromCross", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { "inputs": [], "name": "uniqueCollectionType", "outputs": [{ "internalType": "string", "name": "", "type": "string" }], --- a/tests/src/eth/nonFungible.test.ts +++ b/tests/src/eth/nonFungible.test.ts @@ -246,6 +246,64 @@ } }); + itEth('Can perform burnFromCross()', async ({helper, privateKey}) => { + const alice = await privateKey('//Alice'); + const collection = await helper.nft.mintCollection(alice, {name: 'A', description: 'B', tokenPrefix: 'C'}); + + const owner = await privateKey('//Bob'); + const spender = await helper.eth.createAccountWithBalance(alice, 100n); + + const token = await collection.mintToken(alice, {Substrate: owner.address}); + + const address = helper.ethAddress.fromCollectionId(collection.collectionId); + const contract = helper.ethNativeContract.collection(address, 'nft'); + + { + await token.approve(owner, {Ethereum: spender}); + const ownerCross = helper.ethCrossAccount.fromKeyringPair(owner); + const result = await contract.methods.burnFromCross(ownerCross, token.tokenId).send({from: spender}); + const events = result.events.Transfer; + + expect(events).to.be.like({ + address, + event: 'Transfer', + returnValues: { + from: helper.address.substrateToEth(owner.address), + to: '0x0000000000000000000000000000000000000000', + tokenId: token.tokenId.toString(), + }, + }); + } + }); + + itEth('Can perform approveCross()', async ({helper, privateKey}) => { + const alice = await privateKey('//Alice'); + const collection = await helper.nft.mintCollection(alice, {name: 'A', description: 'B', tokenPrefix: 'C'}); + + const owner = await helper.eth.createAccountWithBalance(alice, 100n); + const receiver = await privateKey('//Charlie'); + + const token = await collection.mintToken(alice, {Ethereum: owner}); + + const address = helper.ethAddress.fromCollectionId(collection.collectionId); + const contract = helper.ethNativeContract.collection(address, 'nft'); + + { + const recieverCross = helper.ethCrossAccount.fromKeyringPair(receiver); + const result = await contract.methods.approveCross(recieverCross, token.tokenId).send({from: owner}); + const event = result.events.Approval; + expect(event).to.be.like({ + address: helper.ethAddress.fromCollectionId(collection.collectionId), + event: 'Approval', + returnValues: { + owner, + approved: helper.address.substrateToEth(receiver.address), + tokenId: token.tokenId.toString(), + }, + }); + } + }); + itEth('Can perform transferFrom()', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); const spender = await helper.eth.createAccountWithBalance(donor); @@ -351,6 +409,40 @@ expect(cost < BigInt(0.2 * Number(helper.balance.getOneTokenNominal()))); }); + itEth('Can perform transferFromCross()', async ({helper, privateKey}) => { + const alice = await privateKey('//Alice'); + const collection = await helper.nft.mintCollection(alice, {name: 'A', description: 'B', tokenPrefix: 'C'}); + + const owner = await privateKey('//Bob'); + const spender = await helper.eth.createAccountWithBalance(alice, 100n); + const receiver = await privateKey('//Charlie'); + + const token = await collection.mintToken(alice, {Substrate: owner.address}); + + const address = helper.ethAddress.fromCollectionId(collection.collectionId); + const contract = helper.ethNativeContract.collection(address, 'nft'); + + await token.approve(owner, {Ethereum: spender}); + + { + const ownerCross = helper.ethCrossAccount.fromKeyringPair(owner); + const recieverCross = helper.ethCrossAccount.fromKeyringPair(receiver); + const result = await contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: spender}); + const event = result.events.Transfer; + expect(event).to.be.like({ + address: helper.ethAddress.fromCollectionId(collection.collectionId), + event: 'Transfer', + returnValues: { + from: helper.address.substrateToEth(owner.address), + to: helper.address.substrateToEth(receiver.address), + tokenId: token.tokenId.toString(), + }, + }); + } + + expect(await token.getOwner()).to.be.like({Substrate: receiver.address}); + }); + itEth('transfer() call fee is less than 0.2UNQ', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); const receiver = helper.eth.createAccount(); --- a/tests/src/eth/nonFungibleAbi.json +++ b/tests/src/eth/nonFungibleAbi.json @@ -91,6 +91,23 @@ }, { "inputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6", + "name": "newAdmin", + "type": "tuple" + } + ], + "name": "addCollectionAdminCross", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ { "internalType": "address", "name": "user", "type": "address" } ], "name": "addToCollectionAllowList", @@ -100,6 +117,23 @@ }, { "inputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6", + "name": "user", + "type": "tuple" + } + ], + "name": "addToCollectionAllowListCross", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ { "internalType": "address", "name": "user", "type": "address" } ], "name": "allowed", @@ -119,6 +153,24 @@ }, { "inputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6", + "name": "approved", + "type": "tuple" + }, + { "internalType": "uint256", "name": "tokenId", "type": "uint256" } + ], + "name": "approveCross", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ { "internalType": "address", "name": "owner", "type": "address" } ], "name": "balanceOf", @@ -147,6 +199,24 @@ }, { "inputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6", + "name": "from", + "type": "tuple" + }, + { "internalType": "uint256", "name": "tokenId", "type": "uint256" } + ], + "name": "burnFromCross", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ { "internalType": "address", "name": "newOwner", "type": "address" } ], "name": "changeCollectionOwner", @@ -163,7 +233,7 @@ { "internalType": "address", "name": "field_0", "type": "address" }, { "internalType": "uint256", "name": "field_1", "type": "uint256" } ], - "internalType": "struct Tuple17[]", + "internalType": "struct Tuple6[]", "name": "", "type": "tuple[]" } @@ -180,7 +250,7 @@ { "internalType": "address", "name": "field_0", "type": "address" }, { "internalType": "uint256", "name": "field_1", "type": "uint256" } ], - "internalType": "struct Tuple17", + "internalType": "struct Tuple6", "name": "", "type": "tuple" } @@ -204,7 +274,7 @@ { "internalType": "address", "name": "field_0", "type": "address" }, { "internalType": "uint256", "name": "field_1", "type": "uint256" } ], - "internalType": "struct Tuple17", + "internalType": "struct Tuple6", "name": "", "type": "tuple" } @@ -286,6 +356,23 @@ "type": "function" }, { + "inputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6", + "name": "user", + "type": "tuple" + } + ], + "name": "isOwnerOrAdminCross", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { "inputs": [{ "internalType": "address", "name": "to", "type": "address" }], "name": "mint", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], @@ -352,6 +439,23 @@ "type": "function" }, { + "inputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6", + "name": "admin", + "type": "tuple" + } + ], + "name": "removeCollectionAdminCross", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { "inputs": [], "name": "removeCollectionSponsor", "outputs": [], @@ -369,6 +473,23 @@ }, { "inputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6", + "name": "user", + "type": "tuple" + } + ], + "name": "removeFromCollectionAllowListCross", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ { "internalType": "address", "name": "from", "type": "address" }, { "internalType": "address", "name": "to", "type": "address" }, { "internalType": "uint256", "name": "tokenId", "type": "uint256" } @@ -476,6 +597,40 @@ }, { "inputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6", + "name": "sponsor", + "type": "tuple" + } + ], + "name": "setCollectionSponsorCross", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6", + "name": "newOwner", + "type": "tuple" + } + ], + "name": "setOwnerCross", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ { "internalType": "uint256", "name": "tokenId", "type": "uint256" }, { "internalType": "string", "name": "key", "type": "string" }, { "internalType": "bytes", "name": "value", "type": "bytes" } @@ -570,6 +725,33 @@ "type": "function" }, { + "inputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6", + "name": "from", + "type": "tuple" + }, + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6", + "name": "to", + "type": "tuple" + }, + { "internalType": "uint256", "name": "tokenId", "type": "uint256" } + ], + "name": "transferFromCross", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { "inputs": [], "name": "uniqueCollectionType", "outputs": [{ "internalType": "string", "name": "", "type": "string" }], --- a/tests/src/eth/reFungible.test.ts +++ b/tests/src/eth/reFungible.test.ts @@ -227,6 +227,108 @@ } }); + itEth('Can perform burnFrom()', async ({helper, privateKey}) => { + const alice = await privateKey('//Alice'); + const collection = await helper.rft.mintCollection(alice, {name: 'A', description: 'B', tokenPrefix: 'C'}); + + const owner = await helper.eth.createAccountWithBalance(alice, 100n); + const spender = await helper.eth.createAccountWithBalance(alice, 100n); + + const token = await collection.mintToken(alice, 100n, {Ethereum: owner}); + + const address = helper.ethAddress.fromCollectionId(collection.collectionId); + const contract = helper.ethNativeContract.collection(address, 'rft'); + + const tokenAddress = helper.ethAddress.fromTokenId(collection.collectionId, token.tokenId); + const tokenContract = helper.ethNativeContract.rftToken(tokenAddress, owner); + await tokenContract.methods.repartition(15).send(); + await tokenContract.methods.approve(spender, 15).send(); + + { + const result = await contract.methods.burnFrom(owner, token.tokenId).send({from: spender}); + const event = result.events.Transfer; + expect(event).to.be.like({ + address: helper.ethAddress.fromCollectionId(collection.collectionId), + event: 'Transfer', + returnValues: { + from: owner, + to: '0x0000000000000000000000000000000000000000', + tokenId: token.tokenId.toString(), + }, + }); + } + + expect(await collection.getTokenBalance(token.tokenId, {Ethereum: owner})).to.be.eq(0n); + }); + + itEth('Can perform burnFromCross()', async ({helper, privateKey}) => { + const alice = await privateKey('//Alice'); + const collection = await helper.rft.mintCollection(alice, {name: 'A', description: 'B', tokenPrefix: 'C'}); + + const owner = await privateKey('//Bob'); + const spender = await helper.eth.createAccountWithBalance(alice, 100n); + + const token = await collection.mintToken(alice, 100n, {Substrate: owner.address}); + + const address = helper.ethAddress.fromCollectionId(collection.collectionId); + const contract = helper.ethNativeContract.collection(address, 'rft'); + + await token.repartition(owner, 15n); + await token.approve(owner, {Ethereum: spender}, 15n); + + { + const ownerCross = helper.ethCrossAccount.fromKeyringPair(owner); + const result = await contract.methods.burnFromCross(ownerCross, token.tokenId).send({from: spender}); + const event = result.events.Transfer; + expect(event).to.be.like({ + address: helper.ethAddress.fromCollectionId(collection.collectionId), + event: 'Transfer', + returnValues: { + from: helper.address.substrateToEth(owner.address), + to: '0x0000000000000000000000000000000000000000', + tokenId: token.tokenId.toString(), + }, + }); + } + + expect(await collection.getTokenBalance(token.tokenId, {Substrate: owner.address})).to.be.eq(0n); + }); + + itEth('Can perform transferFromCross()', async ({helper, privateKey}) => { + const alice = await privateKey('//Alice'); + const collection = await helper.rft.mintCollection(alice, {name: 'A', description: 'B', tokenPrefix: 'C'}); + + const owner = await privateKey('//Bob'); + const spender = await helper.eth.createAccountWithBalance(alice, 100n); + const receiver = await privateKey('//Charlie'); + + const token = await collection.mintToken(alice, 100n, {Substrate: owner.address}); + + const address = helper.ethAddress.fromCollectionId(collection.collectionId); + const contract = helper.ethNativeContract.collection(address, 'rft'); + + await token.repartition(owner, 15n); + await token.approve(owner, {Ethereum: spender}, 15n); + + { + const ownerCross = helper.ethCrossAccount.fromKeyringPair(owner); + const recieverCross = helper.ethCrossAccount.fromKeyringPair(receiver); + const result = await contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: spender}); + const event = result.events.Transfer; + expect(event).to.be.like({ + address: helper.ethAddress.fromCollectionId(collection.collectionId), + event: 'Transfer', + returnValues: { + from: helper.address.substrateToEth(owner.address), + to: helper.address.substrateToEth(receiver.address), + tokenId: token.tokenId.toString(), + }, + }); + } + + expect(await token.getTop10Owners()).to.be.like([{Substrate: receiver.address}]); + }); + itEth('Can perform transfer()', async ({helper}) => { const caller = await helper.eth.createAccountWithBalance(donor); const receiver = helper.eth.createAccount(); --- a/tests/src/eth/reFungibleAbi.json +++ b/tests/src/eth/reFungibleAbi.json @@ -91,6 +91,23 @@ }, { "inputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6", + "name": "newAdmin", + "type": "tuple" + } + ], + "name": "addCollectionAdminCross", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ { "internalType": "address", "name": "user", "type": "address" } ], "name": "addToCollectionAllowList", @@ -100,6 +117,23 @@ }, { "inputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6", + "name": "user", + "type": "tuple" + } + ], + "name": "addToCollectionAllowListCross", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ { "internalType": "address", "name": "user", "type": "address" } ], "name": "allowed", @@ -147,6 +181,24 @@ }, { "inputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6", + "name": "from", + "type": "tuple" + }, + { "internalType": "uint256", "name": "tokenId", "type": "uint256" } + ], + "name": "burnFromCross", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ { "internalType": "address", "name": "newOwner", "type": "address" } ], "name": "changeCollectionOwner", @@ -163,7 +215,7 @@ { "internalType": "address", "name": "field_0", "type": "address" }, { "internalType": "uint256", "name": "field_1", "type": "uint256" } ], - "internalType": "struct Tuple17[]", + "internalType": "struct Tuple6[]", "name": "", "type": "tuple[]" } @@ -180,7 +232,7 @@ { "internalType": "address", "name": "field_0", "type": "address" }, { "internalType": "uint256", "name": "field_1", "type": "uint256" } ], - "internalType": "struct Tuple17", + "internalType": "struct Tuple6", "name": "", "type": "tuple" } @@ -204,7 +256,7 @@ { "internalType": "address", "name": "field_0", "type": "address" }, { "internalType": "uint256", "name": "field_1", "type": "uint256" } ], - "internalType": "struct Tuple17", + "internalType": "struct Tuple6", "name": "", "type": "tuple" } @@ -286,6 +338,23 @@ "type": "function" }, { + "inputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6", + "name": "user", + "type": "tuple" + } + ], + "name": "isOwnerOrAdminCross", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { "inputs": [{ "internalType": "address", "name": "to", "type": "address" }], "name": "mint", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], @@ -352,6 +421,23 @@ "type": "function" }, { + "inputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6", + "name": "admin", + "type": "tuple" + } + ], + "name": "removeCollectionAdminCross", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { "inputs": [], "name": "removeCollectionSponsor", "outputs": [], @@ -369,6 +455,23 @@ }, { "inputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6", + "name": "user", + "type": "tuple" + } + ], + "name": "removeFromCollectionAllowListCross", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ { "internalType": "address", "name": "from", "type": "address" }, { "internalType": "address", "name": "to", "type": "address" }, { "internalType": "uint256", "name": "tokenId", "type": "uint256" } @@ -476,6 +579,40 @@ }, { "inputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6", + "name": "sponsor", + "type": "tuple" + } + ], + "name": "setCollectionSponsorCross", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6", + "name": "newOwner", + "type": "tuple" + } + ], + "name": "setOwnerCross", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ { "internalType": "uint256", "name": "tokenId", "type": "uint256" }, { "internalType": "string", "name": "key", "type": "string" }, { "internalType": "bytes", "name": "value", "type": "bytes" } @@ -579,6 +716,33 @@ "type": "function" }, { + "inputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6", + "name": "from", + "type": "tuple" + }, + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6", + "name": "to", + "type": "tuple" + }, + { "internalType": "uint256", "name": "tokenId", "type": "uint256" } + ], + "name": "transferFromCross", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { "inputs": [], "name": "uniqueCollectionType", "outputs": [{ "internalType": "string", "name": "", "type": "string" }], --- a/tests/src/eth/util/playgrounds/types.ts +++ b/tests/src/eth/util/playgrounds/types.ts @@ -13,3 +13,9 @@ event: string, args: { [key: string]: string } }; +export interface TEthCrossAccount { + readonly 0: string, + readonly 1: string | Uint8Array, + readonly field_0: string, + readonly field_1: string | Uint8Array, +} --- a/tests/src/eth/util/playgrounds/unique.dev.ts +++ b/tests/src/eth/util/playgrounds/unique.dev.ts @@ -18,7 +18,7 @@ import {DevUniqueHelper} from '../../../util/playgrounds/unique.dev'; -import {ContractImports, CompiledContract, NormalizedEvent} from './types'; +import {ContractImports, CompiledContract, TEthCrossAccount, NormalizedEvent} from './types'; // Native contracts ABI import collectionHelpersAbi from '../../collectionHelpersAbi.json'; @@ -343,6 +343,26 @@ export type EthUniqueHelperConstructor = new (...args: any[]) => EthUniqueHelper; +export class EthCrossAccountGroup extends EthGroupBase { + fromAddress(address: TEthereumAccount): TEthCrossAccount { + return { + 0: address, + 1: '0', + field_0: address, + field_1: '0', + }; + } + + fromKeyringPair(keyring: IKeyringPair): TEthCrossAccount { + return { + 0: '0x0000000000000000000000000000000000000000', + 1: keyring.addressRaw, + field_0: '0x0000000000000000000000000000000000000000', + field_1: keyring.addressRaw, + }; + } +} + export class EthUniqueHelper extends DevUniqueHelper { web3: Web3 | null = null; web3Provider: WebsocketProvider | null = null; @@ -351,6 +371,7 @@ ethAddress: EthAddressGroup; ethNativeContract: NativeContractGroup; ethContract: ContractGroup; + ethCrossAccount: EthCrossAccountGroup; constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) { options.helperBase = options.helperBase ?? EthUniqueHelper; @@ -358,6 +379,7 @@ super(logger, options); this.eth = new EthGroup(this); this.ethAddress = new EthAddressGroup(this); + this.ethCrossAccount = new EthCrossAccountGroup(this); this.ethNativeContract = new NativeContractGroup(this); this.ethContract = new ContractGroup(this); }