--- a/Cargo.lock +++ b/Cargo.lock @@ -746,7 +746,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" dependencies = [ + "lazy_static", "memchr", + "regex-automata", ] [[package]] @@ -1055,6 +1057,16 @@ ] [[package]] +name = "concat-idents" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6f90860248d75014b7b103db8fee4f291c07bfb41306cdf77a0a5ab7a10d2f" +dependencies = [ + "quote", + "syn", +] + +[[package]] name = "concurrent-queue" version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1064,6 +1076,19 @@ ] [[package]] +name = "console" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c050367d967ced717c04b65d8c619d863ef9292ce0c5760028655a2fb298718c" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "terminal_size", + "winapi", +] + +[[package]] name = "const-oid" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1846,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", @@ -1858,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", @@ -1873,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", @@ -2140,6 +2165,12 @@ ] [[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] name = "enum-as-inner" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2302,6 +2333,7 @@ name = "evm-coder" version = "0.1.3" dependencies = [ + "concat-idents", "ethereum", "evm-coder-procedural", "evm-core", @@ -2310,6 +2342,7 @@ "hex-literal", "impl-trait-for-tuples", "primitive-types", + "similar-asserts", "sp-std", ] @@ -3528,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", @@ -5792,7 +5825,7 @@ [[package]] name = "pallet-common" -version = "0.1.8" +version = "0.1.9" dependencies = [ "ethereum", "evm-coder", @@ -6325,7 +6358,7 @@ [[package]] name = "pallet-nonfungible" -version = "0.1.5" +version = "0.1.6" dependencies = [ "ethereum", "evm-coder", @@ -6447,7 +6480,7 @@ [[package]] name = "pallet-refungible" -version = "0.2.4" +version = "0.2.5" dependencies = [ "derivative", "ethereum", @@ -6766,7 +6799,7 @@ [[package]] name = "pallet-unique" -version = "0.2.0" +version = "0.2.1" dependencies = [ "ethereum", "evm-coder", @@ -10741,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", @@ -10886,6 +10919,26 @@ ] [[package]] +name = "similar" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ac7f900db32bf3fd12e0117dd3dc4da74bc52ebaac97f39668446d89694803" +dependencies = [ + "bstr", + "unicode-segmentation", +] + +[[package]] +name = "similar-asserts" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbf644ad016b75129f01a34a355dcb8d66a5bc803e417c7a77cc5d5ee9fa0f18" +dependencies = [ + "console", + "similar", +] + +[[package]] name = "slab" version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -12002,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", @@ -12080,6 +12133,16 @@ ] [[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + +[[package]] name = "test-runtime-constants" version = "0.9.30" source = "git+https://github.com/paritytech/polkadot?branch=release-v0.9.30#064536093f5ff70d867f4bbce8d4c41a406d317a" @@ -12660,6 +12723,12 @@ ] [[package]] +name = "unicode-segmentation" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" + +[[package]] name = "unicode-width" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" --- a/crates/evm-coder/Cargo.toml +++ b/crates/evm-coder/Cargo.toml @@ -14,7 +14,7 @@ sp-std = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.30" } frame-support = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.30" } # Error types for execution -evm-core = { default-features = false , git = "https://github.com/uniquenetwork/evm", branch = "unique-polkadot-v0.9.30" } +evm-core = { default-features = false, git = "https://github.com/uniquenetwork/evm", branch = "unique-polkadot-v0.9.30" } # We have tuple-heavy code in solidity.rs impl-trait-for-tuples = "0.2.2" @@ -22,7 +22,14 @@ # We want to assert some large binary blobs equality in tests hex = "0.4.3" hex-literal = "0.3.4" +similar-asserts = "1.4.2" +concat-idents = "1.1.3" [features] default = ["std"] -std = ["ethereum/std", "primitive-types/std", "evm-core/std", "frame-support/std"] +std = [ + "ethereum/std", + "primitive-types/std", + "evm-core/std", + "frame-support/std", +] --- a/crates/evm-coder/src/abi.rs +++ b/crates/evm-coder/src/abi.rs @@ -25,14 +25,18 @@ use crate::{ execution::{Error, ResultWithPostInfo, WithPostDispatchInfo}, - types::{string, self}, + types::*, }; use crate::execution::Result; const ABI_ALIGNMENT: usize = 32; trait TypeHelper { + /// Is type dynamic sized. fn is_dynamic() -> bool; + + /// Size for type aligned to [`ABI_ALIGNMENT`]. + fn size() -> usize; } /// View into RLP data, which provides method to read typed items from it @@ -52,7 +56,7 @@ } } /// Start reading RLP buffer, parsing first 4 bytes as selector - pub fn new_call(buf: &'i [u8]) -> Result<(types::bytes4, Self)> { + pub fn new_call(buf: &'i [u8]) -> Result<(bytes4, Self)> { if buf.len() < 4 { return Err(Error::Error(ExitError::OutOfOffset)); } @@ -218,12 +222,21 @@ static_part: Vec, dynamic_part: Vec<(usize, AbiWriter)>, had_call: bool, + is_dynamic: bool, } impl AbiWriter { /// Initialize internal buffers for output data, assuming no padding required pub fn new() -> Self { Self::default() } + + /// Initialize internal buffers with data size + pub fn new_dynamic(is_dynamic: bool) -> Self { + Self { + is_dynamic, + ..Default::default() + } + } /// Initialize internal buffers, inserting method selector at beginning pub fn new_call(method_id: u32) -> Self { let mut val = Self::new(); @@ -239,11 +252,11 @@ self.static_part.extend(block); } - fn write_padright(&mut self, bytes: &[u8]) { - assert!(bytes.len() <= ABI_ALIGNMENT); - self.static_part.extend(bytes); + fn write_padright(&mut self, block: &[u8]) { + assert!(block.len() <= ABI_ALIGNMENT); + self.static_part.extend(block); self.static_part - .extend(&[0; ABI_ALIGNMENT][0..ABI_ALIGNMENT - bytes.len()]); + .extend(&[0; ABI_ALIGNMENT][0..ABI_ALIGNMENT - block.len()]); } /// Write [`H160`] to end of buffer @@ -313,12 +326,14 @@ /// Finish writer, concatenating all internal buffers pub fn finish(mut self) -> Vec { for (static_offset, part) in self.dynamic_part { - let part_offset = self.static_part.len() - if self.had_call { 4 } else { 0 }; + let part_offset = self.static_part.len() + - if self.had_call { 4 } else { 0 } + - if self.is_dynamic { ABI_ALIGNMENT } else { 0 }; let encoded_dynamic_offset = usize::to_be_bytes(part_offset); - self.static_part[static_offset + ABI_ALIGNMENT - encoded_dynamic_offset.len() - ..static_offset + ABI_ALIGNMENT] - .copy_from_slice(&encoded_dynamic_offset); + let start = static_offset + ABI_ALIGNMENT - encoded_dynamic_offset.len(); + let stop = static_offset + ABI_ALIGNMENT; + self.static_part[start..stop].copy_from_slice(&encoded_dynamic_offset); self.static_part.extend(part.finish()) } self.static_part @@ -333,9 +348,6 @@ pub trait AbiRead { /// Read item from current position, advanding decoder fn abi_read(&mut self) -> Result; - - /// Size for type aligned to [`ABI_ALIGNMENT`]. - fn size() -> usize; } macro_rules! impl_abi_readable { @@ -344,29 +356,43 @@ fn is_dynamic() -> bool { $dynamic } + + fn size() -> usize { + ABI_ALIGNMENT + } } impl AbiRead<$ty> for AbiReader<'_> { fn abi_read(&mut self) -> Result<$ty> { self.$method() - } - - fn size() -> usize { - ABI_ALIGNMENT } } }; } -impl_abi_readable!(u8, uint8, false); -impl_abi_readable!(u32, uint32, false); -impl_abi_readable!(u64, uint64, false); -impl_abi_readable!(u128, uint128, false); -impl_abi_readable!(U256, uint256, false); -impl_abi_readable!([u8; 4], bytes4, false); -impl_abi_readable!(H160, address, false); -impl_abi_readable!(Vec, bytes, true); -impl_abi_readable!(bool, bool, true); +impl_abi_readable!(bool, bool, false); +impl_abi_readable!(uint8, uint8, false); +impl_abi_readable!(uint32, uint32, false); +impl_abi_readable!(uint64, uint64, false); +impl_abi_readable!(uint128, uint128, false); +impl_abi_readable!(uint256, uint256, false); +impl_abi_readable!(bytes4, bytes4, false); +impl_abi_readable!(address, address, false); impl_abi_readable!(string, string, true); +// impl_abi_readable!(bytes, bytes, true); + +impl TypeHelper for bytes { + fn is_dynamic() -> bool { + true + } + fn size() -> usize { + ABI_ALIGNMENT + } +} +impl AbiRead for AbiReader<'_> { + fn abi_read(&mut self) -> Result { + Ok(bytes(self.bytes()?)) + } +} mod sealed { /// Not all types can be placed in vec, i.e `Vec` is restricted, `bytes` should be used instead @@ -390,22 +416,27 @@ out.push(>::abi_read(&mut sub)?); } Ok(out) - } - - fn size() -> usize { - ABI_ALIGNMENT } } macro_rules! impl_tuples { ($($ident:ident)+) => { - impl<$($ident: TypeHelper,)+> TypeHelper for ($($ident,)+) { + impl<$($ident: TypeHelper,)+> TypeHelper for ($($ident,)+) + where + $( + $ident: TypeHelper, + )+ + { fn is_dynamic() -> bool { false $( || <$ident>::is_dynamic() )* } + + fn size() -> usize { + 0 $(+ <$ident>::size())+ + } } impl<$($ident),+> sealed::CanBePlacedInVec for ($($ident,)+) {} impl<$($ident),+> AbiRead<($($ident,)+)> for AbiReader<'_> @@ -416,25 +447,27 @@ ($($ident,)+): TypeHelper, { fn abi_read(&mut self) -> Result<($($ident,)+)> { - let size = if !<($($ident,)+)>::is_dynamic() { Some(>::size()) } else { None }; + let size = if !<($($ident,)+)>::is_dynamic() { Some(<($($ident,)+)>::size()) } else { None }; let mut subresult = self.subresult(size)?; Ok(( $(>::abi_read(&mut subresult)?,)+ )) - } - - fn size() -> usize { - 0 $(+ as AbiRead<$ident>>::size())+ } } #[allow(non_snake_case)] - impl<$($ident),+> AbiWrite for &($($ident,)+) + impl<$($ident),+> AbiWrite for ($($ident,)+) where $($ident: AbiWrite,)+ { fn abi_write(&self, writer: &mut AbiWriter) { let ($($ident,)+) = self; - $($ident.abi_write(writer);)+ + if writer.is_dynamic { + let mut sub = AbiWriter::new(); + $($ident.abi_write(&mut sub);)+ + writer.write_subresult(sub); + } else { + $($ident.abi_write(writer);)+ + } } } }; @@ -505,14 +538,35 @@ impl_abi_writeable!(H160, address); impl_abi_writeable!(bool, bool); impl_abi_writeable!(&str, string); -impl AbiWrite for &string { + +impl AbiWrite for string { fn abi_write(&self, writer: &mut AbiWriter) { writer.string(self) } } -impl AbiWrite for &Vec { + +impl AbiWrite for bytes { + fn abi_write(&self, writer: &mut AbiWriter) { + writer.bytes(self.0.as_slice()) + } +} + +impl AbiWrite for Vec { fn abi_write(&self, writer: &mut AbiWriter) { - writer.bytes(self) + let is_dynamic = T::is_dynamic(); + let mut sub = if is_dynamic { + AbiWriter::new_dynamic(is_dynamic) + } else { + AbiWriter::new() + }; + + // Write items count + (self.len() as u32).abi_write(&mut sub); + + for item in self { + item.abi_write(&mut sub); + } + writer.write_subresult(sub); } } @@ -556,13 +610,145 @@ #[cfg(test)] pub mod test { use crate::{ - abi::AbiRead, - types::{string, uint256}, + abi::{AbiRead, AbiWrite}, + types::*, }; use super::{AbiReader, AbiWriter}; use hex_literal::hex; + use primitive_types::{H160, U256}; + use concat_idents::concat_idents; + + macro_rules! test_impl { + ($name:ident, $type:ty, $function_identifier:expr, $decoded_data:expr, $encoded_data:expr) => { + concat_idents!(test_name = encode_decode_, $name { + #[test] + fn test_name() { + let function_identifier: u32 = $function_identifier; + let decoded_data = $decoded_data; + let encoded_data = $encoded_data; + + let (call, mut decoder) = AbiReader::new_call(encoded_data).unwrap(); + assert_eq!(call, u32::to_be_bytes(function_identifier)); + let data = as AbiRead<$type>>::abi_read(&mut decoder).unwrap(); + assert_eq!(data, decoded_data); + + let mut writer = AbiWriter::new_call(function_identifier); + decoded_data.abi_write(&mut writer); + let ed = writer.finish(); + similar_asserts::assert_eq!(encoded_data, ed.as_slice()); + } + }); + }; + } + + macro_rules! test_impl_uint { + ($type:ident) => { + test_impl!( + $type, + $type, + 0xdeadbeef, + 255 as $type, + &hex!( + " + deadbeef + 00000000000000000000000000000000000000000000000000000000000000ff + " + ) + ); + }; + } + + test_impl_uint!(uint8); + test_impl_uint!(uint32); + test_impl_uint!(uint128); + + test_impl!( + uint256, + uint256, + 0xdeadbeef, + U256([255, 0, 0, 0]), + &hex!( + " + deadbeef + 00000000000000000000000000000000000000000000000000000000000000ff + " + ) + ); + + test_impl!( + vec_tuple_address_uint256, + Vec<(address, uint256)>, + 0x1ACF2D55, + vec![ + ( + H160(hex!("2D2FF76104B7BACB2E8F6731D5BFC184EBECDDBC")), + U256([10, 0, 0, 0]), + ), + ( + H160(hex!("AB8E3D9134955566483B11E6825C9223B6737B10")), + U256([20, 0, 0, 0]), + ), + ( + H160(hex!("8C582BDF2953046705FC56F189385255EFC1BE18")), + U256([30, 0, 0, 0]), + ), + ], + &hex!( + " + 1ACF2D55 + 0000000000000000000000000000000000000000000000000000000000000020 // offset of (address, uint256)[] + 0000000000000000000000000000000000000000000000000000000000000003 // length of (address, uint256)[] + + 0000000000000000000000002D2FF76104B7BACB2E8F6731D5BFC184EBECDDBC // address + 000000000000000000000000000000000000000000000000000000000000000A // uint256 + + 000000000000000000000000AB8E3D9134955566483B11E6825C9223B6737B10 // address + 0000000000000000000000000000000000000000000000000000000000000014 // uint256 + + 0000000000000000000000008C582BDF2953046705FC56F189385255EFC1BE18 // address + 000000000000000000000000000000000000000000000000000000000000001E // uint256 + " + ) + ); + + test_impl!( + vec_tuple_uint256_string, + Vec<(uint256, string)>, + 0xdeadbeef, + vec![ + (1.into(), "Test URI 0".to_string()), + (11.into(), "Test URI 1".to_string()), + (12.into(), "Test URI 2".to_string()), + ], + &hex!( + " + deadbeef + 0000000000000000000000000000000000000000000000000000000000000020 // offset of (uint256, string)[] + 0000000000000000000000000000000000000000000000000000000000000003 // length of (uint256, string)[] + + 0000000000000000000000000000000000000000000000000000000000000060 // offset of first elem + 00000000000000000000000000000000000000000000000000000000000000e0 // offset of second elem + 0000000000000000000000000000000000000000000000000000000000000160 // offset of third elem + + 0000000000000000000000000000000000000000000000000000000000000001 // first token id? #60 + 0000000000000000000000000000000000000000000000000000000000000040 // offset of string + 000000000000000000000000000000000000000000000000000000000000000a // size of string + 5465737420555249203000000000000000000000000000000000000000000000 // string + + 000000000000000000000000000000000000000000000000000000000000000b // second token id? Why ==11? #e0 + 0000000000000000000000000000000000000000000000000000000000000040 // offset of string + 000000000000000000000000000000000000000000000000000000000000000a // size of string + 5465737420555249203100000000000000000000000000000000000000000000 // string + 000000000000000000000000000000000000000000000000000000000000000c // third token id? Why ==12? #160 + 0000000000000000000000000000000000000000000000000000000000000040 // offset of string + 000000000000000000000000000000000000000000000000000000000000000a // size of string + 5465737420555249203200000000000000000000000000000000000000000000 // string + " + ) + ); + #[test] fn dynamic_after_static() { let mut encoder = AbiWriter::new(); @@ -609,8 +795,17 @@ } #[test] - fn mint_bulk() { - let (call, mut decoder) = AbiReader::new_call(&hex!( + fn parse_vec_with_dynamic_type() { + let decoded_data = ( + 0x36543006, + vec![ + (1.into(), "Test URI 0".to_string()), + (11.into(), "Test URI 1".to_string()), + (12.into(), "Test URI 2".to_string()), + ], + ); + + let encoded_data = &hex!( " 36543006 00000000000000000000000053744e6da587ba10b32a2554d2efdcd985bc27a3 // address @@ -636,64 +831,19 @@ 000000000000000000000000000000000000000000000000000000000000000a // size of string 5465737420555249203200000000000000000000000000000000000000000000 // string " - )) - .unwrap(); - assert_eq!(call, u32::to_be_bytes(0x36543006)); - let _ = decoder.address().unwrap(); + ); + + let (call, mut decoder) = AbiReader::new_call(encoded_data).unwrap(); + assert_eq!(call, u32::to_be_bytes(decoded_data.0)); + let address = decoder.address().unwrap(); let data = as AbiRead>>::abi_read(&mut decoder).unwrap(); - assert_eq!( - data, - vec![ - (1.into(), "Test URI 0".to_string()), - (11.into(), "Test URI 1".to_string()), - (12.into(), "Test URI 2".to_string()) - ] - ); - } + assert_eq!(data, decoded_data.1); - #[test] - fn parse_vec_with_simple_type() { - use crate::types::address; - use primitive_types::{H160, U256}; - - let (call, mut decoder) = AbiReader::new_call(&hex!( - " - 1ACF2D55 - 0000000000000000000000000000000000000000000000000000000000000020 // offset of (address, uint256)[] - 0000000000000000000000000000000000000000000000000000000000000003 // length of (address, uint256)[] - - 0000000000000000000000002D2FF76104B7BACB2E8F6731D5BFC184EBECDDBC // address - 000000000000000000000000000000000000000000000000000000000000000A // uint256 - - 000000000000000000000000AB8E3D9134955566483B11E6825C9223B6737B10 // address - 0000000000000000000000000000000000000000000000000000000000000014 // uint256 - - 0000000000000000000000008C582BDF2953046705FC56F189385255EFC1BE18 // address - 000000000000000000000000000000000000000000000000000000000000001E // uint256 - " - )) - .unwrap(); - assert_eq!(call, u32::to_be_bytes(0x1ACF2D55)); - let data = - as AbiRead>>::abi_read(&mut decoder).unwrap(); - assert_eq!(data.len(), 3); - assert_eq!( - data, - vec![ - ( - H160(hex!("2D2FF76104B7BACB2E8F6731D5BFC184EBECDDBC")), - U256([10, 0, 0, 0]) - ), - ( - H160(hex!("AB8E3D9134955566483B11E6825C9223B6737B10")), - U256([20, 0, 0, 0]) - ), - ( - H160(hex!("8C582BDF2953046705FC56F189385255EFC1BE18")), - U256([30, 0, 0, 0]) - ), - ] - ); + let mut writer = AbiWriter::new_call(decoded_data.0); + address.abi_write(&mut writer); + decoded_data.1.abi_write(&mut writer); + let ed = writer.finish(); + similar_asserts::assert_eq!(encoded_data, ed.as_slice()); } } --- a/crates/evm-coder/src/lib.rs +++ b/crates/evm-coder/src/lib.rs @@ -133,8 +133,10 @@ pub type string = ::alloc::string::String; #[cfg(feature = "std")] pub type string = ::std::string::String; - pub type bytes = Vec; + #[derive(Default, Debug)] + pub struct bytes(pub Vec); + /// Solidity doesn't have `void` type, however we have special implementation /// for empty tuple return type pub type void = (); @@ -157,6 +159,30 @@ /// and there is no `receiver()` function defined. pub value: U256, } + + impl From> for bytes { + fn from(src: Vec) -> Self { + Self(src) + } + } + + impl Into> for bytes { + fn into(self) -> Vec { + self.0 + } + } + + impl bytes { + #[must_use] + pub fn len(&self) -> usize { + self.0.len() + } + + #[must_use] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + } } /// Parseable EVM call, this trait should be implemented with [`solidity_interface`] macro --- a/crates/evm-coder/src/solidity.rs +++ b/crates/evm-coder/src/solidity.rs @@ -154,8 +154,10 @@ fn is_simple() -> bool { false } - fn solidity_default(writer: &mut impl fmt::Write, _tc: &TypeCollector) -> fmt::Result { - write!(writer, "[]") + fn solidity_default(writer: &mut impl fmt::Write, tc: &TypeCollector) -> fmt::Result { + write!(writer, "new ")?; + T::solidity_name(writer, tc)?; + write!(writer, "[](0)") } } --- a/node/cli/src/command.rs +++ b/node/cli/src/command.rs @@ -33,12 +33,12 @@ // limitations under the License. use crate::{ - chain_spec::{ - self, RuntimeId, RuntimeIdentification, ServiceId, ServiceIdentification, default_runtime, - }, + chain_spec::{self, RuntimeId, RuntimeIdentification, ServiceId, ServiceIdentification}, cli::{Cli, RelayChainCli, Subcommand}, service::{new_partial, start_node, start_dev_node}, }; +#[cfg(feature = "runtime-benchmarks")] +use crate::chain_spec::default_runtime; #[cfg(feature = "unique-runtime")] use crate::service::UniqueRuntimeExecutor; @@ -46,7 +46,10 @@ #[cfg(feature = "quartz-runtime")] use crate::service::QuartzRuntimeExecutor; -use crate::service::{OpalRuntimeExecutor, DefaultRuntimeExecutor}; +use crate::service::OpalRuntimeExecutor; + +#[cfg(feature = "runtime-benchmarks")] +use crate::service::DefaultRuntimeExecutor; use codec::Encode; use cumulus_primitives_core::ParaId; --- a/node/cli/src/service.rs +++ b/node/cli/src/service.rs @@ -46,7 +46,6 @@ use cumulus_relay_chain_rpc_interface::{RelayChainRpcInterface, create_client_and_start_worker}; // Substrate Imports -use sc_client_api::ExecutorProvider; use sc_executor::NativeElseWasmExecutor; use sc_executor::NativeExecutionDispatch; use sc_network::{NetworkService, NetworkBlock}; @@ -84,13 +83,21 @@ /// Opal native executor instance. pub struct OpalRuntimeExecutor; -#[cfg(feature = "unique-runtime")] +#[cfg(all(feature = "unique-runtime", feature = "runtime-benchmarks"))] pub type DefaultRuntimeExecutor = UniqueRuntimeExecutor; -#[cfg(all(not(feature = "unique-runtime"), feature = "quartz-runtime"))] +#[cfg(all( + not(feature = "unique-runtime"), + feature = "quartz-runtime", + feature = "runtime-benchmarks" +))] pub type DefaultRuntimeExecutor = QuartzRuntimeExecutor; -#[cfg(all(not(feature = "unique-runtime"), not(feature = "quartz-runtime")))] +#[cfg(all( + not(feature = "unique-runtime"), + not(feature = "quartz-runtime"), + feature = "runtime-benchmarks" +))] pub type DefaultRuntimeExecutor = OpalRuntimeExecutor; #[cfg(feature = "unique-runtime")] --- a/pallets/common/CHANGELOG.md +++ b/pallets/common/CHANGELOG.md @@ -2,29 +2,37 @@ All notable changes to this project will be documented in this file. +## [0.1.9] - 2022-10-13 + +## Added + +- EVM event for `destroy_collection`. + ## [0.1.8] - 2022-08-24 ## Added - - Eth methods for collection - + set_collection_sponsor_substrate - + has_collection_pending_sponsor - + remove_collection_sponsor - + get_collection_sponsor + +- Eth methods for collection + - set_collection_sponsor_substrate + - has_collection_pending_sponsor + - remove_collection_sponsor + - get_collection_sponsor - Add convert function from `uint256` to `CrossAccountId`. ## [0.1.7] - 2022-08-19 ### Added - - Add convert funtion from `CrossAccountId` to eth `uint256`. +- Add convert funtion from `CrossAccountId` to eth `uint256`. - ## [0.1.6] - 2022-08-16 ### Added -- New Ethereum API methods: changeOwner, changeOwner(Substrate) and verifyOwnerOrAdmin(Substrate). +- New Ethereum API methods: changeOwner, changeOwner(Substrate) and verifyOwnerOrAdmin(Substrate). + + ## [v0.1.5] 2022-08-16 ### Other changes @@ -45,19 +53,21 @@ - build: Upgrade polkadot to v0.9.25 cdfb9bdc7b205ff1b5134f034ef9973d769e5e6b ## [0.1.3] - 2022-07-25 + ### Add -- Some static property keys and values. +- Some static property keys and values. + ## [0.1.2] - 2022-07-20 ### Fixed -- Some methods in `#[solidity_interface]` for `CollectionHandle` had invalid - mutability modifiers, causing invalid stub/abi generation. +- Some methods in `#[solidity_interface]` for `CollectionHandle` had invalid + mutability modifiers, causing invalid stub/abi generation. ## [0.1.1] - 2022-07-14 ### Added - - Implementation of RPC method `token_owners` returning 10 owners in no particular order. - This was an internal request to improve the web interface and support fractionalization event. +- Implementation of RPC method `token_owners` returning 10 owners in no particular order. + This was an internal request to improve the web interface and support fractionalization event. --- a/pallets/common/Cargo.toml +++ b/pallets/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-common" -version = "0.1.8" +version = "0.1.9" license = "GPLv3" edition = "2021" --- 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, }; @@ -53,6 +53,12 @@ #[indexed] collection_id: address, }, + /// The collection has been destroyed. + CollectionDestroyed { + /// Collection ID. + #[indexed] + collection_id: address, + }, } /// Does not always represent a full collection, for RFT it is either @@ -85,19 +91,45 @@ let key = >::from(key) .try_into() .map_err(|_| "key too large")?; - let value = value.try_into().map_err(|_| "value too large")?; + let value = value.0.try_into().map_err(|_| "value too large")?; >::set_collection_property(self, &caller, Property { key, value }) .map_err(dispatch_to_evm::) } + /// Set collection properties. + /// + /// @param properties Vector of properties key/value pair. + #[weight(>::set_collection_properties(properties.len() as u32))] + fn set_collection_properties( + &mut self, + caller: caller, + properties: Vec<(string, bytes)>, + ) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + + let properties = properties + .into_iter() + .map(|(key, value)| { + let key = >::from(key) + .try_into() + .map_err(|_| "key too large")?; + + let value = value.0.try_into().map_err(|_| "value too large")?; + + Ok(Property { key, value }) + }) + .collect::>>()?; + + >::set_collection_properties(self, &caller, properties) + .map_err(dispatch_to_evm::) + } + /// Delete collection property. /// /// @param key Property key. #[weight(>::delete_collection_properties(1))] fn delete_collection_property(&mut self, caller: caller, key: string) -> Result<()> { - self.consume_store_reads_and_writes(1, 1)?; - let caller = T::CrossAccountId::from_eth(caller); let key = >::from(key) .try_into() @@ -106,6 +138,24 @@ >::delete_collection_property(self, &caller, key).map_err(dispatch_to_evm::) } + /// Delete collection properties. + /// + /// @param keys Properties keys. + #[weight(>::delete_collection_properties(keys.len() as u32))] + fn delete_collection_properties(&mut self, caller: caller, keys: Vec) -> Result<()> { + let caller = T::CrossAccountId::from_eth(caller); + let keys = keys + .into_iter() + .map(|key| { + >::from(key) + .try_into() + .map_err(|_| Error::Revert("key too large".into())) + }) + .collect::>>()?; + + >::delete_collection_properties(self, &caller, keys).map_err(dispatch_to_evm::) + } + /// Get collection property. /// /// @dev Throws error if key not found. @@ -117,12 +167,44 @@ .try_into() .map_err(|_| "key too large")?; - let props = >::get(self.id); + let props = CollectionProperties::::get(self.id); let prop = props.get(&key).ok_or("key not found")?; - Ok(prop.to_vec()) + Ok(bytes(prop.to_vec())) } + /// Get collection properties. + /// + /// @param keys Properties keys. Empty keys for all propertyes. + /// @return Vector of properties key/value pairs. + fn collection_properties(&self, keys: Vec) -> Result> { + let keys = keys + .into_iter() + .map(|key| { + >::from(key) + .try_into() + .map_err(|_| Error::Revert("key too large".into())) + }) + .collect::>>()?; + + let properties = Pallet::::filter_collection_properties( + self.id, + if keys.is_empty() { None } else { Some(keys) }, + ) + .map_err(dispatch_to_evm::)?; + + let properties = properties + .into_iter() + .map(|p| { + let key = + string::from_utf8(p.key.into()).map_err(|e| Error::Revert(format!("{}", e)))?; + let value = bytes(p.value.to_vec()); + Ok((key, value)) + }) + .collect::>>()?; + Ok(properties) + } + /// Set the sponsor of the collection. /// /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. @@ -139,26 +221,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 +381,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 +558,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 +586,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 +633,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 +656,7 @@ /// Get collection owner. /// - /// @return Tuble with sponsor address and his substrate mirror. + /// @return Tuple 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,27 +678,29 @@ .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)?; + /// Get collection administrators + /// + /// @return Vector of tuples with admins address and his substrate mirror. + /// If address is canonical then substrate mirror is zero and vice versa. + fn collection_admins(&self) -> Result> { + let result = crate::IsAdmin::::iter_prefix((self.id,)) + .map(|(admin, _)| crate::eth::convert_cross_account_to_tuple::(&admin)) + .collect(); + Ok(result) + } - // 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::) - // } + /// 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)?; - // TODO: need implement AbiWriter for &Vec - // fn collection_admins(&self) -> Result> { - // let result = pallet_common::IsAdmin::::iter_prefix((self.id,)) - // .map(|(admin, _)| pallet_common::eth::convert_cross_account_to_tuple::(&admin)) - // .collect(); - // Ok(result) - // } + 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/common/src/lib.rs +++ b/pallets/common/src/lib.rs @@ -999,6 +999,13 @@ >::remove(collection.id); >::deposit_event(Event::CollectionDestroyed(collection.id)); + + >::deposit_log( + erc::CollectionHelpersEvents::CollectionDestroyed { + collection_id: eth::collection_id_to_address(collection.id), + } + .to_log(T::ContractAddress::get()), + ); Ok(()) } --- 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 0x62e22290 +/// @dev the ERC-165 identifier for this interface is 0xb3152af3 contract Collection is Dummy, ERC165 { /// Set collection property. /// @@ -33,6 +33,17 @@ dummy = 0; } + /// Set collection properties. + /// + /// @param properties Vector of properties key/value pair. + /// @dev EVM selector for this function is: 0x50b26b2a, + /// or in textual repr: setCollectionProperties((string,bytes)[]) + function setCollectionProperties(Tuple10[] memory properties) public { + require(false, stub_error); + properties; + dummy = 0; + } + /// Delete collection property. /// /// @param key Property key. @@ -44,6 +55,17 @@ dummy = 0; } + /// Delete collection properties. + /// + /// @param keys Properties keys. + /// @dev EVM selector for this function is: 0xee206ee3, + /// or in textual repr: deleteCollectionProperties(string[]) + function deleteCollectionProperties(string[] memory keys) public { + require(false, stub_error); + keys; + dummy = 0; + } + /// Get collection property. /// /// @dev Throws error if key not found. @@ -59,6 +81,19 @@ return hex""; } + /// Get collection properties. + /// + /// @param keys Properties keys. Empty keys for all propertyes. + /// @return Vector of properties key/value pairs. + /// @dev EVM selector for this function is: 0x285fb8e6, + /// or in textual repr: collectionProperties(string[]) + function collectionProperties(string[] memory keys) public view returns (Tuple10[] memory) { + require(false, stub_error); + keys; + dummy; + return new Tuple10[](0); + } + /// Set the sponsor of the collection. /// /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. @@ -72,6 +107,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 +203,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 +302,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 +324,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 +359,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 +385,7 @@ /// Get collection owner. /// - /// @return Tuble with sponsor address and his substrate mirror. + /// @return Tuple 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() @@ -303,10 +406,50 @@ newOwner; dummy = 0; } + + /// Get collection administrators + /// + /// @return Vector of tuples with admins address and his substrate mirror. + /// 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 (Tuple6[] memory) { + require(false, stub_error); + 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 anonymous struct +struct Tuple10 { + string field_0; + bytes field_1; +} + +/// @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. @@ -322,6 +465,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, @@ -332,6 +490,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/CHANGELOG.md +++ b/pallets/nonfungible/CHANGELOG.md @@ -2,12 +2,20 @@ All notable changes to this project will be documented in this file. + + +## [v0.1.6] - 2022-20-10 + +### Change + +- Added `set_properties` method for `TokenProperties` interface. + ## [v0.1.5] - 2022-08-24 ### Change - - Add bound `AsRef<[u8; 32]>` to `T::CrossAccountId`. - +- Add bound `AsRef<[u8; 32]>` to `T::CrossAccountId`. + ## [v0.1.4] 2022-08-16 ### Other changes @@ -28,7 +36,9 @@ - build: Upgrade polkadot to v0.9.25 cdfb9bdc7b205ff1b5134f034ef9973d769e5e6b ## [0.1.2] - 2022-07-25 + ### Changed + - New `token_uri` retrieval logic: If the collection has a `url` property and it is not empty, it is returned. @@ -39,8 +49,9 @@ otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings). ## [0.1.1] - 2022-07-14 + ### Added - Implementation of RPC method `token_owners`. - For reasons of compatibility with this pallet, returns only one owner if token exists. - This was an internal request to improve the web interface and support fractionalization event. + For reasons of compatibility with this pallet, returns only one owner if token exists. + This was an internal request to improve the web interface and support fractionalization event. --- a/pallets/nonfungible/Cargo.toml +++ b/pallets/nonfungible/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-nonfungible" -version = "0.1.5" +version = "0.1.6" license = "GPLv3" edition = "2021" --- 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; @@ -97,7 +98,7 @@ let key = >::from(key) .try_into() .map_err(|_| "key too long")?; - let value = value.try_into().map_err(|_| "value too long")?; + let value = value.0.try_into().map_err(|_| "value too long")?; let nesting_budget = self .recorder @@ -113,6 +114,48 @@ .map_err(dispatch_to_evm::) } + /// @notice Set token properties value. + /// @dev Throws error if `msg.sender` has no permission to edit the property. + /// @param tokenId ID of the token. + /// @param properties settable properties + #[weight(>::set_token_properties(properties.len() as u32))] + fn set_properties( + &mut self, + caller: caller, + token_id: uint256, + properties: Vec<(string, bytes)>, + ) -> Result<()> { + let caller = T::CrossAccountId::from_eth(caller); + let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?; + + let nesting_budget = self + .recorder + .weight_calls_budget(>::find_parent()); + + let properties = properties + .into_iter() + .map(|(key, value)| { + let key = >::from(key) + .try_into() + .map_err(|_| "key too large")?; + + let value = value.0.try_into().map_err(|_| "value too large")?; + + Ok(Property { key, value }) + }) + .collect::>>()?; + + >::set_token_properties( + self, + &caller, + TokenId(token_id), + properties.into_iter(), + >::token_exists(&self, TokenId(token_id)), + &nesting_budget, + ) + .map_err(dispatch_to_evm::) + } + /// @notice Delete token property value. /// @dev Throws error if `msg.sender` has no permission to edit the property. /// @param tokenId ID of the token. @@ -146,7 +189,7 @@ let props = >::get((self.id, token_id)); let prop = props.get(&key).ok_or("key not found")?; - Ok(prop.to_vec()) + Ok(prop.to_vec().into()) } } @@ -198,7 +241,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 +649,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 +665,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 +705,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 +751,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 @@ -18,7 +18,7 @@ } /// @title A contract that allows to set and delete token properties and change token property permissions. -/// @dev the ERC-165 identifier for this interface is 0x41369377 +/// @dev the ERC-165 identifier for this interface is 0x55dba919 contract TokenProperties is Dummy, ERC165 { /// @notice Set permissions for token property. /// @dev Throws error if `msg.sender` is not admin or owner of the collection. @@ -61,6 +61,19 @@ dummy = 0; } + /// @notice Set token properties value. + /// @dev Throws error if `msg.sender` has no permission to edit the property. + /// @param tokenId ID of the token. + /// @param properties settable properties + /// @dev EVM selector for this function is: 0x14ed3a6e, + /// or in textual repr: setProperties(uint256,(string,bytes)[]) + function setProperties(uint256 tokenId, Tuple19[] memory properties) public { + require(false, stub_error); + tokenId; + properties; + dummy = 0; + } + /// @notice Delete token property value. /// @dev Throws error if `msg.sender` has no permission to edit the property. /// @param tokenId ID of the token. @@ -91,7 +104,7 @@ } /// @title A contract that allows you to work with collections. -/// @dev the ERC-165 identifier for this interface is 0x62e22290 +/// @dev the ERC-165 identifier for this interface is 0xb3152af3 contract Collection is Dummy, ERC165 { /// Set collection property. /// @@ -106,6 +119,17 @@ dummy = 0; } + /// Set collection properties. + /// + /// @param properties Vector of properties key/value pair. + /// @dev EVM selector for this function is: 0x50b26b2a, + /// or in textual repr: setCollectionProperties((string,bytes)[]) + function setCollectionProperties(Tuple19[] memory properties) public { + require(false, stub_error); + properties; + dummy = 0; + } + /// Delete collection property. /// /// @param key Property key. @@ -117,6 +141,17 @@ dummy = 0; } + /// Delete collection properties. + /// + /// @param keys Properties keys. + /// @dev EVM selector for this function is: 0xee206ee3, + /// or in textual repr: deleteCollectionProperties(string[]) + function deleteCollectionProperties(string[] memory keys) public { + require(false, stub_error); + keys; + dummy = 0; + } + /// Get collection property. /// /// @dev Throws error if key not found. @@ -132,6 +167,19 @@ return hex""; } + /// Get collection properties. + /// + /// @param keys Properties keys. Empty keys for all propertyes. + /// @return Vector of properties key/value pairs. + /// @dev EVM selector for this function is: 0x285fb8e6, + /// or in textual repr: collectionProperties(string[]) + function collectionProperties(string[] memory keys) public view returns (Tuple19[] memory) { + require(false, stub_error); + keys; + dummy; + return new Tuple19[](0); + } + /// Set the sponsor of the collection. /// /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. @@ -145,6 +193,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 +238,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 +289,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 +388,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 +410,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 +445,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 +471,14 @@ /// Get collection owner. /// - /// @return Tuble with sponsor address and his substrate mirror. + /// @return Tuple 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 @@ -376,12 +492,36 @@ newOwner; dummy = 0; } + + /// Get collection administrators + /// + /// @return Vector of tuples with admins address and his substrate mirror. + /// 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 (Tuple6[] memory) { + require(false, stub_error); + 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 anonymous struct -struct Tuple17 { - address field_0; - uint256 field_1; +struct Tuple19 { + string field_0; + bytes field_1; } /// @title ERC-721 Non-Fungible Token Standard, optional metadata extension @@ -528,7 +668,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, @@ -548,6 +688,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. @@ -562,6 +717,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 @@ -577,6 +752,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() @@ -607,7 +797,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; @@ -618,11 +808,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/CHANGELOG.md +++ b/pallets/refungible/CHANGELOG.md @@ -2,12 +2,20 @@ All notable changes to this project will be documented in this file. +## [v0.2.5] - 2022-20-10 + +### Change + +- Added `set_properties` method for `TokenProperties` interface. + ## [v0.2.4] - 2022-08-24 ### Change - - Add bound `AsRef<[u8; 32]>` to `T::CrossAccountId`. +- Add bound `AsRef<[u8; 32]>` to `T::CrossAccountId`. + + ## [v0.2.3] 2022-08-16 ### Other changes --- a/pallets/refungible/Cargo.toml +++ b/pallets/refungible/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-refungible" -version = "0.2.4" +version = "0.2.5" license = "GPLv3" edition = "2021" --- 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}; @@ -100,7 +101,7 @@ let key = >::from(key) .try_into() .map_err(|_| "key too long")?; - let value = value.try_into().map_err(|_| "value too long")?; + let value = value.0.try_into().map_err(|_| "value too long")?; let nesting_budget = self .recorder @@ -116,6 +117,47 @@ .map_err(dispatch_to_evm::) } + /// @notice Set token properties value. + /// @dev Throws error if `msg.sender` has no permission to edit the property. + /// @param tokenId ID of the token. + /// @param properties settable properties + fn set_properties( + &mut self, + caller: caller, + token_id: uint256, + properties: Vec<(string, bytes)>, + ) -> Result<()> { + let caller = T::CrossAccountId::from_eth(caller); + let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?; + + let nesting_budget = self + .recorder + .weight_calls_budget(>::find_parent()); + + let properties = properties + .into_iter() + .map(|(key, value)| { + let key = >::from(key) + .try_into() + .map_err(|_| "key too large")?; + + let value = value.0.try_into().map_err(|_| "value too large")?; + + Ok(Property { key, value }) + }) + .collect::>>()?; + + >::set_token_properties( + self, + &caller, + TokenId(token_id), + properties.into_iter(), + >::token_exists(&self, TokenId(token_id)), + &nesting_budget, + ) + .map_err(dispatch_to_evm::) + } + /// @notice Delete token property value. /// @dev Throws error if `msg.sender` has no permission to edit the property. /// @param tokenId ID of the token. @@ -149,7 +191,7 @@ let props = >::get((self.id, token_id)); let prop = props.get(&key).ok_or("key not found")?; - Ok(prop.to_vec()) + Ok(prop.to_vec().into()) } } @@ -194,7 +236,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 +676,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 +707,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 +761,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 @@ -18,7 +18,7 @@ } /// @title A contract that allows to set and delete token properties and change token property permissions. -/// @dev the ERC-165 identifier for this interface is 0x41369377 +/// @dev the ERC-165 identifier for this interface is 0x55dba919 contract TokenProperties is Dummy, ERC165 { /// @notice Set permissions for token property. /// @dev Throws error if `msg.sender` is not admin or owner of the collection. @@ -61,6 +61,19 @@ dummy = 0; } + /// @notice Set token properties value. + /// @dev Throws error if `msg.sender` has no permission to edit the property. + /// @param tokenId ID of the token. + /// @param properties settable properties + /// @dev EVM selector for this function is: 0x14ed3a6e, + /// or in textual repr: setProperties(uint256,(string,bytes)[]) + function setProperties(uint256 tokenId, Tuple19[] memory properties) public { + require(false, stub_error); + tokenId; + properties; + dummy = 0; + } + /// @notice Delete token property value. /// @dev Throws error if `msg.sender` has no permission to edit the property. /// @param tokenId ID of the token. @@ -91,7 +104,7 @@ } /// @title A contract that allows you to work with collections. -/// @dev the ERC-165 identifier for this interface is 0x62e22290 +/// @dev the ERC-165 identifier for this interface is 0xb3152af3 contract Collection is Dummy, ERC165 { /// Set collection property. /// @@ -106,6 +119,17 @@ dummy = 0; } + /// Set collection properties. + /// + /// @param properties Vector of properties key/value pair. + /// @dev EVM selector for this function is: 0x50b26b2a, + /// or in textual repr: setCollectionProperties((string,bytes)[]) + function setCollectionProperties(Tuple19[] memory properties) public { + require(false, stub_error); + properties; + dummy = 0; + } + /// Delete collection property. /// /// @param key Property key. @@ -117,6 +141,17 @@ dummy = 0; } + /// Delete collection properties. + /// + /// @param keys Properties keys. + /// @dev EVM selector for this function is: 0xee206ee3, + /// or in textual repr: deleteCollectionProperties(string[]) + function deleteCollectionProperties(string[] memory keys) public { + require(false, stub_error); + keys; + dummy = 0; + } + /// Get collection property. /// /// @dev Throws error if key not found. @@ -132,6 +167,19 @@ return hex""; } + /// Get collection properties. + /// + /// @param keys Properties keys. Empty keys for all propertyes. + /// @return Vector of properties key/value pairs. + /// @dev EVM selector for this function is: 0x285fb8e6, + /// or in textual repr: collectionProperties(string[]) + function collectionProperties(string[] memory keys) public view returns (Tuple19[] memory) { + require(false, stub_error); + keys; + dummy; + return new Tuple19[](0); + } + /// Set the sponsor of the collection. /// /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. @@ -145,6 +193,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 +238,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 +289,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 +388,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 +410,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 +445,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 +471,14 @@ /// Get collection owner. /// - /// @return Tuble with sponsor address and his substrate mirror. + /// @return Tuple 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 @@ -376,12 +492,36 @@ newOwner; dummy = 0; } + + /// Get collection administrators + /// + /// @return Vector of tuples with admins address and his substrate mirror. + /// 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 (Tuple6[] memory) { + require(false, stub_error); + 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 anonymous struct -struct Tuple17 { - address field_0; - uint256 field_1; +struct Tuple19 { + string field_0; + bytes field_1; } /// @dev the ERC-165 identifier for this interface is 0x5b5e139f @@ -526,7 +666,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, @@ -561,6 +701,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 @@ -577,6 +737,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() @@ -608,7 +784,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 +806,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/unique/CHANGELOG.md +++ b/pallets/unique/CHANGELOG.md @@ -4,33 +4,40 @@ +## [v0.2.1] 2022-10-10 + +### Changes + +- Added `destroyCollection` and `createFTCollection` methods to **CollectionHelpers**. + ## [v0.2.0] 2022-09-13 ### Changes -- Change **collectionHelper** method `createRefungibleCollection` to `createRFTCollection`, +- Change **collectionHelper** method `createRefungibleCollection` to `createRFTCollection`, + ## [v0.1.4] 2022-09-05 ### Added -- Methods `force_set_sponsor` , `force_remove_collection_sponsor` to be able to administer sponsorships with other pallets. Added to implement `AppPromotion` pallet logic. +- Methods `force_set_sponsor` , `force_remove_collection_sponsor` to be able to administer sponsorships with other pallets. Added to implement `AppPromotion` pallet logic. ## [v0.1.3] 2022-08-16 ### Other changes -- build: Upgrade polkadot to v0.9.27 2c498572636f2b34d53b1c51b7283a761a7dc90a +- build: Upgrade polkadot to v0.9.27 2c498572636f2b34d53b1c51b7283a761a7dc90a -- build: Upgrade polkadot to v0.9.26 85515e54c4ca1b82a2630034e55dcc804c643bf8 +- build: Upgrade polkadot to v0.9.26 85515e54c4ca1b82a2630034e55dcc804c643bf8 -- refactor: Remove `#[transactional]` from extrinsics 7fd36cea2f6e00c02c67ccc1de9649ae404efd31 +- refactor: Remove `#[transactional]` from extrinsics 7fd36cea2f6e00c02c67ccc1de9649ae404efd31 Every extrinsic now runs in transaction implicitly, and `#[transactional]` on pallet dispatchable is now meaningless Upstream-Change: https://github.com/paritytech/substrate/issues/10806 -- refactor: Switch to new prefix removal methods 26734e9567589d75cdd99e404eabf11d5a97d975 +- refactor: Switch to new prefix removal methods 26734e9567589d75cdd99e404eabf11d5a97d975 New methods allows to call `remove_prefix` with limit multiple times in the same block @@ -39,12 +46,12 @@ Upstream-Change: https://github.com/paritytech/substrate/pull/11490 -- build: Upgrade polkadot to v0.9.25 cdfb9bdc7b205ff1b5134f034ef9973d769e5e6b +- build: Upgrade polkadot to v0.9.25 cdfb9bdc7b205ff1b5134f034ef9973d769e5e6b ## [v0.1.1] - 2022-07-25 ### Added -- Method for creating `ERC721Metadata` compatible NFT collection. -- Method for creating `ERC721Metadata` compatible ReFungible collection. -- Method for creating ReFungible collection. +- Method for creating `ERC721Metadata` compatible NFT collection. +- Method for creating `ERC721Metadata` compatible ReFungible collection. +- Method for creating ReFungible collection. --- a/pallets/unique/Cargo.toml +++ b/pallets/unique/Cargo.toml @@ -9,7 +9,7 @@ license = 'GPLv3' name = 'pallet-unique' repository = 'https://github.com/UniqueNetwork/unique-chain' -version = "0.2.0" +version = "0.2.1" [package.metadata.docs.rs] targets = ['x86_64-unknown-linux-gnu'] --- a/pallets/unique/src/eth/mod.rs +++ b/pallets/unique/src/eth/mod.rs @@ -18,29 +18,29 @@ use core::marker::PhantomData; use ethereum as _; -use evm_coder::{execution::*, generate_stubgen, solidity_interface, solidity, weight, types::*}; +use evm_coder::{execution::*, generate_stubgen, solidity, solidity_interface, types::*, weight}; use frame_support::traits::Get; + +use crate::Pallet; + use pallet_common::{ CollectionById, dispatch::CollectionDispatch, - erc::{ - CollectionHelpersEvents, - static_property::{key}, - }, + erc::{static_property::key, CollectionHelpersEvents}, Pallet as PalletCommon, }; +use pallet_evm::{account::CrossAccountId, OnMethodCall, PrecompileHandle, PrecompileResult}; use pallet_evm_coder_substrate::{dispatch_to_evm, SubstrateRecorder, WithRecorder}; -use pallet_evm::{account::CrossAccountId, OnMethodCall, PrecompileHandle, PrecompileResult}; use sp_std::vec; use up_data_structs::{ - CollectionName, CollectionDescription, CollectionTokenPrefix, CreateCollectionData, - CollectionMode, PropertyValue, CollectionFlags, + CollectionDescription, CollectionMode, CollectionName, CollectionTokenPrefix, + CreateCollectionData, }; -use crate::{Config, SelfWeightOf, weights::WeightInfo}; +use crate::{weights::WeightInfo, Config, SelfWeightOf}; +use alloc::format; use sp_std::vec::Vec; -use alloc::format; /// See [`CollectionHelpersCall`] pub struct EvmCollectionHelpers(SubstrateRecorder); @@ -84,12 +84,12 @@ Ok((caller, name, description, token_prefix)) } -fn create_refungible_collection_internal< - T: Config + pallet_nonfungible::Config + pallet_refungible::Config, ->( +#[inline(always)] +fn create_collection_internal( caller: caller, value: value, name: string, + collection_mode: CollectionMode, description: string, token_prefix: string, ) -> Result
{ @@ -97,7 +97,7 @@ convert_data::(caller, name, description, token_prefix)?; let data = CreateCollectionData { name, - mode: CollectionMode::ReFungible, + mode: collection_mode, description, token_prefix, ..Default::default() @@ -193,7 +193,14 @@ description: string, token_prefix: string, ) -> Result
{ - self.create_nft_collection(caller, value, name, description, token_prefix) + create_collection_internal::( + caller, + value, + name, + CollectionMode::NFT, + description, + token_prefix, + ) } #[weight(>::create_collection())] @@ -206,7 +213,35 @@ description: string, token_prefix: string, ) -> Result
{ - create_refungible_collection_internal::(caller, value, name, description, token_prefix) + create_collection_internal::( + caller, + value, + name, + CollectionMode::ReFungible, + description, + token_prefix, + ) + } + + #[weight(>::create_collection())] + #[solidity(rename_selector = "createFTCollection")] + fn create_fungible_collection( + &mut self, + caller: caller, + value: value, + name: string, + decimals: uint8, + description: string, + token_prefix: string, + ) -> Result
{ + create_collection_internal::( + caller, + value, + name, + CollectionMode::Fungible(decimals), + description, + token_prefix, + ) } #[solidity(rename_selector = "makeCollectionERC721MetadataCompatible")] @@ -296,6 +331,16 @@ Ok(()) } + #[weight(>::destroy_collection())] + fn destroy_collection(&mut self, caller: caller, collection_address: address) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + + let collection_id = pallet_common::eth::map_eth_to_id(&collection_address) + .ok_or("Invalid collection address format")?; + >::destroy_collection_internal(caller, collection_id) + .map_err(pallet_evm_coder_substrate::dispatch_to_evm::) + } + /// Check if a collection exists /// @param collectionAddress Address of the collection in question /// @return bool Does the collection exist? --- a/pallets/unique/src/eth/stubs/CollectionHelpers.sol +++ b/pallets/unique/src/eth/stubs/CollectionHelpers.sol @@ -20,10 +20,11 @@ /// @dev inlined interface contract CollectionHelpersEvents { event CollectionCreated(address indexed owner, address indexed collectionId); + event CollectionDestroyed(address indexed collectionId); } /// @title Contract, which allows users to operate with collections -/// @dev the ERC-165 identifier for this interface is 0x58918631 +/// @dev the ERC-165 identifier for this interface is 0x7dea03b1 contract CollectionHelpers is Dummy, ERC165, CollectionHelpersEvents { /// Create an NFT collection /// @param name Name of the collection @@ -76,6 +77,23 @@ return 0x0000000000000000000000000000000000000000; } + /// @dev EVM selector for this function is: 0x7335b79f, + /// or in textual repr: createFTCollection(string,uint8,string,string) + function createFTCollection( + string memory name, + uint8 decimals, + string memory description, + string memory tokenPrefix + ) public payable returns (address) { + require(false, stub_error); + name; + decimals; + description; + tokenPrefix; + dummy = 0; + return 0x0000000000000000000000000000000000000000; + } + /// @dev EVM selector for this function is: 0x85624258, /// or in textual repr: makeCollectionERC721MetadataCompatible(address,string) function makeCollectionERC721MetadataCompatible(address collection, string memory baseUri) public { @@ -85,6 +103,14 @@ dummy = 0; } + /// @dev EVM selector for this function is: 0x564e321f, + /// or in textual repr: destroyCollection(address) + function destroyCollection(address collectionAddress) public { + require(false, stub_error); + collectionAddress; + dummy = 0; + } + /// Check if a collection exists /// @param collectionAddress Address of the collection in question /// @return bool Does the collection exist? --- a/pallets/unique/src/lib.rs +++ b/pallets/unique/src/lib.rs @@ -362,25 +362,8 @@ #[weight = >::destroy_collection()] pub fn destroy_collection(origin, collection_id: CollectionId) -> DispatchResult { let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?); - let collection = >::try_get(collection_id)?; - collection.check_is_internal()?; - - // ========= - - T::CollectionDispatch::destroy(sender, collection)?; - // TODO: basket cleanup should be moved elsewhere - // Maybe runtime dispatch.rs should perform it? - - let _ = >::clear_prefix(collection_id, u32::MAX, None); - let _ = >::clear_prefix(collection_id, u32::MAX, None); - let _ = >::clear_prefix((collection_id,), u32::MAX, None); - - let _ = >::clear_prefix(collection_id, u32::MAX, None); - let _ = >::clear_prefix(collection_id, u32::MAX, None); - let _ = >::clear_prefix((collection_id,), u32::MAX, None); - - Ok(()) + Self::destroy_collection_internal(sender, collection_id) } /// Add an address to allow list. @@ -1151,4 +1134,28 @@ target_collection.save() } + + #[inline(always)] + pub(crate) fn destroy_collection_internal( + sender: T::CrossAccountId, + collection_id: CollectionId, + ) -> DispatchResult { + let collection = >::try_get(collection_id)?; + collection.check_is_internal()?; + + T::CollectionDispatch::destroy(sender, collection)?; + + // TODO: basket cleanup should be moved elsewhere + // Maybe runtime dispatch.rs should perform it? + + let _ = >::clear_prefix(collection_id, u32::MAX, None); + let _ = >::clear_prefix(collection_id, u32::MAX, None); + let _ = >::clear_prefix((collection_id,), u32::MAX, None); + + let _ = >::clear_prefix(collection_id, u32::MAX, None); + let _ = >::clear_prefix(collection_id, u32::MAX, None); + let _ = >::clear_prefix((collection_id,), u32::MAX, None); + + Ok(()) + } } --- a/tests/.vscode/settings.json +++ b/tests/.vscode/settings.json @@ -1,5 +1,12 @@ { - "mocha.enabled": true, - "mochaExplorer.files": "**/*.test.ts", - "mochaExplorer.require": "ts-node/register" + "mocha.enabled": true, + "mochaExplorer.files": "**/*.test.ts", + "mochaExplorer.require": "ts-node/register", + "eslint.format.enable": true, + "[javascript]": { + "editor.defaultFormatter": "dbaeumer.vscode-eslint" + }, + "[typescript]": { + "editor.defaultFormatter": "dbaeumer.vscode-eslint" + } } --- 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/CollectionHelpers.sol +++ b/tests/src/eth/api/CollectionHelpers.sol @@ -15,10 +15,11 @@ /// @dev inlined interface interface CollectionHelpersEvents { event CollectionCreated(address indexed owner, address indexed collectionId); + event CollectionDestroyed(address indexed collectionId); } /// @title Contract, which allows users to operate with collections -/// @dev the ERC-165 identifier for this interface is 0x58918631 +/// @dev the ERC-165 identifier for this interface is 0x7dea03b1 interface CollectionHelpers is Dummy, ERC165, CollectionHelpersEvents { /// Create an NFT collection /// @param name Name of the collection @@ -50,10 +51,23 @@ string memory tokenPrefix ) external payable returns (address); + /// @dev EVM selector for this function is: 0x7335b79f, + /// or in textual repr: createFTCollection(string,uint8,string,string) + function createFTCollection( + string memory name, + uint8 decimals, + string memory description, + string memory tokenPrefix + ) external payable returns (address); + /// @dev EVM selector for this function is: 0x85624258, /// or in textual repr: makeCollectionERC721MetadataCompatible(address,string) function makeCollectionERC721MetadataCompatible(address collection, string memory baseUri) external; + /// @dev EVM selector for this function is: 0x564e321f, + /// or in textual repr: destroyCollection(address) + function destroyCollection(address collectionAddress) external; + /// Check if a collection exists /// @param collectionAddress Address of the collection in question /// @return bool Does the collection exist? --- 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 0x62e22290 +/// @dev the ERC-165 identifier for this interface is 0xb3152af3 interface Collection is Dummy, ERC165 { /// Set collection property. /// @@ -23,6 +23,13 @@ /// or in textual repr: setCollectionProperty(string,bytes) function setCollectionProperty(string memory key, bytes memory value) external; + /// Set collection properties. + /// + /// @param properties Vector of properties key/value pair. + /// @dev EVM selector for this function is: 0x50b26b2a, + /// or in textual repr: setCollectionProperties((string,bytes)[]) + function setCollectionProperties(Tuple10[] memory properties) external; + /// Delete collection property. /// /// @param key Property key. @@ -30,6 +37,13 @@ /// or in textual repr: deleteCollectionProperty(string) function deleteCollectionProperty(string memory key) external; + /// Delete collection properties. + /// + /// @param keys Properties keys. + /// @dev EVM selector for this function is: 0xee206ee3, + /// or in textual repr: deleteCollectionProperties(string[]) + function deleteCollectionProperties(string[] memory keys) external; + /// Get collection property. /// /// @dev Throws error if key not found. @@ -40,6 +54,14 @@ /// or in textual repr: collectionProperty(string) function collectionProperty(string memory key) external view returns (bytes memory); + /// Get collection properties. + /// + /// @param keys Properties keys. Empty keys for all propertyes. + /// @return Vector of properties key/value pairs. + /// @dev EVM selector for this function is: 0x285fb8e6, + /// or in textual repr: collectionProperties(string[]) + function collectionProperties(string[] memory keys) external view returns (Tuple10[] memory); + /// Set the sponsor of the collection. /// /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. @@ -49,6 +71,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 +135,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 +196,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 +210,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 +232,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 +249,7 @@ /// Get collection owner. /// - /// @return Tuble with sponsor address and his substrate mirror. + /// @return Tuple 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() @@ -197,10 +262,36 @@ /// @dev EVM selector for this function is: 0x4f53e226, /// or in textual repr: changeCollectionOwner(address) function changeCollectionOwner(address newOwner) external; + + /// Get collection administrators + /// + /// @return Vector of tuples with admins address and his substrate mirror. + /// 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 (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 anonymous struct +struct Tuple10 { + string field_0; + bytes field_1; +} + +/// @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. @@ -210,11 +301,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 @@ -13,7 +13,7 @@ } /// @title A contract that allows to set and delete token properties and change token property permissions. -/// @dev the ERC-165 identifier for this interface is 0x41369377 +/// @dev the ERC-165 identifier for this interface is 0x55dba919 interface TokenProperties is Dummy, ERC165 { /// @notice Set permissions for token property. /// @dev Throws error if `msg.sender` is not admin or owner of the collection. @@ -43,6 +43,14 @@ bytes memory value ) external; + /// @notice Set token properties value. + /// @dev Throws error if `msg.sender` has no permission to edit the property. + /// @param tokenId ID of the token. + /// @param properties settable properties + /// @dev EVM selector for this function is: 0x14ed3a6e, + /// or in textual repr: setProperties(uint256,(string,bytes)[]) + function setProperties(uint256 tokenId, Tuple19[] memory properties) external; + /// @notice Delete token property value. /// @dev Throws error if `msg.sender` has no permission to edit the property. /// @param tokenId ID of the token. @@ -62,7 +70,7 @@ } /// @title A contract that allows you to work with collections. -/// @dev the ERC-165 identifier for this interface is 0x62e22290 +/// @dev the ERC-165 identifier for this interface is 0xb3152af3 interface Collection is Dummy, ERC165 { /// Set collection property. /// @@ -72,6 +80,13 @@ /// or in textual repr: setCollectionProperty(string,bytes) function setCollectionProperty(string memory key, bytes memory value) external; + /// Set collection properties. + /// + /// @param properties Vector of properties key/value pair. + /// @dev EVM selector for this function is: 0x50b26b2a, + /// or in textual repr: setCollectionProperties((string,bytes)[]) + function setCollectionProperties(Tuple19[] memory properties) external; + /// Delete collection property. /// /// @param key Property key. @@ -79,6 +94,13 @@ /// or in textual repr: deleteCollectionProperty(string) function deleteCollectionProperty(string memory key) external; + /// Delete collection properties. + /// + /// @param keys Properties keys. + /// @dev EVM selector for this function is: 0xee206ee3, + /// or in textual repr: deleteCollectionProperties(string[]) + function deleteCollectionProperties(string[] memory keys) external; + /// Get collection property. /// /// @dev Throws error if key not found. @@ -89,6 +111,14 @@ /// or in textual repr: collectionProperty(string) function collectionProperty(string memory key) external view returns (bytes memory); + /// Get collection properties. + /// + /// @param keys Properties keys. Empty keys for all propertyes. + /// @return Vector of properties key/value pairs. + /// @dev EVM selector for this function is: 0x285fb8e6, + /// or in textual repr: collectionProperties(string[]) + function collectionProperties(string[] memory keys) external view returns (Tuple19[] memory); + /// Set the sponsor of the collection. /// /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. @@ -98,6 +128,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 +159,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 +192,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 +253,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 +267,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 +289,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 +306,11 @@ /// Get collection owner. /// - /// @return Tuble with sponsor address and his substrate mirror. + /// @return Tuple 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 /// @@ -246,12 +319,28 @@ /// @dev EVM selector for this function is: 0x4f53e226, /// or in textual repr: changeCollectionOwner(address) function changeCollectionOwner(address newOwner) external; + + /// Get collection administrators + /// + /// @return Vector of tuples with admins address and his substrate mirror. + /// 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 (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 anonymous struct -struct Tuple17 { - address field_0; - uint256 field_1; +struct Tuple19 { + string field_0; + bytes field_1; } /// @title ERC-721 Non-Fungible Token Standard, optional metadata extension @@ -349,7 +438,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, @@ -361,6 +450,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. @@ -370,6 +469,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 @@ -380,6 +493,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() @@ -400,16 +523,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 @@ -13,7 +13,7 @@ } /// @title A contract that allows to set and delete token properties and change token property permissions. -/// @dev the ERC-165 identifier for this interface is 0x41369377 +/// @dev the ERC-165 identifier for this interface is 0x55dba919 interface TokenProperties is Dummy, ERC165 { /// @notice Set permissions for token property. /// @dev Throws error if `msg.sender` is not admin or owner of the collection. @@ -43,6 +43,14 @@ bytes memory value ) external; + /// @notice Set token properties value. + /// @dev Throws error if `msg.sender` has no permission to edit the property. + /// @param tokenId ID of the token. + /// @param properties settable properties + /// @dev EVM selector for this function is: 0x14ed3a6e, + /// or in textual repr: setProperties(uint256,(string,bytes)[]) + function setProperties(uint256 tokenId, Tuple19[] memory properties) external; + /// @notice Delete token property value. /// @dev Throws error if `msg.sender` has no permission to edit the property. /// @param tokenId ID of the token. @@ -62,7 +70,7 @@ } /// @title A contract that allows you to work with collections. -/// @dev the ERC-165 identifier for this interface is 0x62e22290 +/// @dev the ERC-165 identifier for this interface is 0xb3152af3 interface Collection is Dummy, ERC165 { /// Set collection property. /// @@ -72,6 +80,13 @@ /// or in textual repr: setCollectionProperty(string,bytes) function setCollectionProperty(string memory key, bytes memory value) external; + /// Set collection properties. + /// + /// @param properties Vector of properties key/value pair. + /// @dev EVM selector for this function is: 0x50b26b2a, + /// or in textual repr: setCollectionProperties((string,bytes)[]) + function setCollectionProperties(Tuple19[] memory properties) external; + /// Delete collection property. /// /// @param key Property key. @@ -79,6 +94,13 @@ /// or in textual repr: deleteCollectionProperty(string) function deleteCollectionProperty(string memory key) external; + /// Delete collection properties. + /// + /// @param keys Properties keys. + /// @dev EVM selector for this function is: 0xee206ee3, + /// or in textual repr: deleteCollectionProperties(string[]) + function deleteCollectionProperties(string[] memory keys) external; + /// Get collection property. /// /// @dev Throws error if key not found. @@ -89,6 +111,14 @@ /// or in textual repr: collectionProperty(string) function collectionProperty(string memory key) external view returns (bytes memory); + /// Get collection properties. + /// + /// @param keys Properties keys. Empty keys for all propertyes. + /// @return Vector of properties key/value pairs. + /// @dev EVM selector for this function is: 0x285fb8e6, + /// or in textual repr: collectionProperties(string[]) + function collectionProperties(string[] memory keys) external view returns (Tuple19[] memory); + /// Set the sponsor of the collection. /// /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. @@ -98,6 +128,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 +159,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 +192,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 +253,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 +267,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 +289,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 +306,11 @@ /// Get collection owner. /// - /// @return Tuble with sponsor address and his substrate mirror. + /// @return Tuple 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 /// @@ -246,12 +319,28 @@ /// @dev EVM selector for this function is: 0x4f53e226, /// or in textual repr: changeCollectionOwner(address) function changeCollectionOwner(address newOwner) external; + + /// Get collection administrators + /// + /// @return Vector of tuples with admins address and his substrate mirror. + /// 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 (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 anonymous struct -struct Tuple17 { - address field_0; - uint256 field_1; +struct Tuple19 { + string field_0; + bytes field_1; } /// @dev the ERC-165 identifier for this interface is 0x5b5e139f @@ -347,7 +436,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, @@ -369,6 +458,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 @@ -380,6 +483,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() @@ -401,7 +515,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 /// @@ -412,11 +526,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 @@ -49,18 +49,38 @@ .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 privateKey('//Bob'); + const newAdminCross = helper.ethCrossAccount.fromKeyringPair(newAdmin); + await collectionEvm.methods.addCollectionAdminCross(newAdminCross).send(); - const [newAdmin] = await helper.arrange.createAccounts([10n], donor); - await collectionEvm.methods.addCollectionAdminSubstrate(newAdmin.addressRaw).send(); + const adminList = await helper.collection.getAdmins(collectionId); + expect(adminList).to.be.like([{Substrate: newAdmin.address}]); + }); - const adminList = await helper.callRpc('api.rpc.unique.adminlist', [collectionId]); - expect(adminList[0].asSubstrate.toString().toLocaleLowerCase()) - .to.be.eq(newAdmin.address.toLocaleLowerCase()); - }); + // itEth('Check adminlist', 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 admin1 = helper.eth.createAccount(); + // const admin2 = await privateKey('admin'); + // await collectionEvm.methods.addCollectionAdmin(admin1).send(); + // await collectionEvm.methods.addCollectionAdminCross(helper.ethCrossAccount.fromKeyringPair(admin2)).send(); + + // const adminListRpc = await helper.collection.getAdmins(collectionId); + // let adminListEth = await collectionEvm.methods.collectionAdmins().call(); + // adminListEth = adminListEth.map((element: IEthCrossAccountId) => { + // return helper.address.convertCrossAccountFromEthCrossAcoount(element); + // }); + // expect(adminListRpc).to.be.like(adminListEth); + // }); itEth('Verify owner or admin', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); @@ -68,11 +88,31 @@ const newAdmin = helper.eth.createAccount(); const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); + expect(await collectionEvm.methods.isOwnerOrAdmin(newAdmin).call()).to.be.false; await collectionEvm.methods.addCollectionAdmin(newAdmin).send(); expect(await collectionEvm.methods.isOwnerOrAdmin(newAdmin).call()).to.be.true; }); + // itEth.skip('Check adminlist', 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 admin1 = helper.eth.createAccount(); + // const admin2 = await privateKey('admin'); + // await collectionEvm.methods.addCollectionAdmin(admin1).send(); + // await collectionEvm.methods.addCollectionAdminSubstrate(admin2.addressRaw).send(); + + // const adminListRpc = await helper.collection.getAdmins(collectionId); + // let adminListEth = await collectionEvm.methods.collectionAdmins().call(); + // adminListEth = adminListEth.map((element: IEthCrossAccountId) => { + // return helper.address.convertCrossAccountFromEthCrossAcoount(element); + // }); + // expect(adminListRpc).to.be.like(adminListEth); + // }); + itEth('(!negative tests!) Add admin by ADMIN is not allowed', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C'); --- a/tests/src/eth/collectionHelpersAbi.json +++ b/tests/src/eth/collectionHelpersAbi.json @@ -19,6 +19,19 @@ "type": "event" }, { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "collectionId", + "type": "address" + } + ], + "name": "CollectionDestroyed", + "type": "event" + }, + { "inputs": [], "name": "collectionCreationFee", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], @@ -28,6 +41,18 @@ { "inputs": [ { "internalType": "string", "name": "name", "type": "string" }, + { "internalType": "uint8", "name": "decimals", "type": "uint8" }, + { "internalType": "string", "name": "description", "type": "string" }, + { "internalType": "string", "name": "tokenPrefix", "type": "string" } + ], + "name": "createFTCollection", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "string", "name": "name", "type": "string" }, { "internalType": "string", "name": "description", "type": "string" }, { "internalType": "string", "name": "tokenPrefix", "type": "string" } ], @@ -55,6 +80,19 @@ "type": "address" } ], + "name": "destroyCollection", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "collectionAddress", + "type": "address" + } + ], "name": "isCollectionExist", "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], "stateMutability": "view", --- a/tests/src/eth/collectionProperties.test.ts +++ b/tests/src/eth/collectionProperties.test.ts @@ -18,6 +18,7 @@ import {Pallets} from '../util'; import {IProperty, ITokenPropertyPermission} from '../util/playgrounds/types'; import {IKeyringPair} from '@polkadot/types/types'; +import {TCollectionMode} from '../util/playgrounds/types'; describe('EVM collection properties', () => { let donor: IKeyringPair; @@ -161,3 +162,91 @@ await checkERC721Metadata(helper, 'rft'); }); }); + +describe('EVM collection property', () => { + let donor: IKeyringPair; + + before(async function() { + await usingEthPlaygrounds(async (_helper, privateKey) => { + donor = await privateKey({filename: __filename}); + }); + }); + + async function testSetReadProperties(helper: EthUniqueHelper, mode: TCollectionMode) { + const collection = await helper[mode].mintCollection(donor, {name: 'A', description: 'B', tokenPrefix: 'C'}); + + const sender = await helper.eth.createAccountWithBalance(donor, 100n); + await collection.addAdmin(donor, {Ethereum: sender}); + + const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId); + const contract = helper.ethNativeContract.collection(collectionAddress, mode, sender); + + const keys = ['key0', 'key1']; + + const writeProperties = [ + helper.ethProperty.property(keys[0], 'value0'), + helper.ethProperty.property(keys[1], 'value1'), + ]; + + await contract.methods.setCollectionProperties(writeProperties).send(); + const readProperties = await contract.methods.collectionProperties([keys[0], keys[1]]).call(); + expect(readProperties).to.be.like(writeProperties); + } + + itEth('Set/read properties ft', async ({helper}) => { + await testSetReadProperties(helper, 'ft'); + }); + itEth.ifWithPallets('Set/read properties rft', [Pallets.ReFungible], async ({helper}) => { + await testSetReadProperties(helper, 'rft'); + }); + itEth('Set/read properties nft', async ({helper}) => { + await testSetReadProperties(helper, 'nft'); + }); + + async function testDeleteProperties(helper: EthUniqueHelper, mode: TCollectionMode) { + const collection = await helper[mode].mintCollection(donor, {name: 'A', description: 'B', tokenPrefix: 'C'}); + + const sender = await helper.eth.createAccountWithBalance(donor, 100n); + await collection.addAdmin(donor, {Ethereum: sender}); + + const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId); + const contract = helper.ethNativeContract.collection(collectionAddress, mode, sender); + + const keys = ['key0', 'key1', 'key2', 'key3']; + + { + const writeProperties = [ + helper.ethProperty.property(keys[0], 'value0'), + helper.ethProperty.property(keys[1], 'value1'), + helper.ethProperty.property(keys[2], 'value2'), + helper.ethProperty.property(keys[3], 'value3'), + ]; + + await contract.methods.setCollectionProperties(writeProperties).send(); + const readProperties = await contract.methods.collectionProperties([keys[0], keys[1], keys[2], keys[3]]).call(); + expect(readProperties).to.be.like(writeProperties); + } + + { + const expectProperties = [ + helper.ethProperty.property(keys[0], 'value0'), + helper.ethProperty.property(keys[1], 'value1'), + ]; + + await contract.methods.deleteCollectionProperties([keys[2], keys[3]]).send(); + const readProperties = await contract.methods.collectionProperties([]).call(); + expect(readProperties).to.be.like(expectProperties); + } + } + + itEth('Delete properties ft', async ({helper}) => { + await testDeleteProperties(helper, 'ft'); + }); + itEth.ifWithPallets('Delete properties rft', [Pallets.ReFungible], async ({helper}) => { + await testDeleteProperties(helper, 'rft'); + }); + itEth('Delete properties nft', async ({helper}) => { + await testDeleteProperties(helper, 'nft'); + }); + +}); --- /dev/null +++ b/tests/src/eth/createFTCollection.test.ts @@ -0,0 +1,262 @@ +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// This file is part of Unique Network. + +// Unique Network is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Unique Network is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Unique Network. If not, see . + +import {IKeyringPair} from '@polkadot/types/types'; +import {evmToAddress} from '@polkadot/util-crypto'; +import {Pallets, requirePalletsOrSkip} from '../util'; +import {expect, itEth, usingEthPlaygrounds} from './util'; + +const DECIMALS = 18; + +describe('Create FT collection from EVM', () => { + let donor: IKeyringPair; + + before(async function() { + await usingEthPlaygrounds(async (helper, privateKey) => { + requirePalletsOrSkip(this, helper, [Pallets.Fungible]); + donor = await privateKey({filename: __filename}); + }); + }); + + itEth('Create collection', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + + const name = 'CollectionEVM'; + const description = 'Some description'; + const prefix = 'token prefix'; + + // todo:playgrounds this might fail when in async environment. + const collectionCountBefore = +(await helper.callRpc('api.rpc.unique.collectionStats')).created; + + const {collectionId} = await helper.eth.createFungibleCollection(owner, name, DECIMALS, description, prefix); + + const collectionCountAfter = +(await helper.callRpc('api.rpc.unique.collectionStats')).created; + const data = (await helper.ft.getData(collectionId))!; + + expect(collectionCountAfter - collectionCountBefore).to.be.eq(1); + expect(collectionId).to.be.eq(collectionCountAfter); + expect(data.name).to.be.eq(name); + expect(data.description).to.be.eq(description); + expect(data.raw.tokenPrefix).to.be.eq(prefix); + expect(data.raw.mode).to.be.deep.eq({Fungible: DECIMALS.toString()}); + }); + + // todo:playgrounds this test will fail when in async environment. + itEth('Check collection address exist', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + + const expectedCollectionId = +(await helper.callRpc('api.rpc.unique.collectionStats')).created + 1; + const expectedCollectionAddress = helper.ethAddress.fromCollectionId(expectedCollectionId); + const collectionHelpers = helper.ethNativeContract.collectionHelpers(owner); + + expect(await collectionHelpers.methods + .isCollectionExist(expectedCollectionAddress) + .call()).to.be.false; + + + await helper.eth.createFungibleCollection(owner, 'A', DECIMALS, 'A', 'A'); + + + expect(await collectionHelpers.methods + .isCollectionExist(expectedCollectionAddress) + .call()).to.be.true; + }); + + itEth('Set sponsorship', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + const sponsor = await helper.eth.createAccountWithBalance(donor); + const ss58Format = helper.chain.getChainProperties().ss58Format; + const {collectionId, collectionAddress} = await helper.eth.createFungibleCollection(owner, 'Sponsor', DECIMALS, 'absolutely anything', 'ENVY'); + + const collection = helper.ethNativeContract.collection(collectionAddress, 'rft', owner); + await collection.methods.setCollectionSponsor(sponsor).send(); + + let data = (await helper.rft.getData(collectionId))!; + expect(data.raw.sponsorship.Unconfirmed).to.be.equal(evmToAddress(sponsor, Number(ss58Format))); + + await expect(collection.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('caller is not set as sponsor'); + + const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'rft', sponsor); + await sponsorCollection.methods.confirmCollectionSponsorship().send(); + + data = (await helper.rft.getData(collectionId))!; + expect(data.raw.sponsorship.Confirmed).to.be.equal(evmToAddress(sponsor, Number(ss58Format))); + }); + + itEth('Set limits', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + const {collectionId, collectionAddress} = await helper.eth.createFungibleCollection(owner, 'Limits', DECIMALS, 'absolutely anything', 'INSI'); + const limits = { + accountTokenOwnershipLimit: 1000, + sponsoredDataSize: 1024, + sponsoredDataRateLimit: 30, + tokenLimit: 1000000, + sponsorTransferTimeout: 6, + sponsorApproveTimeout: 6, + ownerCanTransfer: false, + ownerCanDestroy: false, + transfersEnabled: false, + }; + + const collection = helper.ethNativeContract.collection(collectionAddress, 'ft', owner); + await collection.methods['setCollectionLimit(string,uint32)']('accountTokenOwnershipLimit', limits.accountTokenOwnershipLimit).send(); + await collection.methods['setCollectionLimit(string,uint32)']('sponsoredDataSize', limits.sponsoredDataSize).send(); + await collection.methods['setCollectionLimit(string,uint32)']('sponsoredDataRateLimit', limits.sponsoredDataRateLimit).send(); + await collection.methods['setCollectionLimit(string,uint32)']('tokenLimit', limits.tokenLimit).send(); + await collection.methods['setCollectionLimit(string,uint32)']('sponsorTransferTimeout', limits.sponsorTransferTimeout).send(); + await collection.methods['setCollectionLimit(string,uint32)']('sponsorApproveTimeout', limits.sponsorApproveTimeout).send(); + await collection.methods['setCollectionLimit(string,bool)']('ownerCanTransfer', limits.ownerCanTransfer).send(); + await collection.methods['setCollectionLimit(string,bool)']('ownerCanDestroy', limits.ownerCanDestroy).send(); + await collection.methods['setCollectionLimit(string,bool)']('transfersEnabled', limits.transfersEnabled).send(); + + const data = (await helper.rft.getData(collectionId))!; + expect(data.raw.limits.accountTokenOwnershipLimit).to.be.eq(limits.accountTokenOwnershipLimit); + expect(data.raw.limits.sponsoredDataSize).to.be.eq(limits.sponsoredDataSize); + expect(data.raw.limits.sponsoredDataRateLimit.blocks).to.be.eq(limits.sponsoredDataRateLimit); + expect(data.raw.limits.tokenLimit).to.be.eq(limits.tokenLimit); + expect(data.raw.limits.sponsorTransferTimeout).to.be.eq(limits.sponsorTransferTimeout); + expect(data.raw.limits.sponsorApproveTimeout).to.be.eq(limits.sponsorApproveTimeout); + expect(data.raw.limits.ownerCanTransfer).to.be.eq(limits.ownerCanTransfer); + expect(data.raw.limits.ownerCanDestroy).to.be.eq(limits.ownerCanDestroy); + expect(data.raw.limits.transfersEnabled).to.be.eq(limits.transfersEnabled); + }); + + itEth('Collection address exist', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + const collectionAddressForNonexistentCollection = '0x17C4E6453CC49AAAAEACA894E6D9683E00112233'; + expect(await helper.ethNativeContract.collectionHelpers(collectionAddressForNonexistentCollection) + .methods.isCollectionExist(collectionAddressForNonexistentCollection).call()) + .to.be.false; + + const {collectionAddress} = await helper.eth.createFungibleCollection(owner, 'Exister', DECIMALS, 'absolutely anything', 'WIWT'); + expect(await helper.ethNativeContract.collectionHelpers(collectionAddress) + .methods.isCollectionExist(collectionAddress).call()) + .to.be.true; + }); + + itEth('destroyCollection', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + const {collectionAddress} = await helper.eth.createFungibleCollection(owner, 'Exister', DECIMALS, 'absolutely anything', 'WIWT'); + const collectionHelper = helper.ethNativeContract.collectionHelpers(owner); + + const result = await collectionHelper.methods + .destroyCollection(collectionAddress) + .send({from: owner}); + + const events = helper.eth.normalizeEvents(result.events); + + expect(events).to.be.deep.equal([ + { + address: collectionHelper.options.address, + event: 'CollectionDestroyed', + args: { + collectionId: collectionAddress, + }, + }, + ]); + + expect(await collectionHelper.methods + .isCollectionExist(collectionAddress) + .call()).to.be.false; + }); +}); + +describe('(!negative tests!) Create FT collection from EVM', () => { + let donor: IKeyringPair; + let nominal: bigint; + + before(async function() { + await usingEthPlaygrounds(async (helper, privateKey) => { + requirePalletsOrSkip(this, helper, [Pallets.Fungible]); + donor = await privateKey({filename: __filename}); + nominal = helper.balance.getOneTokenNominal(); + }); + }); + + itEth('(!negative test!) Create collection (bad lengths)', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + const collectionHelper = helper.ethNativeContract.collectionHelpers(owner); + { + const MAX_NAME_LENGTH = 64; + const collectionName = 'A'.repeat(MAX_NAME_LENGTH + 1); + const description = 'A'; + const tokenPrefix = 'A'; + + await expect(collectionHelper.methods + .createFTCollection(collectionName, DECIMALS, description, tokenPrefix) + .call({value: Number(2n * nominal)})).to.be.rejectedWith('name is too long. Max length is ' + MAX_NAME_LENGTH); + } + { + const MAX_DESCRIPTION_LENGTH = 256; + const collectionName = 'A'; + const description = 'A'.repeat(MAX_DESCRIPTION_LENGTH + 1); + const tokenPrefix = 'A'; + await expect(collectionHelper.methods + .createFTCollection(collectionName, DECIMALS, description, tokenPrefix) + .call({value: Number(2n * nominal)})).to.be.rejectedWith('description is too long. Max length is ' + MAX_DESCRIPTION_LENGTH); + } + { + const MAX_TOKEN_PREFIX_LENGTH = 16; + const collectionName = 'A'; + const description = 'A'; + const tokenPrefix = 'A'.repeat(MAX_TOKEN_PREFIX_LENGTH + 1); + await expect(collectionHelper.methods + .createFTCollection(collectionName, DECIMALS, description, tokenPrefix) + .call({value: Number(2n * nominal)})).to.be.rejectedWith('token_prefix is too long. Max length is ' + MAX_TOKEN_PREFIX_LENGTH); + } + }); + + itEth('(!negative test!) Create collection (no funds)', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + const collectionHelper = helper.ethNativeContract.collectionHelpers(owner); + await expect(collectionHelper.methods + .createFTCollection('Peasantry', DECIMALS, 'absolutely anything', 'TWIW') + .call({value: Number(1n * nominal)})).to.be.rejectedWith('Sent amount not equals to collection creation price (2000000000000000000)'); + }); + + itEth('(!negative test!) Check owner', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + const peasant = helper.eth.createAccount(); + const {collectionAddress} = await helper.eth.createFungibleCollection(owner, 'Transgressed', DECIMALS, 'absolutely anything', 'YVNE'); + const peasantCollection = helper.ethNativeContract.collection(collectionAddress, 'ft', peasant); + const EXPECTED_ERROR = 'NoPermission'; + { + const sponsor = await helper.eth.createAccountWithBalance(donor); + await expect(peasantCollection.methods + .setCollectionSponsor(sponsor) + .call()).to.be.rejectedWith(EXPECTED_ERROR); + + const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'ft', sponsor); + await expect(sponsorCollection.methods + .confirmCollectionSponsorship() + .call()).to.be.rejectedWith('caller is not set as sponsor'); + } + { + await expect(peasantCollection.methods + .setCollectionLimit('account_token_ownership_limit', '1000') + .call()).to.be.rejectedWith(EXPECTED_ERROR); + } + }); + + itEth('(!negative test!) Set limits', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + const {collectionAddress} = await helper.eth.createFungibleCollection(owner, 'Limits', DECIMALS, 'absolutely anything', 'ISNI'); + const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'ft', owner); + await expect(collectionEvm.methods + .setCollectionLimit('badLimit', 'true') + .call()).to.be.rejectedWith('unknown boolean limit "badLimit"'); + }); +}); --- a/tests/src/eth/createNFTCollection.test.ts +++ b/tests/src/eth/createNFTCollection.test.ts @@ -22,7 +22,7 @@ describe('Create NFT collection from EVM', () => { let donor: IKeyringPair; - before(async function() { + before(async function () { await usingEthPlaygrounds(async (_helper, privateKey) => { donor = await privateKey({filename: __filename}); }); @@ -35,10 +35,28 @@ const description = 'Some description'; const prefix = 'token prefix'; - const {collectionId} = await helper.eth.createNFTCollection(owner, name, description, prefix); - const data = (await helper.rft.getData(collectionId))!; + // todo:playgrounds this might fail when in async environment. + const collectionCountBefore = +(await helper.callRpc('api.rpc.unique.collectionStats')).created; + const {collectionId, collectionAddress, events} = await helper.eth.createNFTCollection(owner, name, description, prefix); + + expect(events).to.be.deep.equal([ + { + address: '0x6C4E9fE1AE37a41E93CEE429e8E1881aBdcbb54F', + event: 'CollectionCreated', + args: { + owner: owner, + collectionId: collectionAddress, + }, + }, + ]); + + const collectionCountAfter = +(await helper.callRpc('api.rpc.unique.collectionStats')).created; + const collection = helper.nft.getCollectionObject(collectionId); - + const data = (await collection.getData())!; + + expect(collectionCountAfter - collectionCountBefore).to.be.eq(1); + expect(collectionId).to.be.eq(collectionCountAfter); expect(data.name).to.be.eq(name); expect(data.description).to.be.eq(description); expect(data.raw.tokenPrefix).to.be.eq(prefix); @@ -57,8 +75,19 @@ const prefix = 'token prefix'; const baseUri = 'BaseURI'; - const {collectionId} = await helper.eth.createERC721MetadataCompatibleNFTCollection(owner, name, description, prefix, baseUri); + const {collectionId, collectionAddress, events} = await helper.eth.createERC721MetadataCompatibleNFTCollection(owner, name, description, prefix, baseUri); + expect(events).to.be.deep.equal([ + { + address: '0x6C4E9fE1AE37a41E93CEE429e8E1881aBdcbb54F', + event: 'CollectionCreated', + args: { + owner: owner, + collectionId: collectionAddress, + }, + }, + ]); + const collection = helper.nft.getCollectionObject(collectionId); const data = (await collection.getData())!; @@ -95,12 +124,12 @@ await collectionHelpers.methods .createNFTCollection('A', 'A', 'A') .send({value: Number(2n * helper.balance.getOneTokenNominal())}); - + expect(await collectionHelpers.methods .isCollectionExist(expectedCollectionAddress) .call()).to.be.true; }); - + itEth('Set sponsorship', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); const sponsor = await helper.eth.createAccountWithBalance(donor); @@ -147,7 +176,7 @@ await collection.methods['setCollectionLimit(string,bool)']('ownerCanTransfer', limits.ownerCanTransfer).send(); await collection.methods['setCollectionLimit(string,bool)']('ownerCanDestroy', limits.ownerCanDestroy).send(); await collection.methods['setCollectionLimit(string,bool)']('transfersEnabled', limits.transfersEnabled).send(); - + const data = (await helper.nft.getData(collectionId))!; expect(data.raw.limits.accountTokenOwnershipLimit).to.be.eq(limits.accountTokenOwnershipLimit); expect(data.raw.limits.sponsoredDataSize).to.be.eq(limits.sponsoredDataSize); @@ -166,7 +195,7 @@ expect(await helper.ethNativeContract.collectionHelpers(collectionAddressForNonexistentCollection) .methods.isCollectionExist(collectionAddressForNonexistentCollection).call()) .to.be.false; - + const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'Exister', 'absolutely anything', 'EVC'); expect(await helper.ethNativeContract.collectionHelpers(collectionAddress) .methods.isCollectionExist(collectionAddress).call()) @@ -178,7 +207,7 @@ let donor: IKeyringPair; let nominal: bigint; - before(async function() { + before(async function () { await usingEthPlaygrounds(async (helper, privateKey) => { donor = await privateKey({filename: __filename}); nominal = helper.balance.getOneTokenNominal(); @@ -197,7 +226,7 @@ await expect(collectionHelper.methods .createNFTCollection(collectionName, description, tokenPrefix) .call({value: Number(2n * nominal)})).to.be.rejectedWith('name is too long. Max length is ' + MAX_NAME_LENGTH); - + } { const MAX_DESCRIPTION_LENGTH = 256; @@ -218,7 +247,7 @@ .call({value: Number(2n * nominal)})).to.be.rejectedWith('token_prefix is too long. Max length is ' + MAX_TOKEN_PREFIX_LENGTH); } }); - + itEth('(!negative test!) Create collection (no funds)', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); const collectionHelper = helper.ethNativeContract.collectionHelpers(owner); @@ -238,7 +267,7 @@ await expect(malfeasantCollection.methods .setCollectionSponsor(sponsor) .call()).to.be.rejectedWith(EXPECTED_ERROR); - + const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'nft', sponsor); await expect(sponsorCollection.methods .confirmCollectionSponsorship() @@ -259,4 +288,31 @@ .setCollectionLimit('badLimit', 'true') .call()).to.be.rejectedWith('unknown boolean limit "badLimit"'); }); -}); + + itEth('destroyCollection', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'Limits', 'absolutely anything', 'OLF'); + const collectionHelper = helper.ethNativeContract.collectionHelpers(owner); + + + const result = await collectionHelper.methods + .destroyCollection(collectionAddress) + .send({from: owner}); + + const events = helper.eth.normalizeEvents(result.events); + + expect(events).to.be.deep.equal([ + { + address: collectionHelper.options.address, + event: 'CollectionDestroyed', + args: { + collectionId: collectionAddress, + }, + }, + ]); + + expect(await collectionHelper.methods + .isCollectionExist(collectionAddress) + .call()).to.be.false; + }); +}); \ No newline at end of file --- a/tests/src/eth/createRFTCollection.test.ts +++ b/tests/src/eth/createRFTCollection.test.ts @@ -263,4 +263,18 @@ .setCollectionLimit('badLimit', 'true') .call()).to.be.rejectedWith('unknown boolean limit "badLimit"'); }); + + itEth('destroyCollection', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + const {collectionAddress} = await helper.eth.createRFTCollection(owner, 'Limits', 'absolutely anything', 'OLF'); + const collectionHelper = helper.ethNativeContract.collectionHelpers(owner); + + await expect(collectionHelper.methods + .destroyCollection(collectionAddress) + .send({from: owner})).to.be.fulfilled; + + expect(await collectionHelper.methods + .isCollectionExist(collectionAddress) + .call()).to.be.false; + }); }); --- /dev/null +++ b/tests/src/eth/destroyCollection.test.ts @@ -0,0 +1,76 @@ +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// This file is part of Unique Network. + +// Unique Network is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Unique Network is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Unique Network. If not, see . + +import {IKeyringPair} from '@polkadot/types/types'; +import {Pallets, requirePalletsOrSkip} from '../util'; +import {expect, itEth, usingEthPlaygrounds} from './util'; + + +describe('Destroy Collection from EVM', () => { + let donor: IKeyringPair; + + before(async function() { + await usingEthPlaygrounds(async (helper, privateKey) => { + requirePalletsOrSkip(this, helper, [Pallets.ReFungible, Pallets.NFT]); + donor = await privateKey({filename: __filename}); + }); + }); + + + itEth('(!negative test!) RFT', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + const signer = await helper.eth.createAccountWithBalance(donor); + + const unexistedCollection = helper.ethAddress.fromCollectionId(1000000); + + const {collectionAddress} = await helper.eth.createRFTCollection(owner, 'Limits', 'absolutely anything', 'OLF'); + const collectionHelper = helper.ethNativeContract.collectionHelpers(signer); + + await expect(collectionHelper.methods + .destroyCollection(collectionAddress) + .send({from: signer})).to.be.rejected; + + await expect(collectionHelper.methods + .destroyCollection(unexistedCollection) + .send({from: signer})).to.be.rejected; + + expect(await collectionHelper.methods + .isCollectionExist(unexistedCollection) + .call()).to.be.false; + }); + + itEth('(!negative test!) NFT', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + const signer = await helper.eth.createAccountWithBalance(donor); + + const unexistedCollection = helper.ethAddress.fromCollectionId(1000000); + + const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'Limits', 'absolutely anything', 'OLF'); + const collectionHelper = helper.ethNativeContract.collectionHelpers(signer); + + await expect(collectionHelper.methods + .destroyCollection(collectionAddress) + .send({from: signer})).to.be.rejected; + + await expect(collectionHelper.methods + .destroyCollection(unexistedCollection) + .send({from: signer})).to.be.rejected; + + expect(await collectionHelper.methods + .isCollectionExist(unexistedCollection) + .call()).to.be.false; + }); +}); --- 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", @@ -126,6 +196,23 @@ }, { "inputs": [], + "name": "collectionAdmins", + "outputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], "name": "collectionOwner", "outputs": [ { @@ -142,6 +229,25 @@ "type": "function" }, { + "inputs": [ + { "internalType": "string[]", "name": "keys", "type": "string[]" } + ], + "name": "collectionProperties", + "outputs": [ + { + "components": [ + { "internalType": "string", "name": "field_0", "type": "string" }, + { "internalType": "bytes", "name": "field_1", "type": "bytes" } + ], + "internalType": "struct Tuple10[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { "inputs": [{ "internalType": "string", "name": "key", "type": "string" }], "name": "collectionProperty", "outputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }], @@ -187,6 +293,15 @@ "type": "function" }, { + "inputs": [ + { "internalType": "string[]", "name": "keys", "type": "string[]" } + ], + "name": "deleteCollectionProperties", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { "inputs": [{ "internalType": "string", "name": "key", "type": "string" }], "name": "deleteCollectionProperty", "outputs": [], @@ -211,6 +326,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" } ], @@ -253,6 +385,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": [], @@ -269,6 +418,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": [], @@ -325,6 +491,23 @@ }, { "inputs": [ + { + "components": [ + { "internalType": "string", "name": "field_0", "type": "string" }, + { "internalType": "bytes", "name": "field_1", "type": "bytes" } + ], + "internalType": "struct Tuple10[]", + "name": "properties", + "type": "tuple[]" + } + ], + "name": "setCollectionProperties", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ { "internalType": "string", "name": "key", "type": "string" }, { "internalType": "bytes", "name": "value", "type": "bytes" } ], @@ -344,6 +527,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", @@ -387,6 +604,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", @@ -156,6 +226,23 @@ }, { "inputs": [], + "name": "collectionAdmins", + "outputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], "name": "collectionOwner", "outputs": [ { @@ -163,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" } @@ -172,6 +259,25 @@ "type": "function" }, { + "inputs": [ + { "internalType": "string[]", "name": "keys", "type": "string[]" } + ], + "name": "collectionProperties", + "outputs": [ + { + "components": [ + { "internalType": "string", "name": "field_0", "type": "string" }, + { "internalType": "bytes", "name": "field_1", "type": "bytes" } + ], + "internalType": "struct Tuple19[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { "inputs": [{ "internalType": "string", "name": "key", "type": "string" }], "name": "collectionProperty", "outputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }], @@ -187,7 +293,7 @@ { "internalType": "address", "name": "field_0", "type": "address" }, { "internalType": "uint256", "name": "field_1", "type": "uint256" } ], - "internalType": "struct Tuple17", + "internalType": "struct Tuple6", "name": "", "type": "tuple" } @@ -210,6 +316,15 @@ "type": "function" }, { + "inputs": [ + { "internalType": "string[]", "name": "keys", "type": "string[]" } + ], + "name": "deleteCollectionProperties", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { "inputs": [{ "internalType": "string", "name": "key", "type": "string" }], "name": "deleteCollectionProperty", "outputs": [], @@ -269,6 +384,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" }], @@ -335,6 +467,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": [], @@ -352,6 +501,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" } @@ -440,6 +606,23 @@ }, { "inputs": [ + { + "components": [ + { "internalType": "string", "name": "field_0", "type": "string" }, + { "internalType": "bytes", "name": "field_1", "type": "bytes" } + ], + "internalType": "struct Tuple19[]", + "name": "properties", + "type": "tuple[]" + } + ], + "name": "setCollectionProperties", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ { "internalType": "string", "name": "key", "type": "string" }, { "internalType": "bytes", "name": "value", "type": "bytes" } ], @@ -459,6 +642,58 @@ }, { "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" }, + { + "components": [ + { "internalType": "string", "name": "field_0", "type": "string" }, + { "internalType": "bytes", "name": "field_1", "type": "bytes" } + ], + "internalType": "struct Tuple19[]", + "name": "properties", + "type": "tuple[]" + } + ], + "name": "setProperties", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ { "internalType": "uint256", "name": "tokenId", "type": "uint256" }, { "internalType": "string", "name": "key", "type": "string" }, { "internalType": "bytes", "name": "value", "type": "bytes" } @@ -553,6 +788,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", @@ -156,6 +208,23 @@ }, { "inputs": [], + "name": "collectionAdmins", + "outputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], "name": "collectionOwner", "outputs": [ { @@ -163,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" } @@ -172,6 +241,25 @@ "type": "function" }, { + "inputs": [ + { "internalType": "string[]", "name": "keys", "type": "string[]" } + ], + "name": "collectionProperties", + "outputs": [ + { + "components": [ + { "internalType": "string", "name": "field_0", "type": "string" }, + { "internalType": "bytes", "name": "field_1", "type": "bytes" } + ], + "internalType": "struct Tuple19[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { "inputs": [{ "internalType": "string", "name": "key", "type": "string" }], "name": "collectionProperty", "outputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }], @@ -187,7 +275,7 @@ { "internalType": "address", "name": "field_0", "type": "address" }, { "internalType": "uint256", "name": "field_1", "type": "uint256" } ], - "internalType": "struct Tuple17", + "internalType": "struct Tuple6", "name": "", "type": "tuple" } @@ -210,6 +298,15 @@ "type": "function" }, { + "inputs": [ + { "internalType": "string[]", "name": "keys", "type": "string[]" } + ], + "name": "deleteCollectionProperties", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { "inputs": [{ "internalType": "string", "name": "key", "type": "string" }], "name": "deleteCollectionProperty", "outputs": [], @@ -269,6 +366,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" }], @@ -335,6 +449,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": [], @@ -352,6 +483,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" } @@ -440,6 +588,23 @@ }, { "inputs": [ + { + "components": [ + { "internalType": "string", "name": "field_0", "type": "string" }, + { "internalType": "bytes", "name": "field_1", "type": "bytes" } + ], + "internalType": "struct Tuple19[]", + "name": "properties", + "type": "tuple[]" + } + ], + "name": "setCollectionProperties", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ { "internalType": "string", "name": "key", "type": "string" }, { "internalType": "bytes", "name": "value", "type": "bytes" } ], @@ -459,6 +624,58 @@ }, { "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" }, + { + "components": [ + { "internalType": "string", "name": "field_0", "type": "string" }, + { "internalType": "bytes", "name": "field_1", "type": "bytes" } + ], + "internalType": "struct Tuple19[]", + "name": "properties", + "type": "tuple[]" + } + ], + "name": "setProperties", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ { "internalType": "uint256", "name": "tokenId", "type": "uint256" }, { "internalType": "string", "name": "key", "type": "string" }, { "internalType": "bytes", "name": "value", "type": "bytes" } @@ -562,6 +779,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/tokenProperties.test.ts +++ b/tests/src/eth/tokenProperties.test.ts @@ -16,6 +16,8 @@ import {itEth, usingEthPlaygrounds, expect} from './util'; import {IKeyringPair} from '@polkadot/types/types'; +import {ITokenPropertyPermission} from '../util/playgrounds/types'; +import {Pallets} from '../util'; describe('EVM token properties', () => { let donor: IKeyringPair; @@ -68,6 +70,64 @@ const [{value}] = await token.getProperties(['testKey']); expect(value).to.equal('testValue'); }); + + itEth('Can be multiple set for NFT ', async({helper}) => { + const caller = await helper.eth.createAccountWithBalance(donor); + + const properties = Array(5).fill(0).map((_, i) => { return {field_0: `key_${i}`, field_1: Buffer.from(`value_${i}`)}; }); + const permissions: ITokenPropertyPermission[] = properties.map(p => { return {key: p.field_0, permission: {tokenOwner: true, + collectionAdmin: true, + mutable: true}}; }); + + const collection = await helper.nft.mintCollection(alice, { + tokenPrefix: 'ethp', + tokenPropertyPermissions: permissions, + }); + + const token = await collection.mintToken(alice); + + const valuesBefore = await token.getProperties(properties.map(p => p.field_0)); + expect(valuesBefore).to.be.deep.equal([]); + + await collection.addAdmin(alice, {Ethereum: caller}); + + const address = helper.ethAddress.fromCollectionId(collection.collectionId); + const contract = helper.ethNativeContract.collection(address, 'nft', caller); + + await contract.methods.setProperties(token.tokenId, properties).send({from: caller}); + + const values = await token.getProperties(properties.map(p => p.field_0)); + expect(values).to.be.deep.equal(properties.map(p => { return {key: p.field_0, value: p.field_1.toString()}; })); + }); + + itEth.ifWithPallets('Can be multiple set for RFT ', [Pallets.ReFungible], async({helper}) => { + const caller = await helper.eth.createAccountWithBalance(donor); + + const properties = Array(5).fill(0).map((_, i) => { return {field_0: `key_${i}`, field_1: Buffer.from(`value_${i}`)}; }); + const permissions: ITokenPropertyPermission[] = properties.map(p => { return {key: p.field_0, permission: {tokenOwner: true, + collectionAdmin: true, + mutable: true}}; }); + + const collection = await helper.rft.mintCollection(alice, { + tokenPrefix: 'ethp', + tokenPropertyPermissions: permissions, + }); + + const token = await collection.mintToken(alice); + + const valuesBefore = await token.getProperties(properties.map(p => p.field_0)); + expect(valuesBefore).to.be.deep.equal([]); + + await collection.addAdmin(alice, {Ethereum: caller}); + + const address = helper.ethAddress.fromCollectionId(collection.collectionId); + const contract = helper.ethNativeContract.collection(address, 'rft', caller); + + await contract.methods.setProperties(token.tokenId, properties).send({from: caller}); + + const values = await token.getProperties(properties.map(p => p.field_0)); + expect(values).to.be.deep.equal(properties.map(p => { return {key: p.field_0, value: p.field_1.toString()}; })); + }); itEth('Can be deleted', async({helper}) => { const caller = await helper.eth.createAccountWithBalance(donor); --- a/tests/src/eth/util/playgrounds/types.ts +++ b/tests/src/eth/util/playgrounds/types.ts @@ -13,3 +13,12 @@ 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, +} + +export type EthProperty = string[]; + --- 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, EthProperty} from './types'; // Native contracts ABI import collectionHelpersAbi from '../../collectionHelpersAbi.json'; @@ -28,6 +28,7 @@ import refungibleTokenAbi from '../../reFungibleTokenAbi.json'; import contractHelpersAbi from './../contractHelpersAbi.json'; import {ICrossAccountId, TEthereumAccount} from '../../../util/playgrounds/types'; +import {TCollectionMode} from '../../../util/playgrounds/types'; class EthGroupBase { helper: EthUniqueHelper; @@ -107,7 +108,7 @@ return new web3.eth.Contract(collectionHelpersAbi as any, '0x6c4e9fe1ae37a41e93cee429e8e1881abdcbb54f', {from: caller, gas: this.helper.eth.DEFAULT_GAS}); } - collection(address: string, mode: 'nft' | 'rft' | 'ft', caller?: string): Contract { + collection(address: string, mode: TCollectionMode, caller?: string): Contract { const abi = { 'nft': nonFungibleAbi, 'rft': refungibleAbi, @@ -173,49 +174,59 @@ async callEVM(signer: TEthereumAccount, contractAddress: string, abi: string) { return await this.helper.callRpc('api.rpc.eth.call', [{from: signer, to: contractAddress, data: abi}]); } - - async createNFTCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{collectionId: number, collectionAddress: string}> { + + async createCollecion(functionName: string, signer: string, name: string, description: string, tokenPrefix: string): Promise<{ collectionId: number, collectionAddress: string, events: NormalizedEvent[] }> { const collectionCreationPrice = this.helper.balance.getCollectionCreationPrice(); const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer); + + const result = await collectionHelper.methods[functionName](name, description, tokenPrefix).send({value: Number(collectionCreationPrice)}); - const result = await collectionHelper.methods.createNFTCollection(name, description, tokenPrefix).send({value: Number(collectionCreationPrice)}); - const collectionAddress = this.helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId); const collectionId = this.helper.ethAddress.extractCollectionId(collectionAddress); - - return {collectionId, collectionAddress}; + const events = this.helper.eth.normalizeEvents(result.events); + + return {collectionId, collectionAddress, events}; + } + + async createNFTCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{ collectionId: number, collectionAddress: string, events: NormalizedEvent[] }> { + return this.createCollecion('createNFTCollection', signer, name, description, tokenPrefix); } - async createERC721MetadataCompatibleNFTCollection(signer: string, name: string, description: string, tokenPrefix: string, baseUri: string): Promise<{collectionId: number, collectionAddress: string}> { + async createERC721MetadataCompatibleNFTCollection(signer: string, name: string, description: string, tokenPrefix: string, baseUri: string): Promise<{collectionId: number, collectionAddress: string, events: NormalizedEvent[] }> { const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer); - const {collectionId, collectionAddress} = await this.createNFTCollection(signer, name, description, tokenPrefix); + const {collectionId, collectionAddress, events} = await this.createCollecion('createNFTCollection', signer, name, description, tokenPrefix); await collectionHelper.methods.makeCollectionERC721MetadataCompatible(collectionAddress, baseUri).send(); - return {collectionId, collectionAddress}; + return {collectionId, collectionAddress, events}; } - async createRFTCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{collectionId: number, collectionAddress: string}> { + async createRFTCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{collectionId: number, collectionAddress: string, events: NormalizedEvent[]}> { + return this.createCollecion('createRFTCollection', signer, name, description, tokenPrefix); + } + + async createFungibleCollection(signer: string, name: string, decimals: number, description: string, tokenPrefix: string): Promise<{ collectionId: number, collectionAddress: string, events: NormalizedEvent[]}> { const collectionCreationPrice = this.helper.balance.getCollectionCreationPrice(); const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer); - - const result = await collectionHelper.methods.createRFTCollection(name, description, tokenPrefix).send({value: Number(collectionCreationPrice)}); - + + const result = await collectionHelper.methods.createFTCollection(name, decimals, description, tokenPrefix).send({value: Number(collectionCreationPrice)}); const collectionAddress = this.helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId); const collectionId = this.helper.ethAddress.extractCollectionId(collectionAddress); - - return {collectionId, collectionAddress}; + + const events = this.helper.eth.normalizeEvents(result.events); + + return {collectionId, collectionAddress, events}; } - - async createERC721MetadataCompatibleRFTCollection(signer: string, name: string, description: string, tokenPrefix: string, baseUri: string): Promise<{collectionId: number, collectionAddress: string}> { + + async createERC721MetadataCompatibleRFTCollection(signer: string, name: string, description: string, tokenPrefix: string, baseUri: string): Promise<{collectionId: number, collectionAddress: string, events: NormalizedEvent[] }> { const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer); - const {collectionId, collectionAddress} = await this.createRFTCollection(signer, name, description, tokenPrefix); + const {collectionId, collectionAddress, events} = await this.createCollecion('createRFTCollection', signer, name, description, tokenPrefix); await collectionHelper.methods.makeCollectionERC721MetadataCompatible(collectionAddress, baseUri).send(); - return {collectionId, collectionAddress}; + return {collectionId, collectionAddress, events}; } async deployCollectorContract(signer: string): Promise { @@ -339,10 +350,38 @@ normalizeAddress(address: string): string { return '0x' + address.substring(address.length - 40); } +} + +export class EthPropertyGroup extends EthGroupBase { + property(key: string, value: string): EthProperty { + return [ + key, + '0x'+Buffer.from(value).toString('hex'), + ]; + } } - 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 +390,8 @@ ethAddress: EthAddressGroup; ethNativeContract: NativeContractGroup; ethContract: ContractGroup; + ethCrossAccount: EthCrossAccountGroup; + ethProperty: EthPropertyGroup; constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) { options.helperBase = options.helperBase ?? EthUniqueHelper; @@ -358,8 +399,10 @@ 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); + this.ethProperty = new EthPropertyGroup(this); } getWeb3(): Web3 { --- a/tests/src/util/playgrounds/types.ts +++ b/tests/src/util/playgrounds/types.ts @@ -70,6 +70,13 @@ ethereum?: TEthereumAccount; } +export interface IEthCrossAccountId { + 0: TEthereumAccount; + 1: TSubstrateAccount; + field_0: TEthereumAccount; + field_1: TSubstrateAccount; +} + export interface ICollectionLimits { accountTokenOwnershipLimit?: number | null; sponsoredDataSize?: number | null; @@ -217,3 +224,4 @@ export type TRelayNetworks = 'rococo' | 'westend'; export type TNetworks = TUniqueNetworks | TSiblingNetworkds | TRelayNetworks; export type TSigner = IKeyringPair; // | 'string' +export type TCollectionMode = 'nft' | 'rft' | 'ft'; --- a/tests/src/util/playgrounds/unique.ts +++ b/tests/src/util/playgrounds/unique.ts @@ -9,7 +9,36 @@ import {ApiInterfaceEvents, SignerOptions} from '@polkadot/api/types'; import {encodeAddress, decodeAddress, keccakAsHex, evmToAddress, addressToEvm} from '@polkadot/util-crypto'; import {IKeyringPair} from '@polkadot/types/types'; -import {IApiListeners, IBlock, IEvent, IChainProperties, ICollectionCreationOptions, ICollectionLimits, ICollectionPermissions, ICrossAccountId, ICrossAccountIdLower, ILogger, INestingPermissions, IProperty, IStakingInfo, ISchedulerOptions, ISubstrateBalance, IToken, ITokenPropertyPermission, ITransactionResult, IUniqueHelperLog, TApiAllowedListeners, TEthereumAccount, TSigner, TSubstrateAccount, IForeignAssetMetadata, TNetworks, MoonbeamAssetInfo, DemocracyStandardAccountVote, AcalaAssetMetadata} from './types'; +import { + IApiListeners, + IBlock, + IEvent, + IChainProperties, + ICollectionCreationOptions, + ICollectionLimits, + ICollectionPermissions, + ICrossAccountId, + ICrossAccountIdLower, + ILogger, + INestingPermissions, + IProperty, + IStakingInfo, + ISchedulerOptions, + ISubstrateBalance, + IToken, + ITokenPropertyPermission, + ITransactionResult, + IUniqueHelperLog, + TApiAllowedListeners, + TEthereumAccount, + TSigner, + TSubstrateAccount, + TNetworks, + IForeignAssetMetadata, + AcalaAssetMetadata, + MoonbeamAssetInfo, + DemocracyStandardAccountVote, +} from './types'; export class CrossAccountId implements ICrossAccountId { Substrate?: TSubstrateAccount;