From 9efdf394566535cd37eda329accf54be3d3e4736 Mon Sep 17 00:00:00 2001 From: Max Andreev Date: Thu, 27 Oct 2022 09:16:53 +0000 Subject: [PATCH] Merge branch 'develop' into tests/require-await --- --- 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", @@ -5345,12 +5378,14 @@ "pallet-structure", "pallet-sudo", "pallet-template-transaction-payment", + "pallet-test-utils", "pallet-timestamp", "pallet-transaction-payment", "pallet-transaction-payment-rpc-runtime-api", "pallet-treasury", "pallet-unique", "pallet-unique-scheduler", + "pallet-unique-scheduler-v2", "pallet-xcm", "parachain-info", "parity-scale-codec 3.2.1", @@ -5792,7 +5827,7 @@ [[package]] name = "pallet-common" -version = "0.1.8" +version = "0.1.9" dependencies = [ "ethereum", "evm-coder", @@ -6325,7 +6360,7 @@ [[package]] name = "pallet-nonfungible" -version = "0.1.5" +version = "0.1.6" dependencies = [ "ethereum", "evm-coder", @@ -6447,7 +6482,7 @@ [[package]] name = "pallet-refungible" -version = "0.2.4" +version = "0.2.5" dependencies = [ "derivative", "ethereum", @@ -6669,6 +6704,17 @@ ] [[package]] +name = "pallet-test-utils" +version = "0.1.0" +dependencies = [ + "frame-support", + "frame-system", + "pallet-unique-scheduler-v2", + "parity-scale-codec 3.2.1", + "scale-info", +] + +[[package]] name = "pallet-timestamp" version = "4.0.0-dev" source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.30#a3ed0119c45cdd0d571ad34e5b3ee7518c8cef8d" @@ -6766,7 +6812,7 @@ [[package]] name = "pallet-unique" -version = "0.2.0" +version = "0.2.1" dependencies = [ "ethereum", "evm-coder", @@ -6808,6 +6854,24 @@ ] [[package]] +name = "pallet-unique-scheduler-v2" +version = "0.1.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "pallet-preimage", + "parity-scale-codec 3.2.1", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", + "substrate-test-utils", +] + +[[package]] name = "pallet-utility" version = "4.0.0-dev" source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.30#a3ed0119c45cdd0d571ad34e5b3ee7518c8cef8d" @@ -10741,9 +10805,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 +10950,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 +12086,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 +12164,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 +12754,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/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ 'runtime/tests', ] default-members = ['node/*', 'runtime/opal'] + [profile.release] panic = 'unwind' --- a/client/rpc/src/lib.rs +++ b/client/rpc/src/lib.rs @@ -557,7 +557,10 @@ pass_method!(allowlist(collection: CollectionId) -> Vec, unique_api); pass_method!(allowed(collection: CollectionId, user: CrossAccountId) -> bool, unique_api); pass_method!(last_token_id(collection: CollectionId) -> TokenId, unique_api); - pass_method!(collection_by_id(collection: CollectionId) -> Option>, unique_api); + pass_method!( + collection_by_id(collection: CollectionId) -> Option>, unique_api; + changed_in 3, collection_by_id_before_version_3(collection) => |value| value.map(|coll| coll.into()) + ); pass_method!(collection_stats() -> CollectionStats, unique_api); pass_method!(next_sponsored(collection: CollectionId, account: CrossAccountId, token: TokenId) -> Option, unique_api); pass_method!(effective_collection_limits(collection_id: CollectionId) -> Option, unique_api); --- 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 --- /dev/null +++ b/pallets/scheduler-v2/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "pallet-unique-scheduler-v2" +version = "0.1.0" +authors = ["Unique Network "] +edition = "2021" +license = "GPLv3" +homepage = "https://unique.network" +repository = "https://github.com/UniqueNetwork/unique-chain" +description = "Unique Scheduler pallet" +readme = "README.md" + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } +log = { version = "0.4.17", default-features = false } +scale-info = { version = "2.1.1", default-features = false, features = ["derive"] } +frame-benchmarking = { default-features = false, optional = true, 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" } +frame-system = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.30" } +sp-io = { version = "6.0.0", default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.30" } +sp-runtime = { version = "6.0.0", default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.30" } +sp-std = { version = "4.0.0", default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.30" } +sp-core = { version = "6.0.0", default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.30" } + +[dev-dependencies] +pallet-preimage = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.30" } +sp-core = { version = "6.0.0", default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.30" } +substrate-test-utils = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.30" } + +[features] +default = ["std"] +runtime-benchmarks = [ + "frame-benchmarking", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", +] +std = [ + "codec/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "log/std", + "scale-info/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", + "sp-core/std", +] +try-runtime = ["frame-support/try-runtime"] --- /dev/null +++ b/pallets/scheduler-v2/src/benchmarking.rs @@ -0,0 +1,349 @@ +// 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 . + +// Original license: +// This file is part of Substrate. + +// Copyright (C) 2020-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Scheduler pallet benchmarking. + +use super::*; +use frame_benchmarking::{account, benchmarks}; +use frame_support::{ + ensure, + traits::{schedule::Priority, PreimageRecipient}, +}; +use frame_system::RawOrigin; +use sp_std::{prelude::*, vec}; +use sp_io::hashing::blake2_256; + +use crate::{Pallet as Scheduler, ScheduledCall, EncodedCall}; +use frame_system::Call as SystemCall; + +const SEED: u32 = 0; + +const BLOCK_NUMBER: u32 = 2; + +type SystemOrigin = ::Origin; + +/// Add `n` items to the schedule. +/// +/// For `resolved`: +/// - ` +/// - `None`: aborted (hash without preimage) +/// - `Some(true)`: hash resolves into call if possible, plain call otherwise +/// - `Some(false)`: plain call +fn fill_schedule(when: T::BlockNumber, n: u32) -> Result<(), &'static str> { + let t = DispatchTime::At(when); + let origin: ::PalletsOrigin = frame_system::RawOrigin::Root.into(); + for i in 0..n { + let call = make_call::(None); + let period = Some(((i + 100).into(), 100)); + let name = u32_to_name(i); + Scheduler::::do_schedule_named(name, t, period, 0, origin.clone(), call)?; + } + ensure!( + Agenda::::get(when).len() == n as usize, + "didn't fill schedule" + ); + Ok(()) +} + +fn u32_to_name(i: u32) -> TaskName { + i.using_encoded(blake2_256) +} + +fn make_task( + periodic: bool, + named: bool, + signed: bool, + maybe_lookup_len: Option, + priority: Priority, +) -> ScheduledOf { + let call = make_call::(maybe_lookup_len); + let maybe_periodic = match periodic { + true => Some((100u32.into(), 100)), + false => None, + }; + let maybe_id = match named { + true => Some(u32_to_name(0)), + false => None, + }; + let origin = make_origin::(signed); + Scheduled { + maybe_id, + priority, + call, + maybe_periodic, + origin, + _phantom: PhantomData, + } +} + +fn bounded(len: u32) -> Option> { + let call = <::Call>::from(SystemCall::remark { + remark: vec![0; len as usize], + }); + ScheduledCall::new(call).ok() +} + +fn make_call(maybe_lookup_len: Option) -> ScheduledCall { + let bound = EncodedCall::bound() as u32; + let mut len = match maybe_lookup_len { + Some(len) => { + len.min(>::MaxSize::get() - 2) + .max(bound) - 3 + } + None => bound.saturating_sub(4), + }; + + loop { + let c = match bounded::(len) { + Some(x) => x, + None => { + len -= 1; + continue; + } + }; + if c.lookup_needed() == maybe_lookup_len.is_some() { + break c; + } + if maybe_lookup_len.is_some() { + len += 1; + } else { + if len > 0 { + len -= 1; + } else { + break c; + } + } + } +} + +fn make_origin(signed: bool) -> ::PalletsOrigin { + match signed { + true => frame_system::RawOrigin::Signed(account("origin", 0, SEED)).into(), + false => frame_system::RawOrigin::Root.into(), + } +} + +fn dummy_counter() -> WeightCounter { + WeightCounter { + used: Weight::zero(), + limit: Weight::MAX, + } +} + +benchmarks! { + // `service_agendas` when no work is done. + service_agendas_base { + let now = T::BlockNumber::from(BLOCK_NUMBER); + IncompleteSince::::put(now - One::one()); + }: { + Scheduler::::service_agendas(&mut dummy_counter(), now, 0); + } verify { + assert_eq!(IncompleteSince::::get(), Some(now - One::one())); + } + + // `service_agenda` when no work is done. + service_agenda_base { + let now = BLOCK_NUMBER.into(); + let s in 0 .. T::MaxScheduledPerBlock::get(); + fill_schedule::(now, s)?; + let mut executed = 0; + }: { + Scheduler::::service_agenda(&mut dummy_counter(), &mut executed, now, now, 0); + } verify { + assert_eq!(executed, 0); + } + + // `service_task` when the task is a non-periodic, non-named, non-fetched call which is not + // dispatched (e.g. due to being overweight). + service_task_base { + let now = BLOCK_NUMBER.into(); + let task = make_task::(false, false, false, None, 0); + // prevent any tasks from actually being executed as we only want the surrounding weight. + let mut counter = WeightCounter { used: Weight::zero(), limit: Weight::zero() }; + }: { + let result = Scheduler::::service_task(&mut counter, now, now, 0, true, task); + } verify { + //assert_eq!(result, Ok(())); + } + + // `service_task` when the task is a non-periodic, non-named, fetched call (with a known + // preimage length) and which is not dispatched (e.g. due to being overweight). + service_task_fetched { + let s in (EncodedCall::bound() as u32) .. (>::MaxSize::get()); + let now = BLOCK_NUMBER.into(); + let task = make_task::(false, false, false, Some(s), 0); + // prevent any tasks from actually being executed as we only want the surrounding weight. + let mut counter = WeightCounter { used: Weight::zero(), limit: Weight::zero() }; + }: { + let result = Scheduler::::service_task(&mut counter, now, now, 0, true, task); + } verify { + } + + // `service_task` when the task is a non-periodic, named, non-fetched call which is not + // dispatched (e.g. due to being overweight). + service_task_named { + let now = BLOCK_NUMBER.into(); + let task = make_task::(false, true, false, None, 0); + // prevent any tasks from actually being executed as we only want the surrounding weight. + let mut counter = WeightCounter { used: Weight::zero(), limit: Weight::zero() }; + }: { + let result = Scheduler::::service_task(&mut counter, now, now, 0, true, task); + } verify { + } + + // `service_task` when the task is a periodic, non-named, non-fetched call which is not + // dispatched (e.g. due to being overweight). + service_task_periodic { + let now = BLOCK_NUMBER.into(); + let task = make_task::(true, false, false, None, 0); + // prevent any tasks from actually being executed as we only want the surrounding weight. + let mut counter = WeightCounter { used: Weight::zero(), limit: Weight::zero() }; + }: { + let result = Scheduler::::service_task(&mut counter, now, now, 0, true, task); + } verify { + } + + // `execute_dispatch` when the origin is `Signed`, not counting the dispatable's weight. + execute_dispatch_signed { + let mut counter = WeightCounter { used: Weight::zero(), limit: Weight::MAX }; + let origin = make_origin::(true); + let call = T::Preimages::realize(&make_call::(None)).unwrap().0; + }: { + assert!(Scheduler::::execute_dispatch(&mut counter, origin, call).is_ok()); + } + verify { + } + + // `execute_dispatch` when the origin is not `Signed`, not counting the dispatable's weight. + execute_dispatch_unsigned { + let mut counter = WeightCounter { used: Weight::zero(), limit: Weight::MAX }; + let origin = make_origin::(false); + let call = T::Preimages::realize(&make_call::(None)).unwrap().0; + }: { + assert!(Scheduler::::execute_dispatch(&mut counter, origin, call).is_ok()); + } + verify { + } + + schedule { + let s in 0 .. (T::MaxScheduledPerBlock::get() - 1); + let when = BLOCK_NUMBER.into(); + let periodic = Some((T::BlockNumber::one(), 100)); + let priority = Some(0); + // Essentially a no-op call. + let call = Box::new(SystemCall::set_storage { items: vec![] }.into()); + + fill_schedule::(when, s)?; + }: _(RawOrigin::Root, when, periodic, priority, call) + verify { + ensure!( + Agenda::::get(when).len() == (s + 1) as usize, + "didn't add to schedule" + ); + } + + cancel { + let s in 1 .. T::MaxScheduledPerBlock::get(); + let when = BLOCK_NUMBER.into(); + + fill_schedule::(when, s)?; + assert_eq!(Agenda::::get(when).len(), s as usize); + let schedule_origin = T::ScheduleOrigin::successful_origin(); + }: _>(schedule_origin, when, 0) + verify { + ensure!( + Lookup::::get(u32_to_name(0)).is_none(), + "didn't remove from lookup" + ); + // Removed schedule is NONE + ensure!( + Agenda::::get(when)[0].is_none(), + "didn't remove from schedule" + ); + } + + schedule_named { + let s in 0 .. (T::MaxScheduledPerBlock::get() - 1); + let id = u32_to_name(s); + let when = BLOCK_NUMBER.into(); + let periodic = Some((T::BlockNumber::one(), 100)); + let priority = Some(0); + // Essentially a no-op call. + let call = Box::new(SystemCall::set_storage { items: vec![] }.into()); + + fill_schedule::(when, s)?; + }: _(RawOrigin::Root, id, when, periodic, priority, call) + verify { + ensure!( + Agenda::::get(when).len() == (s + 1) as usize, + "didn't add to schedule" + ); + } + + cancel_named { + let s in 1 .. T::MaxScheduledPerBlock::get(); + let when = BLOCK_NUMBER.into(); + + fill_schedule::(when, s)?; + }: _(RawOrigin::Root, u32_to_name(0)) + verify { + ensure!( + Lookup::::get(u32_to_name(0)).is_none(), + "didn't remove from lookup" + ); + // Removed schedule is NONE + ensure!( + Agenda::::get(when)[0].is_none(), + "didn't remove from schedule" + ); + } + + change_named_priority { + let origin: RawOrigin = frame_system::RawOrigin::Root; + let s in 1 .. T::MaxScheduledPerBlock::get(); + let when = BLOCK_NUMBER.into(); + let idx = s - 1; + let id = u32_to_name(idx); + let priority = 42; + fill_schedule::(when, s)?; + }: _(origin, id, priority) + verify { + ensure!( + Agenda::::get(when)[idx as usize].clone().unwrap().priority == priority, + "didn't change the priority" + ); + } + + impl_benchmark_test_suite!(Scheduler, crate::mock::new_test_ext(), crate::mock::Test); +} --- /dev/null +++ b/pallets/scheduler-v2/src/lib.rs @@ -0,0 +1,1102 @@ +// 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 . + +// Original license: +// This file is part of Substrate. + +// Copyright (C) 2017-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! # Scheduler +//! A Pallet for scheduling dispatches. +//! +//! - [`Config`] +//! - [`Call`] +//! - [`Pallet`] +//! +//! ## Overview +//! +//! This Pallet exposes capabilities for scheduling dispatches to occur at a +//! specified block number or at a specified period. These scheduled dispatches +//! may be named or anonymous and may be canceled. +//! +//! **NOTE:** The scheduled calls will be dispatched with the default filter +//! for the origin: namely `frame_system::Config::BaseCallFilter` for all origin +//! except root which will get no filter. And not the filter contained in origin +//! use to call `fn schedule`. +//! +//! If a call is scheduled using proxy or whatever mecanism which adds filter, +//! then those filter will not be used when dispatching the schedule call. +//! +//! ## Interface +//! +//! ### Dispatchable Functions +//! +//! * `schedule` - schedule a dispatch, which may be periodic, to occur at a specified block and +//! with a specified priority. +//! * `cancel` - cancel a scheduled dispatch, specified by block number and index. +//! * `schedule_named` - augments the `schedule` interface with an additional `Vec` parameter +//! that can be used for identification. +//! * `cancel_named` - the named complement to the cancel function. + +// Ensure we're `no_std` when compiling for Wasm. +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; +pub mod weights; + +use codec::{Codec, Decode, Encode, MaxEncodedLen}; +use frame_support::{ + dispatch::{ + DispatchError, DispatchResult, Dispatchable, GetDispatchInfo, Parameter, PostDispatchInfo, + }, + traits::{ + schedule::{self, DispatchTime, LOWEST_PRIORITY}, + EnsureOrigin, Get, IsType, OriginTrait, PrivilegeCmp, StorageVersion, PreimageRecipient, + ConstU32, UnfilteredDispatchable, + }, + weights::Weight, + unsigned::TransactionValidityError, +}; + +use frame_system::{self as system}; +use scale_info::TypeInfo; +use sp_runtime::{ + traits::{BadOrigin, One, Saturating, Zero, Hash}, + BoundedVec, RuntimeDebug, DispatchErrorWithPostInfo, +}; +use sp_core::H160; +use sp_std::{borrow::Borrow, cmp::Ordering, marker::PhantomData, prelude::*}; +pub use weights::WeightInfo; + +pub use pallet::*; + +/// Just a simple index for naming period tasks. +pub type PeriodicIndex = u32; +/// The location of a scheduled task that can be used to remove it. +pub type TaskAddress = (BlockNumber, u32); + +pub type EncodedCall = BoundedVec>; + +#[derive(Clone, Eq, PartialEq, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)] +#[scale_info(skip_type_params(T))] +pub enum ScheduledCall { + Inline(EncodedCall), + PreimageLookup { hash: T::Hash, unbounded_len: u32 }, +} + +impl ScheduledCall { + pub fn new(call: ::RuntimeCall) -> Result { + let encoded = call.encode(); + let len = encoded.len(); + + match EncodedCall::try_from(encoded.clone()) { + Ok(bounded) => Ok(Self::Inline(bounded)), + Err(_) => { + let hash = ::Hashing::hash_of(&encoded); + ::Preimages::note_preimage( + encoded + .try_into() + .map_err(|_| >::TooBigScheduledCall)?, + ); + + Ok(Self::PreimageLookup { + hash, + unbounded_len: len as u32, + }) + } + } + } + + /// The maximum length of the lookup that is needed to peek `Self`. + pub fn lookup_len(&self) -> Option { + match self { + Self::Inline(..) => None, + Self::PreimageLookup { unbounded_len, .. } => Some(*unbounded_len), + } + } + + /// Returns whether the image will require a lookup to be peeked. + pub fn lookup_needed(&self) -> bool { + match self { + Self::Inline(_) => false, + Self::PreimageLookup { .. } => true, + } + } + + fn decode(mut data: &[u8]) -> Result<::RuntimeCall, DispatchError> { + ::RuntimeCall::decode(&mut data) + .map_err(|_| >::ScheduledCallCorrupted.into()) + } +} + +pub trait SchedulerPreimages: PreimageRecipient { + fn drop(call: &ScheduledCall); + + fn peek( + call: &ScheduledCall, + ) -> Result<(::RuntimeCall, Option), DispatchError>; + + /// Convert the given scheduled `call` value back into its original instance. If successful, + /// `drop` any data backing it. This will not break the realisability of independently + /// created instances of `ScheduledCall` which happen to have identical data. + fn realize( + call: &ScheduledCall, + ) -> Result<(::RuntimeCall, Option), DispatchError>; +} + +impl> SchedulerPreimages for PP { + fn drop(call: &ScheduledCall) { + match call { + ScheduledCall::Inline(_) => {} + ScheduledCall::PreimageLookup { hash, .. } => Self::unrequest_preimage(hash), + } + } + + fn peek( + call: &ScheduledCall, + ) -> Result<(::RuntimeCall, Option), DispatchError> { + match call { + ScheduledCall::Inline(data) => Ok((ScheduledCall::::decode(data)?, None)), + ScheduledCall::PreimageLookup { + hash, + unbounded_len, + } => { + let (preimage, len) = Self::get_preimage(hash) + .ok_or(>::PreimageNotFound) + .map(|preimage| (preimage, *unbounded_len))?; + + Ok((ScheduledCall::::decode(preimage.as_slice())?, Some(len))) + } + } + } + + fn realize( + call: &ScheduledCall, + ) -> Result<(::RuntimeCall, Option), DispatchError> { + let r = Self::peek(call)?; + Self::drop(call); + Ok(r) + } +} + +pub enum ScheduledEnsureOriginSuccess { + Root, + Signed(AccountId), +} + +pub type TaskName = [u8; 32]; + +/// Information regarding an item to be executed in the future. +#[cfg_attr(any(feature = "std", test), derive(PartialEq, Eq))] +#[derive(Clone, RuntimeDebug, Encode, Decode, MaxEncodedLen, TypeInfo)] +pub struct Scheduled { + /// The unique identity for this task, if there is one. + maybe_id: Option, + + /// This task's priority. + priority: schedule::Priority, + + /// The call to be dispatched. + call: Call, + + /// If the call is periodic, then this points to the information concerning that. + maybe_periodic: Option>, + + /// The origin with which to dispatch the call. + origin: PalletsOrigin, + _phantom: PhantomData, +} + +pub type ScheduledOf = Scheduled< + TaskName, + ScheduledCall, + ::BlockNumber, + ::PalletsOrigin, + ::AccountId, +>; + +struct WeightCounter { + used: Weight, + limit: Weight, +} + +impl WeightCounter { + fn check_accrue(&mut self, w: Weight) -> bool { + let test = self.used.saturating_add(w); + if test.any_gt(self.limit) { + false + } else { + self.used = test; + true + } + } + + fn can_accrue(&mut self, w: Weight) -> bool { + self.used.saturating_add(w).all_lte(self.limit) + } +} + +pub(crate) trait MarginalWeightInfo: WeightInfo { + fn service_task(maybe_lookup_len: Option, named: bool, periodic: bool) -> Weight { + let base = Self::service_task_base(); + let mut total = match maybe_lookup_len { + None => base, + Some(l) => Self::service_task_fetched(l as u32), + }; + if named { + total.saturating_accrue(Self::service_task_named().saturating_sub(base)); + } + if periodic { + total.saturating_accrue(Self::service_task_periodic().saturating_sub(base)); + } + total + } +} + +impl MarginalWeightInfo for T {} + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::{dispatch::PostDispatchInfo, pallet_prelude::*}; + use system::pallet_prelude::*; + + /// The current storage version. + const STORAGE_VERSION: StorageVersion = StorageVersion::new(0); + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + #[pallet::storage_version(STORAGE_VERSION)] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// The aggregated origin which the dispatch will take. + type RuntimeOrigin: OriginTrait + + From + + IsType<::RuntimeOrigin> + + Clone; + + /// The caller origin, overarching type of all pallets origins. + type PalletsOrigin: From> + + Codec + + Clone + + Eq + + TypeInfo + + MaxEncodedLen; + + /// The aggregated call type. + type RuntimeCall: Parameter + + Dispatchable< + RuntimeOrigin = ::RuntimeOrigin, + PostInfo = PostDispatchInfo, + > + UnfilteredDispatchable::RuntimeOrigin> + + GetDispatchInfo + + From>; + + /// The maximum weight that may be scheduled per block for any dispatchables. + #[pallet::constant] + type MaximumWeight: Get; + + /// Required origin to schedule or cancel calls. + type ScheduleOrigin: EnsureOrigin< + ::RuntimeOrigin, + Success = ScheduledEnsureOriginSuccess, + >; + + /// Compare the privileges of origins. + /// + /// This will be used when canceling a task, to ensure that the origin that tries + /// to cancel has greater or equal privileges as the origin that created the scheduled task. + /// + /// For simplicity the [`EqualPrivilegeOnly`](frame_support::traits::EqualPrivilegeOnly) can + /// be used. This will only check if two given origins are equal. + type OriginPrivilegeCmp: PrivilegeCmp; + + /// The maximum number of scheduled calls in the queue for a single block. + #[pallet::constant] + type MaxScheduledPerBlock: Get; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + + /// The preimage provider with which we look up call hashes to get the call. + type Preimages: SchedulerPreimages; + + /// The helper type used for custom transaction fee logic. + type CallExecutor: DispatchCall; + + /// Required origin to set/change calls' priority. + type PrioritySetOrigin: EnsureOrigin<::RuntimeOrigin>; + } + + #[pallet::storage] + pub type IncompleteSince = StorageValue<_, T::BlockNumber>; + + /// Items to be executed, indexed by the block number that they should be executed on. + #[pallet::storage] + pub type Agenda = StorageMap< + _, + Twox64Concat, + T::BlockNumber, + BoundedVec>, T::MaxScheduledPerBlock>, + ValueQuery, + >; + + /// Lookup from a name to the block number and index of the task. + #[pallet::storage] + pub(crate) type Lookup = + StorageMap<_, Twox64Concat, TaskName, TaskAddress>; + + /// Events type. + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Scheduled some task. + Scheduled { when: T::BlockNumber, index: u32 }, + /// Canceled some task. + Canceled { when: T::BlockNumber, index: u32 }, + /// Dispatched some task. + Dispatched { + task: TaskAddress, + id: Option<[u8; 32]>, + result: DispatchResult, + }, + /// Scheduled task's priority has changed + PriorityChanged { + when: T::BlockNumber, + index: u32, + priority: schedule::Priority, + }, + /// The call for the provided hash was not found so the task has been aborted. + CallUnavailable { + task: TaskAddress, + id: Option<[u8; 32]>, + }, + /// The given task was unable to be renewed since the agenda is full at that block. + PeriodicFailed { + task: TaskAddress, + id: Option<[u8; 32]>, + }, + /// The given task can never be executed since it is overweight. + PermanentlyOverweight { + task: TaskAddress, + id: Option<[u8; 32]>, + }, + } + + #[pallet::error] + pub enum Error { + /// Failed to schedule a call + FailedToSchedule, + /// There is no place for a new task in the agenda + AgendaIsExhausted, + /// Scheduled call is corrupted + ScheduledCallCorrupted, + /// Scheduled call preimage is not found + PreimageNotFound, + /// Scheduled call is too big + TooBigScheduledCall, + /// Cannot find the scheduled call. + NotFound, + /// Given target block number is in the past. + TargetBlockNumberInPast, + /// Attempt to use a non-named function on a named task. + Named, + } + + #[pallet::hooks] + impl Hooks> for Pallet { + /// Execute the scheduled calls + fn on_initialize(now: T::BlockNumber) -> Weight { + let mut weight_counter = WeightCounter { + used: Weight::zero(), + limit: T::MaximumWeight::get(), + }; + Self::service_agendas(&mut weight_counter, now, u32::max_value()); + weight_counter.used + } + } + + #[pallet::call] + impl Pallet { + /// Anonymously schedule a task. + #[pallet::weight(::WeightInfo::schedule(T::MaxScheduledPerBlock::get()))] + pub fn schedule( + origin: OriginFor, + when: T::BlockNumber, + maybe_periodic: Option>, + priority: Option, + call: Box<::RuntimeCall>, + ) -> DispatchResult { + T::ScheduleOrigin::ensure_origin(origin.clone())?; + + if priority.is_some() { + T::PrioritySetOrigin::ensure_origin(origin.clone())?; + } + + let origin = ::RuntimeOrigin::from(origin); + Self::do_schedule( + DispatchTime::At(when), + maybe_periodic, + priority.unwrap_or(LOWEST_PRIORITY), + origin.caller().clone(), + >::new(*call)?, + )?; + Ok(()) + } + + /// Cancel an anonymously scheduled task. + #[pallet::weight(::WeightInfo::cancel(T::MaxScheduledPerBlock::get()))] + pub fn cancel(origin: OriginFor, when: T::BlockNumber, index: u32) -> DispatchResult { + T::ScheduleOrigin::ensure_origin(origin.clone())?; + let origin = ::RuntimeOrigin::from(origin); + Self::do_cancel(Some(origin.caller().clone()), (when, index))?; + Ok(()) + } + + /// Schedule a named task. + #[pallet::weight(::WeightInfo::schedule_named(T::MaxScheduledPerBlock::get()))] + pub fn schedule_named( + origin: OriginFor, + id: TaskName, + when: T::BlockNumber, + maybe_periodic: Option>, + priority: Option, + call: Box<::RuntimeCall>, + ) -> DispatchResult { + T::ScheduleOrigin::ensure_origin(origin.clone())?; + + if priority.is_some() { + T::PrioritySetOrigin::ensure_origin(origin.clone())?; + } + + let origin = ::RuntimeOrigin::from(origin); + Self::do_schedule_named( + id, + DispatchTime::At(when), + maybe_periodic, + priority.unwrap_or(LOWEST_PRIORITY), + origin.caller().clone(), + >::new(*call)?, + )?; + Ok(()) + } + + /// Cancel a named scheduled task. + #[pallet::weight(::WeightInfo::cancel_named(T::MaxScheduledPerBlock::get()))] + pub fn cancel_named(origin: OriginFor, id: TaskName) -> DispatchResult { + T::ScheduleOrigin::ensure_origin(origin.clone())?; + let origin = ::RuntimeOrigin::from(origin); + Self::do_cancel_named(Some(origin.caller().clone()), id)?; + Ok(()) + } + + /// Anonymously schedule a task after a delay. + /// + /// # + /// Same as [`schedule`]. + /// # + #[pallet::weight(::WeightInfo::schedule(T::MaxScheduledPerBlock::get()))] + pub fn schedule_after( + origin: OriginFor, + after: T::BlockNumber, + maybe_periodic: Option>, + priority: Option, + call: Box<::RuntimeCall>, + ) -> DispatchResult { + T::ScheduleOrigin::ensure_origin(origin.clone())?; + + if priority.is_some() { + T::PrioritySetOrigin::ensure_origin(origin.clone())?; + } + + let origin = ::RuntimeOrigin::from(origin); + Self::do_schedule( + DispatchTime::After(after), + maybe_periodic, + priority.unwrap_or(LOWEST_PRIORITY), + origin.caller().clone(), + >::new(*call)?, + )?; + Ok(()) + } + + /// Schedule a named task after a delay. + /// + /// # + /// Same as [`schedule_named`](Self::schedule_named). + /// # + #[pallet::weight(::WeightInfo::schedule_named(T::MaxScheduledPerBlock::get()))] + pub fn schedule_named_after( + origin: OriginFor, + id: TaskName, + after: T::BlockNumber, + maybe_periodic: Option>, + priority: Option, + call: Box<::RuntimeCall>, + ) -> DispatchResult { + T::ScheduleOrigin::ensure_origin(origin.clone())?; + + if priority.is_some() { + T::PrioritySetOrigin::ensure_origin(origin.clone())?; + } + + let origin = ::RuntimeOrigin::from(origin); + Self::do_schedule_named( + id, + DispatchTime::After(after), + maybe_periodic, + priority.unwrap_or(LOWEST_PRIORITY), + origin.caller().clone(), + >::new(*call)?, + )?; + Ok(()) + } + + #[pallet::weight(::WeightInfo::change_named_priority(T::MaxScheduledPerBlock::get()))] + pub fn change_named_priority( + origin: OriginFor, + id: TaskName, + priority: schedule::Priority, + ) -> DispatchResult { + T::PrioritySetOrigin::ensure_origin(origin.clone())?; + let origin = ::RuntimeOrigin::from(origin); + Self::do_change_named_priority(origin.caller().clone(), id, priority) + } + } +} + +impl Pallet { + fn resolve_time(when: DispatchTime) -> Result { + let now = frame_system::Pallet::::block_number(); + + let when = match when { + DispatchTime::At(x) => x, + // The current block has already completed it's scheduled tasks, so + // Schedule the task at lest one block after this current block. + DispatchTime::After(x) => now.saturating_add(x).saturating_add(One::one()), + }; + + if when <= now { + return Err(Error::::TargetBlockNumberInPast.into()); + } + + Ok(when) + } + + fn place_task( + when: T::BlockNumber, + what: ScheduledOf, + ) -> Result, (DispatchError, ScheduledOf)> { + let maybe_name = what.maybe_id; + let index = Self::push_to_agenda(when, what)?; + let address = (when, index); + if let Some(name) = maybe_name { + Lookup::::insert(name, address) + } + Self::deposit_event(Event::Scheduled { + when: address.0, + index: address.1, + }); + Ok(address) + } + + fn push_to_agenda( + when: T::BlockNumber, + what: ScheduledOf, + ) -> Result)> { + let mut agenda = Agenda::::get(when); + let index = if (agenda.len() as u32) < T::MaxScheduledPerBlock::get() { + // will always succeed due to the above check. + let _ = agenda.try_push(Some(what)); + agenda.len() as u32 - 1 + } else { + if let Some(hole_index) = agenda.iter().position(|i| i.is_none()) { + agenda[hole_index] = Some(what); + hole_index as u32 + } else { + return Err((>::AgendaIsExhausted.into(), what)); + } + }; + Agenda::::insert(when, agenda); + Ok(index) + } + + fn do_schedule( + when: DispatchTime, + maybe_periodic: Option>, + priority: schedule::Priority, + origin: T::PalletsOrigin, + call: ScheduledCall, + ) -> Result, DispatchError> { + let when = Self::resolve_time(when)?; + + // sanitize maybe_periodic + let maybe_periodic = maybe_periodic + .filter(|p| p.1 > 1 && !p.0.is_zero()) + // Remove one from the number of repetitions since we will schedule one now. + .map(|(p, c)| (p, c - 1)); + let task = Scheduled { + maybe_id: None, + priority, + call, + maybe_periodic, + origin, + _phantom: PhantomData, + }; + Self::place_task(when, task).map_err(|x| x.0) + } + + fn do_cancel( + origin: Option, + (when, index): TaskAddress, + ) -> Result<(), DispatchError> { + let scheduled = Agenda::::try_mutate(when, |agenda| { + agenda.get_mut(index as usize).map_or( + Ok(None), + |s| -> Result>, DispatchError> { + if let (Some(ref o), Some(ref s)) = (origin, s.borrow()) { + if matches!( + T::OriginPrivilegeCmp::cmp_privilege(o, &s.origin), + Some(Ordering::Less) | None + ) { + return Err(BadOrigin.into()); + } + }; + Ok(s.take()) + }, + ) + })?; + if let Some(s) = scheduled { + T::Preimages::drop(&s.call); + + if let Some(id) = s.maybe_id { + Lookup::::remove(id); + } + Self::deposit_event(Event::Canceled { when, index }); + Ok(()) + } else { + return Err(Error::::NotFound.into()); + } + } + + fn do_schedule_named( + id: TaskName, + when: DispatchTime, + maybe_periodic: Option>, + priority: schedule::Priority, + origin: T::PalletsOrigin, + call: ScheduledCall, + ) -> Result, DispatchError> { + // ensure id it is unique + if Lookup::::contains_key(&id) { + return Err(Error::::FailedToSchedule.into()); + } + + let when = Self::resolve_time(when)?; + + // sanitize maybe_periodic + let maybe_periodic = maybe_periodic + .filter(|p| p.1 > 1 && !p.0.is_zero()) + // Remove one from the number of repetitions since we will schedule one now. + .map(|(p, c)| (p, c - 1)); + + let task = Scheduled { + maybe_id: Some(id), + priority, + call, + maybe_periodic, + origin, + _phantom: Default::default(), + }; + Self::place_task(when, task).map_err(|x| x.0) + } + + fn do_cancel_named(origin: Option, id: TaskName) -> DispatchResult { + Lookup::::try_mutate_exists(id, |lookup| -> DispatchResult { + if let Some((when, index)) = lookup.take() { + let i = index as usize; + Agenda::::try_mutate(when, |agenda| -> DispatchResult { + if let Some(s) = agenda.get_mut(i) { + if let (Some(ref o), Some(ref s)) = (origin, s.borrow()) { + if matches!( + T::OriginPrivilegeCmp::cmp_privilege(o, &s.origin), + Some(Ordering::Less) | None + ) { + return Err(BadOrigin.into()); + } + T::Preimages::drop(&s.call); + } + *s = None; + } + Ok(()) + })?; + Self::deposit_event(Event::Canceled { when, index }); + Ok(()) + } else { + return Err(Error::::NotFound.into()); + } + }) + } + + fn do_change_named_priority( + origin: T::PalletsOrigin, + id: TaskName, + priority: schedule::Priority, + ) -> DispatchResult { + match Lookup::::get(id) { + Some((when, index)) => { + let i = index as usize; + Agenda::::try_mutate(when, |agenda| { + if let Some(Some(s)) = agenda.get_mut(i) { + if matches!( + T::OriginPrivilegeCmp::cmp_privilege(&origin, &s.origin), + Some(Ordering::Less) | None + ) { + return Err(BadOrigin.into()); + } + + s.priority = priority; + Self::deposit_event(Event::PriorityChanged { + when, + index, + priority, + }); + } + Ok(()) + }) + } + None => Err(Error::::NotFound.into()), + } + } +} + +enum ServiceTaskError { + /// Could not be executed due to missing preimage. + Unavailable, + /// Could not be executed due to weight limitations. + Overweight, +} +use ServiceTaskError::*; + +/// A Scheduler-Runtime interface for finer payment handling. +pub trait DispatchCall { + /// Resolve the call dispatch, including any post-dispatch operations. + fn dispatch_call( + signer: Option, + function: ::RuntimeCall, + ) -> Result< + Result>, + TransactionValidityError, + >; +} + +impl Pallet { + /// Service up to `max` agendas queue starting from earliest incompletely executed agenda. + fn service_agendas(weight: &mut WeightCounter, now: T::BlockNumber, max: u32) { + if !weight.check_accrue(T::WeightInfo::service_agendas_base()) { + return; + } + + let mut incomplete_since = now + One::one(); + let mut when = IncompleteSince::::take().unwrap_or(now); + let mut executed = 0; + + let max_items = T::MaxScheduledPerBlock::get(); + let mut count_down = max; + let service_agenda_base_weight = T::WeightInfo::service_agenda_base(max_items); + while count_down > 0 && when <= now && weight.can_accrue(service_agenda_base_weight) { + if !Self::service_agenda(weight, &mut executed, now, when, u32::max_value()) { + incomplete_since = incomplete_since.min(when); + } + when.saturating_inc(); + count_down.saturating_dec(); + } + incomplete_since = incomplete_since.min(when); + if incomplete_since <= now { + IncompleteSince::::put(incomplete_since); + } + } + + /// Returns `true` if the agenda was fully completed, `false` if it should be revisited at a + /// later block. + fn service_agenda( + weight: &mut WeightCounter, + executed: &mut u32, + now: T::BlockNumber, + when: T::BlockNumber, + max: u32, + ) -> bool { + let mut agenda = Agenda::::get(when); + let mut ordered = agenda + .iter() + .enumerate() + .filter_map(|(index, maybe_item)| { + maybe_item + .as_ref() + .map(|item| (index as u32, item.priority)) + }) + .collect::>(); + ordered.sort_by_key(|k| k.1); + let within_limit = + weight.check_accrue(T::WeightInfo::service_agenda_base(ordered.len() as u32)); + debug_assert!( + within_limit, + "weight limit should have been checked in advance" + ); + + // Items which we know can be executed and have postponed for execution in a later block. + let mut postponed = (ordered.len() as u32).saturating_sub(max); + // Items which we don't know can ever be executed. + let mut dropped = 0; + + for (agenda_index, _) in ordered.into_iter().take(max as usize) { + let task = match agenda[agenda_index as usize].take() { + None => continue, + Some(t) => t, + }; + let base_weight = T::WeightInfo::service_task( + task.call.lookup_len().map(|x| x as usize), + task.maybe_id.is_some(), + task.maybe_periodic.is_some(), + ); + if !weight.can_accrue(base_weight) { + postponed += 1; + break; + } + let result = Self::service_task(weight, now, when, agenda_index, *executed == 0, task); + agenda[agenda_index as usize] = match result { + Err((Unavailable, slot)) => { + dropped += 1; + slot + } + Err((Overweight, slot)) => { + postponed += 1; + slot + } + Ok(()) => { + *executed += 1; + None + } + }; + } + if postponed > 0 || dropped > 0 { + Agenda::::insert(when, agenda); + } else { + Agenda::::remove(when); + } + postponed == 0 + } + + /// Service (i.e. execute) the given task, being careful not to overflow the `weight` counter. + /// + /// This involves: + /// - removing and potentially replacing the `Lookup` entry for the task. + /// - realizing the task's call which can include a preimage lookup. + /// - Rescheduling the task for execution in a later agenda if periodic. + fn service_task( + weight: &mut WeightCounter, + now: T::BlockNumber, + when: T::BlockNumber, + agenda_index: u32, + is_first: bool, + mut task: ScheduledOf, + ) -> Result<(), (ServiceTaskError, Option>)> { + let (call, lookup_len) = match T::Preimages::peek(&task.call) { + Ok(c) => c, + Err(_) => { + if let Some(ref id) = task.maybe_id { + Lookup::::remove(id); + } + + return Err((Unavailable, Some(task))); + } + }; + + weight.check_accrue(T::WeightInfo::service_task( + lookup_len.map(|x| x as usize), + task.maybe_id.is_some(), + task.maybe_periodic.is_some(), + )); + + match Self::execute_dispatch(weight, task.origin.clone(), call) { + Err(Unavailable) => { + debug_assert!(false, "Checked to exist with `peek`"); + + if let Some(ref id) = task.maybe_id { + Lookup::::remove(id); + } + + Self::deposit_event(Event::CallUnavailable { + task: (when, agenda_index), + id: task.maybe_id, + }); + Err((Unavailable, Some(task))) + } + Err(Overweight) if is_first && !Self::is_runtime_upgraded() => { + T::Preimages::drop(&task.call); + + if let Some(ref id) = task.maybe_id { + Lookup::::remove(id); + } + + Self::deposit_event(Event::PermanentlyOverweight { + task: (when, agenda_index), + id: task.maybe_id, + }); + Err((Unavailable, Some(task))) + } + Err(Overweight) => { + // Preserve Lookup -- the task will be postponed. + Err((Overweight, Some(task))) + } + Ok(result) => { + Self::deposit_event(Event::Dispatched { + task: (when, agenda_index), + id: task.maybe_id, + result, + }); + + let is_canceled = task + .maybe_id + .as_ref() + .map(|id| !Lookup::::contains_key(id)) + .unwrap_or(false); + + match &task.maybe_periodic { + &Some((period, count)) if !is_canceled => { + if count > 1 { + task.maybe_periodic = Some((period, count - 1)); + } else { + task.maybe_periodic = None; + } + let wake = now.saturating_add(period); + match Self::place_task(wake, task) { + Ok(_) => {} + Err((_, task)) => { + // TODO: Leave task in storage somewhere for it to be rescheduled + // manually. + T::Preimages::drop(&task.call); + Self::deposit_event(Event::PeriodicFailed { + task: (when, agenda_index), + id: task.maybe_id, + }); + } + } + } + _ => { + if let Some(ref id) = task.maybe_id { + Lookup::::remove(id); + } + + T::Preimages::drop(&task.call) + } + } + Ok(()) + } + } + } + + fn is_runtime_upgraded() -> bool { + let last = system::LastRuntimeUpgrade::::get(); + let current = T::Version::get(); + + last.map(|v| v.was_upgraded(¤t)).unwrap_or(true) + } + + /// Make a dispatch to the given `call` from the given `origin`, ensuring that the `weight` + /// counter does not exceed its limit and that it is counted accurately (e.g. accounted using + /// post info if available). + /// + /// NOTE: Only the weight for this function will be counted (origin lookup, dispatch and the + /// call itself). + fn execute_dispatch( + weight: &mut WeightCounter, + origin: T::PalletsOrigin, + call: ::RuntimeCall, + ) -> Result { + let dispatch_origin: ::RuntimeOrigin = origin.into(); + let base_weight = match dispatch_origin.clone().as_signed() { + Some(_) => T::WeightInfo::execute_dispatch_signed(), + _ => T::WeightInfo::execute_dispatch_unsigned(), + }; + let call_weight = call.get_dispatch_info().weight; + // We only allow a scheduled call if it cannot push the weight past the limit. + let max_weight = base_weight.saturating_add(call_weight); + + if !weight.can_accrue(max_weight) { + return Err(Overweight); + } + + // let scheduled_origin = + // <::Origin as From>::from(origin.clone()); + let ensured_origin = T::ScheduleOrigin::ensure_origin(dispatch_origin.into()); + + let r = match ensured_origin { + Ok(ScheduledEnsureOriginSuccess::Root) => { + Ok(call.dispatch_bypass_filter(frame_system::RawOrigin::Root.into())) + } + Ok(ScheduledEnsureOriginSuccess::Signed(sender)) => { + // Execute transaction via chain default pipeline + // That means dispatch will be processed like any user's extrinsic e.g. transaction fees will be taken + T::CallExecutor::dispatch_call(Some(sender), call.clone()) + } + Err(e) => Ok(Err(e.into())), + }; + + let (maybe_actual_call_weight, result) = match r { + Ok(result) => match result { + Ok(post_info) => (post_info.actual_weight, Ok(())), + Err(error_and_info) => ( + error_and_info.post_info.actual_weight, + Err(error_and_info.error), + ), + }, + Err(_) => { + log::error!( + target: "runtime::scheduler", + "Warning: Scheduler has failed to execute a post-dispatch transaction. \ + This block might have become invalid."); + (None, Err(DispatchError::CannotLookup)) + } + }; + let call_weight = maybe_actual_call_weight.unwrap_or(call_weight); + weight.check_accrue(base_weight); + weight.check_accrue(call_weight); + Ok(result) + } +} --- /dev/null +++ b/pallets/scheduler-v2/src/mock.rs @@ -0,0 +1,285 @@ +// 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 . + +// Original license: +// This file is part of Substrate. + +// Copyright (C) 2017-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! # Scheduler test environment. + +use super::*; + +use crate as scheduler; +use frame_support::{ + ord_parameter_types, parameter_types, + traits::{ConstU32, ConstU64, Contains, EqualPrivilegeOnly, OnFinalize, OnInitialize}, + weights::constants::RocksDbWeight, +}; +use frame_system::{EnsureRoot, RawOrigin}; +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, + Perbill, +}; + +// Logger module to track execution. +#[frame_support::pallet] +pub mod logger { + use super::{OriginCaller, OriginTrait}; + use frame_support::{pallet_prelude::*, parameter_types}; + use frame_system::pallet_prelude::*; + + parameter_types! { + static Log: Vec<(OriginCaller, u32)> = Vec::new(); + } + pub fn log() -> Vec<(OriginCaller, u32)> { + Log::get().clone() + } + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(PhantomData); + + #[pallet::hooks] + impl Hooks> for Pallet {} + + #[pallet::config] + pub trait Config: frame_system::Config { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + Logged(u32, Weight), + } + + #[pallet::call] + impl Pallet + where + ::RuntimeOrigin: OriginTrait, + { + #[pallet::weight(*weight)] + pub fn log(origin: OriginFor, i: u32, weight: Weight) -> DispatchResult { + Self::deposit_event(Event::Logged(i, weight)); + Log::mutate(|log| { + log.push((origin.caller().clone(), i)); + }); + Ok(()) + } + + #[pallet::weight(*weight)] + pub fn log_without_filter(origin: OriginFor, i: u32, weight: Weight) -> DispatchResult { + Self::deposit_event(Event::Logged(i, weight)); + Log::mutate(|log| { + log.push((origin.caller().clone(), i)); + }); + Ok(()) + } + } +} + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + Logger: logger::{Pallet, Call, Event}, + Scheduler: scheduler::{Pallet, Call, Storage, Event}, + } +); + +// Scheduler must dispatch with root and no filter, this tests base filter is indeed not used. +pub struct BaseFilter; +impl Contains for BaseFilter { + fn contains(call: &RuntimeCall) -> bool { + !matches!(call, RuntimeCall::Logger(LoggerCall::log { .. })) + } +} + +parameter_types! { + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max( + Weight::from_ref_time(2_000_000_000_000) + // .set_proof_size(u64::MAX), + ); +} +impl system::Config for Test { + type BaseCallFilter = BaseFilter; + type BlockWeights = BlockWeights; + type BlockLength = (); + type DbWeight = RocksDbWeight; + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} +impl logger::Config for Test { + type RuntimeEvent = RuntimeEvent; +} +ord_parameter_types! { + pub const One: u64 = 1; +} + +pub struct TestWeightInfo; +impl WeightInfo for TestWeightInfo { + fn service_agendas_base() -> Weight { + Weight::from_ref_time(0b0000_0001) + } + fn service_agenda_base(i: u32) -> Weight { + Weight::from_ref_time((i << 8) as u64 + 0b0000_0010) + } + fn service_task_base() -> Weight { + Weight::from_ref_time(0b0000_0100) + } + fn service_task_periodic() -> Weight { + Weight::from_ref_time(0b0000_1100) + } + fn service_task_named() -> Weight { + Weight::from_ref_time(0b0001_0100) + } + fn service_task_fetched(s: u32) -> Weight { + Weight::from_ref_time((s << 8) as u64 + 0b0010_0100) + } + fn execute_dispatch_signed() -> Weight { + Weight::from_ref_time(0b0100_0000) + } + fn execute_dispatch_unsigned() -> Weight { + Weight::from_ref_time(0b1000_0000) + } + fn schedule(_s: u32) -> Weight { + Weight::from_ref_time(50) + } + fn cancel(_s: u32) -> Weight { + Weight::from_ref_time(50) + } + fn schedule_named(_s: u32) -> Weight { + Weight::from_ref_time(50) + } + fn cancel_named(_s: u32) -> Weight { + Weight::from_ref_time(50) + } + fn change_named_priority(_s: u32) -> Weight { + Weight::from_ref_time(50) + } +} +parameter_types! { + pub MaximumSchedulerWeight: Weight = Perbill::from_percent(80) * + BlockWeights::get().max_block; +} + +pub struct EnsureSignedOneOrRoot; +impl, O>> + From>> EnsureOrigin + for EnsureSignedOneOrRoot +{ + type Success = ScheduledEnsureOriginSuccess; + fn try_origin(o: O) -> Result { + o.into().and_then(|o| match o { + RawOrigin::Root => Ok(ScheduledEnsureOriginSuccess::Root), + RawOrigin::Signed(1) => Ok(ScheduledEnsureOriginSuccess::Signed(1)), + r => Err(O::from(r)), + }) + } +} + +pub struct Executor; +impl DispatchCall for Executor { + fn dispatch_call( + signer: Option, + function: RuntimeCall, + ) -> Result< + Result>, + TransactionValidityError, + > { + let origin = match signer { + Some(who) => RuntimeOrigin::signed(who), + None => RuntimeOrigin::none(), + }; + Ok(function.dispatch(origin)) + } +} + +impl Config for Test { + type RuntimeEvent = RuntimeEvent; + type RuntimeOrigin = RuntimeOrigin; + type PalletsOrigin = OriginCaller; + type RuntimeCall = RuntimeCall; + type MaximumWeight = MaximumSchedulerWeight; + type ScheduleOrigin = EnsureSignedOneOrRoot; + type MaxScheduledPerBlock = ConstU32<10>; + type WeightInfo = TestWeightInfo; + type OriginPrivilegeCmp = EqualPrivilegeOnly; + type Preimages = (); + type PrioritySetOrigin = EnsureRoot; + type CallExecutor = Executor; +} + +pub type LoggerCall = logger::Call; + +pub fn new_test_ext() -> sp_io::TestExternalities { + let t = system::GenesisConfig::default() + .build_storage::() + .unwrap(); + t.into() +} + +pub fn run_to_block(n: u64) { + while System::block_number() < n { + Scheduler::on_finalize(System::block_number()); + System::set_block_number(System::block_number() + 1); + Scheduler::on_initialize(System::block_number()); + } +} + +pub fn root() -> OriginCaller { + system::RawOrigin::Root.into() +} --- /dev/null +++ b/pallets/scheduler-v2/src/tests.rs @@ -0,0 +1,874 @@ +// 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 . + +// Original license: +// This file is part of Substrate. + +// Copyright (C) 2017-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! # Scheduler tests. + +use super::*; +use crate::mock::{ + logger, new_test_ext, root, run_to_block, LoggerCall, RuntimeCall, Scheduler, Test, *, +}; +use frame_support::{ + assert_noop, assert_ok, + traits::{Contains, OnInitialize}, +}; + +#[test] +fn basic_scheduling_works() { + new_test_ext().execute_with(|| { + let call = RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: Weight::from_ref_time(10), + }); + assert!(!::BaseCallFilter::contains( + &call + )); + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(4), + None, + 127, + root(), + >::new(call).unwrap(), + )); + run_to_block(3); + assert!(logger::log().is_empty()); + run_to_block(4); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + run_to_block(100); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + }); +} + +#[test] +fn schedule_after_works() { + new_test_ext().execute_with(|| { + run_to_block(2); + let call = RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: Weight::from_ref_time(10), + }); + assert!(!::BaseCallFilter::contains( + &call + )); + // This will schedule the call 3 blocks after the next block... so block 3 + 3 = 6 + assert_ok!(Scheduler::do_schedule( + DispatchTime::After(3), + None, + 127, + root(), + >::new(call).unwrap(), + )); + run_to_block(5); + assert!(logger::log().is_empty()); + run_to_block(6); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + run_to_block(100); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + }); +} + +#[test] +fn schedule_after_zero_works() { + new_test_ext().execute_with(|| { + run_to_block(2); + let call = RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: Weight::from_ref_time(10), + }); + assert!(!::BaseCallFilter::contains( + &call + )); + assert_ok!(Scheduler::do_schedule( + DispatchTime::After(0), + None, + 127, + root(), + >::new(call).unwrap(), + )); + // Will trigger on the next block. + run_to_block(3); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + run_to_block(100); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + }); +} + +#[test] +fn periodic_scheduling_works() { + new_test_ext().execute_with(|| { + // at #4, every 3 blocks, 3 times. + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(4), + Some((3, 3)), + 127, + root(), + >::new(RuntimeCall::Logger(logger::Call::log { + i: 42, + weight: Weight::from_ref_time(10) + })) + .unwrap() + )); + run_to_block(3); + assert!(logger::log().is_empty()); + run_to_block(4); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + run_to_block(6); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + run_to_block(7); + assert_eq!(logger::log(), vec![(root(), 42u32), (root(), 42u32)]); + run_to_block(9); + assert_eq!(logger::log(), vec![(root(), 42u32), (root(), 42u32)]); + run_to_block(10); + assert_eq!( + logger::log(), + vec![(root(), 42u32), (root(), 42u32), (root(), 42u32)] + ); + run_to_block(100); + assert_eq!( + logger::log(), + vec![(root(), 42u32), (root(), 42u32), (root(), 42u32)] + ); + }); +} + +#[test] +fn cancel_named_scheduling_works_with_normal_cancel() { + new_test_ext().execute_with(|| { + // at #4. + Scheduler::do_schedule_named( + [1u8; 32], + DispatchTime::At(4), + None, + 127, + root(), + >::new(RuntimeCall::Logger(LoggerCall::log { + i: 69, + weight: Weight::from_ref_time(10), + })) + .unwrap(), + ) + .unwrap(); + let i = Scheduler::do_schedule( + DispatchTime::At(4), + None, + 127, + root(), + >::new(RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: Weight::from_ref_time(10), + })) + .unwrap(), + ) + .unwrap(); + run_to_block(3); + assert!(logger::log().is_empty()); + assert_ok!(Scheduler::do_cancel_named(None, [1u8; 32])); + assert_ok!(Scheduler::do_cancel(None, i)); + run_to_block(100); + assert!(logger::log().is_empty()); + }); +} + +#[test] +fn cancel_named_periodic_scheduling_works() { + new_test_ext().execute_with(|| { + // at #4, every 3 blocks, 3 times. + Scheduler::do_schedule_named( + [1u8; 32], + DispatchTime::At(4), + Some((3, 3)), + 127, + root(), + >::new(RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: Weight::from_ref_time(10), + })) + .unwrap(), + ) + .unwrap(); + // same id results in error. + assert!(Scheduler::do_schedule_named( + [1u8; 32], + DispatchTime::At(4), + None, + 127, + root(), + >::new(RuntimeCall::Logger(LoggerCall::log { + i: 69, + weight: Weight::from_ref_time(10) + })) + .unwrap(), + ) + .is_err()); + // different id is ok. + Scheduler::do_schedule_named( + [2u8; 32], + DispatchTime::At(8), + None, + 127, + root(), + >::new(RuntimeCall::Logger(LoggerCall::log { + i: 69, + weight: Weight::from_ref_time(10), + })) + .unwrap(), + ) + .unwrap(); + run_to_block(3); + assert!(logger::log().is_empty()); + run_to_block(4); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + run_to_block(6); + assert_ok!(Scheduler::do_cancel_named(None, [1u8; 32])); + run_to_block(100); + assert_eq!(logger::log(), vec![(root(), 42u32), (root(), 69u32)]); + }); +} + +#[test] +fn scheduler_respects_weight_limits() { + let max_weight: Weight = ::MaximumWeight::get(); + new_test_ext().execute_with(|| { + let call = RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: max_weight / 3 * 2, + }); + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(4), + None, + 127, + root(), + >::new(call).unwrap(), + )); + let call = RuntimeCall::Logger(LoggerCall::log { + i: 69, + weight: max_weight / 3 * 2, + }); + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(4), + None, + 127, + root(), + >::new(call).unwrap(), + )); + // 69 and 42 do not fit together + run_to_block(4); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + run_to_block(5); + assert_eq!(logger::log(), vec![(root(), 42u32), (root(), 69u32)]); + }); +} + +/// Permanently overweight calls are not deleted but also not executed. +#[test] +fn scheduler_does_not_delete_permanently_overweight_call() { + let max_weight: Weight = ::MaximumWeight::get(); + new_test_ext().execute_with(|| { + let call = RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: max_weight, + }); + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(4), + None, + 127, + root(), + >::new(call).unwrap(), + )); + // Never executes. + run_to_block(100); + assert_eq!(logger::log(), vec![]); + + // Assert the `PermanentlyOverweight` event. + assert_eq!( + System::events().last().unwrap().event, + crate::Event::PermanentlyOverweight { + task: (4, 0), + id: None + } + .into(), + ); + // The call is still in the agenda. + assert!(Agenda::::get(4)[0].is_some()); + }); +} + +#[test] +fn scheduler_handles_periodic_failure() { + let max_weight: Weight = ::MaximumWeight::get(); + let max_per_block = ::MaxScheduledPerBlock::get(); + + new_test_ext().execute_with(|| { + let call = RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: (max_weight / 3) * 2, + }); + let call = >::new(call).unwrap(); + + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(4), + Some((4, u32::MAX)), + 127, + root(), + call.clone(), + )); + // Executes 5 times till block 20. + run_to_block(20); + assert_eq!(logger::log().len(), 5); + + // Block 28 will already be full. + for _ in 0..max_per_block { + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(28), + None, + 120, + root(), + call.clone(), + )); + } + + // Going to block 24 will emit a `PeriodicFailed` event. + run_to_block(24); + assert_eq!(logger::log().len(), 6); + + assert_eq!( + System::events().last().unwrap().event, + crate::Event::PeriodicFailed { + task: (24, 0), + id: None + } + .into(), + ); + }); +} + +#[test] +fn scheduler_respects_priority_ordering() { + let max_weight: Weight = ::MaximumWeight::get(); + new_test_ext().execute_with(|| { + let call = RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: max_weight / 3, + }); + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(4), + None, + 1, + root(), + >::new(call).unwrap(), + )); + let call = RuntimeCall::Logger(LoggerCall::log { + i: 69, + weight: max_weight / 3, + }); + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(4), + None, + 0, + root(), + >::new(call).unwrap(), + )); + run_to_block(4); + assert_eq!(logger::log(), vec![(root(), 69u32), (root(), 42u32)]); + }); +} + +#[test] +fn scheduler_respects_priority_ordering_with_soft_deadlines() { + new_test_ext().execute_with(|| { + let max_weight: Weight = ::MaximumWeight::get(); + let call = RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: max_weight / 5 * 2, + }); + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(4), + None, + 255, + root(), + >::new(call).unwrap(), + )); + let call = RuntimeCall::Logger(LoggerCall::log { + i: 69, + weight: max_weight / 5 * 2, + }); + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(4), + None, + 127, + root(), + >::new(call).unwrap(), + )); + let call = RuntimeCall::Logger(LoggerCall::log { + i: 2600, + weight: max_weight / 5 * 4, + }); + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(4), + None, + 126, + root(), + >::new(call).unwrap(), + )); + + // 2600 does not fit with 69 or 42, but has higher priority, so will go through + run_to_block(4); + assert_eq!(logger::log(), vec![(root(), 2600u32)]); + // 69 and 42 fit together + run_to_block(5); + assert_eq!( + logger::log(), + vec![(root(), 2600u32), (root(), 69u32), (root(), 42u32)] + ); + }); +} + +#[test] +fn on_initialize_weight_is_correct() { + new_test_ext().execute_with(|| { + let call_weight = Weight::from_ref_time(25); + + // Named + let call = RuntimeCall::Logger(LoggerCall::log { + i: 3, + weight: call_weight + Weight::from_ref_time(1), + }); + assert_ok!(Scheduler::do_schedule_named( + [1u8; 32], + DispatchTime::At(3), + None, + 255, + root(), + >::new(call).unwrap(), + )); + let call = RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: call_weight + Weight::from_ref_time(2), + }); + // Anon Periodic + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(2), + Some((1000, 3)), + 128, + root(), + >::new(call).unwrap(), + )); + let call = RuntimeCall::Logger(LoggerCall::log { + i: 69, + weight: call_weight + Weight::from_ref_time(3), + }); + // Anon + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(2), + None, + 127, + root(), + >::new(call).unwrap(), + )); + // Named Periodic + let call = RuntimeCall::Logger(LoggerCall::log { + i: 2600, + weight: call_weight + Weight::from_ref_time(4), + }); + assert_ok!(Scheduler::do_schedule_named( + [2u8; 32], + DispatchTime::At(1), + Some((1000, 3)), + 126, + root(), + >::new(call).unwrap(), + )); + + // Will include the named periodic only + assert_eq!( + Scheduler::on_initialize(1), + TestWeightInfo::service_agendas_base() + + TestWeightInfo::service_agenda_base(1) + + ::service_task(None, true, true) + + TestWeightInfo::execute_dispatch_unsigned() + + call_weight + Weight::from_ref_time(4) + ); + assert_eq!(IncompleteSince::::get(), None); + assert_eq!(logger::log(), vec![(root(), 2600u32)]); + + // Will include anon and anon periodic + assert_eq!( + Scheduler::on_initialize(2), + TestWeightInfo::service_agendas_base() + + TestWeightInfo::service_agenda_base(2) + + ::service_task(None, false, true) + + TestWeightInfo::execute_dispatch_unsigned() + + call_weight + Weight::from_ref_time(3) + + ::service_task(None, false, false) + + TestWeightInfo::execute_dispatch_unsigned() + + call_weight + Weight::from_ref_time(2) + ); + assert_eq!(IncompleteSince::::get(), None); + assert_eq!( + logger::log(), + vec![(root(), 2600u32), (root(), 69u32), (root(), 42u32)] + ); + + // Will include named only + assert_eq!( + Scheduler::on_initialize(3), + TestWeightInfo::service_agendas_base() + + TestWeightInfo::service_agenda_base(1) + + ::service_task(None, true, false) + + TestWeightInfo::execute_dispatch_unsigned() + + call_weight + Weight::from_ref_time(1) + ); + assert_eq!(IncompleteSince::::get(), None); + assert_eq!( + logger::log(), + vec![ + (root(), 2600u32), + (root(), 69u32), + (root(), 42u32), + (root(), 3u32) + ] + ); + + // Will contain none + let actual_weight = Scheduler::on_initialize(4); + assert_eq!( + actual_weight, + TestWeightInfo::service_agendas_base() + TestWeightInfo::service_agenda_base(0) + ); + }); +} + +#[test] +fn root_calls_works() { + new_test_ext().execute_with(|| { + let call = Box::new(RuntimeCall::Logger(LoggerCall::log { + i: 69, + weight: Weight::from_ref_time(10), + })); + let call2 = Box::new(RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: Weight::from_ref_time(10), + })); + assert_ok!(Scheduler::schedule_named( + RuntimeOrigin::root(), + [1u8; 32], + 4, + None, + Some(127), + call, + )); + assert_ok!(Scheduler::schedule( + RuntimeOrigin::root(), + 4, + None, + Some(127), + call2 + )); + run_to_block(3); + // Scheduled calls are in the agenda. + assert_eq!(Agenda::::get(4).len(), 2); + assert!(logger::log().is_empty()); + assert_ok!(Scheduler::cancel_named(RuntimeOrigin::root(), [1u8; 32])); + assert_ok!(Scheduler::cancel(RuntimeOrigin::root(), 4, 1)); + // Scheduled calls are made NONE, so should not effect state + run_to_block(100); + assert!(logger::log().is_empty()); + }); +} + +#[test] +fn fails_to_schedule_task_in_the_past() { + new_test_ext().execute_with(|| { + run_to_block(3); + + let call1 = Box::new(RuntimeCall::Logger(LoggerCall::log { + i: 69, + weight: Weight::from_ref_time(10), + })); + let call2 = Box::new(RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: Weight::from_ref_time(10), + })); + let call3 = Box::new(RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: Weight::from_ref_time(10), + })); + + assert_noop!( + Scheduler::schedule_named(RuntimeOrigin::root(), [1u8; 32], 2, None, Some(127), call1), + Error::::TargetBlockNumberInPast, + ); + + assert_noop!( + Scheduler::schedule(RuntimeOrigin::root(), 2, None, Some(127), call2), + Error::::TargetBlockNumberInPast, + ); + + assert_noop!( + Scheduler::schedule(RuntimeOrigin::root(), 3, None, Some(127), call3), + Error::::TargetBlockNumberInPast, + ); + }); +} + +#[test] +fn should_use_origin() { + new_test_ext().execute_with(|| { + let call = Box::new(RuntimeCall::Logger(LoggerCall::log { + i: 69, + weight: Weight::from_ref_time(10), + })); + let call2 = Box::new(RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: Weight::from_ref_time(10), + })); + assert_ok!(Scheduler::schedule_named( + system::RawOrigin::Signed(1).into(), + [1u8; 32], + 4, + None, + None, + call, + )); + assert_ok!(Scheduler::schedule( + system::RawOrigin::Signed(1).into(), + 4, + None, + None, + call2, + )); + run_to_block(3); + // Scheduled calls are in the agenda. + assert_eq!(Agenda::::get(4).len(), 2); + assert!(logger::log().is_empty()); + assert_ok!(Scheduler::cancel_named( + system::RawOrigin::Signed(1).into(), + [1u8; 32] + )); + assert_ok!(Scheduler::cancel(system::RawOrigin::Signed(1).into(), 4, 1)); + // Scheduled calls are made NONE, so should not effect state + run_to_block(100); + assert!(logger::log().is_empty()); + }); +} + +#[test] +fn should_check_origin() { + new_test_ext().execute_with(|| { + let call = Box::new(RuntimeCall::Logger(LoggerCall::log { + i: 69, + weight: Weight::from_ref_time(10), + })); + let call2 = Box::new(RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: Weight::from_ref_time(10), + })); + assert_noop!( + Scheduler::schedule_named( + system::RawOrigin::Signed(2).into(), + [1u8; 32], + 4, + None, + None, + call + ), + BadOrigin + ); + assert_noop!( + Scheduler::schedule(system::RawOrigin::Signed(2).into(), 4, None, None, call2), + BadOrigin + ); + }); +} + +#[test] +fn should_check_origin_for_cancel() { + new_test_ext().execute_with(|| { + let call = Box::new(RuntimeCall::Logger(LoggerCall::log_without_filter { + i: 69, + weight: Weight::from_ref_time(10), + })); + let call2 = Box::new(RuntimeCall::Logger(LoggerCall::log_without_filter { + i: 42, + weight: Weight::from_ref_time(10), + })); + assert_ok!(Scheduler::schedule_named( + system::RawOrigin::Signed(1).into(), + [1u8; 32], + 4, + None, + None, + call, + )); + assert_ok!(Scheduler::schedule( + system::RawOrigin::Signed(1).into(), + 4, + None, + None, + call2, + )); + run_to_block(3); + // Scheduled calls are in the agenda. + assert_eq!(Agenda::::get(4).len(), 2); + assert!(logger::log().is_empty()); + assert_noop!( + Scheduler::cancel_named(system::RawOrigin::Signed(2).into(), [1u8; 32]), + BadOrigin + ); + assert_noop!( + Scheduler::cancel(system::RawOrigin::Signed(2).into(), 4, 1), + BadOrigin + ); + assert_noop!( + Scheduler::cancel_named(system::RawOrigin::Root.into(), [1u8; 32]), + BadOrigin + ); + assert_noop!( + Scheduler::cancel(system::RawOrigin::Root.into(), 4, 1), + BadOrigin + ); + run_to_block(5); + assert_eq!( + logger::log(), + vec![ + (system::RawOrigin::Signed(1).into(), 69u32), + (system::RawOrigin::Signed(1).into(), 42u32) + ] + ); + }); +} + +/// Cancelling a call and then scheduling a second call for the same +/// block results in different addresses. +#[test] +fn schedule_does_not_resuse_addr() { + new_test_ext().execute_with(|| { + let call = RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: Weight::from_ref_time(10), + }); + + // Schedule both calls. + let addr_1 = Scheduler::do_schedule( + DispatchTime::At(4), + None, + 127, + root(), + >::new(call.clone()).unwrap(), + ) + .unwrap(); + // Cancel the call. + assert_ok!(Scheduler::do_cancel(None, addr_1)); + let addr_2 = Scheduler::do_schedule( + DispatchTime::At(4), + None, + 127, + root(), + >::new(call).unwrap(), + ) + .unwrap(); + + // Should not re-use the address. + assert!(addr_1 != addr_2); + }); +} + +#[test] +fn schedule_agenda_overflows() { + let max: u32 = ::MaxScheduledPerBlock::get(); + + new_test_ext().execute_with(|| { + let call = RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: Weight::from_ref_time(10), + }); + let call = >::new(call).unwrap(); + + // Schedule the maximal number allowed per block. + for _ in 0..max { + Scheduler::do_schedule(DispatchTime::At(4), None, 127, root(), call.clone()).unwrap(); + } + + // One more time and it errors. + assert_noop!( + Scheduler::do_schedule(DispatchTime::At(4), None, 127, root(), call,), + >::AgendaIsExhausted, + ); + + run_to_block(4); + // All scheduled calls are executed. + assert_eq!(logger::log().len() as u32, max); + }); +} + +/// Cancelling and scheduling does not overflow the agenda but fills holes. +#[test] +fn cancel_and_schedule_fills_holes() { + let max: u32 = ::MaxScheduledPerBlock::get(); + assert!( + max > 3, + "This test only makes sense for MaxScheduledPerBlock > 3" + ); + + new_test_ext().execute_with(|| { + let call = RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: Weight::from_ref_time(10), + }); + let call = >::new(call).unwrap(); + let mut addrs = Vec::<_>::default(); + + // Schedule the maximal number allowed per block. + for _ in 0..max { + addrs.push( + Scheduler::do_schedule(DispatchTime::At(4), None, 127, root(), call.clone()) + .unwrap(), + ); + } + // Cancel three of them. + for addr in addrs.into_iter().take(3) { + Scheduler::do_cancel(None, addr).unwrap(); + } + // Schedule three new ones. + for i in 0..3 { + let (_block, index) = + Scheduler::do_schedule(DispatchTime::At(4), None, 127, root(), call.clone()) + .unwrap(); + assert_eq!(i, index); + } + + run_to_block(4); + // Maximum number of calls are executed. + assert_eq!(logger::log().len() as u32, max); + }); +} --- /dev/null +++ b/pallets/scheduler-v2/src/weights.rs @@ -0,0 +1,270 @@ +// 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 . + +// Original license: +// This file is part of Substrate. + +// Copyright (C) 2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Autogenerated weights for pallet_scheduler +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2022-10-03, STEPS: `50`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! HOSTNAME: `bm3`, CPU: `Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// /home/benchbot/cargo_target_dir/production/substrate +// benchmark +// pallet +// --steps=50 +// --repeat=20 +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --pallet=pallet_scheduler +// --chain=dev +// --output=./frame/scheduler/src/weights.rs +// --template=./.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for pallet_scheduler. +pub trait WeightInfo { + fn service_agendas_base() -> Weight; + fn service_agenda_base(s: u32, ) -> Weight; + fn service_task_base() -> Weight; + fn service_task_fetched(s: u32, ) -> Weight; + fn service_task_named() -> Weight; + fn service_task_periodic() -> Weight; + fn execute_dispatch_signed() -> Weight; + fn execute_dispatch_unsigned() -> Weight; + fn schedule(s: u32, ) -> Weight; + fn cancel(s: u32, ) -> Weight; + fn schedule_named(s: u32, ) -> Weight; + fn cancel_named(s: u32, ) -> Weight; + fn change_named_priority(s: u32, ) -> Weight; +} + +/// Weights for pallet_scheduler using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + // Storage: Scheduler IncompleteSince (r:1 w:1) + fn service_agendas_base() -> Weight { + Weight::from_ref_time(4_992_000 as u64) + .saturating_add(T::DbWeight::get().reads(1 as u64)) + .saturating_add(T::DbWeight::get().writes(1 as u64)) + } + // Storage: Scheduler Agenda (r:1 w:1) + /// The range of component `s` is `[0, 512]`. + fn service_agenda_base(s: u32, ) -> Weight { + Weight::from_ref_time(4_320_000 as u64) + // Standard Error: 619 + .saturating_add(Weight::from_ref_time(336_713 as u64).saturating_mul(s as u64)) + .saturating_add(T::DbWeight::get().reads(1 as u64)) + .saturating_add(T::DbWeight::get().writes(1 as u64)) + } + fn service_task_base() -> Weight { + Weight::from_ref_time(10_864_000 as u64) + } + // Storage: Preimage PreimageFor (r:1 w:1) + // Storage: Preimage StatusFor (r:1 w:1) + /// The range of component `s` is `[128, 4194304]`. + fn service_task_fetched(s: u32, ) -> Weight { + Weight::from_ref_time(24_586_000 as u64) + // Standard Error: 1 + .saturating_add(Weight::from_ref_time(1_138 as u64).saturating_mul(s as u64)) + .saturating_add(T::DbWeight::get().reads(2 as u64)) + .saturating_add(T::DbWeight::get().writes(2 as u64)) + } + // Storage: Scheduler Lookup (r:0 w:1) + fn service_task_named() -> Weight { + Weight::from_ref_time(13_127_000 as u64) + .saturating_add(T::DbWeight::get().writes(1 as u64)) + } + fn service_task_periodic() -> Weight { + Weight::from_ref_time(11_053_000 as u64) + } + fn execute_dispatch_signed() -> Weight { + Weight::from_ref_time(4_158_000 as u64) + } + fn execute_dispatch_unsigned() -> Weight { + Weight::from_ref_time(4_104_000 as u64) + } + // Storage: Scheduler Agenda (r:1 w:1) + /// The range of component `s` is `[0, 511]`. + fn schedule(s: u32, ) -> Weight { + Weight::from_ref_time(20_074_000 as u64) + // Standard Error: 765 + .saturating_add(Weight::from_ref_time(343_285 as u64).saturating_mul(s as u64)) + .saturating_add(T::DbWeight::get().reads(1 as u64)) + .saturating_add(T::DbWeight::get().writes(1 as u64)) + } + // Storage: Scheduler Agenda (r:1 w:1) + // Storage: Scheduler Lookup (r:0 w:1) + /// The range of component `s` is `[1, 512]`. + fn cancel(s: u32, ) -> Weight { + Weight::from_ref_time(21_509_000 as u64) + // Standard Error: 708 + .saturating_add(Weight::from_ref_time(323_013 as u64).saturating_mul(s as u64)) + .saturating_add(T::DbWeight::get().reads(1 as u64)) + .saturating_add(T::DbWeight::get().writes(2 as u64)) + } + // Storage: Scheduler Lookup (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + /// The range of component `s` is `[0, 511]`. + fn schedule_named(s: u32, ) -> Weight { + Weight::from_ref_time(22_427_000 as u64) + // Standard Error: 850 + .saturating_add(Weight::from_ref_time(357_265 as u64).saturating_mul(s as u64)) + .saturating_add(T::DbWeight::get().reads(2 as u64)) + .saturating_add(T::DbWeight::get().writes(2 as u64)) + } + // Storage: Scheduler Lookup (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + /// The range of component `s` is `[1, 512]`. + fn cancel_named(s: u32, ) -> Weight { + Weight::from_ref_time(22_875_000 as u64) + // Standard Error: 693 + .saturating_add(Weight::from_ref_time(336_643 as u64).saturating_mul(s as u64)) + .saturating_add(T::DbWeight::get().reads(2 as u64)) + .saturating_add(T::DbWeight::get().writes(2 as u64)) + } + + // Storage: Scheduler Lookup (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn change_named_priority(s: u32, ) -> Weight { + Weight::from_ref_time(8_642_000) + // Standard Error: 0 + .saturating_add(Weight::from_ref_time(431_000).saturating_mul(s as u64)) + .saturating_add(T::DbWeight::get().reads(2 as u64)) + .saturating_add(T::DbWeight::get().writes(2 as u64)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + // Storage: Scheduler IncompleteSince (r:1 w:1) + fn service_agendas_base() -> Weight { + Weight::from_ref_time(4_992_000 as u64) + .saturating_add(RocksDbWeight::get().reads(1 as u64)) + .saturating_add(RocksDbWeight::get().writes(1 as u64)) + } + // Storage: Scheduler Agenda (r:1 w:1) + /// The range of component `s` is `[0, 512]`. + fn service_agenda_base(s: u32, ) -> Weight { + Weight::from_ref_time(4_320_000 as u64) + // Standard Error: 619 + .saturating_add(Weight::from_ref_time(336_713 as u64).saturating_mul(s as u64)) + .saturating_add(RocksDbWeight::get().reads(1 as u64)) + .saturating_add(RocksDbWeight::get().writes(1 as u64)) + } + fn service_task_base() -> Weight { + Weight::from_ref_time(10_864_000 as u64) + } + // Storage: Preimage PreimageFor (r:1 w:1) + // Storage: Preimage StatusFor (r:1 w:1) + /// The range of component `s` is `[128, 4194304]`. + fn service_task_fetched(s: u32, ) -> Weight { + Weight::from_ref_time(24_586_000 as u64) + // Standard Error: 1 + .saturating_add(Weight::from_ref_time(1_138 as u64).saturating_mul(s as u64)) + .saturating_add(RocksDbWeight::get().reads(2 as u64)) + .saturating_add(RocksDbWeight::get().writes(2 as u64)) + } + // Storage: Scheduler Lookup (r:0 w:1) + fn service_task_named() -> Weight { + Weight::from_ref_time(13_127_000 as u64) + .saturating_add(RocksDbWeight::get().writes(1 as u64)) + } + fn service_task_periodic() -> Weight { + Weight::from_ref_time(11_053_000 as u64) + } + fn execute_dispatch_signed() -> Weight { + Weight::from_ref_time(4_158_000 as u64) + } + fn execute_dispatch_unsigned() -> Weight { + Weight::from_ref_time(4_104_000 as u64) + } + // Storage: Scheduler Agenda (r:1 w:1) + /// The range of component `s` is `[0, 511]`. + fn schedule(s: u32, ) -> Weight { + Weight::from_ref_time(20_074_000 as u64) + // Standard Error: 765 + .saturating_add(Weight::from_ref_time(343_285 as u64).saturating_mul(s as u64)) + .saturating_add(RocksDbWeight::get().reads(1 as u64)) + .saturating_add(RocksDbWeight::get().writes(1 as u64)) + } + // Storage: Scheduler Agenda (r:1 w:1) + // Storage: Scheduler Lookup (r:0 w:1) + /// The range of component `s` is `[1, 512]`. + fn cancel(s: u32, ) -> Weight { + Weight::from_ref_time(21_509_000 as u64) + // Standard Error: 708 + .saturating_add(Weight::from_ref_time(323_013 as u64).saturating_mul(s as u64)) + .saturating_add(RocksDbWeight::get().reads(1 as u64)) + .saturating_add(RocksDbWeight::get().writes(2 as u64)) + } + // Storage: Scheduler Lookup (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + /// The range of component `s` is `[0, 511]`. + fn schedule_named(s: u32, ) -> Weight { + Weight::from_ref_time(22_427_000 as u64) + // Standard Error: 850 + .saturating_add(Weight::from_ref_time(357_265 as u64).saturating_mul(s as u64)) + .saturating_add(RocksDbWeight::get().reads(2 as u64)) + .saturating_add(RocksDbWeight::get().writes(2 as u64)) + } + // Storage: Scheduler Lookup (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + /// The range of component `s` is `[1, 512]`. + fn cancel_named(s: u32, ) -> Weight { + Weight::from_ref_time(22_875_000 as u64) + // Standard Error: 693 + .saturating_add(Weight::from_ref_time(336_643 as u64).saturating_mul(s as u64)) + .saturating_add(RocksDbWeight::get().reads(2 as u64)) + .saturating_add(RocksDbWeight::get().writes(2 as u64)) + } + + // Storage: Scheduler Lookup (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn change_named_priority(s: u32, ) -> Weight { + Weight::from_ref_time(8_642_000) + // Standard Error: 0 + .saturating_add(Weight::from_ref_time(431_000).saturating_mul(s as u64)) + .saturating_add(RocksDbWeight::get().reads(2 as u64)) + .saturating_add(RocksDbWeight::get().writes(2 as u64)) + } +} --- a/pallets/scheduler/src/benchmarking.rs +++ b/pallets/scheduler/src/benchmarking.rs @@ -50,6 +50,13 @@ const BLOCK_NUMBER: u32 = 2; +fn make_scheduled_id(src: u32) -> ScheduledId { + let slice_id: [u8; 4] = src.encode().try_into().unwrap(); + let mut id: [u8; 16] = [0; 16]; + id[..4].clone_from_slice(&slice_id); + id +} + /// Add `n` named items to the schedule. /// /// For `resolved`: @@ -79,10 +86,7 @@ false => None, }; - let slice_id: [u8; 4] = i.encode().try_into().unwrap(); - let mut id: [u8; 16] = [0; 16]; - id[..4].clone_from_slice(&slice_id); - + let id = make_scheduled_id(i); let origin = frame_system::RawOrigin::Signed(caller.clone()).into(); Scheduler::::do_schedule_named(id, t, period, 0, origin, call_or_hash)?; } @@ -202,7 +206,7 @@ id[..4].clone_from_slice(&slice_id); let when = BLOCK_NUMBER.into(); let periodic = Some((T::BlockNumber::one(), 100)); - let priority = 0; + let priority = None; // Essentially a no-op call. let inner_call = frame_system::Call::set_storage { items: vec![] }.into(); let call = Box::new(CallOrHashOf::::Value(inner_call)); @@ -220,7 +224,8 @@ let origin: RawOrigin = frame_system::RawOrigin::Signed(caller.clone()); let s in 1 .. T::MaxScheduledPerBlock::get(); let when = BLOCK_NUMBER.into(); - let id = 0.encode().try_into().unwrap_or([0; MAX_TASK_ID_LENGTH_IN_BYTES as usize]); + let idx = s - 1; + let id = make_scheduled_id(idx); fill_schedule::(when, s, true, Some(false))?; }: _(origin, id) verify { @@ -230,10 +235,26 @@ ); // Removed schedule is NONE ensure!( - Agenda::::get(when)[0].is_none(), + Agenda::::get(when)[idx as usize].is_none(), "didn't remove from schedule" ); } + change_named_priority { + let origin: RawOrigin = frame_system::RawOrigin::Root; + let s in 1 .. T::MaxScheduledPerBlock::get(); + let when = BLOCK_NUMBER.into(); + let idx = s - 1; + let id = make_scheduled_id(idx); + let priority = 42; + fill_schedule::(when, s, true, Some(false))?; + }: _(origin, id, priority) + verify { + ensure!( + Agenda::::get(when)[idx as usize].clone().unwrap().priority == priority, + "didn't change the priority" + ); + } + impl_benchmark_test_suite!(Scheduler, crate::mock::new_test_ext(), crate::mock::Test); } --- a/pallets/scheduler/src/lib.rs +++ b/pallets/scheduler/src/lib.rs @@ -86,7 +86,10 @@ use sp_std::{borrow::Borrow, cmp::Ordering, marker::PhantomData, prelude::*}; use frame_support::{ - dispatch::{DispatchError, DispatchResult, Dispatchable, Parameter, GetDispatchInfo}, + dispatch::{ + DispatchError, DispatchResult, Dispatchable, UnfilteredDispatchable, Parameter, + GetDispatchInfo, + }, traits::{ schedule::{self, DispatchTime, MaybeHashed}, NamedReservableCurrency, EnsureOrigin, Get, IsType, OriginTrait, PrivilegeCmp, @@ -97,13 +100,11 @@ pub use weights::WeightInfo; -/// Just a simple index for naming period tasks. -pub type PeriodicIndex = u32; /// The location of a scheduled task that can be used to remove it. pub type TaskAddress = (BlockNumber, u32); pub const MAX_TASK_ID_LENGTH_IN_BYTES: u8 = 16; -type ScheduledId = [u8; MAX_TASK_ID_LENGTH_IN_BYTES as usize]; +pub type ScheduledId = [u8; MAX_TASK_ID_LENGTH_IN_BYTES as usize]; pub type CallOrHashOf = MaybeHashed<::RuntimeCall, ::Hash>; @@ -137,6 +138,11 @@ pub type Scheduled = ScheduledV3; +pub enum ScheduledEnsureOriginSuccess { + Root, + Signed(AccountId), +} + #[cfg(feature = "runtime-benchmarks")] mod preimage_provider { use frame_support::traits::PreimageRecipient; @@ -193,7 +199,10 @@ use frame_support::{ dispatch::PostDispatchInfo, pallet_prelude::*, - traits::{schedule::LookupError, PreimageProvider}, + traits::{ + schedule::{LookupError, LOWEST_PRIORITY}, + PreimageProvider, + }, }; use frame_system::pallet_prelude::*; @@ -227,7 +236,8 @@ + Dispatchable< RuntimeOrigin = ::RuntimeOrigin, PostInfo = PostDispatchInfo, - > + GetDispatchInfo + > + UnfilteredDispatchable::RuntimeOrigin> + + GetDispatchInfo + From>; /// The maximum weight that may be scheduled per block for any dispatchables of less @@ -236,7 +246,13 @@ type MaximumWeight: Get; /// Required origin to schedule or cancel calls. - type ScheduleOrigin: EnsureOrigin<::RuntimeOrigin>; + type ScheduleOrigin: EnsureOrigin< + ::RuntimeOrigin, + Success = ScheduledEnsureOriginSuccess, + >; + + /// Required origin to set/change calls' priority. + type PrioritySetOrigin: EnsureOrigin<::RuntimeOrigin>; /// Compare the privileges of origins. /// @@ -287,7 +303,7 @@ /// Resolve the call dispatch, including any post-dispatch operations. fn dispatch_call( - signer: T::AccountId, + signer: Option, function: ::RuntimeCall, ) -> Result< Result>, @@ -319,6 +335,12 @@ Scheduled { when: T::BlockNumber, index: u32 }, /// Canceled some task. Canceled { when: T::BlockNumber, index: u32 }, + /// Scheduled task's priority has changed + PriorityChanged { + when: T::BlockNumber, + index: u32, + priority: schedule::Priority, + }, /// Dispatched some task. Dispatched { task: TaskAddress, @@ -371,12 +393,7 @@ let mut total_weight: Weight = T::WeightInfo::on_initialize(0); for (order, (index, mut s)) in queued.into_iter().enumerate() { - let named = if let Some(ref id) = s.maybe_id { - Lookup::::remove(id); - true - } else { - false - }; + let named = s.maybe_id.is_some(); let (call, maybe_completed) = s.call.resolved::(); s.call = call; @@ -399,6 +416,8 @@ Lookup::::insert(id, (until, index as u32)); } Agenda::::append(until, Some(s)); + } else if let Some(ref id) = s.maybe_id { + Lookup::::remove(id); } continue; } @@ -439,26 +458,23 @@ continue; } - let sender = ensure_signed( + let scheduled_origin = <::RuntimeOrigin as From>::from( s.origin.clone(), - ) - .into(), - ) - .unwrap(); - - // // if call have id it was be reserved - // if s.maybe_id.is_some() { - // let _ = T::CallExecutor::pay_for_call( - // s.maybe_id.unwrap(), - // sender.clone(), - // call.clone(), - // ); - // } + ); + let ensured_origin = T::ScheduleOrigin::ensure_origin(scheduled_origin.into()); - // Execute transaction via chain default pipeline - // That means dispatch will be processed like any user's extrinsic e.g. transaction fees will be taken - let r = T::CallExecutor::dispatch_call(sender, call.clone()); + let r = match ensured_origin { + Ok(ScheduledEnsureOriginSuccess::Root) => { + Ok(call.dispatch_bypass_filter(frame_system::RawOrigin::Root.into())) + } + Ok(ScheduledEnsureOriginSuccess::Signed(sender)) => { + // Execute transaction via chain default pipeline + // That means dispatch will be processed like any user's extrinsic e.g. transaction fees will be taken + T::CallExecutor::dispatch_call(Some(sender), call.clone()) + } + Err(e) => Ok(Err(e.into())), + }; let mut actual_call_weight: Weight = item_weight; let result: Result<_, DispatchError> = match r { @@ -494,16 +510,28 @@ s.maybe_periodic = None; } let wake = now + period; + let is_canceled; + // If scheduled is named, place its information in `Lookup` if let Some(ref id) = s.maybe_id { + is_canceled = Lookup::::get(id).is_none(); let wake_index = Agenda::::decode_len(wake).unwrap_or(0); - Lookup::::insert(id, (wake, wake_index as u32)); + + if !is_canceled { + Lookup::::insert(id, (wake, wake_index as u32)); + } + } else { + is_canceled = false; + } + + if !is_canceled { + Agenda::::append(wake, Some(s)); } - Agenda::::append(wake, Some(s)); + } else if let Some(ref id) = s.maybe_id { + Lookup::::remove(id); } } - // Total weight should be 0, because the transaction is already paid for - Weight::zero() + total_weight } } @@ -516,16 +544,21 @@ id: ScheduledId, when: T::BlockNumber, maybe_periodic: Option>, - priority: schedule::Priority, + priority: Option, call: Box>, ) -> DispatchResult { T::ScheduleOrigin::ensure_origin(origin.clone())?; + + if priority.is_some() { + T::PrioritySetOrigin::ensure_origin(origin.clone())?; + } + let origin = ::RuntimeOrigin::from(origin); Self::do_schedule_named( id, DispatchTime::At(when), maybe_periodic, - priority, + priority.unwrap_or(LOWEST_PRIORITY), origin.caller().clone(), *call, )?; @@ -552,21 +585,37 @@ id: ScheduledId, after: T::BlockNumber, maybe_periodic: Option>, - priority: schedule::Priority, + priority: Option, call: Box>, ) -> DispatchResult { T::ScheduleOrigin::ensure_origin(origin.clone())?; + + if priority.is_some() { + T::PrioritySetOrigin::ensure_origin(origin.clone())?; + } + let origin = ::RuntimeOrigin::from(origin); Self::do_schedule_named( id, DispatchTime::After(after), maybe_periodic, - priority, + priority.unwrap_or(LOWEST_PRIORITY), origin.caller().clone(), *call, )?; Ok(()) } + + #[pallet::weight(::WeightInfo::change_named_priority(T::MaxScheduledPerBlock::get()))] + pub fn change_named_priority( + origin: OriginFor, + id: ScheduledId, + priority: schedule::Priority, + ) -> DispatchResult { + T::PrioritySetOrigin::ensure_origin(origin.clone())?; + let origin = ::RuntimeOrigin::from(origin); + Self::do_change_named_priority(origin.caller().clone(), id, priority) + } } } @@ -719,4 +768,35 @@ } }) } + + fn do_change_named_priority( + origin: T::PalletsOrigin, + id: ScheduledId, + priority: schedule::Priority, + ) -> DispatchResult { + match Lookup::::get(id) { + Some((when, index)) => { + let i = index as usize; + Agenda::::try_mutate(when, |agenda| { + if let Some(Some(s)) = agenda.get_mut(i) { + if matches!( + T::OriginPrivilegeCmp::cmp_privilege(&origin, &s.origin), + Some(Ordering::Less) | None + ) { + return Err(BadOrigin.into()); + } + + s.priority = priority; + Self::deposit_event(Event::PriorityChanged { + when, + index, + priority, + }); + } + Ok(()) + }) + } + None => Err(Error::::NotFound.into()), + } + } } --- a/pallets/scheduler/src/weights.rs +++ b/pallets/scheduler/src/weights.rs @@ -3,7 +3,7 @@ //! Autogenerated weights for pallet_unique_scheduler //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2022-08-15, STEPS: `50`, REPEAT: 80, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2022-09-14, STEPS: `50`, REPEAT: 80, LOW RANGE: `[]`, HIGH RANGE: `[]` //! EXECUTION: None, WASM-EXECUTION: Compiled, CHAIN: None, DB CACHE: 1024 // Executed Command: @@ -26,6 +26,7 @@ #![cfg_attr(rustfmt, rustfmt_skip)] #![allow(unused_parens)] #![allow(unused_imports)] +#![allow(missing_docs)] #![allow(clippy::unnecessary_cast)] use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; @@ -44,6 +45,7 @@ fn on_initialize_resolved(s: u32, ) -> Weight; fn schedule_named(s: u32, ) -> Weight; fn cancel_named(s: u32, ) -> Weight; + fn change_named_priority(s: u32, ) -> Weight; } /// Weights for pallet_unique_scheduler using the Substrate node and recommended hardware. @@ -53,7 +55,9 @@ // Storage: System Account (r:1 w:1) // Storage: System AllExtrinsicsLen (r:1 w:1) // Storage: System BlockWeight (r:1 w:1) - // Storage: Scheduler Lookup (r:0 w:1) + // Storage: Configuration WeightToFeeCoefficientOverride (r:1 w:0) + // Storage: TransactionPayment NextFeeMultiplier (r:1 w:0) + // Storage: Scheduler Lookup (r:1 w:1) fn on_initialize_periodic_named_resolved(s: u32, ) -> Weight { Weight::from_ref_time(26_641_000) // Standard Error: 9_000 @@ -67,6 +71,8 @@ // Storage: System Account (r:1 w:1) // Storage: System AllExtrinsicsLen (r:1 w:1) // Storage: System BlockWeight (r:1 w:1) + // Storage: Configuration WeightToFeeCoefficientOverride (r:1 w:0) + // Storage: TransactionPayment NextFeeMultiplier (r:1 w:0) // Storage: Scheduler Lookup (r:0 w:1) fn on_initialize_named_resolved(s: u32, ) -> Weight { Weight::from_ref_time(23_941_000) @@ -80,7 +86,9 @@ // Storage: System Account (r:1 w:1) // Storage: System AllExtrinsicsLen (r:1 w:1) // Storage: System BlockWeight (r:1 w:1) - // Storage: Scheduler Lookup (r:0 w:1) + // Storage: Configuration WeightToFeeCoefficientOverride (r:1 w:0) + // Storage: TransactionPayment NextFeeMultiplier (r:1 w:0) + // Storage: Scheduler Lookup (r:1 w:1) fn on_initialize_periodic(s: u32, ) -> Weight { Weight::from_ref_time(24_858_000) // Standard Error: 7_000 @@ -94,7 +102,9 @@ // Storage: System Account (r:1 w:1) // Storage: System AllExtrinsicsLen (r:1 w:1) // Storage: System BlockWeight (r:1 w:1) - // Storage: Scheduler Lookup (r:0 w:1) + // Storage: Configuration WeightToFeeCoefficientOverride (r:1 w:0) + // Storage: TransactionPayment NextFeeMultiplier (r:1 w:0) + // Storage: Scheduler Lookup (r:1 w:1) fn on_initialize_periodic_resolved(s: u32, ) -> Weight { Weight::from_ref_time(25_515_000) // Standard Error: 14_000 @@ -118,6 +128,8 @@ // Storage: System Account (r:1 w:1) // Storage: System AllExtrinsicsLen (r:1 w:1) // Storage: System BlockWeight (r:1 w:1) + // Storage: Configuration WeightToFeeCoefficientOverride (r:1 w:0) + // Storage: TransactionPayment NextFeeMultiplier (r:1 w:0) // Storage: Scheduler Lookup (r:0 w:1) fn on_initialize_named_aborted(s: u32, ) -> Weight { Weight::from_ref_time(25_552_000) @@ -141,6 +153,8 @@ // Storage: System Account (r:1 w:1) // Storage: System AllExtrinsicsLen (r:1 w:1) // Storage: System BlockWeight (r:1 w:1) + // Storage: Configuration WeightToFeeCoefficientOverride (r:1 w:0) + // Storage: TransactionPayment NextFeeMultiplier (r:1 w:0) // Storage: Scheduler Lookup (r:0 w:1) fn on_initialize(s: u32, ) -> Weight { Weight::from_ref_time(24_482_000) @@ -154,6 +168,8 @@ // Storage: System Account (r:1 w:1) // Storage: System AllExtrinsicsLen (r:1 w:1) // Storage: System BlockWeight (r:1 w:1) + // Storage: Configuration WeightToFeeCoefficientOverride (r:1 w:0) + // Storage: TransactionPayment NextFeeMultiplier (r:1 w:0) // Storage: Scheduler Lookup (r:0 w:1) fn on_initialize_resolved(s: u32, ) -> Weight { Weight::from_ref_time(25_187_000) @@ -181,6 +197,15 @@ .saturating_add(T::DbWeight::get().reads(2 as u64)) .saturating_add(T::DbWeight::get().writes(2 as u64)) } + // Storage: Scheduler Lookup (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn change_named_priority(s: u32, ) -> Weight { + Weight::from_ref_time(8_642_000) + // Standard Error: 0 + .saturating_add(Weight::from_ref_time(431_000).saturating_mul(s as u64)) + .saturating_add(T::DbWeight::get().reads(2 as u64)) + .saturating_add(T::DbWeight::get().writes(2 as u64)) + } } // For backwards compatibility and tests @@ -189,7 +214,9 @@ // Storage: System Account (r:1 w:1) // Storage: System AllExtrinsicsLen (r:1 w:1) // Storage: System BlockWeight (r:1 w:1) - // Storage: Scheduler Lookup (r:0 w:1) + // Storage: Configuration WeightToFeeCoefficientOverride (r:1 w:0) + // Storage: TransactionPayment NextFeeMultiplier (r:1 w:0) + // Storage: Scheduler Lookup (r:1 w:1) fn on_initialize_periodic_named_resolved(s: u32, ) -> Weight { Weight::from_ref_time(26_641_000) // Standard Error: 9_000 @@ -203,6 +230,8 @@ // Storage: System Account (r:1 w:1) // Storage: System AllExtrinsicsLen (r:1 w:1) // Storage: System BlockWeight (r:1 w:1) + // Storage: Configuration WeightToFeeCoefficientOverride (r:1 w:0) + // Storage: TransactionPayment NextFeeMultiplier (r:1 w:0) // Storage: Scheduler Lookup (r:0 w:1) fn on_initialize_named_resolved(s: u32, ) -> Weight { Weight::from_ref_time(23_941_000) @@ -216,7 +245,9 @@ // Storage: System Account (r:1 w:1) // Storage: System AllExtrinsicsLen (r:1 w:1) // Storage: System BlockWeight (r:1 w:1) - // Storage: Scheduler Lookup (r:0 w:1) + // Storage: Configuration WeightToFeeCoefficientOverride (r:1 w:0) + // Storage: TransactionPayment NextFeeMultiplier (r:1 w:0) + // Storage: Scheduler Lookup (r:1 w:1) fn on_initialize_periodic(s: u32, ) -> Weight { Weight::from_ref_time(24_858_000) // Standard Error: 7_000 @@ -230,7 +261,9 @@ // Storage: System Account (r:1 w:1) // Storage: System AllExtrinsicsLen (r:1 w:1) // Storage: System BlockWeight (r:1 w:1) - // Storage: Scheduler Lookup (r:0 w:1) + // Storage: Configuration WeightToFeeCoefficientOverride (r:1 w:0) + // Storage: TransactionPayment NextFeeMultiplier (r:1 w:0) + // Storage: Scheduler Lookup (r:1 w:1) fn on_initialize_periodic_resolved(s: u32, ) -> Weight { Weight::from_ref_time(25_515_000) // Standard Error: 14_000 @@ -254,6 +287,8 @@ // Storage: System Account (r:1 w:1) // Storage: System AllExtrinsicsLen (r:1 w:1) // Storage: System BlockWeight (r:1 w:1) + // Storage: Configuration WeightToFeeCoefficientOverride (r:1 w:0) + // Storage: TransactionPayment NextFeeMultiplier (r:1 w:0) // Storage: Scheduler Lookup (r:0 w:1) fn on_initialize_named_aborted(s: u32, ) -> Weight { Weight::from_ref_time(25_552_000) @@ -277,6 +312,8 @@ // Storage: System Account (r:1 w:1) // Storage: System AllExtrinsicsLen (r:1 w:1) // Storage: System BlockWeight (r:1 w:1) + // Storage: Configuration WeightToFeeCoefficientOverride (r:1 w:0) + // Storage: TransactionPayment NextFeeMultiplier (r:1 w:0) // Storage: Scheduler Lookup (r:0 w:1) fn on_initialize(s: u32, ) -> Weight { Weight::from_ref_time(24_482_000) @@ -290,6 +327,8 @@ // Storage: System Account (r:1 w:1) // Storage: System AllExtrinsicsLen (r:1 w:1) // Storage: System BlockWeight (r:1 w:1) + // Storage: Configuration WeightToFeeCoefficientOverride (r:1 w:0) + // Storage: TransactionPayment NextFeeMultiplier (r:1 w:0) // Storage: Scheduler Lookup (r:0 w:1) fn on_initialize_resolved(s: u32, ) -> Weight { Weight::from_ref_time(25_187_000) @@ -317,4 +356,14 @@ .saturating_add(RocksDbWeight::get().reads(2 as u64)) .saturating_add(RocksDbWeight::get().writes(2 as u64)) } + + // Storage: Scheduler Lookup (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn change_named_priority(s: u32, ) -> Weight { + Weight::from_ref_time(8_642_000) + // Standard Error: 0 + .saturating_add(Weight::from_ref_time(431_000).saturating_mul(s as u64)) + .saturating_add(RocksDbWeight::get().reads(2 as u64)) + .saturating_add(RocksDbWeight::get().writes(2 as u64)) + } } --- 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/primitives/data-structs/src/lib.rs +++ b/primitives/data-structs/src/lib.rs @@ -447,6 +447,7 @@ } /// Collection parameters, used in RPC calls (see [`Collection`] for the storage version). +#[struct_versioning::versioned(version = 2, upper)] #[derive(Encode, Decode, Clone, PartialEq, TypeInfo)] #[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))] pub struct RpcCollection { @@ -484,6 +485,7 @@ pub read_only: bool, /// Extra collection flags + #[version(2.., upper(RpcCollectionFlags {foreign: false, erc721metadata: false}))] pub flags: RpcCollectionFlags, } --- a/primitives/rpc/src/lib.rs +++ b/primitives/rpc/src/lib.rs @@ -16,9 +16,11 @@ #![cfg_attr(not(feature = "std"), no_std)] +extern crate alloc; + use up_data_structs::{ CollectionId, TokenId, RpcCollection, CollectionStats, CollectionLimits, Property, - PropertyKeyPermission, TokenData, TokenChild, + PropertyKeyPermission, TokenData, TokenChild, RpcCollectionVersion1, }; use sp_std::vec::Vec; @@ -28,15 +30,12 @@ type Result = core::result::Result; sp_api::decl_runtime_apis! { - #[api_version(2)] + #[api_version(3)] /// Trait for generate rpc. pub trait UniqueApi where AccountId: Decode, CrossAccountId: pallet_evm::account::CrossAccountId, { - #[changed_in(2)] - fn token_owner(collection: CollectionId, token: TokenId) -> Result; - /// Get number of tokens in collection owned by account. fn account_tokens(collection: CollectionId, account: CrossAccountId) -> Result>; @@ -110,6 +109,9 @@ /// Get collection by id. fn collection_by_id(collection: CollectionId) -> Result>>; + #[changed_in(3)] + fn collection_by_id(collection: CollectionId) -> Result>>; + /// Get collection stats. fn collection_stats() -> Result; --- a/runtime/common/config/mod.rs +++ b/runtime/common/config/mod.rs @@ -21,3 +21,6 @@ pub mod sponsoring; pub mod substrate; pub mod xcm; + +#[cfg(feature = "pallet-test-utils")] +pub mod test_pallets; --- a/runtime/common/config/pallets/scheduler.rs +++ b/runtime/common/config/pallets/scheduler.rs @@ -14,14 +14,20 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . -use frame_support::{traits::PrivilegeCmp, weights::Weight, parameter_types}; -use frame_system::EnsureSigned; +use frame_support::{ + traits::{PrivilegeCmp, EnsureOrigin}, + weights::Weight, + parameter_types, +}; +use frame_system::{EnsureRoot, RawOrigin}; use sp_runtime::Perbill; -use sp_std::cmp::Ordering; +use core::cmp::Ordering; +use codec::Decode; use crate::{ runtime_common::{scheduler::SchedulerPaymentExecutor, config::substrate::RuntimeBlockWeights}, - Runtime, Call, Event, Origin, OriginCaller, Balances, + Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, OriginCaller, Balances, }; +use pallet_unique_scheduler_v2::ScheduledEnsureOriginSuccess; use up_common::types::AccountId; parameter_types! { @@ -33,27 +39,65 @@ pub const Preimage: Option = Some(10); } -/// Used the compare the privilege of an origin inside the scheduler. -pub struct OriginPrivilegeCmp; +pub struct EnsureSignedOrRoot(sp_std::marker::PhantomData); +impl, O>> + From>, AccountId: Decode> + EnsureOrigin for EnsureSignedOrRoot +{ + type Success = ScheduledEnsureOriginSuccess; + fn try_origin(o: O) -> Result { + o.into().and_then(|o| match o { + RawOrigin::Root => Ok(ScheduledEnsureOriginSuccess::Root), + RawOrigin::Signed(who) => Ok(ScheduledEnsureOriginSuccess::Signed(who)), + r => Err(O::from(r)), + }) + } +} + +pub struct EqualOrRootOnly; +impl PrivilegeCmp for EqualOrRootOnly { + fn cmp_privilege(left: &OriginCaller, right: &OriginCaller) -> Option { + use RawOrigin::*; + + let left = left.clone().try_into().ok()?; + let right = right.clone().try_into().ok()?; -impl PrivilegeCmp for OriginPrivilegeCmp { - fn cmp_privilege(_left: &OriginCaller, _right: &OriginCaller) -> Option { - Some(Ordering::Equal) + match (left, right) { + (Root, Root) => Some(Ordering::Equal), + (Root, _) => Some(Ordering::Greater), + (_, Root) => Some(Ordering::Less), + lr @ _ => (lr.0 == lr.1).then(|| Ordering::Equal), + } } } -impl pallet_unique_scheduler::Config for Runtime { +// impl pallet_unique_scheduler::Config for Runtime { +// type RuntimeEvent = RuntimeEvent; +// type RuntimeOrigin = RuntimeOrigin; +// type Currency = Balances; +// type PalletsOrigin = OriginCaller; +// type RuntimeCall = RuntimeCall; +// type MaximumWeight = MaximumSchedulerWeight; +// type ScheduleOrigin = EnsureSignedOrRoot; +// type PrioritySetOrigin = EnsureRoot; +// type MaxScheduledPerBlock = MaxScheduledPerBlock; +// type WeightInfo = (); +// type CallExecutor = SchedulerPaymentExecutor; +// type OriginPrivilegeCmp = EqualOrRootOnly; +// type PreimageProvider = (); +// type NoPreimagePostponement = NoPreimagePostponement; +// } + +impl pallet_unique_scheduler_v2::Config for Runtime { type RuntimeEvent = RuntimeEvent; type RuntimeOrigin = RuntimeOrigin; - type Currency = Balances; type PalletsOrigin = OriginCaller; type RuntimeCall = RuntimeCall; type MaximumWeight = MaximumSchedulerWeight; - type ScheduleOrigin = EnsureSigned; + type ScheduleOrigin = EnsureSignedOrRoot; + type OriginPrivilegeCmp = EqualOrRootOnly; type MaxScheduledPerBlock = MaxScheduledPerBlock; type WeightInfo = (); + type Preimages = (); type CallExecutor = SchedulerPaymentExecutor; - type OriginPrivilegeCmp = OriginPrivilegeCmp; - type PreimageProvider = (); - type NoPreimagePostponement = NoPreimagePostponement; + type PrioritySetOrigin = EnsureRoot; } --- /dev/null +++ b/runtime/common/config/test_pallets.rs @@ -0,0 +1,21 @@ +// 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 . + +use crate::{Runtime, RuntimeEvent}; + +impl pallet_test_utils::Config for Runtime { + type RuntimeEvent = RuntimeEvent; +} --- a/runtime/common/construct_runtime/mod.rs +++ b/runtime/common/construct_runtime/mod.rs @@ -93,6 +93,12 @@ EvmContractHelpers: pallet_evm_contract_helpers::{Pallet, Storage, Event} = 151, EvmTransactionPayment: pallet_evm_transaction_payment::{Pallet} = 152, EvmMigration: pallet_evm_migration::{Pallet, Call, Storage} = 153, + + #[runtimes(opal)] + Scheduler: pallet_unique_scheduler_v2::{Pallet, Call, Storage, Event} = 154, + + #[runtimes(opal)] + TestUtils: pallet_test_utils = 255, } } } --- a/runtime/common/scheduler.rs +++ b/runtime/common/scheduler.rs @@ -16,7 +16,7 @@ use frame_support::{ traits::NamedReservableCurrency, - weights::{GetDispatchInfo, PostDispatchInfo, DispatchInfo}, + dispatch::{GetDispatchInfo, PostDispatchInfo, DispatchInfo}, }; use sp_runtime::{ traits::{Dispatchable, Applyable, Member}, @@ -24,10 +24,15 @@ transaction_validity::TransactionValidityError, DispatchErrorWithPostInfo, DispatchError, }; -use crate::{Runtime, Call, Origin, Balances, ChargeTransactionPayment}; +use codec::Encode; +use crate::{Runtime, RuntimeCall, RuntimeOrigin, Balances}; use up_common::types::{AccountId, Balance}; use fp_self_contained::SelfContainedCall; -use pallet_unique_scheduler::DispatchCall; +use pallet_unique_scheduler_v2::DispatchCall; +use pallet_transaction_payment::ChargeTransactionPayment; + +// type SponsorshipChargeTransactionPayment = +// pallet_charge_transaction::ChargeTransactionPayment; /// The SignedExtension to the basic transaction logic. pub type SignedExtraScheduler = ( @@ -36,6 +41,7 @@ frame_system::CheckEra, frame_system::CheckNonce, frame_system::CheckWeight, + ChargeTransactionPayment, ); fn get_signed_extras(from: ::AccountId) -> SignedExtraScheduler { @@ -47,95 +53,148 @@ from, )), frame_system::CheckWeight::::new(), - // sponsoring transaction logic - // pallet_charge_transaction::ChargeTransactionPayment::::new(0), + ChargeTransactionPayment::::from(0), ) } pub struct SchedulerPaymentExecutor; -impl +impl DispatchCall for SchedulerPaymentExecutor where - ::Call: Member - + Dispatchable + ::RuntimeCall: Member + + Dispatchable + SelfContainedCall + GetDispatchInfo + From>, SelfContainedSignedInfo: Send + Sync + 'static, - Call: From<::Call> - + From<::Call> + RuntimeCall: From<::RuntimeCall> + + From<::RuntimeCall> + SelfContainedCall, sp_runtime::AccountId32: From<::AccountId>, { fn dispatch_call( - signer: ::AccountId, - call: ::Call, + signer: Option<::AccountId>, + call: ::RuntimeCall, ) -> Result< Result>, TransactionValidityError, > { let dispatch_info = call.get_dispatch_info(); + let len = call.encoded_size(); + + let signed = match signer { + Some(signer) => fp_self_contained::CheckedSignature::Signed( + signer.clone().into(), + get_signed_extras(signer.into()), + ), + None => fp_self_contained::CheckedSignature::Unsigned, + }; + let extrinsic = fp_self_contained::CheckedExtrinsic::< AccountId, - Call, + RuntimeCall, SignedExtraScheduler, SelfContainedSignedInfo, > { - signed: fp_self_contained::CheckedSignature::< - AccountId, - SignedExtraScheduler, - SelfContainedSignedInfo, - >::Signed(signer.clone().into(), get_signed_extras(signer.into())), + signed, function: call.into(), }; - extrinsic.apply::(&dispatch_info, 0) + extrinsic.apply::(&dispatch_info, len) } +} - fn reserve_balance( - id: [u8; 16], - sponsor: ::AccountId, - call: ::Call, - count: u32, - ) -> Result<(), DispatchError> { - let dispatch_info = call.get_dispatch_info(); - let weight: Balance = ChargeTransactionPayment::traditional_fee(0, &dispatch_info, 0) - .saturating_mul(count.into()); +// impl +// DispatchCall for SchedulerPaymentExecutor +// where +// ::Call: Member +// + Dispatchable +// + SelfContainedCall +// + GetDispatchInfo +// + From>, +// SelfContainedSignedInfo: Send + Sync + 'static, +// Call: From<::Call> +// + From<::Call> +// + SelfContainedCall, +// sp_runtime::AccountId32: From<::AccountId>, +// { +// fn dispatch_call( +// signer: Option<::AccountId>, +// call: ::Call, +// ) -> Result< +// Result>, +// TransactionValidityError, +// > { +// let dispatch_info = call.get_dispatch_info(); +// let len = call.encoded_size(); - >::reserve_named( - &id, - &(sponsor.into()), - weight, - ) - } +// let signed = match signer { +// Some(signer) => fp_self_contained::CheckedSignature::Signed( +// signer.clone().into(), +// get_signed_extras(signer.into()), +// ), +// None => fp_self_contained::CheckedSignature::Unsigned, +// }; - fn pay_for_call( - id: [u8; 16], - sponsor: ::AccountId, - call: ::Call, - ) -> Result { - let dispatch_info = call.get_dispatch_info(); - let weight: Balance = ChargeTransactionPayment::traditional_fee(0, &dispatch_info, 0); - Ok( - >::unreserve_named( - &id, - &(sponsor.into()), - weight, - ), - ) - } +// let extrinsic = fp_self_contained::CheckedExtrinsic::< +// AccountId, +// Call, +// SignedExtraScheduler, +// SelfContainedSignedInfo, +// > { +// signed, +// function: call.into(), +// }; + +// extrinsic.apply::(&dispatch_info, len) +// } - fn cancel_reserve( - id: [u8; 16], - sponsor: ::AccountId, - ) -> Result { - Ok( - >::unreserve_named( - &id, - &(sponsor.into()), - u128::MAX, - ), - ) - } -} +// fn reserve_balance( +// id: [u8; 16], +// sponsor: ::AccountId, +// call: ::Call, +// count: u32, +// ) -> Result<(), DispatchError> { +// let dispatch_info = call.get_dispatch_info(); +// let weight: Balance = +// SponsorshipChargeTransactionPayment::traditional_fee(0, &dispatch_info, 0) +// .saturating_mul(count.into()); + +// >::reserve_named( +// &id, +// &(sponsor.into()), +// weight, +// ) +// } + +// fn pay_for_call( +// id: [u8; 16], +// sponsor: ::AccountId, +// call: ::Call, +// ) -> Result { +// let dispatch_info = call.get_dispatch_info(); +// let weight: Balance = +// SponsorshipChargeTransactionPayment::traditional_fee(0, &dispatch_info, 0); +// Ok( +// >::unreserve_named( +// &id, +// &(sponsor.into()), +// weight, +// ), +// ) +// } + +// fn cancel_reserve( +// id: [u8; 16], +// sponsor: ::AccountId, +// ) -> Result { +// Ok( +// >::unreserve_named( +// &id, +// &(sponsor.into()), +// u128::MAX, +// ), +// ) +// } +// } --- a/runtime/opal/Cargo.toml +++ b/runtime/opal/Cargo.toml @@ -42,6 +42,7 @@ 'pallet-inflation/runtime-benchmarks', 'pallet-app-promotion/runtime-benchmarks', 'pallet-unique-scheduler/runtime-benchmarks', + 'pallet-unique-scheduler-v2/runtime-benchmarks', 'pallet-xcm/runtime-benchmarks', 'sp-runtime/runtime-benchmarks', 'xcm-builder/runtime-benchmarks', @@ -140,6 +141,7 @@ 'pallet-proxy-rmrk-equip/std', 'pallet-unique/std', 'pallet-unique-scheduler/std', + 'pallet-unique-scheduler-v2/std', 'pallet-charge-transaction/std', 'up-data-structs/std', 'sp-api/std', @@ -167,9 +169,11 @@ "orml-xtokens/std", "orml-traits/std", "pallet-foreign-assets/std", + + 'pallet-test-utils?/std', ] limit-testing = ['pallet-unique/limit-testing', 'up-data-structs/limit-testing'] -opal-runtime = ['refungible', 'rmrk', 'app-promotion', 'foreign-assets'] +opal-runtime = ['refungible', 'scheduler', 'rmrk', 'app-promotion', 'foreign-assets', 'pallet-test-utils'] refungible = [] scheduler = [] @@ -470,6 +474,7 @@ pallet-proxy-rmrk-equip = { default-features = false, path = "../../pallets/proxy-rmrk-equip", package = "pallet-rmrk-equip" } pallet-unique-scheduler = { path = '../../pallets/scheduler', default-features = false } pallet-charge-transaction = { git = "https://github.com/uniquenetwork/pallet-sponsoring", branch = "polkadot-v0.9.30", package = "pallet-template-transaction-payment", default-features = false, version = "3.0.0" } +pallet-unique-scheduler-v2 = { path = '../../pallets/scheduler-v2', default-features = false } pallet-evm-migration = { path = '../../pallets/evm-migration', default-features = false } pallet-evm-contract-helpers = { path = '../../pallets/evm-contract-helpers', default-features = false } pallet-evm-transaction-payment = { path = '../../pallets/evm-transaction-payment', default-features = false } @@ -484,6 +489,11 @@ pallet-foreign-assets = { default-features = false, path = "../../pallets/foreign-assets" } ################################################################################ +# Test dependencies + +pallet-test-utils = { optional = true, default-features = false, path = "../../test-pallets/utils" } + +################################################################################ # Other Dependencies impl-trait-for-tuples = "0.2.2" --- a/runtime/opal/src/lib.rs +++ b/runtime/opal/src/lib.rs @@ -50,7 +50,7 @@ spec_name: create_runtime_str!(RUNTIME_NAME), impl_name: create_runtime_str!(RUNTIME_NAME), authoring_version: 1, - spec_version: 930031, + spec_version: 930032, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 2, --- a/runtime/quartz/src/lib.rs +++ b/runtime/quartz/src/lib.rs @@ -50,7 +50,7 @@ spec_name: create_runtime_str!(RUNTIME_NAME), impl_name: create_runtime_str!(RUNTIME_NAME), authoring_version: 1, - spec_version: 930031, + spec_version: 930032, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 2, --- a/runtime/unique/src/lib.rs +++ b/runtime/unique/src/lib.rs @@ -50,7 +50,7 @@ spec_name: create_runtime_str!(RUNTIME_NAME), impl_name: create_runtime_str!(RUNTIME_NAME), authoring_version: 1, - spec_version: 930031, + spec_version: 930032, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 2, --- /dev/null +++ b/test-pallets/utils/Cargo.lock @@ -0,0 +1,2585 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "addr2line" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom 0.2.7", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7ed72e1635e121ca3e79420540282af22da58be50de153d36f81ddc6b83aa9e" +dependencies = [ + "libc", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "anyhow" +version = "1.0.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1485d4d2cc45e7b201ee3767015c96faa5904387c9d87c6efdd0fb511f12d305" + +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9" +dependencies = [ + "nodrop", +] + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + +[[package]] +name = "async-trait" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base16ct" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" + +[[package]] +name = "base58" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6107fe1be6682a68940da878d9e9f5e90ca5745b3dec9fd1bb393c8777d4f581" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cf849ee05b2ee5fba5e36f97ff8ec2533916700fc0758d40d92136a42f3388" +dependencies = [ + "digest 0.10.3", +] + +[[package]] +name = "blake2-rfc" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d530bdd2d52966a6d03b7a964add7ae1a288d25214066fd4b600f0f796400" +dependencies = [ + "arrayvec 0.4.12", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +dependencies = [ + "block-padding", + "byte-tools", + "byteorder", + "generic-array 0.12.4", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array 0.14.6", +] + +[[package]] +name = "block-buffer" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +dependencies = [ + "generic-array 0.14.6", +] + +[[package]] +name = "block-padding" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +dependencies = [ + "byte-tools", +] + +[[package]] +name = "bumpalo" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" + +[[package]] +name = "byte-slice-cast" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87c5fdd0166095e1d463fc6cc01aa8ce547ad77a4e84d42eb6762b084e28067e" + +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" +dependencies = [ + "iana-time-zone", + "num-integer", + "num-traits", + "winapi", +] + +[[package]] +name = "const-oid" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "cpufeatures" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1079fb8528d9f9c888b1e8aa651e6e079ade467323d58f75faf1d30b1808f540" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-bigint" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c6a1d5fa1de37e071642dfa44ec552ca5b299adb128fab16138e24b548fd21" +dependencies = [ + "generic-array 0.14.6", + "rand_core 0.6.3", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array 0.14.6", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array 0.14.6", + "subtle", +] + +[[package]] +name = "crypto-mac" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +dependencies = [ + "generic-array 0.14.6", + "subtle", +] + +[[package]] +name = "curve25519-dalek" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b85542f99a2dfa2a1b8e192662741c9859a846b296bef1c92ef9b58b5a216" +dependencies = [ + "byteorder", + "digest 0.8.1", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + +[[package]] +name = "der" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6919815d73839e7ad218de758883aae3a257ba6759ce7a9992501efbb53d705c" +dependencies = [ + "const-oid", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array 0.12.4", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array 0.14.6", +] + +[[package]] +name = "digest" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +dependencies = [ + "block-buffer 0.10.2", + "crypto-common", + "subtle", +] + +[[package]] +name = "downcast-rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + +[[package]] +name = "dyn-clonable" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e9232f0e607a262ceb9bd5141a3dfb3e4db6994b31989bbfd845878cba59fd4" +dependencies = [ + "dyn-clonable-impl", + "dyn-clone", +] + +[[package]] +name = "dyn-clonable-impl" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "558e40ea573c374cf53507fd240b7ee2f5477df7cfebdb97323ec61c719399c5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dyn-clone" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f94fa09c2aeea5b8839e414b7b841bf429fd25b9c522116ac97ee87856d88b2" + +[[package]] +name = "ecdsa" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0d69ae62e0ce582d56380743515fefaf1a8c70cec685d9677636d7e30ae9dc9" +dependencies = [ + "der", + "elliptic-curve", + "rfc6979", + "signature", +] + +[[package]] +name = "ed25519" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +dependencies = [ + "curve25519-dalek 3.2.0", + "ed25519", + "rand 0.7.3", + "serde", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + +[[package]] +name = "elliptic-curve" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25b477563c2bfed38a3b7a60964c49e058b2510ad3f12ba3483fd8f62c2306d6" +dependencies = [ + "base16ct", + "crypto-bigint", + "der", + "ff", + "generic-array 0.14.6", + "group", + "rand_core 0.6.3", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "environmental" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b91989ae21441195d7d9b9993a2f9295c7e1a8c96255d8b729accddc124797" + +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" + +[[package]] +name = "ff" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "131655483be284720a17d74ff97592b8e76576dc25563148601df2d7c9080924" +dependencies = [ + "rand_core 0.6.3", + "subtle", +] + +[[package]] +name = "fixed-hash" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c" +dependencies = [ + "byteorder", + "rand 0.8.5", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "frame-metadata" +version = "15.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df6bb8542ef006ef0de09a5c4420787d79823c0ed7924225822362fd2bf2ff2d" +dependencies = [ + "cfg-if", + "parity-scale-codec", + "scale-info", + "serde", +] + +[[package]] +name = "frame-support" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.27#8eff668a42325aeb4433eace1604f4d286a6ec05" +dependencies = [ + "bitflags", + "frame-metadata", + "frame-support-procedural", + "impl-trait-for-tuples", + "k256", + "log", + "once_cell", + "parity-scale-codec", + "paste", + "scale-info", + "serde", + "smallvec", + "sp-arithmetic", + "sp-core", + "sp-core-hashing-proc-macro", + "sp-inherents", + "sp-io", + "sp-runtime", + "sp-staking", + "sp-state-machine", + "sp-std", + "sp-tracing", + "tt-call", +] + +[[package]] +name = "frame-support-procedural" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.27#8eff668a42325aeb4433eace1604f4d286a6ec05" +dependencies = [ + "Inflector", + "frame-support-procedural-tools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "frame-support-procedural-tools" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.27#8eff668a42325aeb4433eace1604f4d286a6ec05" +dependencies = [ + "frame-support-procedural-tools-derive", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "frame-support-procedural-tools-derive" +version = "3.0.0" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.27#8eff668a42325aeb4433eace1604f4d286a6ec05" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "frame-system" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.27#8eff668a42325aeb4433eace1604f4d286a6ec05" +dependencies = [ + "frame-support", + "log", + "parity-scale-codec", + "scale-info", + "serde", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", + "sp-version", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab30e97ab6aacfe635fad58f22c2bb06c8b685f7421eb1e064a729e2a5f481fa" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bfc52cbddcfd745bf1740338492bb0bd83d76c67b445f91c5fb29fae29ecaa1" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2acedae88d38235936c3922476b10fced7b2b68136f5e3c03c2d5be348a1115" + +[[package]] +name = "futures-executor" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d11aa21b5b587a64682c0094c2bdd4df0076c5324961a40cc3abd7f37930528" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", + "num_cpus", +] + +[[package]] +name = "futures-io" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93a66fc6d035a26a3ae255a6d2bca35eda63ae4c5512bef54449113f7a1228e5" + +[[package]] +name = "futures-macro" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0db9cce532b0eae2ccf2766ab246f114b56b9cf6d445e00c2549fbc100ca045d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca0bae1fe9752cf7fd9b0064c674ae63f97b37bc714d745cbde0afb7ec4e6765" + +[[package]] +name = "futures-task" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "842fc63b931f4056a24d59de13fb1272134ce261816e063e634ad0c15cdc5306" + +[[package]] +name = "futures-util" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0828a5471e340229c11c77ca80017937ce3c58cb788a17e5f1c2d5c485a9577" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "gimli" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" + +[[package]] +name = "group" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5ac374b108929de78460075f3dc439fa66df9d8fc77e8f12caa5165fcf0c89" +dependencies = [ + "ff", + "rand_core 0.6.3", + "subtle", +] + +[[package]] +name = "hash-db" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d23bd4e7b5eda0d0f3a307e8b381fdc8ba9000f26fbe912250c0a4cc3956364a" + +[[package]] +name = "hash256-std-hasher" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92c171d55b98633f4ed3860808f004099b36c1cc29c42cfc53aa8591b21efcf2" +dependencies = [ + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" +dependencies = [ + "crypto-mac 0.8.0", + "digest 0.9.0", +] + +[[package]] +name = "hmac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac 0.11.1", + "digest 0.9.0", +] + +[[package]] +name = "hmac-drbg" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1" +dependencies = [ + "digest 0.9.0", + "generic-array 0.14.6", + "hmac 0.8.1", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad2bfd338099682614d3ee3fe0cd72e0b6a41ca6a87f6a74a3bd593c91650501" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "js-sys", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "impl-codec" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-serde" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4551f042f3438e64dbd6226b20527fc84a6e1fe65688b58746a2f53623f25f5c" +dependencies = [ + "serde", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d7a9f6330b71fea57921c9b61c47ee6e84f72d394754eff6163ae67e7395eb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "integer-sqrt" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276ec31bcb4a9ee45f58bec6f9ec700ae4cf4f4f8f2fa7e06cb406bd5ffdd770" +dependencies = [ + "num-traits", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "itoa" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" + +[[package]] +name = "js-sys" +version = "0.3.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19c3a5e0a0b8450278feda242592512e09f61c72e018b8cd5c859482802daf2d" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "sec1", +] + +[[package]] +name = "keccak" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9b7d56ba4a8344d6be9729995e6b06f928af29998cdf79fe390cbf6b1fee838" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.132" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" + +[[package]] +name = "libsecp256k1" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b09eff1b35ed3b33b877ced3a691fc7a481919c7e29c53c906226fcf55e2a1" +dependencies = [ + "arrayref", + "base64", + "digest 0.9.0", + "hmac-drbg", + "libsecp256k1-core", + "libsecp256k1-gen-ecmult", + "libsecp256k1-gen-genmult", + "rand 0.8.5", + "serde", + "sha2 0.9.9", + "typenum", +] + +[[package]] +name = "libsecp256k1-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" +dependencies = [ + "crunchy", + "digest 0.9.0", + "subtle", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3038c808c55c87e8a172643a7d87187fc6c4174468159cb3090659d55bcb4809" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db8d6ba2cec9eacc40e6e8ccc98931840301f1006e95647ceb2dd5c3aa06f7c" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "lock_api" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "matchers" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f099785f7595cc4b4553a174ce30dd7589ef93391ff414dbb67f62392b9e0ce1" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memory-db" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6566c70c1016f525ced45d7b7f97730a2bafb037c788211d0c186ef5b2189f0a" +dependencies = [ + "hash-db", + "hashbrown", + "parity-util-mem", +] + +[[package]] +name = "memory_units" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d96e3f3c0b6325d8ccd83c33b28acb183edcb6c67938ba104ec546854b0882" + +[[package]] +name = "merlin" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e261cf0f8b3c42ded9f7d2bb59dea03aa52bc8a1cbc7482f9fc3fd1229d3b42" +dependencies = [ + "byteorder", + "keccak", + "rand_core 0.5.1", + "zeroize", +] + +[[package]] +name = "miniz_oxide" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" +dependencies = [ + "adler", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-format" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bafe4179722c2894288ee77a9f044f02811c86af699344c498b0840c698a2465" +dependencies = [ + "arrayvec 0.4.12", + "itoa 0.4.8", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e" + +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "parity-scale-codec" +version = "3.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9182e4a71cae089267ab03e67c99368db7cd877baf50f931e5d6d4b71e195ac0" +dependencies = [ + "arrayvec 0.7.2", + "bitvec", + "byte-slice-cast", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9299338969a3d2f491d65f140b00ddec470858402f888af98e8642fb5e8965cd" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "parity-util-mem" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c32561d248d352148124f036cac253a644685a21dc9fea383eb4907d7bd35a8f" +dependencies = [ + "cfg-if", + "hashbrown", + "impl-trait-for-tuples", + "parity-util-mem-derive", + "parking_lot", + "primitive-types", + "winapi", +] + +[[package]] +name = "parity-util-mem-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f557c32c6d268a07c921471619c0295f5efad3a0e76d4f97a05c091a51d110b2" +dependencies = [ + "proc-macro2", + "syn", + "synstructure", +] + +[[package]] +name = "parity-wasm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be5e13c266502aadf83426d87d81a0f5d1ef45b8027f5a471c360abfe4bfae92" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "paste" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9423e2b32f7a043629287a536f21951e8c6a82482d0acb1eeebfc90bc2225b22" + +[[package]] +name = "pbkdf2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216eaa586a190f0a738f2f918511eecfa90f13295abec0e457cdebcceda80cbd" +dependencies = [ + "crypto-mac 0.8.0", +] + +[[package]] +name = "pbkdf2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95f5254224e617595d2cc3cc73ff0a5eaf2637519e25f03388154e9378b6ffa" +dependencies = [ + "crypto-mac 0.11.1", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + +[[package]] +name = "primitive-types" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28720988bff275df1f51b171e1b2a18c30d194c4d2b61defdacecd625a5d94a" +dependencies = [ + "fixed-hash", + "impl-codec", + "impl-serde", + "scale-info", + "uint", +] + +[[package]] +name = "proc-macro-crate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" +dependencies = [ + "once_cell", + "thiserror", + "toml", +] + +[[package]] +name = "proc-macro2" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom 0.2.7", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ref-cast" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed13bcd201494ab44900a96490291651d200730904221832b9547d24a87d332b" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5234cd6063258a5e32903b53b1b6ac043a0541c8adc1f610f67b0326c7a578fa" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "regex" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" + +[[package]] +name = "rfc6979" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96ef608575f6392792f9ecf7890c00086591d29a83910939d430753f7c050525" +dependencies = [ + "crypto-bigint", + "hmac 0.11.0", + "zeroize", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "ryu" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" + +[[package]] +name = "scale-info" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c46be926081c9f4dd5dd9b6f1d3e3229f2360bc6502dd8836f84a93b7c75e99a" +dependencies = [ + "bitvec", + "cfg-if", + "derive_more", + "parity-scale-codec", + "scale-info-derive", + "serde", +] + +[[package]] +name = "scale-info-derive" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e334bb10a245e28e5fd755cabcafd96cfcd167c99ae63a46924ca8d8703a3c" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "schnorrkel" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "021b403afe70d81eea68f6ea12f6b3c9588e5d536a94c3bf80f15e7faa267862" +dependencies = [ + "arrayref", + "arrayvec 0.5.2", + "curve25519-dalek 2.1.3", + "getrandom 0.1.16", + "merlin", + "rand 0.7.3", + "rand_core 0.5.1", + "sha2 0.8.2", + "subtle", + "zeroize", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "sec1" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08da66b8b0965a5555b6bd6639e68ccba85e1e2506f5fbb089e93f8a04e1a2d1" +dependencies = [ + "der", + "generic-array 0.14.6", + "subtle", + "zeroize", +] + +[[package]] +name = "secp256k1" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7649a0b3ffb32636e60c7ce0d70511eda9c52c658cd0634e194d5a19943aeff" +dependencies = [ + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7058dc8eaf3f2810d7828680320acda0b25a288f6d288e19278e249bbf74226b" +dependencies = [ + "cc", +] + +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "zeroize", +] + +[[package]] +name = "serde" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e8e5d5b70924f74ff5c6d64d9a5acd91422117c60f48c4e07855238a254553" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d8e8de557aee63c26b85b947f5e59b690d0454c753f3adeb5cd7835ab88391" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38dd04e3c8279e75b31ef29dbdceebfe5ad89f4d0937213c53f7d49d01b3d5a7" +dependencies = [ + "itoa 1.0.3", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a256f46ea78a0c0d9ff00077504903ac881a1dafdc20da66545699e7776b3e69" +dependencies = [ + "block-buffer 0.7.3", + "digest 0.8.1", + "fake-simd", + "opaque-debug 0.2.3", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug 0.3.0", +] + +[[package]] +name = "sha2" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.3", +] + +[[package]] +name = "sha3" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a31480366ec990f395a61b7c08122d99bd40544fdb5abcfc1b06bb29994312c" +dependencies = [ + "digest 0.10.3", + "keccak", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signature" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02658e48d89f2bec991f9a78e69cfa4c316f8d6a6c4ec12fae1aeb263d486788" +dependencies = [ + "digest 0.9.0", + "rand_core 0.6.3", +] + +[[package]] +name = "slab" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" + +[[package]] +name = "sp-application-crypto" +version = "6.0.0" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.27#8eff668a42325aeb4433eace1604f4d286a6ec05" +dependencies = [ + "parity-scale-codec", + "scale-info", + "serde", + "sp-core", + "sp-io", + "sp-std", +] + +[[package]] +name = "sp-arithmetic" +version = "5.0.0" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.27#8eff668a42325aeb4433eace1604f4d286a6ec05" +dependencies = [ + "integer-sqrt", + "num-traits", + "parity-scale-codec", + "scale-info", + "serde", + "sp-debug-derive", + "sp-std", + "static_assertions", +] + +[[package]] +name = "sp-core" +version = "6.0.0" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.27#8eff668a42325aeb4433eace1604f4d286a6ec05" +dependencies = [ + "base58", + "bitflags", + "blake2-rfc", + "byteorder", + "dyn-clonable", + "ed25519-dalek", + "futures", + "hash-db", + "hash256-std-hasher", + "hex", + "impl-serde", + "lazy_static", + "libsecp256k1", + "log", + "merlin", + "num-traits", + "parity-scale-codec", + "parity-util-mem", + "parking_lot", + "primitive-types", + "rand 0.7.3", + "regex", + "scale-info", + "schnorrkel", + "secp256k1", + "secrecy", + "serde", + "sp-core-hashing", + "sp-debug-derive", + "sp-externalities", + "sp-runtime-interface", + "sp-std", + "sp-storage", + "ss58-registry", + "substrate-bip39", + "thiserror", + "tiny-bip39", + "wasmi", + "zeroize", +] + +[[package]] +name = "sp-core-hashing" +version = "4.0.0" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.27#8eff668a42325aeb4433eace1604f4d286a6ec05" +dependencies = [ + "blake2", + "byteorder", + "digest 0.10.3", + "sha2 0.10.2", + "sha3", + "sp-std", + "twox-hash", +] + +[[package]] +name = "sp-core-hashing-proc-macro" +version = "5.0.0" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.27#8eff668a42325aeb4433eace1604f4d286a6ec05" +dependencies = [ + "proc-macro2", + "quote", + "sp-core-hashing", + "syn", +] + +[[package]] +name = "sp-debug-derive" +version = "4.0.0" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.27#8eff668a42325aeb4433eace1604f4d286a6ec05" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sp-externalities" +version = "0.12.0" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.27#8eff668a42325aeb4433eace1604f4d286a6ec05" +dependencies = [ + "environmental", + "parity-scale-codec", + "sp-std", + "sp-storage", +] + +[[package]] +name = "sp-inherents" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.27#8eff668a42325aeb4433eace1604f4d286a6ec05" +dependencies = [ + "async-trait", + "impl-trait-for-tuples", + "parity-scale-codec", + "sp-core", + "sp-runtime", + "sp-std", + "thiserror", +] + +[[package]] +name = "sp-io" +version = "6.0.0" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.27#8eff668a42325aeb4433eace1604f4d286a6ec05" +dependencies = [ + "futures", + "hash-db", + "libsecp256k1", + "log", + "parity-scale-codec", + "parking_lot", + "secp256k1", + "sp-core", + "sp-externalities", + "sp-keystore", + "sp-runtime-interface", + "sp-state-machine", + "sp-std", + "sp-tracing", + "sp-trie", + "sp-wasm-interface", + "tracing", + "tracing-core", +] + +[[package]] +name = "sp-keystore" +version = "0.12.0" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.27#8eff668a42325aeb4433eace1604f4d286a6ec05" +dependencies = [ + "async-trait", + "futures", + "merlin", + "parity-scale-codec", + "parking_lot", + "schnorrkel", + "sp-core", + "sp-externalities", + "thiserror", +] + +[[package]] +name = "sp-panic-handler" +version = "4.0.0" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.27#8eff668a42325aeb4433eace1604f4d286a6ec05" +dependencies = [ + "backtrace", + "lazy_static", + "regex", +] + +[[package]] +name = "sp-runtime" +version = "6.0.0" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.27#8eff668a42325aeb4433eace1604f4d286a6ec05" +dependencies = [ + "either", + "hash256-std-hasher", + "impl-trait-for-tuples", + "log", + "parity-scale-codec", + "parity-util-mem", + "paste", + "rand 0.7.3", + "scale-info", + "serde", + "sp-application-crypto", + "sp-arithmetic", + "sp-core", + "sp-io", + "sp-std", +] + +[[package]] +name = "sp-runtime-interface" +version = "6.0.0" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.27#8eff668a42325aeb4433eace1604f4d286a6ec05" +dependencies = [ + "impl-trait-for-tuples", + "parity-scale-codec", + "primitive-types", + "sp-externalities", + "sp-runtime-interface-proc-macro", + "sp-std", + "sp-storage", + "sp-tracing", + "sp-wasm-interface", + "static_assertions", +] + +[[package]] +name = "sp-runtime-interface-proc-macro" +version = "5.0.0" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.27#8eff668a42325aeb4433eace1604f4d286a6ec05" +dependencies = [ + "Inflector", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sp-staking" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.27#8eff668a42325aeb4433eace1604f4d286a6ec05" +dependencies = [ + "parity-scale-codec", + "scale-info", + "sp-runtime", + "sp-std", +] + +[[package]] +name = "sp-state-machine" +version = "0.12.0" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.27#8eff668a42325aeb4433eace1604f4d286a6ec05" +dependencies = [ + "hash-db", + "log", + "num-traits", + "parity-scale-codec", + "parking_lot", + "rand 0.7.3", + "smallvec", + "sp-core", + "sp-externalities", + "sp-panic-handler", + "sp-std", + "sp-trie", + "thiserror", + "tracing", + "trie-root", +] + +[[package]] +name = "sp-std" +version = "4.0.0" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.27#8eff668a42325aeb4433eace1604f4d286a6ec05" + +[[package]] +name = "sp-storage" +version = "6.0.0" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.27#8eff668a42325aeb4433eace1604f4d286a6ec05" +dependencies = [ + "impl-serde", + "parity-scale-codec", + "ref-cast", + "serde", + "sp-debug-derive", + "sp-std", +] + +[[package]] +name = "sp-tracing" +version = "5.0.0" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.27#8eff668a42325aeb4433eace1604f4d286a6ec05" +dependencies = [ + "parity-scale-codec", + "sp-std", + "tracing", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "sp-trie" +version = "6.0.0" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.27#8eff668a42325aeb4433eace1604f4d286a6ec05" +dependencies = [ + "hash-db", + "memory-db", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-std", + "thiserror", + "trie-db", + "trie-root", +] + +[[package]] +name = "sp-version" +version = "5.0.0" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.27#8eff668a42325aeb4433eace1604f4d286a6ec05" +dependencies = [ + "impl-serde", + "parity-scale-codec", + "parity-wasm", + "scale-info", + "serde", + "sp-core-hashing-proc-macro", + "sp-runtime", + "sp-std", + "sp-version-proc-macro", + "thiserror", +] + +[[package]] +name = "sp-version-proc-macro" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.27#8eff668a42325aeb4433eace1604f4d286a6ec05" +dependencies = [ + "parity-scale-codec", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sp-wasm-interface" +version = "6.0.0" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.27#8eff668a42325aeb4433eace1604f4d286a6ec05" +dependencies = [ + "impl-trait-for-tuples", + "log", + "parity-scale-codec", + "sp-std", + "wasmi", +] + +[[package]] +name = "ss58-registry" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a039906277e0d8db996cd9d1ef19278c10209d994ecfc1025ced16342873a17c" +dependencies = [ + "Inflector", + "num-format", + "proc-macro2", + "quote", + "serde", + "serde_json", + "unicode-xid", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "substrate-bip39" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49eee6965196b32f882dd2ee85a92b1dbead41b04e53907f269de3b0dc04733c" +dependencies = [ + "hmac 0.11.0", + "pbkdf2 0.8.0", + "schnorrkel", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "test-pallet-rollback-changes" +version = "0.1.0" +dependencies = [ + "frame-support", + "frame-system", + "parity-scale-codec", + "scale-info", +] + +[[package]] +name = "thiserror" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5f6586b7f764adc0231f4c79be7b920e766bb2f3e51b3661cdb263828f19994" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12bafc5b54507e0149cdf1b145a5d80ab80a90bcd9275df43d4fff68460f6c21" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tiny-bip39" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc59cb9dfc85bb312c3a78fd6aa8a8582e310b0fa885d5bb877f6dcc601839d" +dependencies = [ + "anyhow", + "hmac 0.8.1", + "once_cell", + "pbkdf2 0.4.0", + "rand 0.7.3", + "rustc-hash", + "sha2 0.9.9", + "thiserror", + "unicode-normalization", + "wasm-bindgen", + "zeroize", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + +[[package]] +name = "tracing" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" +dependencies = [ + "ansi_term", + "chrono", + "lazy_static", + "matchers", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "trie-db" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32d034c0d3db64b43c31de38e945f15b40cd4ca6d2dcfc26d4798ce8de4ab83" +dependencies = [ + "hash-db", + "hashbrown", + "log", + "rustc-hex", + "smallvec", +] + +[[package]] +name = "trie-root" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a36c5ca3911ed3c9a5416ee6c679042064b93fc637ded67e25f92e68d783891" +dependencies = [ + "hash-db", +] + +[[package]] +name = "tt-call" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e66dcbec4290c69dd03c57e76c2469ea5c7ce109c6dd4351c13055cf71ea055" + +[[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "digest 0.10.3", + "rand 0.8.5", + "static_assertions", +] + +[[package]] +name = "typenum" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + +[[package]] +name = "uint" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f03af7ccf01dd611cc450a0d10dbc9b745770d096473e2faf0ca6e2d66d1e0" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unicode-ident" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" + +[[package]] +name = "unicode-normalization" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-xid" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" + +[[package]] +name = "wasmi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca00c5147c319a8ec91ec1a0edbec31e566ce2c9cc93b3f9bb86a9efd0eb795d" +dependencies = [ + "downcast-rs", + "libc", + "memory_units", + "num-rational", + "num-traits", + "parity-wasm", + "wasmi-validation", +] + +[[package]] +name = "wasmi-validation" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165343ecd6c018fc09ebcae280752702c9a2ef3e6f8d02f1cfcbdb53ef6d7937" +dependencies = [ + "parity-wasm", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + +[[package]] +name = "wyz" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b31594f29d27036c383b53b59ed3476874d518f0efb151b27a4c275141390e" +dependencies = [ + "tap", +] + +[[package]] +name = "zeroize" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f8f187641dad4f680d25c4bfc4225b418165984179f26ca76ec4fb6441d3a17" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] --- /dev/null +++ b/test-pallets/utils/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "pallet-test-utils" +version = "0.1.0" +license = "GPLv3" +edition = "2021" +publish = false + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } +scale-info = { version = "2.1.1", default-features = false, features = ["derive"] } +frame-support = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.30" } +frame-system = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.30" } +# pallet-unique-scheduler = { path = '../../pallets/scheduler', default-features = false } +pallet-unique-scheduler-v2 = { path = '../../pallets/scheduler-v2', default-features = false } + +[features] +default = ["std"] +std = [ + "codec/std", + "scale-info/std", + "frame-support/std", + "frame-system/std", + "pallet-unique-scheduler-v2/std", +] --- /dev/null +++ b/test-pallets/utils/src/lib.rs @@ -0,0 +1,125 @@ +// 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 . + +#![cfg_attr(not(feature = "std"), no_std)] + +pub use pallet::*; +use frame_support::pallet_prelude::*; +use frame_system::pallet_prelude::*; + +#[frame_support::pallet] +pub mod pallet { + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + use pallet_unique_scheduler_v2::{TaskName, Pallet as SchedulerPallet}; + + #[pallet::config] + pub trait Config: frame_system::Config + pallet_unique_scheduler_v2::Config { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + ValueIsSet, + ShouldRollback, + } + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + #[pallet::storage] + #[pallet::getter(fn is_enabled)] + pub type Enabled = StorageValue<_, bool, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn test_value)] + pub type TestValue = StorageValue<_, u32, ValueQuery>; + + #[pallet::error] + pub enum Error { + TestPalletDisabled, + TriggerRollback, + } + + #[pallet::call] + impl Pallet { + #[pallet::weight(10_000)] + pub fn enable(origin: OriginFor) -> DispatchResult { + ensure_root(origin)?; + >::set(true); + + Ok(()) + } + + #[pallet::weight(10_000)] + pub fn set_test_value(origin: OriginFor, value: u32) -> DispatchResult { + Self::ensure_origin_and_enabled(origin)?; + + >::put(value); + + Self::deposit_event(Event::ValueIsSet); + + Ok(()) + } + + #[pallet::weight(10_000)] + pub fn set_test_value_and_rollback(origin: OriginFor, value: u32) -> DispatchResult { + Self::set_test_value(origin, value)?; + + Self::deposit_event(Event::ShouldRollback); + + Err(>::TriggerRollback.into()) + } + + #[pallet::weight(10_000)] + pub fn inc_test_value(origin: OriginFor) -> DispatchResult { + Self::set_test_value(origin, >::get() + 1) + } + + #[pallet::weight(10_000)] + pub fn self_canceling_inc( + origin: OriginFor, + id: TaskName, + max_test_value: u32, + ) -> DispatchResult { + Self::ensure_origin_and_enabled(origin.clone())?; + Self::inc_test_value(origin.clone())?; + + if >::get() == max_test_value { + SchedulerPallet::::cancel_named(origin, id)?; + } + + Ok(()) + } + + #[pallet::weight(100_000_000)] + pub fn just_take_fee(origin: OriginFor) -> DispatchResult { + Self::ensure_origin_and_enabled(origin)?; + Ok(()) + } + } +} + +impl Pallet { + fn ensure_origin_and_enabled(origin: OriginFor) -> DispatchResult { + ensure_signed(origin)?; + >::get() + .then(|| ()) + .ok_or(>::TestPalletDisabled.into()) + } +} --- 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/.outdated/eth/scheduling.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. -// This file is part of Unique Network. - -// Unique Network is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Unique Network is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Unique Network. If not, see . - -import {expect} from 'chai'; -import {createEthAccountWithBalance, deployFlipper, GAS_ARGS, itWeb3, subToEth, transferBalanceToEth} from '../../deprecated-helpers/eth/helpers'; -import {scheduleExpectSuccess, waitNewBlocks, requirePallets, Pallets} from '../../deprecated-helpers/helpers'; - -// TODO mrshiposha update this test in #581 -describe.skip('Scheduing EVM smart contracts', () => { - before(async function() { - await requirePallets(this, [Pallets.Scheduler]); - }); - - itWeb3('Successfully schedules and periodically executes an EVM contract', async ({api, web3, privateKeyWrapper}) => { - const deployer = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - const flipper = await deployFlipper(web3, deployer); - const initialValue = await flipper.methods.getValue().call(); - const alice = privateKeyWrapper('//Alice'); - await transferBalanceToEth(api, alice, subToEth(alice.address)); - - { - const tx = api.tx.evm.call( - subToEth(alice.address), - flipper.options.address, - flipper.methods.flip().encodeABI(), - '0', - GAS_ARGS.gas, - await web3.eth.getGasPrice(), - null, - null, - [], - ); - const waitForBlocks = 4; - const periodBlocks = 2; - - await scheduleExpectSuccess(tx, alice, waitForBlocks, '0x' + '0'.repeat(32), periodBlocks, 2); - expect(await flipper.methods.getValue().call()).to.be.equal(initialValue); - - await waitNewBlocks(waitForBlocks - 1); - expect(await flipper.methods.getValue().call()).to.be.not.equal(initialValue); - - await waitNewBlocks(periodBlocks); - expect(await flipper.methods.getValue().call()).to.be.equal(initialValue); - } - }); -}); --- a/tests/src/.outdated/scheduler.test.ts +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. -// This file is part of Unique Network. - -// Unique Network is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Unique Network is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Unique Network. If not, see . - -import chai, {expect} from 'chai'; -import chaiAsPromised from 'chai-as-promised'; -import { - default as usingApi, - submitTransactionAsync, -} from '../substrate/substrate-api'; -import { - createItemExpectSuccess, - createCollectionExpectSuccess, - scheduleTransferExpectSuccess, - scheduleTransferAndWaitExpectSuccess, - setCollectionSponsorExpectSuccess, - confirmSponsorshipExpectSuccess, - findUnusedAddress, - UNIQUE, - enablePublicMintingExpectSuccess, - addToAllowListExpectSuccess, - waitNewBlocks, - normalizeAccountId, - getTokenOwner, - getGenericResult, - scheduleTransferFundsPeriodicExpectSuccess, - getFreeBalance, - confirmSponsorshipByKeyExpectSuccess, - scheduleExpectFailure, -} from '../deprecated-helpers/helpers'; -import {IKeyringPair} from '@polkadot/types/types'; - -chai.use(chaiAsPromised); - -// todo:playgrounds skipped ~ postponed -describe.skip('Scheduling token and balance transfers', () => { - let alice: IKeyringPair; - let bob: IKeyringPair; - let scheduledIdBase: string; - let scheduledIdSlider: number; - - before(async() => { - await usingApi(async (api, privateKeyWrapper) => { - alice = privateKeyWrapper('//Alice'); - bob = privateKeyWrapper('//Bob'); - }); - - scheduledIdBase = '0x' + '0'.repeat(31); - scheduledIdSlider = 0; - }); - - // Loop scheduledId around 10. Unless there are concurrent tasks with long periods/repetitions, tests' tasks' ids shouldn't ovelap. - function makeScheduledId(): string { - return scheduledIdBase + ((scheduledIdSlider++) % 10); - } - - it('Can schedule a transfer of an owned token with delay', async () => { - await usingApi(async () => { - const nftCollectionId = await createCollectionExpectSuccess(); - const newNftTokenId = await createItemExpectSuccess(alice, nftCollectionId, 'NFT'); - await setCollectionSponsorExpectSuccess(nftCollectionId, alice.address); - await confirmSponsorshipExpectSuccess(nftCollectionId); - - await scheduleTransferAndWaitExpectSuccess(nftCollectionId, newNftTokenId, alice, bob, 1, 4, makeScheduledId()); - }); - }); - - it('Can transfer funds periodically', async () => { - await usingApi(async () => { - const waitForBlocks = 4; - const period = 2; - await scheduleTransferFundsPeriodicExpectSuccess(1n * UNIQUE, alice, bob, waitForBlocks, makeScheduledId(), period, 2); - const bobsBalanceBefore = await getFreeBalance(bob); - - // discounting already waited-for operations - await waitNewBlocks(waitForBlocks - 2); - const bobsBalanceAfterFirst = await getFreeBalance(bob); - expect(bobsBalanceAfterFirst > bobsBalanceBefore).to.be.true; - - await waitNewBlocks(period); - const bobsBalanceAfterSecond = await getFreeBalance(bob); - expect(bobsBalanceAfterSecond > bobsBalanceAfterFirst).to.be.true; - }); - }); - - it('Can sponsor scheduling a transaction', async () => { - const collectionId = await createCollectionExpectSuccess(); - await setCollectionSponsorExpectSuccess(collectionId, bob.address); - await confirmSponsorshipExpectSuccess(collectionId, '//Bob'); - - await usingApi(async () => { - const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address); - - const bobBalanceBefore = await getFreeBalance(bob); - const waitForBlocks = 4; - // no need to wait to check, fees must be deducted on scheduling, immediately - await scheduleTransferExpectSuccess(collectionId, tokenId, alice, bob, 0, waitForBlocks, makeScheduledId()); - const bobBalanceAfter = await getFreeBalance(bob); - // expect(aliceBalanceAfter == aliceBalanceBefore).to.be.true; - expect(bobBalanceAfter < bobBalanceBefore).to.be.true; - // wait for sequentiality matters - await waitNewBlocks(waitForBlocks - 1); - }); - }); - - it('Schedules and dispatches a transaction even if the caller has no funds at the time of the dispatch', async () => { - await usingApi(async (api, privateKeyWrapper) => { - // Find an empty, unused account - const zeroBalance = await findUnusedAddress(api, privateKeyWrapper); - - const collectionId = await createCollectionExpectSuccess(); - - // Add zeroBalance address to allow list - await enablePublicMintingExpectSuccess(alice, collectionId); - await addToAllowListExpectSuccess(alice, collectionId, zeroBalance.address); - - // Grace zeroBalance with money, enough to cover future transactions - const balanceTx = api.tx.balances.transfer(zeroBalance.address, 1n * UNIQUE); - await submitTransactionAsync(alice, balanceTx); - - // Mint a fresh NFT - const tokenId = await createItemExpectSuccess(zeroBalance, collectionId, 'NFT'); - - // Schedule transfer of the NFT a few blocks ahead - const waitForBlocks = 5; - await scheduleTransferExpectSuccess(collectionId, tokenId, zeroBalance, alice, 1, waitForBlocks, makeScheduledId()); - - // Get rid of the account's funds before the scheduled transaction takes place - const balanceTx2 = api.tx.balances.transfer(alice.address, UNIQUE * 68n / 100n); - const events = await submitTransactionAsync(zeroBalance, balanceTx2); - expect(getGenericResult(events).success).to.be.true; - /*const emptyBalanceTx = api.tx.balances.setBalance(zeroBalance.address, 0, 0); // do not null reserved? - const sudoTx = api.tx.sudo.sudo(emptyBalanceTx as any); - const events = await submitTransactionAsync(alice, sudoTx); - expect(getGenericResult(events).success).to.be.true;*/ - - // Wait for a certain number of blocks, discarding the ones that already happened while accepting the late transactions - await waitNewBlocks(waitForBlocks - 3); - - expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(alice.address)); - }); - }); - - it('Sponsor going bankrupt does not impact a scheduled transaction', async () => { - const collectionId = await createCollectionExpectSuccess(); - - await usingApi(async (api, privateKeyWrapper) => { - const zeroBalance = await findUnusedAddress(api, privateKeyWrapper); - const balanceTx = api.tx.balances.transfer(zeroBalance.address, 1n * UNIQUE); - await submitTransactionAsync(alice, balanceTx); - - await setCollectionSponsorExpectSuccess(collectionId, zeroBalance.address); - await confirmSponsorshipByKeyExpectSuccess(collectionId, zeroBalance); - - const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address); - - const waitForBlocks = 5; - await scheduleTransferExpectSuccess(collectionId, tokenId, alice, zeroBalance, 1, waitForBlocks, makeScheduledId()); - - const emptyBalanceSponsorTx = api.tx.balances.setBalance(zeroBalance.address, 0, 0); - const sudoTx = api.tx.sudo.sudo(emptyBalanceSponsorTx as any); - const events = await submitTransactionAsync(alice, sudoTx); - expect(getGenericResult(events).success).to.be.true; - - // Wait for a certain number of blocks, save for the ones that already happened while accepting the late transactions - await waitNewBlocks(waitForBlocks - 3); - - expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(zeroBalance.address)); - }); - }); - - it.skip('Exceeding sponsor rate limit without having enough funds prevents scheduling a periodic transaction', async () => { - const collectionId = await createCollectionExpectSuccess(); - await setCollectionSponsorExpectSuccess(collectionId, bob.address); - await confirmSponsorshipExpectSuccess(collectionId, '//Bob'); - - await usingApi(async (api, privateKeyWrapper) => { - const zeroBalance = await findUnusedAddress(api, privateKeyWrapper); - - await enablePublicMintingExpectSuccess(alice, collectionId); - await addToAllowListExpectSuccess(alice, collectionId, zeroBalance.address); - - const bobBalanceBefore = await getFreeBalance(bob); - - const createData = {nft: {const_data: [], variable_data: []}}; - const creationTx = api.tx.unique.createItem(collectionId, normalizeAccountId(zeroBalance), createData as any); - - /*const badTransaction = async function () { - await submitTransactionExpectFailAsync(zeroBalance, zeroToAlice); - }; - await expect(badTransaction()).to.be.rejectedWith('Inability to pay some fees');*/ - - await scheduleExpectFailure(creationTx, zeroBalance, 3, makeScheduledId(), 1, 3); - - expect(await getFreeBalance(bob)).to.be.equal(bobBalanceBefore); - }); - }); -}); --- 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" }], --- /dev/null +++ b/tests/src/eth/scheduling.test.ts @@ -0,0 +1,62 @@ +// 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 {expect} from 'chai'; +import {EthUniqueHelper, itEth} from './util'; +import {Pallets, usingPlaygrounds} from '../util'; + +describe('Scheduing EVM smart contracts', () => { + + before(async () => { + await usingPlaygrounds(async (helper) => { + await helper.testUtils.enable(); + }); + }); + + itEth.ifWithPallets('Successfully schedules and periodically executes an EVM contract', [Pallets.Scheduler], async ({helper, privateKey}) => { + const alice = await privateKey('//Alice'); + + const scheduledId = await helper.arrange.makeScheduledId(); + + const deployer = await helper.eth.createAccountWithBalance(alice); + const flipper = await helper.eth.deployFlipper(deployer); + + const initialValue = await flipper.methods.getValue().call(); + await helper.eth.transferBalanceFromSubstrate(alice, helper.address.substrateToEth(alice.address)); + + const waitForBlocks = 4; + const periodic = { + period: 2, + repetitions: 2, + }; + + await helper.scheduler.scheduleAfter(scheduledId, waitForBlocks, {periodic}) + .eth.sendEVM( + alice, + flipper.options.address, + flipper.methods.flip().encodeABI(), + '0', + ); + + expect(await flipper.methods.getValue().call()).to.be.equal(initialValue); + + await helper.wait.newBlocks(waitForBlocks + 1); + expect(await flipper.methods.getValue().call()).to.be.not.equal(initialValue); + + await helper.wait.newBlocks(periodic.period); + expect(await flipper.methods.getValue().call()).to.be.equal(initialValue); + }); +}); --- 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, @@ -174,48 +175,58 @@ 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.createNFTCollection(name, description, tokenPrefix).send({value: Number(collectionCreationPrice)}); + const result = await collectionHelper.methods[functionName](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); + const events = this.helper.eth.normalizeEvents(result.events); - return {collectionId, collectionAddress}; + return {collectionId, collectionAddress, events}; } - async createERC721MetadataCompatibleNFTCollection(signer: string, name: string, description: string, tokenPrefix: string, baseUri: string): Promise<{collectionId: number, collectionAddress: string}> { + 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, 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 { @@ -340,9 +351,36 @@ 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 +389,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 +398,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/pallet-presence.test.ts +++ b/tests/src/pallet-presence.test.ts @@ -62,10 +62,11 @@ const chain = await helper.callRpc('api.rpc.system.chain', []); const refungible = 'refungible'; - // const scheduler = 'scheduler'; + const scheduler = 'scheduler'; const foreignAssets = 'foreignassets'; const rmrkPallets = ['rmrkcore', 'rmrkequip']; const appPromotion = 'apppromotion'; + const testUtils = 'testutils'; if (chain.eq('OPAL by UNIQUE')) { requiredPallets.push( @@ -73,6 +74,8 @@ // scheduler, foreignAssets, appPromotion, + testUtils, + scheduler, ...rmrkPallets, ); } else if (chain.eq('QUARTZ by UNIQUE')) { --- /dev/null +++ b/tests/src/scheduler.test.ts @@ -0,0 +1,647 @@ +// 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 {expect, itSub, Pallets, usingPlaygrounds} from './util'; +import {IKeyringPair} from '@polkadot/types/types'; +import {DevUniqueHelper} from './util/playgrounds/unique.dev'; + +describe('Scheduling token and balance transfers', () => { + let alice: IKeyringPair; + let bob: IKeyringPair; + let charlie: IKeyringPair; + + before(async () => { + await usingPlaygrounds(async (helper, privateKeyWrapper) => { + alice = await privateKeyWrapper('//Alice'); + bob = await privateKeyWrapper('//Bob'); + charlie = await privateKeyWrapper('//Charlie'); + + await helper.testUtils.enable(); + }); + }); + + itSub.ifWithPallets('Can delay a transfer of an owned token', [Pallets.Scheduler], async ({helper}) => { + const collection = await helper.nft.mintCollection(alice, {tokenPrefix: 'schd'}); + const token = await collection.mintToken(alice); + const schedulerId = await helper.arrange.makeScheduledId(); + const blocksBeforeExecution = 4; + + await token.scheduleAfter(schedulerId, blocksBeforeExecution) + .transfer(alice, {Substrate: bob.address}); + + expect(await token.getOwner()).to.be.deep.equal({Substrate: alice.address}); + + await helper.wait.newBlocks(blocksBeforeExecution + 1); + + expect(await token.getOwner()).to.be.deep.equal({Substrate: bob.address}); + }); + + itSub.ifWithPallets('Can transfer funds periodically', [Pallets.Scheduler], async ({helper}) => { + const scheduledId = await helper.arrange.makeScheduledId(); + const waitForBlocks = 1; + + const amount = 1n * helper.balance.getOneTokenNominal(); + const periodic = { + period: 2, + repetitions: 2, + }; + + const bobsBalanceBefore = await helper.balance.getSubstrate(bob.address); + + await helper.scheduler.scheduleAfter(scheduledId, waitForBlocks, {periodic}) + .balance.transferToSubstrate(alice, bob.address, amount); + + await helper.wait.newBlocks(waitForBlocks + 1); + + const bobsBalanceAfterFirst = await helper.balance.getSubstrate(bob.address); + expect(bobsBalanceAfterFirst) + .to.be.equal( + bobsBalanceBefore + 1n * amount, + '#1 Balance of the recipient should be increased by 1 * amount', + ); + + await helper.wait.newBlocks(periodic.period); + + const bobsBalanceAfterSecond = await helper.balance.getSubstrate(bob.address); + expect(bobsBalanceAfterSecond) + .to.be.equal( + bobsBalanceBefore + 2n * amount, + '#2 Balance of the recipient should be increased by 2 * amount', + ); + }); + + itSub.ifWithPallets('Can cancel a scheduled operation which has not yet taken effect', [Pallets.Scheduler], async ({helper}) => { + const collection = await helper.nft.mintCollection(alice, {tokenPrefix: 'schd'}); + const token = await collection.mintToken(alice); + + const scheduledId = await helper.arrange.makeScheduledId(); + const waitForBlocks = 4; + + expect(await token.getOwner()).to.be.deep.equal({Substrate: alice.address}); + + await token.scheduleAfter(scheduledId, waitForBlocks) + .transfer(alice, {Substrate: bob.address}); + + await helper.scheduler.cancelScheduled(alice, scheduledId); + + await helper.wait.newBlocks(waitForBlocks + 1); + + expect(await token.getOwner()).to.be.deep.equal({Substrate: alice.address}); + }); + + itSub.ifWithPallets('Can cancel a periodic operation (transfer of funds)', [Pallets.Scheduler], async ({helper}) => { + const waitForBlocks = 1; + const periodic = { + period: 3, + repetitions: 2, + }; + + const scheduledId = await helper.arrange.makeScheduledId(); + + const amount = 1n * helper.balance.getOneTokenNominal(); + + const bobsBalanceBefore = await helper.balance.getSubstrate(bob.address); + + await helper.scheduler.scheduleAfter(scheduledId, waitForBlocks, {periodic}) + .balance.transferToSubstrate(alice, bob.address, amount); + + await helper.wait.newBlocks(waitForBlocks + 1); + + const bobsBalanceAfterFirst = await helper.balance.getSubstrate(bob.address); + + expect(bobsBalanceAfterFirst) + .to.be.equal( + bobsBalanceBefore + 1n * amount, + '#1 Balance of the recipient should be increased by 1 * amount', + ); + + await helper.scheduler.cancelScheduled(alice, scheduledId); + await helper.wait.newBlocks(periodic.period); + + const bobsBalanceAfterSecond = await helper.balance.getSubstrate(bob.address); + expect(bobsBalanceAfterSecond) + .to.be.equal( + bobsBalanceAfterFirst, + '#2 Balance of the recipient should not be changed', + ); + }); + + itSub.ifWithPallets('Scheduled tasks are transactional', [Pallets.Scheduler, Pallets.TestUtils], async ({helper}) => { + const scheduledId = await helper.arrange.makeScheduledId(); + const waitForBlocks = 4; + + const initTestVal = 42; + const changedTestVal = 111; + + await helper.testUtils.setTestValue(alice, initTestVal); + + await helper.scheduler.scheduleAfter(scheduledId, waitForBlocks) + .testUtils.setTestValueAndRollback(alice, changedTestVal); + + await helper.wait.newBlocks(waitForBlocks + 1); + + const testVal = await helper.testUtils.testValue(); + expect(testVal, 'The test value should NOT be commited') + .to.be.equal(initTestVal); + }); + + itSub.ifWithPallets('Scheduled tasks should take correct fees', [Pallets.Scheduler, Pallets.TestUtils], async function({helper}) { + const scheduledId = await helper.arrange.makeScheduledId(); + const waitForBlocks = 4; + const periodic = { + period: 2, + repetitions: 2, + }; + + const dummyTx = helper.constructApiCall('api.tx.testUtils.justTakeFee', []); + const scheduledLen = dummyTx.callIndex.length; + + const expectedScheduledFee = (await helper.getPaymentInfo(alice, dummyTx, scheduledLen)) + .partialFee.toBigInt(); + + await helper.scheduler.scheduleAfter(scheduledId, waitForBlocks, {periodic}) + .testUtils.justTakeFee(alice); + + await helper.wait.newBlocks(1); + + const aliceInitBalance = await helper.balance.getSubstrate(alice.address); + let diff; + + await helper.wait.newBlocks(waitForBlocks); + + const aliceBalanceAfterFirst = await helper.balance.getSubstrate(alice.address); + expect( + aliceBalanceAfterFirst < aliceInitBalance, + '[after execution #1] Scheduled task should take a fee', + ).to.be.true; + + diff = aliceInitBalance - aliceBalanceAfterFirst; + expect(diff).to.be.equal( + expectedScheduledFee, + 'Scheduled task should take the right amount of fees', + ); + + await helper.wait.newBlocks(periodic.period); + + const aliceBalanceAfterSecond = await helper.balance.getSubstrate(alice.address); + expect( + aliceBalanceAfterSecond < aliceBalanceAfterFirst, + '[after execution #2] Scheduled task should take a fee', + ).to.be.true; + + diff = aliceBalanceAfterFirst - aliceBalanceAfterSecond; + expect(diff).to.be.equal( + expectedScheduledFee, + 'Scheduled task should take the right amount of fees', + ); + }); + + // Check if we can cancel a scheduled periodic operation + // in the same block in which it is running + itSub.ifWithPallets('Can cancel the periodic sheduled tx when the tx is running', [Pallets.Scheduler, Pallets.TestUtils], async ({helper}) => { + const currentBlockNumber = await helper.chain.getLatestBlockNumber(); + const blocksBeforeExecution = 10; + const firstExecutionBlockNumber = currentBlockNumber + blocksBeforeExecution; + + const [ + scheduledId, + scheduledCancelId, + ] = await helper.arrange.makeScheduledIds(2); + + const periodic = { + period: 5, + repetitions: 5, + }; + + const initTestVal = 0; + const incTestVal = initTestVal + 1; + const finalTestVal = initTestVal + 2; + + await helper.testUtils.setTestValue(alice, initTestVal); + + await helper.scheduler.scheduleAt(scheduledId, firstExecutionBlockNumber, {periodic}) + .testUtils.incTestValue(alice); + + // Cancel the inc tx after 2 executions + // *in the same block* in which the second execution is scheduled + await helper.scheduler.scheduleAt( + scheduledCancelId, + firstExecutionBlockNumber + periodic.period, + ).scheduler.cancelScheduled(alice, scheduledId); + + await helper.wait.newBlocks(blocksBeforeExecution); + + // execution #0 + expect(await helper.testUtils.testValue()) + .to.be.equal(incTestVal); + + await helper.wait.newBlocks(periodic.period); + + // execution #1 + expect(await helper.testUtils.testValue()) + .to.be.equal(finalTestVal); + + for (let i = 1; i < periodic.repetitions; i++) { + await helper.wait.newBlocks(periodic.period); + expect(await helper.testUtils.testValue()) + .to.be.equal(finalTestVal); + } + }); + + itSub.ifWithPallets('A scheduled operation can cancel itself', [Pallets.Scheduler, Pallets.TestUtils], async ({helper}) => { + const scheduledId = await helper.arrange.makeScheduledId(); + const waitForBlocks = 4; + const periodic = { + period: 2, + repetitions: 5, + }; + + const initTestVal = 0; + const maxTestVal = 2; + + await helper.testUtils.setTestValue(alice, initTestVal); + + await helper.scheduler.scheduleAfter(scheduledId, waitForBlocks, {periodic}) + .testUtils.selfCancelingInc(alice, scheduledId, maxTestVal); + + await helper.wait.newBlocks(waitForBlocks + 1); + + // execution #0 + expect(await helper.testUtils.testValue()) + .to.be.equal(initTestVal + 1); + + await helper.wait.newBlocks(periodic.period); + + // execution #1 + expect(await helper.testUtils.testValue()) + .to.be.equal(initTestVal + 2); + + await helper.wait.newBlocks(periodic.period); + + // + expect(await helper.testUtils.testValue()) + .to.be.equal(initTestVal + 2); + }); + + itSub.ifWithPallets('Root can cancel any scheduled operation', [Pallets.Scheduler], async ({helper}) => { + const collection = await helper.nft.mintCollection(bob, {tokenPrefix: 'schd'}); + const token = await collection.mintToken(bob); + + const scheduledId = await helper.arrange.makeScheduledId(); + const waitForBlocks = 4; + + await token.scheduleAfter(scheduledId, waitForBlocks) + .transfer(bob, {Substrate: alice.address}); + + await helper.getSudo().scheduler.cancelScheduled(alice, scheduledId); + + await helper.wait.newBlocks(waitForBlocks + 1); + + expect(await token.getOwner()).to.be.deep.equal({Substrate: bob.address}); + }); + + itSub.ifWithPallets('Root can set prioritized scheduled operation', [Pallets.Scheduler], async ({helper}) => { + const scheduledId = await helper.arrange.makeScheduledId(); + const waitForBlocks = 4; + + const amount = 42n * helper.balance.getOneTokenNominal(); + + const balanceBefore = await helper.balance.getSubstrate(charlie.address); + + await helper.getSudo() + .scheduler.scheduleAfter(scheduledId, waitForBlocks, {priority: 42}) + .balance.forceTransferToSubstrate(alice, bob.address, charlie.address, amount); + + await helper.wait.newBlocks(waitForBlocks + 1); + + const balanceAfter = await helper.balance.getSubstrate(charlie.address); + + expect(balanceAfter > balanceBefore).to.be.true; + + const diff = balanceAfter - balanceBefore; + expect(diff).to.be.equal(amount); + }); + + itSub.ifWithPallets("Root can change scheduled operation's priority", [Pallets.Scheduler], async ({helper}) => { + const collection = await helper.nft.mintCollection(bob, {tokenPrefix: 'schd'}); + const token = await collection.mintToken(bob); + + const scheduledId = await helper.arrange.makeScheduledId(); + const waitForBlocks = 6; + + await token.scheduleAfter(scheduledId, waitForBlocks) + .transfer(bob, {Substrate: alice.address}); + + const priority = 112; + await helper.getSudo().scheduler.changePriority(alice, scheduledId, priority); + + const priorityChanged = await helper.wait.event( + waitForBlocks, + 'scheduler', + 'PriorityChanged', + ); + + expect(priorityChanged !== null).to.be.true; + expect(priorityChanged!.event.data[2].toString()).to.be.equal(priority.toString()); + }); + + itSub.ifWithPallets('Prioritized operations executes in valid order', [Pallets.Scheduler], async ({helper}) => { + const [ + scheduledFirstId, + scheduledSecondId, + ] = await helper.arrange.makeScheduledIds(2); + + const currentBlockNumber = await helper.chain.getLatestBlockNumber(); + const blocksBeforeExecution = 4; + const firstExecutionBlockNumber = currentBlockNumber + blocksBeforeExecution; + + const prioHigh = 0; + const prioLow = 255; + + const periodic = { + period: 6, + repetitions: 2, + }; + + const amount = 1n * helper.balance.getOneTokenNominal(); + + // Scheduler a task with a lower priority first, then with a higher priority + await helper.getSudo().scheduler.scheduleAt(scheduledFirstId, firstExecutionBlockNumber, {priority: prioLow, periodic}) + .balance.forceTransferToSubstrate(alice, alice.address, bob.address, amount); + + await helper.getSudo().scheduler.scheduleAt(scheduledSecondId, firstExecutionBlockNumber, {priority: prioHigh, periodic}) + .balance.forceTransferToSubstrate(alice, alice.address, bob.address, amount); + + const capture = await helper.arrange.captureEvents('scheduler', 'Dispatched'); + + await helper.wait.newBlocks(blocksBeforeExecution); + + // Flip priorities + await helper.getSudo().scheduler.changePriority(alice, scheduledFirstId, prioHigh); + await helper.getSudo().scheduler.changePriority(alice, scheduledSecondId, prioLow); + + await helper.wait.newBlocks(periodic.period); + + const dispatchEvents = capture.extractCapturedEvents(); + expect(dispatchEvents.length).to.be.equal(4); + + const dispatchedIds = dispatchEvents.map(r => r.event.data[1].toString()); + + const firstExecuctionIds = [dispatchedIds[0], dispatchedIds[1]]; + const secondExecuctionIds = [dispatchedIds[2], dispatchedIds[3]]; + + expect(firstExecuctionIds[0]).to.be.equal(scheduledSecondId); + expect(firstExecuctionIds[1]).to.be.equal(scheduledFirstId); + + expect(secondExecuctionIds[0]).to.be.equal(scheduledFirstId); + expect(secondExecuctionIds[1]).to.be.equal(scheduledSecondId); + }); +}); + +describe('Negative Test: Scheduling', () => { + let alice: IKeyringPair; + let bob: IKeyringPair; + + before(async () => { + await usingPlaygrounds(async (helper, privateKeyWrapper) => { + alice = await privateKeyWrapper('//Alice'); + bob = await privateKeyWrapper('//Bob'); + + await helper.testUtils.enable(); + }); + }); + + itSub.ifWithPallets("Can't overwrite a scheduled ID", [Pallets.Scheduler], async ({helper}) => { + const collection = await helper.nft.mintCollection(alice, {tokenPrefix: 'schd'}); + const token = await collection.mintToken(alice); + + const scheduledId = await helper.arrange.makeScheduledId(); + const waitForBlocks = 4; + + await token.scheduleAfter(scheduledId, waitForBlocks) + .transfer(alice, {Substrate: bob.address}); + + const scheduled = helper.scheduler.scheduleAfter(scheduledId, waitForBlocks); + await expect(scheduled.balance.transferToSubstrate(alice, bob.address, 1n * helper.balance.getOneTokenNominal())) + .to.be.rejectedWith(/scheduler\.FailedToSchedule/); + + const bobsBalanceBefore = await helper.balance.getSubstrate(bob.address); + + await helper.wait.newBlocks(waitForBlocks + 1); + + const bobsBalanceAfter = await helper.balance.getSubstrate(bob.address); + + expect(await token.getOwner()).to.be.deep.equal({Substrate: bob.address}); + expect(bobsBalanceBefore).to.be.equal(bobsBalanceAfter); + }); + + itSub.ifWithPallets("Can't cancel an operation which is not scheduled", [Pallets.Scheduler], async ({helper}) => { + const scheduledId = await helper.arrange.makeScheduledId(); + await expect(helper.scheduler.cancelScheduled(alice, scheduledId)) + .to.be.rejectedWith(/scheduler\.NotFound/); + }); + + itSub.ifWithPallets("Can't cancel a non-owned scheduled operation", [Pallets.Scheduler], async ({helper}) => { + const collection = await helper.nft.mintCollection(alice, {tokenPrefix: 'schd'}); + const token = await collection.mintToken(alice); + + const scheduledId = await helper.arrange.makeScheduledId(); + const waitForBlocks = 4; + + await token.scheduleAfter(scheduledId, waitForBlocks) + .transfer(alice, {Substrate: bob.address}); + + await expect(helper.scheduler.cancelScheduled(bob, scheduledId)) + .to.be.rejectedWith(/BadOrigin/); + + await helper.wait.newBlocks(waitForBlocks + 1); + + expect(await token.getOwner()).to.be.deep.equal({Substrate: bob.address}); + }); + + itSub.ifWithPallets("Regular user can't set prioritized scheduled operation", [Pallets.Scheduler], async ({helper}) => { + const scheduledId = await helper.arrange.makeScheduledId(); + const waitForBlocks = 4; + + const amount = 42n * helper.balance.getOneTokenNominal(); + + const balanceBefore = await helper.balance.getSubstrate(bob.address); + + const scheduled = helper.scheduler.scheduleAfter(scheduledId, waitForBlocks, {priority: 42}); + + await expect(scheduled.balance.transferToSubstrate(alice, bob.address, amount)) + .to.be.rejectedWith(/BadOrigin/); + + await helper.wait.newBlocks(waitForBlocks + 1); + + const balanceAfter = await helper.balance.getSubstrate(bob.address); + + expect(balanceAfter).to.be.equal(balanceBefore); + }); + + itSub.ifWithPallets("Regular user can't change scheduled operation's priority", [Pallets.Scheduler], async ({helper}) => { + const collection = await helper.nft.mintCollection(bob, {tokenPrefix: 'schd'}); + const token = await collection.mintToken(bob); + + const scheduledId = await helper.arrange.makeScheduledId(); + const waitForBlocks = 4; + + await token.scheduleAfter(scheduledId, waitForBlocks) + .transfer(bob, {Substrate: alice.address}); + + const priority = 112; + await expect(helper.scheduler.changePriority(alice, scheduledId, priority)) + .to.be.rejectedWith(/BadOrigin/); + + const priorityChanged = await helper.wait.event( + waitForBlocks, + 'scheduler', + 'PriorityChanged', + ); + + expect(priorityChanged === null).to.be.true; + }); +}); + +// Implementation of the functionality tested here was postponed/shelved +describe.skip('Sponsoring scheduling', () => { + // let alice: IKeyringPair; + // let bob: IKeyringPair; + + // before(async() => { + // await usingApi(async (_, privateKeyWrapper) => { + // alice = privateKeyWrapper('//Alice'); + // bob = privateKeyWrapper('//Bob'); + // }); + // }); + + it('Can sponsor scheduling a transaction', async () => { + // const collectionId = await createCollectionExpectSuccess(); + // await setCollectionSponsorExpectSuccess(collectionId, bob.address); + // await confirmSponsorshipExpectSuccess(collectionId, '//Bob'); + + // await usingApi(async api => { + // const scheduledId = await makeScheduledId(); + // const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address); + + // const bobBalanceBefore = await getFreeBalance(bob); + // const waitForBlocks = 4; + // // no need to wait to check, fees must be deducted on scheduling, immediately + // await scheduleTransferExpectSuccess(api, collectionId, tokenId, alice, bob, 0, waitForBlocks, scheduledId); + // const bobBalanceAfter = await getFreeBalance(bob); + // // expect(aliceBalanceAfter == aliceBalanceBefore).to.be.true; + // expect(bobBalanceAfter < bobBalanceBefore).to.be.true; + // // wait for sequentiality matters + // await waitNewBlocks(waitForBlocks - 1); + // }); + }); + + it('Schedules and dispatches a transaction even if the caller has no funds at the time of the dispatch', async () => { + // await usingApi(async (api, privateKeyWrapper) => { + // // Find an empty, unused account + // const zeroBalance = await findUnusedAddress(api, privateKeyWrapper); + + // const collectionId = await createCollectionExpectSuccess(); + + // // Add zeroBalance address to allow list + // await enablePublicMintingExpectSuccess(alice, collectionId); + // await addToAllowListExpectSuccess(alice, collectionId, zeroBalance.address); + + // // Grace zeroBalance with money, enough to cover future transactions + // const balanceTx = api.tx.balances.transfer(zeroBalance.address, 1n * UNIQUE); + // await submitTransactionAsync(alice, balanceTx); + + // // Mint a fresh NFT + // const tokenId = await createItemExpectSuccess(zeroBalance, collectionId, 'NFT'); + // const scheduledId = await makeScheduledId(); + + // // Schedule transfer of the NFT a few blocks ahead + // const waitForBlocks = 5; + // await scheduleTransferExpectSuccess(api, collectionId, tokenId, zeroBalance, alice, 1, waitForBlocks, scheduledId); + + // // Get rid of the account's funds before the scheduled transaction takes place + // const balanceTx2 = api.tx.balances.transfer(alice.address, UNIQUE * 68n / 100n); + // const events = await submitTransactionAsync(zeroBalance, balanceTx2); + // expect(getGenericResult(events).success).to.be.true; + // /*const emptyBalanceTx = api.tx.balances.setBalance(zeroBalance.address, 0, 0); // do not null reserved? + // const sudoTx = api.tx.sudo.sudo(emptyBalanceTx as any); + // const events = await submitTransactionAsync(alice, sudoTx); + // expect(getGenericResult(events).success).to.be.true;*/ + + // // Wait for a certain number of blocks, discarding the ones that already happened while accepting the late transactions + // await waitNewBlocks(waitForBlocks - 3); + + // expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(alice.address)); + // }); + }); + + it('Sponsor going bankrupt does not impact a scheduled transaction', async () => { + // const collectionId = await createCollectionExpectSuccess(); + + // await usingApi(async (api, privateKeyWrapper) => { + // const zeroBalance = await findUnusedAddress(api, privateKeyWrapper); + // const balanceTx = api.tx.balances.transfer(zeroBalance.address, 1n * UNIQUE); + // await submitTransactionAsync(alice, balanceTx); + + // await setCollectionSponsorExpectSuccess(collectionId, zeroBalance.address); + // await confirmSponsorshipByKeyExpectSuccess(collectionId, zeroBalance); + + // const scheduledId = await makeScheduledId(); + // const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address); + + // const waitForBlocks = 5; + // await scheduleTransferExpectSuccess(api, collectionId, tokenId, alice, zeroBalance, 1, waitForBlocks, scheduledId); + + // const emptyBalanceSponsorTx = api.tx.balances.setBalance(zeroBalance.address, 0, 0); + // const sudoTx = api.tx.sudo.sudo(emptyBalanceSponsorTx as any); + // const events = await submitTransactionAsync(alice, sudoTx); + // expect(getGenericResult(events).success).to.be.true; + + // // Wait for a certain number of blocks, save for the ones that already happened while accepting the late transactions + // await waitNewBlocks(waitForBlocks - 3); + + // expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(zeroBalance.address)); + // }); + }); + + it('Exceeding sponsor rate limit without having enough funds prevents scheduling a periodic transaction', async () => { + // const collectionId = await createCollectionExpectSuccess(); + // await setCollectionSponsorExpectSuccess(collectionId, bob.address); + // await confirmSponsorshipExpectSuccess(collectionId, '//Bob'); + + // await usingApi(async (api, privateKeyWrapper) => { + // const zeroBalance = await findUnusedAddress(api, privateKeyWrapper); + + // await enablePublicMintingExpectSuccess(alice, collectionId); + // await addToAllowListExpectSuccess(alice, collectionId, zeroBalance.address); + + // const bobBalanceBefore = await getFreeBalance(bob); + + // const createData = {nft: {const_data: [], variable_data: []}}; + // const creationTx = api.tx.unique.createItem(collectionId, normalizeAccountId(zeroBalance), createData as any); + // const scheduledId = await makeScheduledId(); + + // /*const badTransaction = async function () { + // await submitTransactionExpectFailAsync(zeroBalance, zeroToAlice); + // }; + // await expect(badTransaction()).to.be.rejectedWith('Inability to pay some fees');*/ + + // await expect(scheduleAfter(api, creationTx, zeroBalance, 3, scheduledId, 1, 3)).to.be.rejectedWith(/Inability to pay some fees/); + + // expect(await getFreeBalance(bob)).to.be.equal(bobBalanceBefore); + // }); + }); +}); --- a/tests/src/util/index.ts +++ b/tests/src/util/index.ts @@ -95,6 +95,7 @@ NFT = 'nonfungible', Scheduler = 'scheduler', AppPromotion = 'apppromotion', + TestUtils = 'testutils', } export function requirePalletsOrSkip(test: Context, helper: DevUniqueHelper, requiredPallets: string[]) { --- 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.dev.ts +++ b/tests/src/util/playgrounds/unique.dev.ts @@ -8,8 +8,10 @@ import * as defs from '../../interfaces/definitions'; import {IKeyringPair} from '@polkadot/types/types'; import {EventRecord} from '@polkadot/types/interfaces'; -import {ICrossAccountId} from './types'; +import {ICrossAccountId, TSigner} from './types'; import {FrameSystemEventRecord} from '@polkadot/types/lookup'; +import {VoidFn} from '@polkadot/api/types'; +import {Pallets} from '..'; export class SilentLogger { log(_msg: any, _level: any): void { } @@ -63,6 +65,7 @@ arrange: ArrangeGroup; wait: WaitGroup; admin: AdminGroup; + testUtils: TestUtilGroup; constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) { options.helperBase = options.helperBase ?? DevUniqueHelper; @@ -71,6 +74,7 @@ this.arrange = new ArrangeGroup(this); this.wait = new WaitGroup(this); this.admin = new AdminGroup(this); + this.testUtils = new TestUtilGroup(this); } async connect(wsEndpoint: string, _listeners?: any): Promise { @@ -154,6 +158,8 @@ class ArrangeGroup { helper: DevUniqueHelper; + scheduledIdSlider = 0; + constructor(helper: DevUniqueHelper) { this.helper = helper; } @@ -297,6 +303,39 @@ const address = stringToU8a(('modl' + palletId).padEnd(32, '\0')); return encodeAddress(address); } + + async makeScheduledIds(num: number): Promise { + await this.helper.wait.noScheduledTasks(); + + function makeId(slider: number) { + const scheduledIdSize = 64; + const hexId = slider.toString(16); + const prefixSize = scheduledIdSize - hexId.length; + + const scheduledId = '0x' + '0'.repeat(prefixSize) + hexId; + + return scheduledId; + } + + const ids = []; + for (let i = 0; i < num; i++) { + ids.push(makeId(this.scheduledIdSlider)); + this.scheduledIdSlider += 1; + } + + return ids; + } + + async makeScheduledId(): Promise { + return (await this.makeScheduledIds(1))[0]; + } + + async captureEvents(eventSection: string, eventMethod: string): Promise { + const capture = new EventCapture(this.helper, eventSection, eventMethod); + await capture.startCapture(); + + return capture; + } } class MoonbeamAccountGroup { @@ -389,8 +428,26 @@ }); } - event(maxBlocksToWait: number, eventSection: string, eventMethod: string) { + async noScheduledTasks() { + const api = this.helper.getApi(); + // eslint-disable-next-line no-async-promise-executor + const promise = new Promise(async resolve => { + const unsubscribe = await api.rpc.chain.subscribeNewHeads(async () => { + const areThereScheduledTasks = await api.query.scheduler.lookup.entries(); + + if(areThereScheduledTasks.length == 0) { + unsubscribe(); + resolve(); + } + }); + }); + + return promise; + } + + async event(maxBlocksToWait: number, eventSection: string, eventMethod: string) { + // eslint-disable-next-line no-async-promise-executor const promise = new Promise(async (resolve) => { const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(async header => { const blockNumber = header.number.toHuman(); @@ -414,7 +471,6 @@ maxBlocksToWait--; } else { this.helper.logger.log(`Event \`${eventIdStr}\` is NOT found`); - unsubscribe(); resolve(null); } @@ -424,6 +480,86 @@ } } +class TestUtilGroup { + helper: DevUniqueHelper; + + constructor(helper: DevUniqueHelper) { + this.helper = helper; + } + + async enable() { + if (this.helper.fetchMissingPalletNames([Pallets.TestUtils]).length != 0) { + return; + } + + const signer = this.helper.util.fromSeed('//Alice'); + await this.helper.getSudo().executeExtrinsic(signer, 'api.tx.testUtils.enable', [], true); + } + + async setTestValue(signer: TSigner, testVal: number) { + await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValue', [testVal], true); + } + + async incTestValue(signer: TSigner) { + await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.incTestValue', [], true); + } + + async setTestValueAndRollback(signer: TSigner, testVal: number) { + await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValueAndRollback', [testVal], true); + } + + async testValue() { + return (await this.helper.callRpc('api.query.testUtils.testValue', [])).toNumber(); + } + + async justTakeFee(signer: TSigner) { + await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.justTakeFee', [], true); + } + + async selfCancelingInc(signer: TSigner, scheduledId: string, maxTestVal: number) { + await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.selfCancelingInc', [scheduledId, maxTestVal], true); + } +} + +class EventCapture { + helper: DevUniqueHelper; + eventSection: string; + eventMethod: string; + events: EventRecord[] = []; + unsubscribe: VoidFn | null = null; + + constructor( + helper: DevUniqueHelper, + eventSection: string, + eventMethod: string, + ) { + this.helper = helper; + this.eventSection = eventSection; + this.eventMethod = eventMethod; + } + + async startCapture() { + this.stopCapture(); + this.unsubscribe = (await this.helper.getApi().query.system.events((eventRecords: FrameSystemEventRecord[]) => { + const newEvents = eventRecords.filter(r => { + return r.event.section == this.eventSection && r.event.method == this.eventMethod; + }); + + this.events.push(...newEvents); + })) as any; + } + + stopCapture() { + if (this.unsubscribe !== null) { + this.unsubscribe(); + } + } + + extractCapturedEvents() { + return this.events; + } +} + class AdminGroup { helper: UniqueHelper; --- a/tests/src/util/playgrounds/unique.ts +++ b/tests/src/util/playgrounds/unique.ts @@ -9,7 +9,37 @@ 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'; +import {RuntimeDispatchInfo} from '@polkadot/types/interfaces'; export class CrossAccountId implements ICrossAccountId { Substrate?: TSubstrateAccount; @@ -38,7 +68,7 @@ static withNormalizedSubstrate(address: TSubstrateAccount, ss58Format = 42): CrossAccountId { return new CrossAccountId({Substrate: CrossAccountId.normalizeSubstrateAddress(address, ss58Format)}); } - + withNormalizedSubstrate(ss58Format = 42): CrossAccountId { if (this.Substrate) return CrossAccountId.withNormalizedSubstrate(this.Substrate, ss58Format); return this; @@ -61,7 +91,7 @@ if (this.Ethereum) return new CrossAccountId({Substrate: CrossAccountId.translateEthToSub(this.Ethereum, ss58Format)}); return this; } - + toLowerCase(): CrossAccountId { if (this.Substrate) this.Substrate = this.Substrate.toLowerCase(); if (this.Ethereum) this.Ethereum = this.Ethereum.toLowerCase(); @@ -161,7 +191,7 @@ } static extractTokensFromCreationResult(creationResult: ITransactionResult): { - success: boolean, + success: boolean, tokens: {collectionId: number, tokenId: number, owner: CrossAccountId, amount: bigint}[], } { if (creationResult.status !== this.transactionStatus.SUCCESS) { @@ -185,7 +215,7 @@ } static extractTokensFromBurnResult(burnResult: ITransactionResult): { - success: boolean, + success: boolean, tokens: {collectionId: number, tokenId: number, owner: CrossAccountId, amount: bigint}[], } { if (burnResult.status !== this.transactionStatus.SUCCESS) { @@ -256,7 +286,7 @@ static bigIntToDecimals(number: bigint, decimals = 18) { const numberStr = number.toString(); const dotPos = numberStr.length - decimals; - + if (dotPos <= 0) { return '0.' + '0'.repeat(Math.abs(dotPos)) + numberStr; } else { @@ -286,7 +316,7 @@ return obj; } - + private static extractData(data: any, type: any): any { if(!type) return data.toHuman(); if (['u16', 'u32'].indexOf(type.type) > -1) return data.toNumber(); @@ -532,6 +562,26 @@ }); } + async getPaymentInfo(signer: TSigner, tx: any, len: number | null) { + const api = this.getApi(); + const signingInfo = await api.derive.tx.signingInfo(signer.address); + + // We need to sign the tx because + // unsigned transactions does not have an inclusion fee + tx.sign(signer, { + blockHash: api.genesisHash, + genesisHash: api.genesisHash, + runtimeVersion: api.runtimeVersion, + nonce: signingInfo.nonce, + }); + + if (len === null) { + return (await this.callRpc('api.rpc.payment.queryInfo', [tx.toHex()])) as RuntimeDispatchInfo; + } else { + return (await api.call.transactionPaymentApi.queryInfo(tx, len)) as RuntimeDispatchInfo; + } + } + constructApiCall(apiCall: string, params: any[]) { if(!apiCall.startsWith('api.')) throw Error(`Invalid api call: ${apiCall}`); let call = this.getApi() as any; @@ -906,7 +956,7 @@ /** * Check if user is in allow list. - * + * * @param collectionId ID of collection * @param user Account to check * @example await getAdmins(1) @@ -1016,7 +1066,7 @@ /** * Get collection properties. - * + * * @param collectionId ID of collection * @param propertyKeys optionally filter the returned properties to only these keys * @example getProperties(1219, ['location', 'date', 'time', 'isParadise']); @@ -1237,8 +1287,8 @@ if (tokenData === null || tokenData.owner === null) return null; const owner = {} as any; for (const key of Object.keys(tokenData.owner)) { - owner[key.toLocaleLowerCase()] = key.toLocaleLowerCase() == 'substrate' - ? CrossAccountId.normalizeSubstrateAddress(tokenData.owner[key]) + owner[key.toLocaleLowerCase()] = key.toLocaleLowerCase() == 'substrate' + ? CrossAccountId.normalizeSubstrateAddress(tokenData.owner[key]) : tokenData.owner[key]; } tokenData.normalizedOwner = CrossAccountId.fromLowerCaseKeys(owner); @@ -1268,7 +1318,7 @@ /** * Get token property permissions. - * + * * @param collectionId ID of collection * @param propertyKeys optionally filter the returned property permissions to only these keys * @example getPropertyPermissions(1219, ['location', 'date', 'time', 'isParadise']); @@ -1300,7 +1350,7 @@ /** * Get properties, metadata assigned to a token. - * + * * @param collectionId ID of collection * @param tokenId ID of token * @param propertyKeys optionally filter the returned properties to only these keys @@ -2135,8 +2185,8 @@ }; } }); - const isSuccess = this.helper.address.normalizeSubstrate(typeof signer === 'string' ? signer : signer.address) === transfer.from - && this.helper.address.normalizeSubstrate(address) === transfer.to + const isSuccess = this.helper.address.normalizeSubstrate(typeof signer === 'string' ? signer : signer.address) === transfer.from + && this.helper.address.normalizeSubstrate(address) === transfer.to && BigInt(amount) === transfer.amount; return isSuccess; } @@ -2184,8 +2234,8 @@ }; } }); - const isSuccess = (typeof signer === 'string' ? signer : signer.address) === transfer.from - && address === transfer.to + const isSuccess = (typeof signer === 'string' ? signer : signer.address) === transfer.from + && address === transfer.to && BigInt(amount) === transfer.amount; return isSuccess; } @@ -2254,6 +2304,25 @@ transferToSubstrate(signer: TSigner, address: TSubstrateAccount, amount: bigint | string): Promise { return this.subBalanceGroup.transferToSubstrate(signer, address, amount); } + + async forceTransferToSubstrate(signer: TSigner, from: TSubstrateAccount, to: TSubstrateAccount, amount: bigint | string): Promise { + const result = await this.helper.executeExtrinsic(signer, 'api.tx.balances.forceTransfer', [from, to, amount], true); + + let transfer = {from: null, to: null, amount: 0n} as any; + result.result.events.forEach(({event: {data, method, section}}) => { + if ((section === 'balances') && (method === 'Transfer')) { + transfer = { + from: this.helper.address.normalizeSubstrate(data[0]), + to: this.helper.address.normalizeSubstrate(data[1]), + amount: BigInt(data[2]), + }; + } + }); + let isSuccess = this.helper.address.normalizeSubstrate(from) === transfer.from; + isSuccess = isSuccess && this.helper.address.normalizeSubstrate(to) === transfer.to; + isSuccess = isSuccess && BigInt(amount) === transfer.amount; + return isSuccess; + } } class AddressGroup extends HelperGroup { @@ -2364,7 +2433,7 @@ async getTotalStakedPerBlock(address: ICrossAccountId): Promise { const rawTotalStakerdPerBlock = await this.helper.callRpc('api.rpc.appPromotion.totalStakedPerBlock', [address]); return rawTotalStakerdPerBlock.map(([block, amount]: any[]) => { - return { + return { block: block.toBigInt(), amount: amount.toBigInt(), }; @@ -2769,7 +2838,7 @@ this.blocksNum, this.options.periodic ? [this.options.periodic.period, this.options.periodic.repetitions] : null, this.options.priority ?? null, - {Value: scheduledTx}, + scheduledTx, ], expectSuccess, ); @@ -2791,7 +2860,6 @@ expectSuccess?: boolean, ): Promise { const call = this.constructApiCall(extrinsic, params); - return super.executeExtrinsic( sender, 'api.tx.sudo.sudo', -- gitstuff