difftreelog
Merge pull request #681 from UniqueNetwork/eth_temp_develop
in: master
Eth temp develop
59 files changed
Cargo.lockdiffbeforeafterboth--- a/Cargo.lock
+++ b/Cargo.lock
@@ -746,7 +746,9 @@
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
dependencies = [
+ "lazy_static",
"memchr",
+ "regex-automata",
]
[[package]]
@@ -1055,6 +1057,16 @@
]
[[package]]
+name = "concat-idents"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b6f90860248d75014b7b103db8fee4f291c07bfb41306cdf77a0a5ab7a10d2f"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
name = "concurrent-queue"
version = "1.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1064,6 +1076,19 @@
]
[[package]]
+name = "console"
+version = "0.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c050367d967ced717c04b65d8c619d863ef9292ce0c5760028655a2fb298718c"
+dependencies = [
+ "encode_unicode",
+ "lazy_static",
+ "libc",
+ "terminal_size",
+ "winapi",
+]
+
+[[package]]
name = "const-oid"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1846,9 +1871,9 @@
[[package]]
name = "cxx"
-version = "1.0.79"
+version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f83d0ebf42c6eafb8d7c52f7e5f2d3003b89c7aa4fd2b79229209459a849af8"
+checksum = "19f39818dcfc97d45b03953c1292efc4e80954e1583c4aa770bac1383e2310a4"
dependencies = [
"cc",
"cxxbridge-flags",
@@ -1858,9 +1883,9 @@
[[package]]
name = "cxx-build"
-version = "1.0.79"
+version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07d050484b55975889284352b0ffc2ecbda25c0c55978017c132b29ba0818a86"
+checksum = "3e580d70777c116df50c390d1211993f62d40302881e54d4b79727acb83d0199"
dependencies = [
"cc",
"codespan-reporting",
@@ -1873,15 +1898,15 @@
[[package]]
name = "cxxbridge-flags"
-version = "1.0.79"
+version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "99d2199b00553eda8012dfec8d3b1c75fce747cf27c169a270b3b99e3448ab78"
+checksum = "56a46460b88d1cec95112c8c363f0e2c39afdb237f60583b0b36343bf627ea9c"
[[package]]
name = "cxxbridge-macro"
-version = "1.0.79"
+version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dcb67a6de1f602736dd7eaead0080cf3435df806c61b24b13328db128c58868f"
+checksum = "747b608fecf06b0d72d440f27acc99288207324b793be2c17991839f3d4995ea"
dependencies = [
"proc-macro2",
"quote",
@@ -2140,6 +2165,12 @@
]
[[package]]
+name = "encode_unicode"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
+
+[[package]]
name = "enum-as-inner"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2302,6 +2333,7 @@
name = "evm-coder"
version = "0.1.3"
dependencies = [
+ "concat-idents",
"ethereum",
"evm-coder-procedural",
"evm-core",
@@ -2310,6 +2342,7 @@
"hex-literal",
"impl-trait-for-tuples",
"primitive-types",
+ "similar-asserts",
"sp-std",
]
@@ -3528,9 +3561,9 @@
[[package]]
name = "iana-time-zone-haiku"
-version = "0.1.1"
+version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
+checksum = "fde6edd6cef363e9359ed3c98ba64590ba9eecba2293eb5a723ab32aee8926aa"
dependencies = [
"cxx",
"cxx-build",
@@ -5792,7 +5825,7 @@
[[package]]
name = "pallet-common"
-version = "0.1.8"
+version = "0.1.9"
dependencies = [
"ethereum",
"evm-coder",
@@ -6325,7 +6358,7 @@
[[package]]
name = "pallet-nonfungible"
-version = "0.1.5"
+version = "0.1.6"
dependencies = [
"ethereum",
"evm-coder",
@@ -6447,7 +6480,7 @@
[[package]]
name = "pallet-refungible"
-version = "0.2.4"
+version = "0.2.5"
dependencies = [
"derivative",
"ethereum",
@@ -6766,7 +6799,7 @@
[[package]]
name = "pallet-unique"
-version = "0.2.0"
+version = "0.2.1"
dependencies = [
"ethereum",
"evm-coder",
@@ -10741,9 +10774,9 @@
[[package]]
name = "serde_json"
-version = "1.0.87"
+version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45"
+checksum = "41feea4228a6f1cd09ec7a3593a682276702cd67b5273544757dae23c096f074"
dependencies = [
"itoa",
"ryu",
@@ -10886,6 +10919,26 @@
]
[[package]]
+name = "similar"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62ac7f900db32bf3fd12e0117dd3dc4da74bc52ebaac97f39668446d89694803"
+dependencies = [
+ "bstr",
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "similar-asserts"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbf644ad016b75129f01a34a355dcb8d66a5bc803e417c7a77cc5d5ee9fa0f18"
+dependencies = [
+ "console",
+ "similar",
+]
+
+[[package]]
name = "slab"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -12002,9 +12055,9 @@
[[package]]
name = "syn"
-version = "1.0.103"
+version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d"
+checksum = "3fcd952facd492f9be3ef0d0b7032a6e442ee9b361d4acc2b1d0c4aaa5f613a1"
dependencies = [
"proc-macro2",
"quote",
@@ -12080,6 +12133,16 @@
]
[[package]]
+name = "terminal_size"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
name = "test-runtime-constants"
version = "0.9.30"
source = "git+https://github.com/paritytech/polkadot?branch=release-v0.9.30#064536093f5ff70d867f4bbce8d4c41a406d317a"
@@ -12660,6 +12723,12 @@
]
[[package]]
+name = "unicode-segmentation"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a"
+
+[[package]]
name = "unicode-width"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
crates/evm-coder/Cargo.tomldiffbeforeafterboth--- 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",
+]
crates/evm-coder/src/abi.rsdiffbeforeafterboth--- 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<u8>,
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<u8> {
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<T> {
/// Read item from current position, advanding decoder
fn abi_read(&mut self) -> Result<T>;
-
- /// 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<u8>, 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<bytes> for AbiReader<'_> {
+ fn abi_read(&mut self) -> Result<bytes> {
+ Ok(bytes(self.bytes()?))
+ }
+}
mod sealed {
/// Not all types can be placed in vec, i.e `Vec<u8>` is restricted, `bytes` should be used instead
@@ -390,22 +416,27 @@
out.push(<Self as AbiRead<R>>::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(<Self as AbiRead<($($ident,)+)>>::size()) } else { None };
+ let size = if !<($($ident,)+)>::is_dynamic() { Some(<($($ident,)+)>::size()) } else { None };
let mut subresult = self.subresult(size)?;
Ok((
$(<Self as AbiRead<$ident>>::abi_read(&mut subresult)?,)+
))
- }
-
- fn size() -> usize {
- 0 $(+ <AbiReader<'_> 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<u8> {
+
+impl AbiWrite for bytes {
+ fn abi_write(&self, writer: &mut AbiWriter) {
+ writer.bytes(self.0.as_slice())
+ }
+}
+
+impl<T: AbiWrite + TypeHelper> AbiWrite for Vec<T> {
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 = <AbiReader<'_> 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 =
<AbiReader<'_> as AbiRead<Vec<(uint256, string)>>>::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 =
- <AbiReader<'_> as AbiRead<Vec<(address, uint256)>>>::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());
}
}
crates/evm-coder/src/lib.rsdiffbeforeafterboth--- 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<u8>;
+ #[derive(Default, Debug)]
+ pub struct bytes(pub Vec<u8>);
+
/// 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<Vec<u8>> for bytes {
+ fn from(src: Vec<u8>) -> Self {
+ Self(src)
+ }
+ }
+
+ impl Into<Vec<u8>> for bytes {
+ fn into(self) -> Vec<u8> {
+ 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
crates/evm-coder/src/solidity.rsdiffbeforeafterboth--- 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)")
}
}
node/cli/src/command.rsdiffbeforeafterboth--- 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;
node/cli/src/service.rsdiffbeforeafterboth--- 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")]
pallets/common/CHANGELOG.mddiffbeforeafterboth--- 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).
+
<!-- bureaucrate goes here -->
+
## [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.
pallets/common/Cargo.tomldiffbeforeafterboth--- 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"
pallets/common/src/erc.rsdiffbeforeafterboth--- 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 = <Vec<u8>>::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")?;
<Pallet<T>>::set_collection_property(self, &caller, Property { key, value })
.map_err(dispatch_to_evm::<T>)
}
+ /// Set collection properties.
+ ///
+ /// @param properties Vector of properties key/value pair.
+ #[weight(<SelfWeightOf<T>>::set_collection_properties(properties.len() as u32))]
+ fn set_collection_properties(
+ &mut self,
+ caller: caller,
+ properties: Vec<(string, bytes)>,
+ ) -> Result<void> {
+ let caller = T::CrossAccountId::from_eth(caller);
+
+ let properties = properties
+ .into_iter()
+ .map(|(key, value)| {
+ let key = <Vec<u8>>::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::<Result<Vec<_>>>()?;
+
+ <Pallet<T>>::set_collection_properties(self, &caller, properties)
+ .map_err(dispatch_to_evm::<T>)
+ }
+
/// Delete collection property.
///
/// @param key Property key.
#[weight(<SelfWeightOf<T>>::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 = <Vec<u8>>::from(key)
.try_into()
@@ -106,6 +138,24 @@
<Pallet<T>>::delete_collection_property(self, &caller, key).map_err(dispatch_to_evm::<T>)
}
+ /// Delete collection properties.
+ ///
+ /// @param keys Properties keys.
+ #[weight(<SelfWeightOf<T>>::delete_collection_properties(keys.len() as u32))]
+ fn delete_collection_properties(&mut self, caller: caller, keys: Vec<string>) -> Result<()> {
+ let caller = T::CrossAccountId::from_eth(caller);
+ let keys = keys
+ .into_iter()
+ .map(|key| {
+ <Vec<u8>>::from(key)
+ .try_into()
+ .map_err(|_| Error::Revert("key too large".into()))
+ })
+ .collect::<Result<Vec<_>>>()?;
+
+ <Pallet<T>>::delete_collection_properties(self, &caller, keys).map_err(dispatch_to_evm::<T>)
+ }
+
/// Get collection property.
///
/// @dev Throws error if key not found.
@@ -117,12 +167,44 @@
.try_into()
.map_err(|_| "key too large")?;
- let props = <CollectionProperties<T>>::get(self.id);
+ let props = CollectionProperties::<T>::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<string>) -> Result<Vec<(string, bytes)>> {
+ let keys = keys
+ .into_iter()
+ .map(|key| {
+ <Vec<u8>>::from(key)
+ .try_into()
+ .map_err(|_| Error::Revert("key too large".into()))
+ })
+ .collect::<Result<Vec<_>>>()?;
+
+ let properties = Pallet::<T>::filter_collection_properties(
+ self.id,
+ if keys.is_empty() { None } else { Some(keys) },
+ )
+ .map_err(dispatch_to_evm::<T>)?;
+
+ 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::<Result<Vec<_>>>()?;
+ 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<void> {
- // 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<void> {
+ 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::<T>(sponsor);
- // self.set_sponsor(sponsor.as_sub().clone())
- // .map_err(dispatch_to_evm::<T>)?;
- // save(self)
- // }
+ let sponsor = convert_tuple_to_cross_account::<T>(sponsor)?;
+ self.set_sponsor(sponsor.as_sub().clone())
+ .map_err(dispatch_to_evm::<T>)?;
+ save(self)
+ }
/// Whether there is a pending sponsor.
fn has_collection_pending_sponsor(&self) -> Result<bool> {
@@ -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<void> {
- // 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<void> {
+ self.consume_store_writes(2)?;
- // let caller = T::CrossAccountId::from_eth(caller);
- // let new_admin = convert_uint256_to_cross_account::<T>(new_admin);
- // <Pallet<T>>::toggle_admin(self, &caller, &new_admin, true).map_err(dispatch_to_evm::<T>)?;
- // Ok(())
- // }
+ let caller = T::CrossAccountId::from_eth(caller);
+ let new_admin = convert_tuple_to_cross_account::<T>(new_admin)?;
+ <Pallet<T>>::toggle_admin(self, &caller, &new_admin, true).map_err(dispatch_to_evm::<T>)?;
+ 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<void> {
- // 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<void> {
+ self.consume_store_writes(2)?;
- // let caller = T::CrossAccountId::from_eth(caller);
- // let admin = convert_uint256_to_cross_account::<T>(admin);
- // <Pallet<T>>::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::<T>)?;
- // Ok(())
- // }
+ let caller = T::CrossAccountId::from_eth(caller);
+ let admin = convert_tuple_to_cross_account::<T>(admin)?;
+ <Pallet<T>>::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::<T>)?;
+ 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<void> {
- // 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<void> {
+ self.consume_store_writes(1)?;
- // let caller = T::CrossAccountId::from_eth(caller);
- // let user = convert_uint256_to_cross_account::<T>(user);
- // Pallet::<T>::toggle_allowlist(self, &caller, &user, true).map_err(dispatch_to_evm::<T>)?;
- // Ok(())
- // }
+ let caller = T::CrossAccountId::from_eth(caller);
+ let user = convert_tuple_to_cross_account::<T>(user)?;
+ Pallet::<T>::toggle_allowlist(self, &caller, &user, true).map_err(dispatch_to_evm::<T>)?;
+ 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<void> {
- // 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<void> {
+ self.consume_store_writes(1)?;
- // let caller = T::CrossAccountId::from_eth(caller);
- // let user = convert_uint256_to_cross_account::<T>(user);
- // Pallet::<T>::toggle_allowlist(self, &caller, &user, false).map_err(dispatch_to_evm::<T>)?;
- // Ok(())
- // }
+ let caller = T::CrossAccountId::from_eth(caller);
+ let user = convert_tuple_to_cross_account::<T>(user)?;
+ Pallet::<T>::toggle_allowlist(self, &caller, &user, false).map_err(dispatch_to_evm::<T>)?;
+ 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<bool> {
- // let user = convert_uint256_to_cross_account::<T>(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<bool> {
+ let user = convert_tuple_to_cross_account::<T>(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::<T>(
@@ -602,27 +678,29 @@
.map_err(dispatch_to_evm::<T>)
}
- // 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<void> {
- // 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<Vec<(address, uint256)>> {
+ let result = crate::IsAdmin::<T>::iter_prefix((self.id,))
+ .map(|(admin, _)| crate::eth::convert_cross_account_to_tuple::<T>(&admin))
+ .collect();
+ Ok(result)
+ }
- // let caller = T::CrossAccountId::from_eth(caller);
- // let new_owner = convert_uint256_to_cross_account::<T>(new_owner);
- // self.set_owner_internal(caller, new_owner)
- // .map_err(dispatch_to_evm::<T>)
- // }
+ /// 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<void> {
+ self.consume_store_writes(1)?;
- // TODO: need implement AbiWriter for &Vec<T>
- // fn collection_admins(&self) -> Result<Vec<(address, uint256)>> {
- // let result = pallet_common::IsAdmin::<T>::iter_prefix((self.id,))
- // .map(|(admin, _)| pallet_common::eth::convert_cross_account_to_tuple::<T>(&admin))
- // .collect();
- // Ok(result)
- // }
+ let caller = T::CrossAccountId::from_eth(caller);
+ let new_owner = convert_tuple_to_cross_account::<T>(new_owner)?;
+ self.set_owner_internal(caller, new_owner)
+ .map_err(dispatch_to_evm::<T>)
+ }
}
/// ### Note
pallets/common/src/eth.rsdiffbeforeafterboth--- 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<T: Config>(
+ eth_cross_account_id: (address, uint256),
+) -> evm_coder::execution::Result<T::CrossAccountId>
+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::<T>(
+ 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())
+ }
+}
pallets/common/src/lib.rsdiffbeforeafterboth1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617//! # Common pallet18//!19//! The Common pallet provides functionality for handling collections.20//!21//! ## Overview22//!23//! The Common pallet provides an interface for common collection operations for different collection types24//! (see [CommonCollectionOperations]), as well as a generic dispatcher for these, see [dispatch] module.25//! It also provides this functionality to EVM, see [erc] and [eth] modules.26//!27//! The Common pallet provides functions for:28//!29//! - Setting and approving collection sponsor.30//! - Get\set\delete allow list.31//! - Get\set\delete collection properties.32//! - Get\set\delete collection property permissions.33//! - Get\set\delete token property permissions.34//! - Get\set\delete collection administrators.35//! - Checking access permissions.36//!37//! ### Terminology38//! **Collection sponsor** - For the collection, you can set a sponsor, at whose expense it will39//! be possible to mint tokens.40//!41//! **Allow list** - List of users who have the right to minting tokens.42//!43//! **Collection properties** - Collection properties are simply key-value stores where various44//! metadata can be placed.45//!46//! **Permissions on token properties** - For each property in the token can be set permission47//! to change, see [`PropertyPermission`].48//!49//! **Collection administrator** - For a collection, you can set administrators who have the right50//! to most actions on the collection.5152#![warn(missing_docs)]53#![cfg_attr(not(feature = "std"), no_std)]54extern crate alloc;5556use core::ops::{Deref, DerefMut};57use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};58use sp_std::vec::Vec;59use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};60use evm_coder::ToLog;61use frame_support::{62 dispatch::{DispatchErrorWithPostInfo, DispatchResultWithPostInfo, Weight, PostDispatchInfo},63 ensure,64 traits::{Imbalance, Get, Currency, WithdrawReasons, ExistenceRequirement},65 dispatch::Pays,66 transactional,67};68use pallet_evm::GasWeightMapping;69use up_data_structs::{70 COLLECTION_NUMBER_LIMIT,71 Collection,72 RpcCollection,73 CollectionFlags,74 RpcCollectionFlags,75 CollectionId,76 CreateItemData,77 MAX_TOKEN_PREFIX_LENGTH,78 COLLECTION_ADMINS_LIMIT,79 TokenId,80 TokenChild,81 CollectionStats,82 MAX_TOKEN_OWNERSHIP,83 CollectionMode,84 NFT_SPONSOR_TRANSFER_TIMEOUT,85 FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,86 REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,87 MAX_SPONSOR_TIMEOUT,88 CUSTOM_DATA_LIMIT,89 CollectionLimits,90 CreateCollectionData,91 SponsorshipState,92 CreateItemExData,93 SponsoringRateLimit,94 budget::Budget,95 PhantomType,96 Property,97 Properties,98 PropertiesPermissionMap,99 PropertyKey,100 PropertyValue,101 PropertyPermission,102 PropertiesError,103 PropertyKeyPermission,104 TokenData,105 TrySetProperty,106 PropertyScope,107 // RMRK108 RmrkCollectionInfo,109 RmrkInstanceInfo,110 RmrkResourceInfo,111 RmrkPropertyInfo,112 RmrkBaseInfo,113 RmrkPartType,114 RmrkBoundedTheme,115 RmrkNftChild,116 CollectionPermissions,117};118119pub use pallet::*;120use sp_core::H160;121use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};122#[cfg(feature = "runtime-benchmarks")]123pub mod benchmarking;124pub mod dispatch;125pub mod erc;126pub mod eth;127pub mod weights;128129/// Weight info.130pub type SelfWeightOf<T> = <T as Config>::WeightInfo;131132/// Collection handle contains information about collection data and id.133/// Also provides functionality to count consumed gas.134///135/// CollectionHandle is used as a generic wrapper for collections of all types.136/// It allows to perform common operations and queries on any collection type,137/// both completely general for all, as well as their respective implementations of [`CommonCollectionOperations`].138#[must_use = "Should call submit_logs or save, otherwise some data will be lost for evm side"]139pub struct CollectionHandle<T: Config> {140 /// Collection id141 pub id: CollectionId,142 collection: Collection<T::AccountId>,143 /// Substrate recorder for counting consumed gas144 pub recorder: SubstrateRecorder<T>,145}146147impl<T: Config> WithRecorder<T> for CollectionHandle<T> {148 fn recorder(&self) -> &SubstrateRecorder<T> {149 &self.recorder150 }151 fn into_recorder(self) -> SubstrateRecorder<T> {152 self.recorder153 }154}155156impl<T: Config> CollectionHandle<T> {157 /// Same as [CollectionHandle::new] but with an explicit gas limit.158 pub fn new_with_gas_limit(id: CollectionId, gas_limit: u64) -> Option<Self> {159 <CollectionById<T>>::get(id).map(|collection| Self {160 id,161 collection,162 recorder: SubstrateRecorder::new(gas_limit),163 })164 }165166 /// Same as [CollectionHandle::new] but with an existed [`SubstrateRecorder`].167 pub fn new_with_recorder(id: CollectionId, recorder: SubstrateRecorder<T>) -> Option<Self> {168 <CollectionById<T>>::get(id).map(|collection| Self {169 id,170 collection,171 recorder,172 })173 }174175 /// Retrives collection data from storage and creates collection handle with default parameters.176 /// If collection not found return `None`177 pub fn new(id: CollectionId) -> Option<Self> {178 Self::new_with_gas_limit(id, u64::MAX)179 }180181 /// Same as [`CollectionHandle::new`] but if collection not found [CollectionNotFound](Error::CollectionNotFound) returned.182 pub fn try_get(id: CollectionId) -> Result<Self, DispatchError> {183 Ok(Self::new(id).ok_or(<Error<T>>::CollectionNotFound)?)184 }185186 /// Consume gas for reading.187 pub fn consume_store_reads(&self, reads: u64) -> evm_coder::execution::Result<()> {188 self.recorder189 .consume_gas(T::GasWeightMapping::weight_to_gas(Weight::from_ref_time(190 <T as frame_system::Config>::DbWeight::get()191 .read192 .saturating_mul(reads),193 )))194 }195196 /// Consume gas for writing.197 pub fn consume_store_writes(&self, writes: u64) -> evm_coder::execution::Result<()> {198 self.recorder199 .consume_gas(T::GasWeightMapping::weight_to_gas(Weight::from_ref_time(200 <T as frame_system::Config>::DbWeight::get()201 .write202 .saturating_mul(writes),203 )))204 }205206 /// Consume gas for reading and writing.207 pub fn consume_store_reads_and_writes(208 &self,209 reads: u64,210 writes: u64,211 ) -> evm_coder::execution::Result<()> {212 let weight = <T as frame_system::Config>::DbWeight::get();213 let reads = weight.read.saturating_mul(reads);214 let writes = weight.read.saturating_mul(writes);215 self.recorder216 .consume_gas(T::GasWeightMapping::weight_to_gas(Weight::from_ref_time(217 reads.saturating_add(writes),218 )))219 }220221 /// Save collection to storage.222 pub fn save(&self) -> DispatchResult {223 <CollectionById<T>>::insert(self.id, &self.collection);224 Ok(())225 }226227 /// Set collection sponsor.228 ///229 /// Unique collections allows sponsoring for certain actions.230 /// This method allows you to set the sponsor of the collection.231 /// In order for sponsorship to become active, it must be confirmed through [`Self::confirm_sponsorship`].232 pub fn set_sponsor(&mut self, sponsor: T::AccountId) -> DispatchResult {233 self.collection.sponsorship = SponsorshipState::Unconfirmed(sponsor);234 Ok(())235 }236237 /// Confirm sponsorship238 ///239 /// In order for the sponsorship to become active, the user set as the sponsor must confirm their participation.240 /// Before confirming sponsorship, the user must be specified as the sponsor of the collection via [`Self::set_sponsor`].241 pub fn confirm_sponsorship(&mut self, sender: &T::AccountId) -> Result<bool, DispatchError> {242 if self.collection.sponsorship.pending_sponsor() != Some(sender) {243 return Ok(false);244 }245246 self.collection.sponsorship = SponsorshipState::Confirmed(sender.clone());247 Ok(true)248 }249250 /// Remove collection sponsor.251 pub fn remove_sponsor(&mut self) -> DispatchResult {252 self.collection.sponsorship = SponsorshipState::Disabled;253 Ok(())254 }255256 /// Checks that the collection was created with, and must be operated upon through **Unique API**.257 /// Now check only the `external` flag and if it's **true**, then return [`Error::CollectionIsExternal`] error.258 pub fn check_is_internal(&self) -> DispatchResult {259 if self.flags.external {260 return Err(<Error<T>>::CollectionIsExternal)?;261 }262263 Ok(())264 }265266 /// Checks that the collection was created with, and must be operated upon through an **assimilated API**.267 /// Now check only the `external` flag and if it's **false**, then return [`Error::CollectionIsInternal`] error.268 pub fn check_is_external(&self) -> DispatchResult {269 if !self.flags.external {270 return Err(<Error<T>>::CollectionIsInternal)?;271 }272273 Ok(())274 }275}276277impl<T: Config> Deref for CollectionHandle<T> {278 type Target = Collection<T::AccountId>;279280 fn deref(&self) -> &Self::Target {281 &self.collection282 }283}284285impl<T: Config> DerefMut for CollectionHandle<T> {286 fn deref_mut(&mut self) -> &mut Self::Target {287 &mut self.collection288 }289}290291impl<T: Config> CollectionHandle<T> {292 /// Checks if the `user` is the owner of the collection.293 pub fn check_is_owner(&self, user: &T::CrossAccountId) -> DispatchResult {294 ensure!(*user.as_sub() == self.owner, <Error<T>>::NoPermission);295 Ok(())296 }297298 /// Returns **true** if the `user` is the owner or administrator of the collection.299 pub fn is_owner_or_admin(&self, user: &T::CrossAccountId) -> bool {300 *user.as_sub() == self.owner || <IsAdmin<T>>::get((self.id, user))301 }302303 /// Checks if the `user` is the owner or administrator of the collection.304 pub fn check_is_owner_or_admin(&self, user: &T::CrossAccountId) -> DispatchResult {305 ensure!(self.is_owner_or_admin(user), <Error<T>>::NoPermission);306 Ok(())307 }308309 /// Return **true** if `user` was not allowed to have tokens, and he can ignore such restrictions.310 pub fn ignores_allowance(&self, user: &T::CrossAccountId) -> bool {311 self.limits.owner_can_transfer() && self.is_owner_or_admin(user)312 }313314 /// Return **true** if `user` does not have enough token parts, and he can ignore such restrictions.315 pub fn ignores_owned_amount(&self, user: &T::CrossAccountId) -> bool {316 self.limits.owner_can_transfer() && self.is_owner_or_admin(user)317 }318319 /// Checks if the user is in the allow list. If not [Error::AddressNotInAllowlist] returns.320 pub fn check_allowlist(&self, user: &T::CrossAccountId) -> DispatchResult {321 ensure!(322 <Allowlist<T>>::get((self.id, user)),323 <Error<T>>::AddressNotInAllowlist324 );325 Ok(())326 }327328 /// Changes collection owner to another account329 /// #### Store read/writes330 /// 1 writes331 fn set_owner_internal(332 &mut self,333 caller: T::CrossAccountId,334 new_owner: T::CrossAccountId,335 ) -> DispatchResult {336 self.check_is_owner(&caller)?;337 self.collection.owner = new_owner.as_sub().clone();338 self.save()339 }340}341342#[frame_support::pallet]343pub mod pallet {344 use super::*;345 use pallet_evm::account;346 use dispatch::CollectionDispatch;347 use frame_support::{Blake2_128Concat, pallet_prelude::*, storage::Key, traits::StorageVersion};348 use frame_system::pallet_prelude::*;349 use frame_support::traits::Currency;350 use up_data_structs::{TokenId, mapping::TokenAddressMapping};351 use scale_info::TypeInfo;352 use weights::WeightInfo;353354 #[pallet::config]355 pub trait Config:356 frame_system::Config357 + pallet_evm_coder_substrate::Config358 + pallet_evm::Config359 + TypeInfo360 + account::Config361 {362 /// Weight information for functions of this pallet.363 type WeightInfo: WeightInfo;364365 /// Events compatible with [`frame_system::Config::Event`].366 type RuntimeEvent: IsType<<Self as frame_system::Config>::RuntimeEvent> + From<Event<Self>>;367368 /// Handler of accounts and payment.369 type Currency: Currency<Self::AccountId>;370371 /// Set price to create a collection.372 #[pallet::constant]373 type CollectionCreationPrice: Get<374 <<Self as Config>::Currency as Currency<Self::AccountId>>::Balance,375 >;376377 /// Dispatcher of operations on collections.378 type CollectionDispatch: CollectionDispatch<Self>;379380 /// Account which holds the chain's treasury.381 type TreasuryAccountId: Get<Self::AccountId>;382383 /// Address under which the CollectionHelper contract would be available.384 type ContractAddress: Get<H160>;385386 /// Mapper for token addresses to Ethereum addresses.387 type EvmTokenAddressMapping: TokenAddressMapping<H160>;388389 /// Mapper for token addresses to [`CrossAccountId`].390 type CrossTokenAddressMapping: TokenAddressMapping<Self::CrossAccountId>;391 }392393 const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);394395 #[pallet::pallet]396 #[pallet::storage_version(STORAGE_VERSION)]397 #[pallet::generate_store(pub(super) trait Store)]398 pub struct Pallet<T>(_);399400 #[pallet::extra_constants]401 impl<T: Config> Pallet<T> {402 /// Maximum admins per collection.403 pub fn collection_admins_limit() -> u32 {404 COLLECTION_ADMINS_LIMIT405 }406 }407408 #[pallet::event]409 #[pallet::generate_deposit(pub fn deposit_event)]410 pub enum Event<T: Config> {411 /// New collection was created412 CollectionCreated(413 /// Globally unique identifier of newly created collection.414 CollectionId,415 /// [`CollectionMode`] converted into _u8_.416 u8,417 /// Collection owner.418 T::AccountId,419 ),420421 /// New collection was destroyed422 CollectionDestroyed(423 /// Globally unique identifier of collection.424 CollectionId,425 ),426427 /// New item was created.428 ItemCreated(429 /// Id of the collection where item was created.430 CollectionId,431 /// Id of an item. Unique within the collection.432 TokenId,433 /// Owner of newly created item434 T::CrossAccountId,435 /// Always 1 for NFT436 u128,437 ),438439 /// Collection item was burned.440 ItemDestroyed(441 /// Id of the collection where item was destroyed.442 CollectionId,443 /// Identifier of burned NFT.444 TokenId,445 /// Which user has destroyed its tokens.446 T::CrossAccountId,447 /// Amount of token pieces destroed. Always 1 for NFT.448 u128,449 ),450451 /// Item was transferred452 Transfer(453 /// Id of collection to which item is belong.454 CollectionId,455 /// Id of an item.456 TokenId,457 /// Original owner of item.458 T::CrossAccountId,459 /// New owner of item.460 T::CrossAccountId,461 /// Amount of token pieces transfered. Always 1 for NFT.462 u128,463 ),464465 /// Amount pieces of token owned by `sender` was approved for `spender`.466 Approved(467 /// Id of collection to which item is belong.468 CollectionId,469 /// Id of an item.470 TokenId,471 /// Original owner of item.472 T::CrossAccountId,473 /// Id for which the approval was granted.474 T::CrossAccountId,475 /// Amount of token pieces transfered. Always 1 for NFT.476 u128,477 ),478479 /// The colletion property has been added or edited.480 CollectionPropertySet(481 /// Id of collection to which property has been set.482 CollectionId,483 /// The property that was set.484 PropertyKey,485 ),486487 /// The property has been deleted.488 CollectionPropertyDeleted(489 /// Id of collection to which property has been deleted.490 CollectionId,491 /// The property that was deleted.492 PropertyKey,493 ),494495 /// The token property has been added or edited.496 TokenPropertySet(497 /// Identifier of the collection whose token has the property set.498 CollectionId,499 /// The token for which the property was set.500 TokenId,501 /// The property that was set.502 PropertyKey,503 ),504505 /// The token property has been deleted.506 TokenPropertyDeleted(507 /// Identifier of the collection whose token has the property deleted.508 CollectionId,509 /// The token for which the property was deleted.510 TokenId,511 /// The property that was deleted.512 PropertyKey,513 ),514515 /// The token property permission of a collection has been set.516 PropertyPermissionSet(517 /// ID of collection to which property permission has been set.518 CollectionId,519 /// The property permission that was set.520 PropertyKey,521 ),522 }523524 #[pallet::error]525 pub enum Error<T> {526 /// This collection does not exist.527 CollectionNotFound,528 /// Sender parameter and item owner must be equal.529 MustBeTokenOwner,530 /// No permission to perform action531 NoPermission,532 /// Destroying only empty collections is allowed533 CantDestroyNotEmptyCollection,534 /// Collection is not in mint mode.535 PublicMintingNotAllowed,536 /// Address is not in allow list.537 AddressNotInAllowlist,538539 /// Collection name can not be longer than 63 char.540 CollectionNameLimitExceeded,541 /// Collection description can not be longer than 255 char.542 CollectionDescriptionLimitExceeded,543 /// Token prefix can not be longer than 15 char.544 CollectionTokenPrefixLimitExceeded,545 /// Total collections bound exceeded.546 TotalCollectionsLimitExceeded,547 /// Exceeded max admin count548 CollectionAdminCountExceeded,549 /// Collection limit bounds per collection exceeded550 CollectionLimitBoundsExceeded,551 /// Tried to enable permissions which are only permitted to be disabled552 OwnerPermissionsCantBeReverted,553 /// Collection settings not allowing items transferring554 TransferNotAllowed,555 /// Account token limit exceeded per collection556 AccountTokenLimitExceeded,557 /// Collection token limit exceeded558 CollectionTokenLimitExceeded,559 /// Metadata flag frozen560 MetadataFlagFrozen,561562 /// Item does not exist563 TokenNotFound,564 /// Item is balance not enough565 TokenValueTooLow,566 /// Requested value is more than the approved567 ApprovedValueTooLow,568 /// Tried to approve more than owned569 CantApproveMoreThanOwned,570571 /// Can't transfer tokens to ethereum zero address572 AddressIsZero,573574 /// The operation is not supported575 UnsupportedOperation,576577 /// Insufficient funds to perform an action578 NotSufficientFounds,579580 /// User does not satisfy the nesting rule581 UserIsNotAllowedToNest,582 /// Only tokens from specific collections may nest tokens under this one583 SourceCollectionIsNotAllowedToNest,584585 /// Tried to store more data than allowed in collection field586 CollectionFieldSizeExceeded,587588 /// Tried to store more property data than allowed589 NoSpaceForProperty,590591 /// Tried to store more property keys than allowed592 PropertyLimitReached,593594 /// Property key is too long595 PropertyKeyIsTooLong,596597 /// Only ASCII letters, digits, and symbols `_`, `-`, and `.` are allowed598 InvalidCharacterInPropertyKey,599600 /// Empty property keys are forbidden601 EmptyPropertyKey,602603 /// Tried to access an external collection with an internal API604 CollectionIsExternal,605606 /// Tried to access an internal collection with an external API607 CollectionIsInternal,608 }609610 /// Storage of the count of created collections. Essentially contains the last collection ID.611 #[pallet::storage]612 pub type CreatedCollectionCount<T> = StorageValue<Value = CollectionId, QueryKind = ValueQuery>;613614 /// Storage of the count of deleted collections.615 #[pallet::storage]616 pub type DestroyedCollectionCount<T> =617 StorageValue<Value = CollectionId, QueryKind = ValueQuery>;618619 /// Storage of collection info.620 #[pallet::storage]621 pub type CollectionById<T> = StorageMap<622 Hasher = Blake2_128Concat,623 Key = CollectionId,624 Value = Collection<<T as frame_system::Config>::AccountId>,625 QueryKind = OptionQuery,626 >;627628 /// Storage of collection properties.629 #[pallet::storage]630 #[pallet::getter(fn collection_properties)]631 pub type CollectionProperties<T> = StorageMap<632 Hasher = Blake2_128Concat,633 Key = CollectionId,634 Value = Properties,635 QueryKind = ValueQuery,636 OnEmpty = up_data_structs::CollectionProperties,637 >;638639 /// Storage of token property permissions of a collection.640 #[pallet::storage]641 #[pallet::getter(fn property_permissions)]642 pub type CollectionPropertyPermissions<T> = StorageMap<643 Hasher = Blake2_128Concat,644 Key = CollectionId,645 Value = PropertiesPermissionMap,646 QueryKind = ValueQuery,647 >;648649 /// Storage of the amount of collection admins.650 #[pallet::storage]651 pub type AdminAmount<T> = StorageMap<652 Hasher = Blake2_128Concat,653 Key = CollectionId,654 Value = u32,655 QueryKind = ValueQuery,656 >;657658 /// List of collection admins.659 #[pallet::storage]660 pub type IsAdmin<T: Config> = StorageNMap<661 Key = (662 Key<Blake2_128Concat, CollectionId>,663 Key<Blake2_128Concat, T::CrossAccountId>,664 ),665 Value = bool,666 QueryKind = ValueQuery,667 >;668669 /// Allowlisted collection users.670 #[pallet::storage]671 pub type Allowlist<T: Config> = StorageNMap<672 Key = (673 Key<Blake2_128Concat, CollectionId>,674 Key<Blake2_128Concat, T::CrossAccountId>,675 ),676 Value = bool,677 QueryKind = ValueQuery,678 >;679680 /// Not used by code, exists only to provide some types to metadata.681 #[pallet::storage]682 pub type DummyStorageValue<T: Config> = StorageValue<683 Value = (684 CollectionStats,685 CollectionId,686 TokenId,687 TokenChild,688 PhantomType<(689 TokenData<T::CrossAccountId>,690 RpcCollection<T::AccountId>,691 // RMRK692 RmrkCollectionInfo<T::AccountId>,693 RmrkInstanceInfo<T::AccountId>,694 RmrkResourceInfo,695 RmrkPropertyInfo,696 RmrkBaseInfo<T::AccountId>,697 RmrkPartType,698 RmrkBoundedTheme,699 RmrkNftChild,700 )>,701 ),702 QueryKind = OptionQuery,703 >;704705 #[pallet::hooks]706 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {707 fn on_runtime_upgrade() -> Weight {708 StorageVersion::new(1).put::<Pallet<T>>();709710 Weight::zero()711 }712 }713}714715impl<T: Config> Pallet<T> {716 /// Enshure that receiver address is correct.717 ///718 /// Ethereum receiver 0x0000000000000000000000000000000000000000 is reserved, and shouldn't own tokens.719 pub fn ensure_correct_receiver(receiver: &T::CrossAccountId) -> DispatchResult {720 ensure!(721 &T::CrossAccountId::from_eth(H160([0; 20])) != receiver,722 <Error<T>>::AddressIsZero723 );724 Ok(())725 }726727 /// Get a vector of collection admins.728 pub fn adminlist(collection: CollectionId) -> Vec<T::CrossAccountId> {729 <IsAdmin<T>>::iter_prefix((collection,))730 .map(|(a, _)| a)731 .collect()732 }733734 /// Get a vector of users allowed to mint tokens.735 pub fn allowlist(collection: CollectionId) -> Vec<T::CrossAccountId> {736 <Allowlist<T>>::iter_prefix((collection,))737 .map(|(a, _)| a)738 .collect()739 }740741 /// Is `user` allowed to mint token in `collection`.742 pub fn allowed(collection: CollectionId, user: T::CrossAccountId) -> bool {743 <Allowlist<T>>::get((collection, user))744 }745746 /// Get statistics of collections.747 pub fn collection_stats() -> CollectionStats {748 let created = <CreatedCollectionCount<T>>::get();749 let destroyed = <DestroyedCollectionCount<T>>::get();750 CollectionStats {751 created: created.0,752 destroyed: destroyed.0,753 alive: created.0 - destroyed.0,754 }755 }756757 /// Get the effective limits for the collection.758 pub fn effective_collection_limits(collection: CollectionId) -> Option<CollectionLimits> {759 let collection = <CollectionById<T>>::get(collection)?;760 let limits = collection.limits;761 let effective_limits = CollectionLimits {762 account_token_ownership_limit: Some(limits.account_token_ownership_limit()),763 sponsored_data_size: Some(limits.sponsored_data_size()),764 sponsored_data_rate_limit: Some(765 limits766 .sponsored_data_rate_limit767 .unwrap_or(SponsoringRateLimit::SponsoringDisabled),768 ),769 token_limit: Some(limits.token_limit()),770 sponsor_transfer_timeout: Some(limits.sponsor_transfer_timeout(771 match collection.mode {772 CollectionMode::NFT => NFT_SPONSOR_TRANSFER_TIMEOUT,773 CollectionMode::Fungible(_) => FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,774 CollectionMode::ReFungible => REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,775 },776 )),777 sponsor_approve_timeout: Some(limits.sponsor_approve_timeout()),778 owner_can_transfer: Some(limits.owner_can_transfer()),779 owner_can_destroy: Some(limits.owner_can_destroy()),780 transfers_enabled: Some(limits.transfers_enabled()),781 };782783 Some(effective_limits)784 }785786 /// Returns information about the `collection` adapted for rpc.787 pub fn rpc_collection(collection: CollectionId) -> Option<RpcCollection<T::AccountId>> {788 let Collection {789 name,790 description,791 owner,792 mode,793 token_prefix,794 sponsorship,795 limits,796 permissions,797 flags,798 } = <CollectionById<T>>::get(collection)?;799800 let token_property_permissions = <CollectionPropertyPermissions<T>>::get(collection)801 .into_iter()802 .map(|(key, permission)| PropertyKeyPermission { key, permission })803 .collect();804805 let properties = <CollectionProperties<T>>::get(collection)806 .into_iter()807 .map(|(key, value)| Property { key, value })808 .collect();809810 let permissions = CollectionPermissions {811 access: Some(permissions.access()),812 mint_mode: Some(permissions.mint_mode()),813 nesting: Some(permissions.nesting().clone()),814 };815816 Some(RpcCollection {817 name: name.into_inner(),818 description: description.into_inner(),819 owner,820 mode,821 token_prefix: token_prefix.into_inner(),822 sponsorship,823 limits,824 permissions,825 token_property_permissions,826 properties,827 read_only: flags.external,828829 flags: RpcCollectionFlags {830 foreign: flags.foreign,831 erc721metadata: flags.erc721metadata,832 },833 })834 }835}836837macro_rules! limit_default {838 ($old:ident, $new:ident, $($field:ident $(($arg:expr))? => $check:expr),* $(,)?) => {{839 $(840 if let Some($new) = $new.$field {841 let $old = $old.$field($($arg)?);842 let _ = $new;843 let _ = $old;844 $check845 } else {846 $new.$field = $old.$field847 }848 )*849 }};850}851macro_rules! limit_default_clone {852 ($old:ident, $new:ident, $($field:ident $(($arg:expr))? => $check:expr),* $(,)?) => {{853 $(854 if let Some($new) = $new.$field.clone() {855 let $old = $old.$field($($arg)?);856 let _ = $new;857 let _ = $old;858 $check859 } else {860 $new.$field = $old.$field.clone()861 }862 )*863 }};864}865866impl<T: Config> Pallet<T> {867 /// Create new collection.868 ///869 /// * `owner` - The owner of the collection.870 /// * `data` - Description of the created collection.871 /// * `flags` - Extra flags to store.872 pub fn init_collection(873 owner: T::CrossAccountId,874 payer: T::CrossAccountId,875 data: CreateCollectionData<T::AccountId>,876 flags: CollectionFlags,877 ) -> Result<CollectionId, DispatchError> {878 {879 ensure!(880 data.token_prefix.len() <= MAX_TOKEN_PREFIX_LENGTH as usize,881 Error::<T>::CollectionTokenPrefixLimitExceeded882 );883 }884885 let created_count = <CreatedCollectionCount<T>>::get()886 .0887 .checked_add(1)888 .ok_or(ArithmeticError::Overflow)?;889 let destroyed_count = <DestroyedCollectionCount<T>>::get().0;890 let id = CollectionId(created_count);891892 // bound Total number of collections893 ensure!(894 created_count - destroyed_count <= COLLECTION_NUMBER_LIMIT,895 <Error<T>>::TotalCollectionsLimitExceeded896 );897898 // =========899900 let collection = Collection {901 owner: owner.as_sub().clone(),902 name: data.name,903 mode: data.mode.clone(),904 description: data.description,905 token_prefix: data.token_prefix,906 sponsorship: data907 .pending_sponsor908 .map(SponsorshipState::Unconfirmed)909 .unwrap_or_default(),910 limits: data911 .limits912 .map(|limits| Self::clamp_limits(data.mode.clone(), &Default::default(), limits))913 .unwrap_or_else(|| Ok(CollectionLimits::default()))?,914 permissions: data915 .permissions916 .map(|permissions| {917 Self::clamp_permissions(data.mode.clone(), &Default::default(), permissions)918 })919 .unwrap_or_else(|| Ok(CollectionPermissions::default()))?,920 flags,921 };922923 let mut collection_properties = up_data_structs::CollectionProperties::get();924 collection_properties925 .try_set_from_iter(data.properties.into_iter())926 .map_err(<Error<T>>::from)?;927928 CollectionProperties::<T>::insert(id, collection_properties);929930 let mut token_props_permissions = PropertiesPermissionMap::new();931 token_props_permissions932 .try_set_from_iter(data.token_property_permissions.into_iter())933 .map_err(<Error<T>>::from)?;934935 CollectionPropertyPermissions::<T>::insert(id, token_props_permissions);936937 // Take a (non-refundable) deposit of collection creation938 {939 let mut imbalance =940 <<<T as Config>::Currency as Currency<T::AccountId>>::PositiveImbalance>::zero();941 imbalance.subsume(942 <<T as Config>::Currency as Currency<T::AccountId>>::deposit_creating(943 &T::TreasuryAccountId::get(),944 T::CollectionCreationPrice::get(),945 ),946 );947 <T as Config>::Currency::settle(948 payer.as_sub(),949 imbalance,950 WithdrawReasons::TRANSFER,951 ExistenceRequirement::KeepAlive,952 )953 .map_err(|_| Error::<T>::NotSufficientFounds)?;954 }955956 <CreatedCollectionCount<T>>::put(created_count);957 <Pallet<T>>::deposit_event(Event::CollectionCreated(958 id,959 data.mode.id(),960 owner.as_sub().clone(),961 ));962 <PalletEvm<T>>::deposit_log(963 erc::CollectionHelpersEvents::CollectionCreated {964 owner: *owner.as_eth(),965 collection_id: eth::collection_id_to_address(id),966 }967 .to_log(T::ContractAddress::get()),968 );969 <CollectionById<T>>::insert(id, collection);970 Ok(id)971 }972973 /// Destroy collection.974 ///975 /// * `collection` - Collection handler.976 /// * `sender` - The owner or administrator of the collection.977 pub fn destroy_collection(978 collection: CollectionHandle<T>,979 sender: &T::CrossAccountId,980 ) -> DispatchResult {981 ensure!(982 collection.limits.owner_can_destroy(),983 <Error<T>>::NoPermission,984 );985 collection.check_is_owner(sender)?;986987 let destroyed_collections = <DestroyedCollectionCount<T>>::get()988 .0989 .checked_add(1)990 .ok_or(ArithmeticError::Overflow)?;991992 // =========993994 <DestroyedCollectionCount<T>>::put(destroyed_collections);995 <CollectionById<T>>::remove(collection.id);996 <AdminAmount<T>>::remove(collection.id);997 let _ = <IsAdmin<T>>::clear_prefix((collection.id,), u32::MAX, None);998 let _ = <Allowlist<T>>::clear_prefix((collection.id,), u32::MAX, None);999 <CollectionProperties<T>>::remove(collection.id);10001001 <Pallet<T>>::deposit_event(Event::CollectionDestroyed(collection.id));1002 Ok(())1003 }10041005 /// Set collection property.1006 ///1007 /// * `collection` - Collection handler.1008 /// * `sender` - The owner or administrator of the collection.1009 /// * `property` - The property to set.1010 pub fn set_collection_property(1011 collection: &CollectionHandle<T>,1012 sender: &T::CrossAccountId,1013 property: Property,1014 ) -> DispatchResult {1015 collection.check_is_owner_or_admin(sender)?;10161017 CollectionProperties::<T>::try_mutate(collection.id, |properties| {1018 let property = property.clone();1019 properties.try_set(property.key, property.value)1020 })1021 .map_err(<Error<T>>::from)?;10221023 Self::deposit_event(Event::CollectionPropertySet(collection.id, property.key));10241025 Ok(())1026 }10271028 /// Set scouped collection property.1029 ///1030 /// * `collection_id` - ID of the collection for which the property is being set.1031 /// * `scope` - Property scope.1032 /// * `property` - The property to set.1033 pub fn set_scoped_collection_property(1034 collection_id: CollectionId,1035 scope: PropertyScope,1036 property: Property,1037 ) -> DispatchResult {1038 CollectionProperties::<T>::try_mutate(collection_id, |properties| {1039 properties.try_scoped_set(scope, property.key, property.value)1040 })1041 .map_err(<Error<T>>::from)?;10421043 Ok(())1044 }10451046 /// Set scouped collection properties.1047 ///1048 /// * `collection_id` - ID of the collection for which the properties is being set.1049 /// * `scope` - Property scope.1050 /// * `properties` - The properties to set.1051 pub fn set_scoped_collection_properties(1052 collection_id: CollectionId,1053 scope: PropertyScope,1054 properties: impl Iterator<Item = Property>,1055 ) -> DispatchResult {1056 CollectionProperties::<T>::try_mutate(collection_id, |stored_properties| {1057 stored_properties.try_scoped_set_from_iter(scope, properties)1058 })1059 .map_err(<Error<T>>::from)?;10601061 Ok(())1062 }10631064 /// Set collection properties.1065 ///1066 /// * `collection` - Collection handler.1067 /// * `sender` - The owner or administrator of the collection.1068 /// * `properties` - The properties to set.1069 #[transactional]1070 pub fn set_collection_properties(1071 collection: &CollectionHandle<T>,1072 sender: &T::CrossAccountId,1073 properties: Vec<Property>,1074 ) -> DispatchResult {1075 for property in properties {1076 Self::set_collection_property(collection, sender, property)?;1077 }10781079 Ok(())1080 }10811082 /// Delete collection property.1083 ///1084 /// * `collection` - Collection handler.1085 /// * `sender` - The owner or administrator of the collection.1086 /// * `property` - The property to delete.1087 pub fn delete_collection_property(1088 collection: &CollectionHandle<T>,1089 sender: &T::CrossAccountId,1090 property_key: PropertyKey,1091 ) -> DispatchResult {1092 collection.check_is_owner_or_admin(sender)?;10931094 CollectionProperties::<T>::try_mutate(collection.id, |properties| {1095 properties.remove(&property_key)1096 })1097 .map_err(<Error<T>>::from)?;10981099 Self::deposit_event(Event::CollectionPropertyDeleted(1100 collection.id,1101 property_key,1102 ));11031104 Ok(())1105 }11061107 /// Delete collection properties.1108 ///1109 /// * `collection` - Collection handler.1110 /// * `sender` - The owner or administrator of the collection.1111 /// * `properties` - The properties to delete.1112 #[transactional]1113 pub fn delete_collection_properties(1114 collection: &CollectionHandle<T>,1115 sender: &T::CrossAccountId,1116 property_keys: Vec<PropertyKey>,1117 ) -> DispatchResult {1118 for key in property_keys {1119 Self::delete_collection_property(collection, sender, key)?;1120 }11211122 Ok(())1123 }11241125 /// Set collection propetry permission without any checks.1126 ///1127 /// Used for migrations.1128 ///1129 /// * `collection` - Collection handler.1130 /// * `property_permissions` - Property permissions.1131 pub fn set_property_permission_unchecked(1132 collection: CollectionId,1133 property_permission: PropertyKeyPermission,1134 ) -> DispatchResult {1135 <CollectionPropertyPermissions<T>>::try_mutate(collection, |permissions| {1136 permissions.try_set(property_permission.key, property_permission.permission)1137 })1138 .map_err(<Error<T>>::from)?;1139 Ok(())1140 }11411142 /// Set collection property permission.1143 ///1144 /// * `collection` - Collection handler.1145 /// * `sender` - The owner or administrator of the collection.1146 /// * `property_permission` - Property permission.1147 pub fn set_property_permission(1148 collection: &CollectionHandle<T>,1149 sender: &T::CrossAccountId,1150 property_permission: PropertyKeyPermission,1151 ) -> DispatchResult {1152 Self::set_scoped_property_permission(1153 collection,1154 sender,1155 PropertyScope::None,1156 property_permission,1157 )1158 }11591160 /// Set collection property permission with scope.1161 ///1162 /// * `collection` - Collection handler.1163 /// * `sender` - The owner or administrator of the collection.1164 /// * `scope` - Property scope.1165 /// * `property_permission` - Property permission.1166 pub fn set_scoped_property_permission(1167 collection: &CollectionHandle<T>,1168 sender: &T::CrossAccountId,1169 scope: PropertyScope,1170 property_permission: PropertyKeyPermission,1171 ) -> DispatchResult {1172 collection.check_is_owner_or_admin(sender)?;11731174 let all_permissions = CollectionPropertyPermissions::<T>::get(collection.id);1175 let current_permission = all_permissions.get(&property_permission.key);1176 if matches![1177 current_permission,1178 Some(PropertyPermission { mutable: false, .. })1179 ] {1180 return Err(<Error<T>>::NoPermission.into());1181 }11821183 CollectionPropertyPermissions::<T>::try_mutate(collection.id, |permissions| {1184 let property_permission = property_permission.clone();1185 permissions.try_scoped_set(1186 scope,1187 property_permission.key,1188 property_permission.permission,1189 )1190 })1191 .map_err(<Error<T>>::from)?;11921193 Self::deposit_event(Event::PropertyPermissionSet(1194 collection.id,1195 property_permission.key,1196 ));11971198 Ok(())1199 }12001201 /// Set token property permission.1202 ///1203 /// * `collection` - Collection handler.1204 /// * `sender` - The owner or administrator of the collection.1205 /// * `property_permissions` - Property permissions.1206 #[transactional]1207 pub fn set_token_property_permissions(1208 collection: &CollectionHandle<T>,1209 sender: &T::CrossAccountId,1210 property_permissions: Vec<PropertyKeyPermission>,1211 ) -> DispatchResult {1212 Self::set_scoped_token_property_permissions(1213 collection,1214 sender,1215 PropertyScope::None,1216 property_permissions,1217 )1218 }12191220 /// Set token property permission with scope.1221 ///1222 /// * `collection` - Collection handler.1223 /// * `sender` - The owner or administrator of the collection.1224 /// * `scope` - Property scope.1225 /// * `property_permissions` - Property permissions.1226 #[transactional]1227 pub fn set_scoped_token_property_permissions(1228 collection: &CollectionHandle<T>,1229 sender: &T::CrossAccountId,1230 scope: PropertyScope,1231 property_permissions: Vec<PropertyKeyPermission>,1232 ) -> DispatchResult {1233 for prop_pemission in property_permissions {1234 Self::set_scoped_property_permission(collection, sender, scope, prop_pemission)?;1235 }12361237 Ok(())1238 }12391240 /// Get collection property.1241 pub fn get_collection_property(1242 collection_id: CollectionId,1243 key: &PropertyKey,1244 ) -> Option<PropertyValue> {1245 Self::collection_properties(collection_id).get(key).cloned()1246 }12471248 /// Convert byte vector to property key vector.1249 pub fn bytes_keys_to_property_keys(1250 keys: Vec<Vec<u8>>,1251 ) -> Result<Vec<PropertyKey>, DispatchError> {1252 keys.into_iter()1253 .map(|key| -> Result<PropertyKey, DispatchError> {1254 key.try_into()1255 .map_err(|_| <Error<T>>::PropertyKeyIsTooLong.into())1256 })1257 .collect::<Result<Vec<PropertyKey>, DispatchError>>()1258 }12591260 /// Get properties according to given keys.1261 pub fn filter_collection_properties(1262 collection_id: CollectionId,1263 keys: Option<Vec<PropertyKey>>,1264 ) -> Result<Vec<Property>, DispatchError> {1265 let properties = Self::collection_properties(collection_id);12661267 let properties = keys1268 .map(|keys| {1269 keys.into_iter()1270 .filter_map(|key| {1271 properties.get(&key).map(|value| Property {1272 key,1273 value: value.clone(),1274 })1275 })1276 .collect()1277 })1278 .unwrap_or_else(|| {1279 properties1280 .into_iter()1281 .map(|(key, value)| Property { key, value })1282 .collect()1283 });12841285 Ok(properties)1286 }12871288 /// Get property permissions according to given keys.1289 pub fn filter_property_permissions(1290 collection_id: CollectionId,1291 keys: Option<Vec<PropertyKey>>,1292 ) -> Result<Vec<PropertyKeyPermission>, DispatchError> {1293 let permissions = Self::property_permissions(collection_id);12941295 let key_permissions = keys1296 .map(|keys| {1297 keys.into_iter()1298 .filter_map(|key| {1299 permissions1300 .get(&key)1301 .map(|permission| PropertyKeyPermission {1302 key,1303 permission: permission.clone(),1304 })1305 })1306 .collect()1307 })1308 .unwrap_or_else(|| {1309 permissions1310 .into_iter()1311 .map(|(key, permission)| PropertyKeyPermission { key, permission })1312 .collect()1313 });13141315 Ok(key_permissions)1316 }13171318 /// Toggle `user` participation in the `collection`'s allow list.1319 /// #### Store read/writes1320 /// 1 writes1321 pub fn toggle_allowlist(1322 collection: &CollectionHandle<T>,1323 sender: &T::CrossAccountId,1324 user: &T::CrossAccountId,1325 allowed: bool,1326 ) -> DispatchResult {1327 collection.check_is_owner_or_admin(sender)?;13281329 // =========13301331 if allowed {1332 <Allowlist<T>>::insert((collection.id, user), true);1333 } else {1334 <Allowlist<T>>::remove((collection.id, user));1335 }13361337 Ok(())1338 }13391340 /// Toggle `user` participation in the `collection`'s admin list.1341 /// #### Store read/writes1342 /// 2 writes1343 pub fn toggle_admin(1344 collection: &CollectionHandle<T>,1345 sender: &T::CrossAccountId,1346 user: &T::CrossAccountId,1347 admin: bool,1348 ) -> DispatchResult {1349 collection.check_is_owner(sender)?;13501351 let was_admin = <IsAdmin<T>>::get((collection.id, user));1352 if was_admin == admin {1353 return Ok(());1354 }1355 let amount = <AdminAmount<T>>::get(collection.id);13561357 if admin {1358 let amount = amount1359 .checked_add(1)1360 .ok_or(<Error<T>>::CollectionAdminCountExceeded)?;1361 ensure!(1362 amount <= Self::collection_admins_limit(),1363 <Error<T>>::CollectionAdminCountExceeded,1364 );13651366 // =========13671368 <AdminAmount<T>>::insert(collection.id, amount);1369 <IsAdmin<T>>::insert((collection.id, user), true);1370 } else {1371 <AdminAmount<T>>::insert(collection.id, amount.saturating_sub(1));1372 <IsAdmin<T>>::remove((collection.id, user));1373 }13741375 Ok(())1376 }13771378 /// Merge set fields from `new_limit` to `old_limit`.1379 pub fn clamp_limits(1380 mode: CollectionMode,1381 old_limit: &CollectionLimits,1382 mut new_limit: CollectionLimits,1383 ) -> Result<CollectionLimits, DispatchError> {1384 let limits = old_limit;1385 limit_default!(old_limit, new_limit,1386 account_token_ownership_limit => ensure!(1387 new_limit <= MAX_TOKEN_OWNERSHIP,1388 <Error<T>>::CollectionLimitBoundsExceeded,1389 ),1390 sponsored_data_size => ensure!(1391 new_limit <= CUSTOM_DATA_LIMIT,1392 <Error<T>>::CollectionLimitBoundsExceeded,1393 ),13941395 sponsored_data_rate_limit => {},1396 token_limit => ensure!(1397 old_limit >= new_limit && new_limit > 0,1398 <Error<T>>::CollectionTokenLimitExceeded1399 ),14001401 sponsor_transfer_timeout(match mode {1402 CollectionMode::NFT => NFT_SPONSOR_TRANSFER_TIMEOUT,1403 CollectionMode::Fungible(_) => FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,1404 CollectionMode::ReFungible => REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,1405 }) => ensure!(1406 new_limit <= MAX_SPONSOR_TIMEOUT,1407 <Error<T>>::CollectionLimitBoundsExceeded,1408 ),1409 sponsor_approve_timeout => {},1410 owner_can_transfer => ensure!(1411 !limits.owner_can_transfer_instaled() ||1412 old_limit || !new_limit,1413 <Error<T>>::OwnerPermissionsCantBeReverted,1414 ),1415 owner_can_destroy => ensure!(1416 old_limit || !new_limit,1417 <Error<T>>::OwnerPermissionsCantBeReverted,1418 ),1419 transfers_enabled => {},1420 );1421 Ok(new_limit)1422 }14231424 /// Merge set fields from `new_permission` to `old_permission`.1425 pub fn clamp_permissions(1426 _mode: CollectionMode,1427 old_permission: &CollectionPermissions,1428 mut new_permission: CollectionPermissions,1429 ) -> Result<CollectionPermissions, DispatchError> {1430 limit_default_clone!(old_permission, new_permission,1431 access => {},1432 mint_mode => {},1433 nesting => { /* todo check for permissive, if only it gets out of benchmarks */ },1434 );1435 Ok(new_permission)1436 }1437}14381439/// Indicates unsupported methods by returning [Error::UnsupportedOperation].1440#[macro_export]1441macro_rules! unsupported {1442 ($runtime:path) => {1443 Err($crate::Error::<$runtime>::UnsupportedOperation.into())1444 };1445}14461447/// Return weights for various worst-case operations.1448pub trait CommonWeightInfo<CrossAccountId> {1449 /// Weight of item creation.1450 fn create_item() -> Weight;14511452 /// Weight of items creation.1453 fn create_multiple_items(amount: &[CreateItemData]) -> Weight;14541455 /// Weight of items creation.1456 fn create_multiple_items_ex(cost: &CreateItemExData<CrossAccountId>) -> Weight;14571458 /// The weight of the burning item.1459 fn burn_item() -> Weight;14601461 /// Property setting weight.1462 ///1463 /// * `amount`- The number of properties to set.1464 fn set_collection_properties(amount: u32) -> Weight;14651466 /// Collection property deletion weight.1467 ///1468 /// * `amount`- The number of properties to set.1469 fn delete_collection_properties(amount: u32) -> Weight;14701471 /// Token property setting weight.1472 ///1473 /// * `amount`- The number of properties to set.1474 fn set_token_properties(amount: u32) -> Weight;14751476 /// Token property deletion weight.1477 ///1478 /// * `amount`- The number of properties to delete.1479 fn delete_token_properties(amount: u32) -> Weight;14801481 /// Token property permissions set weight.1482 ///1483 /// * `amount`- The number of property permissions to set.1484 fn set_token_property_permissions(amount: u32) -> Weight;14851486 /// Transfer price of the token or its parts.1487 fn transfer() -> Weight;14881489 /// The price of setting the permission of the operation from another user.1490 fn approve() -> Weight;14911492 /// Transfer price from another user.1493 fn transfer_from() -> Weight;14941495 /// The price of burning a token from another user.1496 fn burn_from() -> Weight;14971498 /// Differs from burn_item in case of Fungible and Refungible, as it should burn1499 /// whole users's balance.1500 ///1501 /// This method shouldn't be used directly, as it doesn't count breadth price, use [burn_recursively](CommonWeightInfo::burn_recursively) instead1502 fn burn_recursively_self_raw() -> Weight;15031504 /// Cost of iterating over `amount` children while burning, without counting child burning itself.1505 ///1506 /// This method shouldn't be used directly, as it doesn't count depth price, use [burn_recursively](CommonWeightInfo::burn_recursively) instead1507 fn burn_recursively_breadth_raw(amount: u32) -> Weight;15081509 /// The price of recursive burning a token.1510 ///1511 /// `max_selfs` - The maximum burning weight of the token itself.1512 /// `max_breadth` - The maximum number of nested tokens to burn.1513 fn burn_recursively(max_selfs: u32, max_breadth: u32) -> Weight {1514 Self::burn_recursively_self_raw()1515 .saturating_mul(max_selfs.max(1) as u64)1516 .saturating_add(Self::burn_recursively_breadth_raw(max_breadth))1517 }15181519 /// The price of retrieving token owner1520 fn token_owner() -> Weight;1521}15221523/// Weight info extension trait for refungible pallet.1524pub trait RefungibleExtensionsWeightInfo {1525 /// Weight of token repartition.1526 fn repartition() -> Weight;1527}15281529/// Common collection operations.1530///1531/// It wraps methods in Fungible, Nonfungible and Refungible pallets1532/// and adds weight info.1533pub trait CommonCollectionOperations<T: Config> {1534 /// Create token.1535 ///1536 /// * `sender` - The user who mint the token and pays for the transaction.1537 /// * `to` - The user who will own the token.1538 /// * `data` - Token data.1539 /// * `nesting_budget` - A budget that can be spent on nesting tokens.1540 fn create_item(1541 &self,1542 sender: T::CrossAccountId,1543 to: T::CrossAccountId,1544 data: CreateItemData,1545 nesting_budget: &dyn Budget,1546 ) -> DispatchResultWithPostInfo;15471548 /// Create multiple tokens.1549 ///1550 /// * `sender` - The user who mint the token and pays for the transaction.1551 /// * `to` - The user who will own the token.1552 /// * `data` - Token data.1553 /// * `nesting_budget` - A budget that can be spent on nesting tokens.1554 fn create_multiple_items(1555 &self,1556 sender: T::CrossAccountId,1557 to: T::CrossAccountId,1558 data: Vec<CreateItemData>,1559 nesting_budget: &dyn Budget,1560 ) -> DispatchResultWithPostInfo;15611562 /// Create multiple tokens.1563 ///1564 /// * `sender` - The user who mint the token and pays for the transaction.1565 /// * `to` - The user who will own the token.1566 /// * `data` - Token data.1567 /// * `nesting_budget` - A budget that can be spent on nesting tokens.1568 fn create_multiple_items_ex(1569 &self,1570 sender: T::CrossAccountId,1571 data: CreateItemExData<T::CrossAccountId>,1572 nesting_budget: &dyn Budget,1573 ) -> DispatchResultWithPostInfo;15741575 /// Burn token.1576 ///1577 /// * `sender` - The user who owns the token.1578 /// * `token` - Token id that will burned.1579 /// * `amount` - The number of parts of the token that will be burned.1580 fn burn_item(1581 &self,1582 sender: T::CrossAccountId,1583 token: TokenId,1584 amount: u128,1585 ) -> DispatchResultWithPostInfo;15861587 /// Burn token and all nested tokens recursievly.1588 ///1589 /// * `sender` - The user who owns the token.1590 /// * `token` - Token id that will burned.1591 /// * `self_budget` - The budget that can be spent on burning tokens.1592 /// * `breadth_budget` - The budget that can be spent on burning nested tokens.1593 fn burn_item_recursively(1594 &self,1595 sender: T::CrossAccountId,1596 token: TokenId,1597 self_budget: &dyn Budget,1598 breadth_budget: &dyn Budget,1599 ) -> DispatchResultWithPostInfo;16001601 /// Set collection properties.1602 ///1603 /// * `sender` - Must be either the owner of the collection or its admin.1604 /// * `properties` - Properties to be set.1605 fn set_collection_properties(1606 &self,1607 sender: T::CrossAccountId,1608 properties: Vec<Property>,1609 ) -> DispatchResultWithPostInfo;16101611 /// Delete collection properties.1612 ///1613 /// * `sender` - Must be either the owner of the collection or its admin.1614 /// * `properties` - The properties to be removed.1615 fn delete_collection_properties(1616 &self,1617 sender: &T::CrossAccountId,1618 property_keys: Vec<PropertyKey>,1619 ) -> DispatchResultWithPostInfo;16201621 /// Set token properties.1622 ///1623 /// The appropriate [`PropertyPermission`] for the token property1624 /// must be set with [`Self::set_token_property_permissions`].1625 ///1626 /// * `sender` - Must be either the owner of the token or its admin.1627 /// * `token_id` - The token for which the properties are being set.1628 /// * `properties` - Properties to be set.1629 /// * `budget` - Budget for setting properties.1630 fn set_token_properties(1631 &self,1632 sender: T::CrossAccountId,1633 token_id: TokenId,1634 properties: Vec<Property>,1635 budget: &dyn Budget,1636 ) -> DispatchResultWithPostInfo;16371638 /// Remove token properties.1639 ///1640 /// The appropriate [`PropertyPermission`] for the token property1641 /// must be set with [`Self::set_token_property_permissions`].1642 ///1643 /// * `sender` - Must be either the owner of the token or its admin.1644 /// * `token_id` - The token for which the properties are being remove.1645 /// * `property_keys` - Keys to remove corresponding properties.1646 /// * `budget` - Budget for removing properties.1647 fn delete_token_properties(1648 &self,1649 sender: T::CrossAccountId,1650 token_id: TokenId,1651 property_keys: Vec<PropertyKey>,1652 budget: &dyn Budget,1653 ) -> DispatchResultWithPostInfo;16541655 /// Set token property permissions.1656 ///1657 /// * `sender` - Must be either the owner of the token or its admin.1658 /// * `token_id` - The token for which the properties are being set.1659 /// * `property_permissions` - Property permissions to be set.1660 /// * `budget` - Budget for setting properties.1661 fn set_token_property_permissions(1662 &self,1663 sender: &T::CrossAccountId,1664 property_permissions: Vec<PropertyKeyPermission>,1665 ) -> DispatchResultWithPostInfo;16661667 /// Transfer amount of token pieces.1668 ///1669 /// * `sender` - Donor user.1670 /// * `to` - Recepient user.1671 /// * `token` - The token of which parts are being sent.1672 /// * `amount` - The number of parts of the token that will be transferred.1673 /// * `budget` - The maximum budget that can be spent on the transfer.1674 fn transfer(1675 &self,1676 sender: T::CrossAccountId,1677 to: T::CrossAccountId,1678 token: TokenId,1679 amount: u128,1680 budget: &dyn Budget,1681 ) -> DispatchResultWithPostInfo;16821683 /// Grant access to another account to transfer parts of the token owned by the calling user via [Self::transfer_from].1684 ///1685 /// * `sender` - The user who grants access to the token.1686 /// * `spender` - The user to whom the rights are granted.1687 /// * `token` - The token to which access is granted.1688 /// * `amount` - The amount of pieces that another user can dispose of.1689 fn approve(1690 &self,1691 sender: T::CrossAccountId,1692 spender: T::CrossAccountId,1693 token: TokenId,1694 amount: u128,1695 ) -> DispatchResultWithPostInfo;16961697 /// Send parts of a token owned by another user.1698 ///1699 /// Before calling this method, you must grant rights to the calling user via [`Self::approve`].1700 ///1701 /// * `sender` - The user who must have access to the token (see [`Self::approve`]).1702 /// * `from` - The user who owns the token.1703 /// * `to` - Recepient user.1704 /// * `token` - The token of which parts are being sent.1705 /// * `amount` - The number of parts of the token that will be transferred.1706 /// * `budget` - The maximum budget that can be spent on the transfer.1707 fn transfer_from(1708 &self,1709 sender: T::CrossAccountId,1710 from: T::CrossAccountId,1711 to: T::CrossAccountId,1712 token: TokenId,1713 amount: u128,1714 budget: &dyn Budget,1715 ) -> DispatchResultWithPostInfo;17161717 /// Burn parts of a token owned by another user.1718 ///1719 /// Before calling this method, you must grant rights to the calling user via [`Self::approve`].1720 ///1721 /// * `sender` - The user who must have access to the token (see [`Self::approve`]).1722 /// * `from` - The user who owns the token.1723 /// * `token` - The token of which parts are being sent.1724 /// * `amount` - The number of parts of the token that will be transferred.1725 /// * `budget` - The maximum budget that can be spent on the burn.1726 fn burn_from(1727 &self,1728 sender: T::CrossAccountId,1729 from: T::CrossAccountId,1730 token: TokenId,1731 amount: u128,1732 budget: &dyn Budget,1733 ) -> DispatchResultWithPostInfo;17341735 /// Check permission to nest token.1736 ///1737 /// * `sender` - The user who initiated the check.1738 /// * `from` - The token that is checked for embedding.1739 /// * `under` - Token under which to check.1740 /// * `budget` - The maximum budget that can be spent on the check.1741 fn check_nesting(1742 &self,1743 sender: T::CrossAccountId,1744 from: (CollectionId, TokenId),1745 under: TokenId,1746 budget: &dyn Budget,1747 ) -> DispatchResult;17481749 /// Nest one token into another.1750 ///1751 /// * `under` - Token holder.1752 /// * `to_nest` - Nested token.1753 fn nest(&self, under: TokenId, to_nest: (CollectionId, TokenId));17541755 /// Unnest token.1756 ///1757 /// * `under` - Token holder.1758 /// * `to_nest` - Token to unnest.1759 fn unnest(&self, under: TokenId, to_nest: (CollectionId, TokenId));17601761 /// Get all user tokens.1762 ///1763 /// * `account` - Account for which you need to get tokens.1764 fn account_tokens(&self, account: T::CrossAccountId) -> Vec<TokenId>;17651766 /// Get all the tokens in the collection.1767 fn collection_tokens(&self) -> Vec<TokenId>;17681769 /// Check if the token exists.1770 ///1771 /// * `token` - Id token to check.1772 fn token_exists(&self, token: TokenId) -> bool;17731774 /// Get the id of the last minted token.1775 fn last_token_id(&self) -> TokenId;17761777 /// Get the owner of the token.1778 ///1779 /// * `token` - The token for which you need to find out the owner.1780 fn token_owner(&self, token: TokenId) -> Option<T::CrossAccountId>;17811782 /// Returns 10 tokens owners in no particular order.1783 ///1784 /// * `token` - The token for which you need to find out the owners.1785 fn token_owners(&self, token: TokenId) -> Vec<T::CrossAccountId>;17861787 /// Get the value of the token property by key.1788 ///1789 /// * `token` - Token with the property to get.1790 /// * `key` - Property name.1791 fn token_property(&self, token_id: TokenId, key: &PropertyKey) -> Option<PropertyValue>;17921793 /// Get a set of token properties by key vector.1794 ///1795 /// * `token` - Token with the property to get.1796 /// * `keys` - Vector of property keys. If this parameter is [None](sp_std::result::Result),1797 /// then all properties are returned.1798 fn token_properties(&self, token: TokenId, keys: Option<Vec<PropertyKey>>) -> Vec<Property>;17991800 /// Amount of unique collection tokens1801 fn total_supply(&self) -> u32;18021803 /// Amount of different tokens account has.1804 ///1805 /// * `account` - The account for which need to get the balance.1806 fn account_balance(&self, account: T::CrossAccountId) -> u32;18071808 /// Amount of specific token account have.1809 fn balance(&self, account: T::CrossAccountId, token: TokenId) -> u128;18101811 /// Amount of token pieces1812 fn total_pieces(&self, token: TokenId) -> Option<u128>;18131814 /// Get the number of parts of the token that a trusted user can manage.1815 ///1816 /// * `sender` - Trusted user.1817 /// * `spender` - Owner of the token.1818 /// * `token` - The token for which to get the value.1819 fn allowance(1820 &self,1821 sender: T::CrossAccountId,1822 spender: T::CrossAccountId,1823 token: TokenId,1824 ) -> u128;18251826 /// Get extension for RFT collection.1827 fn refungible_extensions(&self) -> Option<&dyn RefungibleExtensions<T>>;1828}18291830/// Extension for RFT collection.1831pub trait RefungibleExtensions<T>1832where1833 T: Config,1834{1835 /// Change the number of parts of the token.1836 ///1837 /// When the value changes down, this function is equivalent to burning parts of the token.1838 ///1839 /// * `sender` - The user calling the repartition operation. Must be the owner of the token.1840 /// * `token` - The token for which you want to change the number of parts.1841 /// * `amount` - The new value of the parts of the token.1842 fn repartition(1843 &self,1844 sender: &T::CrossAccountId,1845 token: TokenId,1846 amount: u128,1847 ) -> DispatchResultWithPostInfo;1848}18491850/// Merge [`DispatchResult`] with [`Weight`] into [`DispatchResultWithPostInfo`].1851///1852/// Used for [`CommonCollectionOperations`] implementations and flexible enough to do so.1853pub fn with_weight(res: DispatchResult, weight: Weight) -> DispatchResultWithPostInfo {1854 let post_info = PostDispatchInfo {1855 actual_weight: Some(weight),1856 pays_fee: Pays::Yes,1857 };1858 match res {1859 Ok(()) => Ok(post_info),1860 Err(error) => Err(DispatchErrorWithPostInfo { post_info, error }),1861 }1862}18631864impl<T: Config> From<PropertiesError> for Error<T> {1865 fn from(error: PropertiesError) -> Self {1866 match error {1867 PropertiesError::NoSpaceForProperty => Self::NoSpaceForProperty,1868 PropertiesError::PropertyLimitReached => Self::PropertyLimitReached,1869 PropertiesError::InvalidCharacterInPropertyKey => Self::InvalidCharacterInPropertyKey,1870 PropertiesError::PropertyKeyIsTooLong => Self::PropertyKeyIsTooLong,1871 PropertiesError::EmptyPropertyKey => Self::EmptyPropertyKey,1872 }1873 }1874}1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617//! # Common pallet18//!19//! The Common pallet provides functionality for handling collections.20//!21//! ## Overview22//!23//! The Common pallet provides an interface for common collection operations for different collection types24//! (see [CommonCollectionOperations]), as well as a generic dispatcher for these, see [dispatch] module.25//! It also provides this functionality to EVM, see [erc] and [eth] modules.26//!27//! The Common pallet provides functions for:28//!29//! - Setting and approving collection sponsor.30//! - Get\set\delete allow list.31//! - Get\set\delete collection properties.32//! - Get\set\delete collection property permissions.33//! - Get\set\delete token property permissions.34//! - Get\set\delete collection administrators.35//! - Checking access permissions.36//!37//! ### Terminology38//! **Collection sponsor** - For the collection, you can set a sponsor, at whose expense it will39//! be possible to mint tokens.40//!41//! **Allow list** - List of users who have the right to minting tokens.42//!43//! **Collection properties** - Collection properties are simply key-value stores where various44//! metadata can be placed.45//!46//! **Permissions on token properties** - For each property in the token can be set permission47//! to change, see [`PropertyPermission`].48//!49//! **Collection administrator** - For a collection, you can set administrators who have the right50//! to most actions on the collection.5152#![warn(missing_docs)]53#![cfg_attr(not(feature = "std"), no_std)]54extern crate alloc;5556use core::ops::{Deref, DerefMut};57use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};58use sp_std::vec::Vec;59use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};60use evm_coder::ToLog;61use frame_support::{62 dispatch::{DispatchErrorWithPostInfo, DispatchResultWithPostInfo, Weight, PostDispatchInfo},63 ensure,64 traits::{Imbalance, Get, Currency, WithdrawReasons, ExistenceRequirement},65 dispatch::Pays,66 transactional,67};68use pallet_evm::GasWeightMapping;69use up_data_structs::{70 COLLECTION_NUMBER_LIMIT,71 Collection,72 RpcCollection,73 CollectionFlags,74 RpcCollectionFlags,75 CollectionId,76 CreateItemData,77 MAX_TOKEN_PREFIX_LENGTH,78 COLLECTION_ADMINS_LIMIT,79 TokenId,80 TokenChild,81 CollectionStats,82 MAX_TOKEN_OWNERSHIP,83 CollectionMode,84 NFT_SPONSOR_TRANSFER_TIMEOUT,85 FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,86 REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,87 MAX_SPONSOR_TIMEOUT,88 CUSTOM_DATA_LIMIT,89 CollectionLimits,90 CreateCollectionData,91 SponsorshipState,92 CreateItemExData,93 SponsoringRateLimit,94 budget::Budget,95 PhantomType,96 Property,97 Properties,98 PropertiesPermissionMap,99 PropertyKey,100 PropertyValue,101 PropertyPermission,102 PropertiesError,103 PropertyKeyPermission,104 TokenData,105 TrySetProperty,106 PropertyScope,107 // RMRK108 RmrkCollectionInfo,109 RmrkInstanceInfo,110 RmrkResourceInfo,111 RmrkPropertyInfo,112 RmrkBaseInfo,113 RmrkPartType,114 RmrkBoundedTheme,115 RmrkNftChild,116 CollectionPermissions,117};118119pub use pallet::*;120use sp_core::H160;121use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};122#[cfg(feature = "runtime-benchmarks")]123pub mod benchmarking;124pub mod dispatch;125pub mod erc;126pub mod eth;127pub mod weights;128129/// Weight info.130pub type SelfWeightOf<T> = <T as Config>::WeightInfo;131132/// Collection handle contains information about collection data and id.133/// Also provides functionality to count consumed gas.134///135/// CollectionHandle is used as a generic wrapper for collections of all types.136/// It allows to perform common operations and queries on any collection type,137/// both completely general for all, as well as their respective implementations of [`CommonCollectionOperations`].138#[must_use = "Should call submit_logs or save, otherwise some data will be lost for evm side"]139pub struct CollectionHandle<T: Config> {140 /// Collection id141 pub id: CollectionId,142 collection: Collection<T::AccountId>,143 /// Substrate recorder for counting consumed gas144 pub recorder: SubstrateRecorder<T>,145}146147impl<T: Config> WithRecorder<T> for CollectionHandle<T> {148 fn recorder(&self) -> &SubstrateRecorder<T> {149 &self.recorder150 }151 fn into_recorder(self) -> SubstrateRecorder<T> {152 self.recorder153 }154}155156impl<T: Config> CollectionHandle<T> {157 /// Same as [CollectionHandle::new] but with an explicit gas limit.158 pub fn new_with_gas_limit(id: CollectionId, gas_limit: u64) -> Option<Self> {159 <CollectionById<T>>::get(id).map(|collection| Self {160 id,161 collection,162 recorder: SubstrateRecorder::new(gas_limit),163 })164 }165166 /// Same as [CollectionHandle::new] but with an existed [`SubstrateRecorder`].167 pub fn new_with_recorder(id: CollectionId, recorder: SubstrateRecorder<T>) -> Option<Self> {168 <CollectionById<T>>::get(id).map(|collection| Self {169 id,170 collection,171 recorder,172 })173 }174175 /// Retrives collection data from storage and creates collection handle with default parameters.176 /// If collection not found return `None`177 pub fn new(id: CollectionId) -> Option<Self> {178 Self::new_with_gas_limit(id, u64::MAX)179 }180181 /// Same as [`CollectionHandle::new`] but if collection not found [CollectionNotFound](Error::CollectionNotFound) returned.182 pub fn try_get(id: CollectionId) -> Result<Self, DispatchError> {183 Ok(Self::new(id).ok_or(<Error<T>>::CollectionNotFound)?)184 }185186 /// Consume gas for reading.187 pub fn consume_store_reads(&self, reads: u64) -> evm_coder::execution::Result<()> {188 self.recorder189 .consume_gas(T::GasWeightMapping::weight_to_gas(Weight::from_ref_time(190 <T as frame_system::Config>::DbWeight::get()191 .read192 .saturating_mul(reads),193 )))194 }195196 /// Consume gas for writing.197 pub fn consume_store_writes(&self, writes: u64) -> evm_coder::execution::Result<()> {198 self.recorder199 .consume_gas(T::GasWeightMapping::weight_to_gas(Weight::from_ref_time(200 <T as frame_system::Config>::DbWeight::get()201 .write202 .saturating_mul(writes),203 )))204 }205206 /// Consume gas for reading and writing.207 pub fn consume_store_reads_and_writes(208 &self,209 reads: u64,210 writes: u64,211 ) -> evm_coder::execution::Result<()> {212 let weight = <T as frame_system::Config>::DbWeight::get();213 let reads = weight.read.saturating_mul(reads);214 let writes = weight.read.saturating_mul(writes);215 self.recorder216 .consume_gas(T::GasWeightMapping::weight_to_gas(Weight::from_ref_time(217 reads.saturating_add(writes),218 )))219 }220221 /// Save collection to storage.222 pub fn save(&self) -> DispatchResult {223 <CollectionById<T>>::insert(self.id, &self.collection);224 Ok(())225 }226227 /// Set collection sponsor.228 ///229 /// Unique collections allows sponsoring for certain actions.230 /// This method allows you to set the sponsor of the collection.231 /// In order for sponsorship to become active, it must be confirmed through [`Self::confirm_sponsorship`].232 pub fn set_sponsor(&mut self, sponsor: T::AccountId) -> DispatchResult {233 self.collection.sponsorship = SponsorshipState::Unconfirmed(sponsor);234 Ok(())235 }236237 /// Confirm sponsorship238 ///239 /// In order for the sponsorship to become active, the user set as the sponsor must confirm their participation.240 /// Before confirming sponsorship, the user must be specified as the sponsor of the collection via [`Self::set_sponsor`].241 pub fn confirm_sponsorship(&mut self, sender: &T::AccountId) -> Result<bool, DispatchError> {242 if self.collection.sponsorship.pending_sponsor() != Some(sender) {243 return Ok(false);244 }245246 self.collection.sponsorship = SponsorshipState::Confirmed(sender.clone());247 Ok(true)248 }249250 /// Remove collection sponsor.251 pub fn remove_sponsor(&mut self) -> DispatchResult {252 self.collection.sponsorship = SponsorshipState::Disabled;253 Ok(())254 }255256 /// Checks that the collection was created with, and must be operated upon through **Unique API**.257 /// Now check only the `external` flag and if it's **true**, then return [`Error::CollectionIsExternal`] error.258 pub fn check_is_internal(&self) -> DispatchResult {259 if self.flags.external {260 return Err(<Error<T>>::CollectionIsExternal)?;261 }262263 Ok(())264 }265266 /// Checks that the collection was created with, and must be operated upon through an **assimilated API**.267 /// Now check only the `external` flag and if it's **false**, then return [`Error::CollectionIsInternal`] error.268 pub fn check_is_external(&self) -> DispatchResult {269 if !self.flags.external {270 return Err(<Error<T>>::CollectionIsInternal)?;271 }272273 Ok(())274 }275}276277impl<T: Config> Deref for CollectionHandle<T> {278 type Target = Collection<T::AccountId>;279280 fn deref(&self) -> &Self::Target {281 &self.collection282 }283}284285impl<T: Config> DerefMut for CollectionHandle<T> {286 fn deref_mut(&mut self) -> &mut Self::Target {287 &mut self.collection288 }289}290291impl<T: Config> CollectionHandle<T> {292 /// Checks if the `user` is the owner of the collection.293 pub fn check_is_owner(&self, user: &T::CrossAccountId) -> DispatchResult {294 ensure!(*user.as_sub() == self.owner, <Error<T>>::NoPermission);295 Ok(())296 }297298 /// Returns **true** if the `user` is the owner or administrator of the collection.299 pub fn is_owner_or_admin(&self, user: &T::CrossAccountId) -> bool {300 *user.as_sub() == self.owner || <IsAdmin<T>>::get((self.id, user))301 }302303 /// Checks if the `user` is the owner or administrator of the collection.304 pub fn check_is_owner_or_admin(&self, user: &T::CrossAccountId) -> DispatchResult {305 ensure!(self.is_owner_or_admin(user), <Error<T>>::NoPermission);306 Ok(())307 }308309 /// Return **true** if `user` was not allowed to have tokens, and he can ignore such restrictions.310 pub fn ignores_allowance(&self, user: &T::CrossAccountId) -> bool {311 self.limits.owner_can_transfer() && self.is_owner_or_admin(user)312 }313314 /// Return **true** if `user` does not have enough token parts, and he can ignore such restrictions.315 pub fn ignores_owned_amount(&self, user: &T::CrossAccountId) -> bool {316 self.limits.owner_can_transfer() && self.is_owner_or_admin(user)317 }318319 /// Checks if the user is in the allow list. If not [Error::AddressNotInAllowlist] returns.320 pub fn check_allowlist(&self, user: &T::CrossAccountId) -> DispatchResult {321 ensure!(322 <Allowlist<T>>::get((self.id, user)),323 <Error<T>>::AddressNotInAllowlist324 );325 Ok(())326 }327328 /// Changes collection owner to another account329 /// #### Store read/writes330 /// 1 writes331 fn set_owner_internal(332 &mut self,333 caller: T::CrossAccountId,334 new_owner: T::CrossAccountId,335 ) -> DispatchResult {336 self.check_is_owner(&caller)?;337 self.collection.owner = new_owner.as_sub().clone();338 self.save()339 }340}341342#[frame_support::pallet]343pub mod pallet {344 use super::*;345 use pallet_evm::account;346 use dispatch::CollectionDispatch;347 use frame_support::{Blake2_128Concat, pallet_prelude::*, storage::Key, traits::StorageVersion};348 use frame_system::pallet_prelude::*;349 use frame_support::traits::Currency;350 use up_data_structs::{TokenId, mapping::TokenAddressMapping};351 use scale_info::TypeInfo;352 use weights::WeightInfo;353354 #[pallet::config]355 pub trait Config:356 frame_system::Config357 + pallet_evm_coder_substrate::Config358 + pallet_evm::Config359 + TypeInfo360 + account::Config361 {362 /// Weight information for functions of this pallet.363 type WeightInfo: WeightInfo;364365 /// Events compatible with [`frame_system::Config::Event`].366 type RuntimeEvent: IsType<<Self as frame_system::Config>::RuntimeEvent> + From<Event<Self>>;367368 /// Handler of accounts and payment.369 type Currency: Currency<Self::AccountId>;370371 /// Set price to create a collection.372 #[pallet::constant]373 type CollectionCreationPrice: Get<374 <<Self as Config>::Currency as Currency<Self::AccountId>>::Balance,375 >;376377 /// Dispatcher of operations on collections.378 type CollectionDispatch: CollectionDispatch<Self>;379380 /// Account which holds the chain's treasury.381 type TreasuryAccountId: Get<Self::AccountId>;382383 /// Address under which the CollectionHelper contract would be available.384 type ContractAddress: Get<H160>;385386 /// Mapper for token addresses to Ethereum addresses.387 type EvmTokenAddressMapping: TokenAddressMapping<H160>;388389 /// Mapper for token addresses to [`CrossAccountId`].390 type CrossTokenAddressMapping: TokenAddressMapping<Self::CrossAccountId>;391 }392393 const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);394395 #[pallet::pallet]396 #[pallet::storage_version(STORAGE_VERSION)]397 #[pallet::generate_store(pub(super) trait Store)]398 pub struct Pallet<T>(_);399400 #[pallet::extra_constants]401 impl<T: Config> Pallet<T> {402 /// Maximum admins per collection.403 pub fn collection_admins_limit() -> u32 {404 COLLECTION_ADMINS_LIMIT405 }406 }407408 #[pallet::event]409 #[pallet::generate_deposit(pub fn deposit_event)]410 pub enum Event<T: Config> {411 /// New collection was created412 CollectionCreated(413 /// Globally unique identifier of newly created collection.414 CollectionId,415 /// [`CollectionMode`] converted into _u8_.416 u8,417 /// Collection owner.418 T::AccountId,419 ),420421 /// New collection was destroyed422 CollectionDestroyed(423 /// Globally unique identifier of collection.424 CollectionId,425 ),426427 /// New item was created.428 ItemCreated(429 /// Id of the collection where item was created.430 CollectionId,431 /// Id of an item. Unique within the collection.432 TokenId,433 /// Owner of newly created item434 T::CrossAccountId,435 /// Always 1 for NFT436 u128,437 ),438439 /// Collection item was burned.440 ItemDestroyed(441 /// Id of the collection where item was destroyed.442 CollectionId,443 /// Identifier of burned NFT.444 TokenId,445 /// Which user has destroyed its tokens.446 T::CrossAccountId,447 /// Amount of token pieces destroed. Always 1 for NFT.448 u128,449 ),450451 /// Item was transferred452 Transfer(453 /// Id of collection to which item is belong.454 CollectionId,455 /// Id of an item.456 TokenId,457 /// Original owner of item.458 T::CrossAccountId,459 /// New owner of item.460 T::CrossAccountId,461 /// Amount of token pieces transfered. Always 1 for NFT.462 u128,463 ),464465 /// Amount pieces of token owned by `sender` was approved for `spender`.466 Approved(467 /// Id of collection to which item is belong.468 CollectionId,469 /// Id of an item.470 TokenId,471 /// Original owner of item.472 T::CrossAccountId,473 /// Id for which the approval was granted.474 T::CrossAccountId,475 /// Amount of token pieces transfered. Always 1 for NFT.476 u128,477 ),478479 /// The colletion property has been added or edited.480 CollectionPropertySet(481 /// Id of collection to which property has been set.482 CollectionId,483 /// The property that was set.484 PropertyKey,485 ),486487 /// The property has been deleted.488 CollectionPropertyDeleted(489 /// Id of collection to which property has been deleted.490 CollectionId,491 /// The property that was deleted.492 PropertyKey,493 ),494495 /// The token property has been added or edited.496 TokenPropertySet(497 /// Identifier of the collection whose token has the property set.498 CollectionId,499 /// The token for which the property was set.500 TokenId,501 /// The property that was set.502 PropertyKey,503 ),504505 /// The token property has been deleted.506 TokenPropertyDeleted(507 /// Identifier of the collection whose token has the property deleted.508 CollectionId,509 /// The token for which the property was deleted.510 TokenId,511 /// The property that was deleted.512 PropertyKey,513 ),514515 /// The token property permission of a collection has been set.516 PropertyPermissionSet(517 /// ID of collection to which property permission has been set.518 CollectionId,519 /// The property permission that was set.520 PropertyKey,521 ),522 }523524 #[pallet::error]525 pub enum Error<T> {526 /// This collection does not exist.527 CollectionNotFound,528 /// Sender parameter and item owner must be equal.529 MustBeTokenOwner,530 /// No permission to perform action531 NoPermission,532 /// Destroying only empty collections is allowed533 CantDestroyNotEmptyCollection,534 /// Collection is not in mint mode.535 PublicMintingNotAllowed,536 /// Address is not in allow list.537 AddressNotInAllowlist,538539 /// Collection name can not be longer than 63 char.540 CollectionNameLimitExceeded,541 /// Collection description can not be longer than 255 char.542 CollectionDescriptionLimitExceeded,543 /// Token prefix can not be longer than 15 char.544 CollectionTokenPrefixLimitExceeded,545 /// Total collections bound exceeded.546 TotalCollectionsLimitExceeded,547 /// Exceeded max admin count548 CollectionAdminCountExceeded,549 /// Collection limit bounds per collection exceeded550 CollectionLimitBoundsExceeded,551 /// Tried to enable permissions which are only permitted to be disabled552 OwnerPermissionsCantBeReverted,553 /// Collection settings not allowing items transferring554 TransferNotAllowed,555 /// Account token limit exceeded per collection556 AccountTokenLimitExceeded,557 /// Collection token limit exceeded558 CollectionTokenLimitExceeded,559 /// Metadata flag frozen560 MetadataFlagFrozen,561562 /// Item does not exist563 TokenNotFound,564 /// Item is balance not enough565 TokenValueTooLow,566 /// Requested value is more than the approved567 ApprovedValueTooLow,568 /// Tried to approve more than owned569 CantApproveMoreThanOwned,570571 /// Can't transfer tokens to ethereum zero address572 AddressIsZero,573574 /// The operation is not supported575 UnsupportedOperation,576577 /// Insufficient funds to perform an action578 NotSufficientFounds,579580 /// User does not satisfy the nesting rule581 UserIsNotAllowedToNest,582 /// Only tokens from specific collections may nest tokens under this one583 SourceCollectionIsNotAllowedToNest,584585 /// Tried to store more data than allowed in collection field586 CollectionFieldSizeExceeded,587588 /// Tried to store more property data than allowed589 NoSpaceForProperty,590591 /// Tried to store more property keys than allowed592 PropertyLimitReached,593594 /// Property key is too long595 PropertyKeyIsTooLong,596597 /// Only ASCII letters, digits, and symbols `_`, `-`, and `.` are allowed598 InvalidCharacterInPropertyKey,599600 /// Empty property keys are forbidden601 EmptyPropertyKey,602603 /// Tried to access an external collection with an internal API604 CollectionIsExternal,605606 /// Tried to access an internal collection with an external API607 CollectionIsInternal,608 }609610 /// Storage of the count of created collections. Essentially contains the last collection ID.611 #[pallet::storage]612 pub type CreatedCollectionCount<T> = StorageValue<Value = CollectionId, QueryKind = ValueQuery>;613614 /// Storage of the count of deleted collections.615 #[pallet::storage]616 pub type DestroyedCollectionCount<T> =617 StorageValue<Value = CollectionId, QueryKind = ValueQuery>;618619 /// Storage of collection info.620 #[pallet::storage]621 pub type CollectionById<T> = StorageMap<622 Hasher = Blake2_128Concat,623 Key = CollectionId,624 Value = Collection<<T as frame_system::Config>::AccountId>,625 QueryKind = OptionQuery,626 >;627628 /// Storage of collection properties.629 #[pallet::storage]630 #[pallet::getter(fn collection_properties)]631 pub type CollectionProperties<T> = StorageMap<632 Hasher = Blake2_128Concat,633 Key = CollectionId,634 Value = Properties,635 QueryKind = ValueQuery,636 OnEmpty = up_data_structs::CollectionProperties,637 >;638639 /// Storage of token property permissions of a collection.640 #[pallet::storage]641 #[pallet::getter(fn property_permissions)]642 pub type CollectionPropertyPermissions<T> = StorageMap<643 Hasher = Blake2_128Concat,644 Key = CollectionId,645 Value = PropertiesPermissionMap,646 QueryKind = ValueQuery,647 >;648649 /// Storage of the amount of collection admins.650 #[pallet::storage]651 pub type AdminAmount<T> = StorageMap<652 Hasher = Blake2_128Concat,653 Key = CollectionId,654 Value = u32,655 QueryKind = ValueQuery,656 >;657658 /// List of collection admins.659 #[pallet::storage]660 pub type IsAdmin<T: Config> = StorageNMap<661 Key = (662 Key<Blake2_128Concat, CollectionId>,663 Key<Blake2_128Concat, T::CrossAccountId>,664 ),665 Value = bool,666 QueryKind = ValueQuery,667 >;668669 /// Allowlisted collection users.670 #[pallet::storage]671 pub type Allowlist<T: Config> = StorageNMap<672 Key = (673 Key<Blake2_128Concat, CollectionId>,674 Key<Blake2_128Concat, T::CrossAccountId>,675 ),676 Value = bool,677 QueryKind = ValueQuery,678 >;679680 /// Not used by code, exists only to provide some types to metadata.681 #[pallet::storage]682 pub type DummyStorageValue<T: Config> = StorageValue<683 Value = (684 CollectionStats,685 CollectionId,686 TokenId,687 TokenChild,688 PhantomType<(689 TokenData<T::CrossAccountId>,690 RpcCollection<T::AccountId>,691 // RMRK692 RmrkCollectionInfo<T::AccountId>,693 RmrkInstanceInfo<T::AccountId>,694 RmrkResourceInfo,695 RmrkPropertyInfo,696 RmrkBaseInfo<T::AccountId>,697 RmrkPartType,698 RmrkBoundedTheme,699 RmrkNftChild,700 )>,701 ),702 QueryKind = OptionQuery,703 >;704705 #[pallet::hooks]706 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {707 fn on_runtime_upgrade() -> Weight {708 StorageVersion::new(1).put::<Pallet<T>>();709710 Weight::zero()711 }712 }713}714715impl<T: Config> Pallet<T> {716 /// Enshure that receiver address is correct.717 ///718 /// Ethereum receiver 0x0000000000000000000000000000000000000000 is reserved, and shouldn't own tokens.719 pub fn ensure_correct_receiver(receiver: &T::CrossAccountId) -> DispatchResult {720 ensure!(721 &T::CrossAccountId::from_eth(H160([0; 20])) != receiver,722 <Error<T>>::AddressIsZero723 );724 Ok(())725 }726727 /// Get a vector of collection admins.728 pub fn adminlist(collection: CollectionId) -> Vec<T::CrossAccountId> {729 <IsAdmin<T>>::iter_prefix((collection,))730 .map(|(a, _)| a)731 .collect()732 }733734 /// Get a vector of users allowed to mint tokens.735 pub fn allowlist(collection: CollectionId) -> Vec<T::CrossAccountId> {736 <Allowlist<T>>::iter_prefix((collection,))737 .map(|(a, _)| a)738 .collect()739 }740741 /// Is `user` allowed to mint token in `collection`.742 pub fn allowed(collection: CollectionId, user: T::CrossAccountId) -> bool {743 <Allowlist<T>>::get((collection, user))744 }745746 /// Get statistics of collections.747 pub fn collection_stats() -> CollectionStats {748 let created = <CreatedCollectionCount<T>>::get();749 let destroyed = <DestroyedCollectionCount<T>>::get();750 CollectionStats {751 created: created.0,752 destroyed: destroyed.0,753 alive: created.0 - destroyed.0,754 }755 }756757 /// Get the effective limits for the collection.758 pub fn effective_collection_limits(collection: CollectionId) -> Option<CollectionLimits> {759 let collection = <CollectionById<T>>::get(collection)?;760 let limits = collection.limits;761 let effective_limits = CollectionLimits {762 account_token_ownership_limit: Some(limits.account_token_ownership_limit()),763 sponsored_data_size: Some(limits.sponsored_data_size()),764 sponsored_data_rate_limit: Some(765 limits766 .sponsored_data_rate_limit767 .unwrap_or(SponsoringRateLimit::SponsoringDisabled),768 ),769 token_limit: Some(limits.token_limit()),770 sponsor_transfer_timeout: Some(limits.sponsor_transfer_timeout(771 match collection.mode {772 CollectionMode::NFT => NFT_SPONSOR_TRANSFER_TIMEOUT,773 CollectionMode::Fungible(_) => FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,774 CollectionMode::ReFungible => REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,775 },776 )),777 sponsor_approve_timeout: Some(limits.sponsor_approve_timeout()),778 owner_can_transfer: Some(limits.owner_can_transfer()),779 owner_can_destroy: Some(limits.owner_can_destroy()),780 transfers_enabled: Some(limits.transfers_enabled()),781 };782783 Some(effective_limits)784 }785786 /// Returns information about the `collection` adapted for rpc.787 pub fn rpc_collection(collection: CollectionId) -> Option<RpcCollection<T::AccountId>> {788 let Collection {789 name,790 description,791 owner,792 mode,793 token_prefix,794 sponsorship,795 limits,796 permissions,797 flags,798 } = <CollectionById<T>>::get(collection)?;799800 let token_property_permissions = <CollectionPropertyPermissions<T>>::get(collection)801 .into_iter()802 .map(|(key, permission)| PropertyKeyPermission { key, permission })803 .collect();804805 let properties = <CollectionProperties<T>>::get(collection)806 .into_iter()807 .map(|(key, value)| Property { key, value })808 .collect();809810 let permissions = CollectionPermissions {811 access: Some(permissions.access()),812 mint_mode: Some(permissions.mint_mode()),813 nesting: Some(permissions.nesting().clone()),814 };815816 Some(RpcCollection {817 name: name.into_inner(),818 description: description.into_inner(),819 owner,820 mode,821 token_prefix: token_prefix.into_inner(),822 sponsorship,823 limits,824 permissions,825 token_property_permissions,826 properties,827 read_only: flags.external,828829 flags: RpcCollectionFlags {830 foreign: flags.foreign,831 erc721metadata: flags.erc721metadata,832 },833 })834 }835}836837macro_rules! limit_default {838 ($old:ident, $new:ident, $($field:ident $(($arg:expr))? => $check:expr),* $(,)?) => {{839 $(840 if let Some($new) = $new.$field {841 let $old = $old.$field($($arg)?);842 let _ = $new;843 let _ = $old;844 $check845 } else {846 $new.$field = $old.$field847 }848 )*849 }};850}851macro_rules! limit_default_clone {852 ($old:ident, $new:ident, $($field:ident $(($arg:expr))? => $check:expr),* $(,)?) => {{853 $(854 if let Some($new) = $new.$field.clone() {855 let $old = $old.$field($($arg)?);856 let _ = $new;857 let _ = $old;858 $check859 } else {860 $new.$field = $old.$field.clone()861 }862 )*863 }};864}865866impl<T: Config> Pallet<T> {867 /// Create new collection.868 ///869 /// * `owner` - The owner of the collection.870 /// * `data` - Description of the created collection.871 /// * `flags` - Extra flags to store.872 pub fn init_collection(873 owner: T::CrossAccountId,874 payer: T::CrossAccountId,875 data: CreateCollectionData<T::AccountId>,876 flags: CollectionFlags,877 ) -> Result<CollectionId, DispatchError> {878 {879 ensure!(880 data.token_prefix.len() <= MAX_TOKEN_PREFIX_LENGTH as usize,881 Error::<T>::CollectionTokenPrefixLimitExceeded882 );883 }884885 let created_count = <CreatedCollectionCount<T>>::get()886 .0887 .checked_add(1)888 .ok_or(ArithmeticError::Overflow)?;889 let destroyed_count = <DestroyedCollectionCount<T>>::get().0;890 let id = CollectionId(created_count);891892 // bound Total number of collections893 ensure!(894 created_count - destroyed_count <= COLLECTION_NUMBER_LIMIT,895 <Error<T>>::TotalCollectionsLimitExceeded896 );897898 // =========899900 let collection = Collection {901 owner: owner.as_sub().clone(),902 name: data.name,903 mode: data.mode.clone(),904 description: data.description,905 token_prefix: data.token_prefix,906 sponsorship: data907 .pending_sponsor908 .map(SponsorshipState::Unconfirmed)909 .unwrap_or_default(),910 limits: data911 .limits912 .map(|limits| Self::clamp_limits(data.mode.clone(), &Default::default(), limits))913 .unwrap_or_else(|| Ok(CollectionLimits::default()))?,914 permissions: data915 .permissions916 .map(|permissions| {917 Self::clamp_permissions(data.mode.clone(), &Default::default(), permissions)918 })919 .unwrap_or_else(|| Ok(CollectionPermissions::default()))?,920 flags,921 };922923 let mut collection_properties = up_data_structs::CollectionProperties::get();924 collection_properties925 .try_set_from_iter(data.properties.into_iter())926 .map_err(<Error<T>>::from)?;927928 CollectionProperties::<T>::insert(id, collection_properties);929930 let mut token_props_permissions = PropertiesPermissionMap::new();931 token_props_permissions932 .try_set_from_iter(data.token_property_permissions.into_iter())933 .map_err(<Error<T>>::from)?;934935 CollectionPropertyPermissions::<T>::insert(id, token_props_permissions);936937 // Take a (non-refundable) deposit of collection creation938 {939 let mut imbalance =940 <<<T as Config>::Currency as Currency<T::AccountId>>::PositiveImbalance>::zero();941 imbalance.subsume(942 <<T as Config>::Currency as Currency<T::AccountId>>::deposit_creating(943 &T::TreasuryAccountId::get(),944 T::CollectionCreationPrice::get(),945 ),946 );947 <T as Config>::Currency::settle(948 payer.as_sub(),949 imbalance,950 WithdrawReasons::TRANSFER,951 ExistenceRequirement::KeepAlive,952 )953 .map_err(|_| Error::<T>::NotSufficientFounds)?;954 }955956 <CreatedCollectionCount<T>>::put(created_count);957 <Pallet<T>>::deposit_event(Event::CollectionCreated(958 id,959 data.mode.id(),960 owner.as_sub().clone(),961 ));962 <PalletEvm<T>>::deposit_log(963 erc::CollectionHelpersEvents::CollectionCreated {964 owner: *owner.as_eth(),965 collection_id: eth::collection_id_to_address(id),966 }967 .to_log(T::ContractAddress::get()),968 );969 <CollectionById<T>>::insert(id, collection);970 Ok(id)971 }972973 /// Destroy collection.974 ///975 /// * `collection` - Collection handler.976 /// * `sender` - The owner or administrator of the collection.977 pub fn destroy_collection(978 collection: CollectionHandle<T>,979 sender: &T::CrossAccountId,980 ) -> DispatchResult {981 ensure!(982 collection.limits.owner_can_destroy(),983 <Error<T>>::NoPermission,984 );985 collection.check_is_owner(sender)?;986987 let destroyed_collections = <DestroyedCollectionCount<T>>::get()988 .0989 .checked_add(1)990 .ok_or(ArithmeticError::Overflow)?;991992 // =========993994 <DestroyedCollectionCount<T>>::put(destroyed_collections);995 <CollectionById<T>>::remove(collection.id);996 <AdminAmount<T>>::remove(collection.id);997 let _ = <IsAdmin<T>>::clear_prefix((collection.id,), u32::MAX, None);998 let _ = <Allowlist<T>>::clear_prefix((collection.id,), u32::MAX, None);999 <CollectionProperties<T>>::remove(collection.id);10001001 <Pallet<T>>::deposit_event(Event::CollectionDestroyed(collection.id));10021003 <PalletEvm<T>>::deposit_log(1004 erc::CollectionHelpersEvents::CollectionDestroyed {1005 collection_id: eth::collection_id_to_address(collection.id),1006 }1007 .to_log(T::ContractAddress::get()),1008 );1009 Ok(())1010 }10111012 /// Set collection property.1013 ///1014 /// * `collection` - Collection handler.1015 /// * `sender` - The owner or administrator of the collection.1016 /// * `property` - The property to set.1017 pub fn set_collection_property(1018 collection: &CollectionHandle<T>,1019 sender: &T::CrossAccountId,1020 property: Property,1021 ) -> DispatchResult {1022 collection.check_is_owner_or_admin(sender)?;10231024 CollectionProperties::<T>::try_mutate(collection.id, |properties| {1025 let property = property.clone();1026 properties.try_set(property.key, property.value)1027 })1028 .map_err(<Error<T>>::from)?;10291030 Self::deposit_event(Event::CollectionPropertySet(collection.id, property.key));10311032 Ok(())1033 }10341035 /// Set scouped collection property.1036 ///1037 /// * `collection_id` - ID of the collection for which the property is being set.1038 /// * `scope` - Property scope.1039 /// * `property` - The property to set.1040 pub fn set_scoped_collection_property(1041 collection_id: CollectionId,1042 scope: PropertyScope,1043 property: Property,1044 ) -> DispatchResult {1045 CollectionProperties::<T>::try_mutate(collection_id, |properties| {1046 properties.try_scoped_set(scope, property.key, property.value)1047 })1048 .map_err(<Error<T>>::from)?;10491050 Ok(())1051 }10521053 /// Set scouped collection properties.1054 ///1055 /// * `collection_id` - ID of the collection for which the properties is being set.1056 /// * `scope` - Property scope.1057 /// * `properties` - The properties to set.1058 pub fn set_scoped_collection_properties(1059 collection_id: CollectionId,1060 scope: PropertyScope,1061 properties: impl Iterator<Item = Property>,1062 ) -> DispatchResult {1063 CollectionProperties::<T>::try_mutate(collection_id, |stored_properties| {1064 stored_properties.try_scoped_set_from_iter(scope, properties)1065 })1066 .map_err(<Error<T>>::from)?;10671068 Ok(())1069 }10701071 /// Set collection properties.1072 ///1073 /// * `collection` - Collection handler.1074 /// * `sender` - The owner or administrator of the collection.1075 /// * `properties` - The properties to set.1076 #[transactional]1077 pub fn set_collection_properties(1078 collection: &CollectionHandle<T>,1079 sender: &T::CrossAccountId,1080 properties: Vec<Property>,1081 ) -> DispatchResult {1082 for property in properties {1083 Self::set_collection_property(collection, sender, property)?;1084 }10851086 Ok(())1087 }10881089 /// Delete collection property.1090 ///1091 /// * `collection` - Collection handler.1092 /// * `sender` - The owner or administrator of the collection.1093 /// * `property` - The property to delete.1094 pub fn delete_collection_property(1095 collection: &CollectionHandle<T>,1096 sender: &T::CrossAccountId,1097 property_key: PropertyKey,1098 ) -> DispatchResult {1099 collection.check_is_owner_or_admin(sender)?;11001101 CollectionProperties::<T>::try_mutate(collection.id, |properties| {1102 properties.remove(&property_key)1103 })1104 .map_err(<Error<T>>::from)?;11051106 Self::deposit_event(Event::CollectionPropertyDeleted(1107 collection.id,1108 property_key,1109 ));11101111 Ok(())1112 }11131114 /// Delete collection properties.1115 ///1116 /// * `collection` - Collection handler.1117 /// * `sender` - The owner or administrator of the collection.1118 /// * `properties` - The properties to delete.1119 #[transactional]1120 pub fn delete_collection_properties(1121 collection: &CollectionHandle<T>,1122 sender: &T::CrossAccountId,1123 property_keys: Vec<PropertyKey>,1124 ) -> DispatchResult {1125 for key in property_keys {1126 Self::delete_collection_property(collection, sender, key)?;1127 }11281129 Ok(())1130 }11311132 /// Set collection propetry permission without any checks.1133 ///1134 /// Used for migrations.1135 ///1136 /// * `collection` - Collection handler.1137 /// * `property_permissions` - Property permissions.1138 pub fn set_property_permission_unchecked(1139 collection: CollectionId,1140 property_permission: PropertyKeyPermission,1141 ) -> DispatchResult {1142 <CollectionPropertyPermissions<T>>::try_mutate(collection, |permissions| {1143 permissions.try_set(property_permission.key, property_permission.permission)1144 })1145 .map_err(<Error<T>>::from)?;1146 Ok(())1147 }11481149 /// Set collection property permission.1150 ///1151 /// * `collection` - Collection handler.1152 /// * `sender` - The owner or administrator of the collection.1153 /// * `property_permission` - Property permission.1154 pub fn set_property_permission(1155 collection: &CollectionHandle<T>,1156 sender: &T::CrossAccountId,1157 property_permission: PropertyKeyPermission,1158 ) -> DispatchResult {1159 Self::set_scoped_property_permission(1160 collection,1161 sender,1162 PropertyScope::None,1163 property_permission,1164 )1165 }11661167 /// Set collection property permission with scope.1168 ///1169 /// * `collection` - Collection handler.1170 /// * `sender` - The owner or administrator of the collection.1171 /// * `scope` - Property scope.1172 /// * `property_permission` - Property permission.1173 pub fn set_scoped_property_permission(1174 collection: &CollectionHandle<T>,1175 sender: &T::CrossAccountId,1176 scope: PropertyScope,1177 property_permission: PropertyKeyPermission,1178 ) -> DispatchResult {1179 collection.check_is_owner_or_admin(sender)?;11801181 let all_permissions = CollectionPropertyPermissions::<T>::get(collection.id);1182 let current_permission = all_permissions.get(&property_permission.key);1183 if matches![1184 current_permission,1185 Some(PropertyPermission { mutable: false, .. })1186 ] {1187 return Err(<Error<T>>::NoPermission.into());1188 }11891190 CollectionPropertyPermissions::<T>::try_mutate(collection.id, |permissions| {1191 let property_permission = property_permission.clone();1192 permissions.try_scoped_set(1193 scope,1194 property_permission.key,1195 property_permission.permission,1196 )1197 })1198 .map_err(<Error<T>>::from)?;11991200 Self::deposit_event(Event::PropertyPermissionSet(1201 collection.id,1202 property_permission.key,1203 ));12041205 Ok(())1206 }12071208 /// Set token property permission.1209 ///1210 /// * `collection` - Collection handler.1211 /// * `sender` - The owner or administrator of the collection.1212 /// * `property_permissions` - Property permissions.1213 #[transactional]1214 pub fn set_token_property_permissions(1215 collection: &CollectionHandle<T>,1216 sender: &T::CrossAccountId,1217 property_permissions: Vec<PropertyKeyPermission>,1218 ) -> DispatchResult {1219 Self::set_scoped_token_property_permissions(1220 collection,1221 sender,1222 PropertyScope::None,1223 property_permissions,1224 )1225 }12261227 /// Set token property permission with scope.1228 ///1229 /// * `collection` - Collection handler.1230 /// * `sender` - The owner or administrator of the collection.1231 /// * `scope` - Property scope.1232 /// * `property_permissions` - Property permissions.1233 #[transactional]1234 pub fn set_scoped_token_property_permissions(1235 collection: &CollectionHandle<T>,1236 sender: &T::CrossAccountId,1237 scope: PropertyScope,1238 property_permissions: Vec<PropertyKeyPermission>,1239 ) -> DispatchResult {1240 for prop_pemission in property_permissions {1241 Self::set_scoped_property_permission(collection, sender, scope, prop_pemission)?;1242 }12431244 Ok(())1245 }12461247 /// Get collection property.1248 pub fn get_collection_property(1249 collection_id: CollectionId,1250 key: &PropertyKey,1251 ) -> Option<PropertyValue> {1252 Self::collection_properties(collection_id).get(key).cloned()1253 }12541255 /// Convert byte vector to property key vector.1256 pub fn bytes_keys_to_property_keys(1257 keys: Vec<Vec<u8>>,1258 ) -> Result<Vec<PropertyKey>, DispatchError> {1259 keys.into_iter()1260 .map(|key| -> Result<PropertyKey, DispatchError> {1261 key.try_into()1262 .map_err(|_| <Error<T>>::PropertyKeyIsTooLong.into())1263 })1264 .collect::<Result<Vec<PropertyKey>, DispatchError>>()1265 }12661267 /// Get properties according to given keys.1268 pub fn filter_collection_properties(1269 collection_id: CollectionId,1270 keys: Option<Vec<PropertyKey>>,1271 ) -> Result<Vec<Property>, DispatchError> {1272 let properties = Self::collection_properties(collection_id);12731274 let properties = keys1275 .map(|keys| {1276 keys.into_iter()1277 .filter_map(|key| {1278 properties.get(&key).map(|value| Property {1279 key,1280 value: value.clone(),1281 })1282 })1283 .collect()1284 })1285 .unwrap_or_else(|| {1286 properties1287 .into_iter()1288 .map(|(key, value)| Property { key, value })1289 .collect()1290 });12911292 Ok(properties)1293 }12941295 /// Get property permissions according to given keys.1296 pub fn filter_property_permissions(1297 collection_id: CollectionId,1298 keys: Option<Vec<PropertyKey>>,1299 ) -> Result<Vec<PropertyKeyPermission>, DispatchError> {1300 let permissions = Self::property_permissions(collection_id);13011302 let key_permissions = keys1303 .map(|keys| {1304 keys.into_iter()1305 .filter_map(|key| {1306 permissions1307 .get(&key)1308 .map(|permission| PropertyKeyPermission {1309 key,1310 permission: permission.clone(),1311 })1312 })1313 .collect()1314 })1315 .unwrap_or_else(|| {1316 permissions1317 .into_iter()1318 .map(|(key, permission)| PropertyKeyPermission { key, permission })1319 .collect()1320 });13211322 Ok(key_permissions)1323 }13241325 /// Toggle `user` participation in the `collection`'s allow list.1326 /// #### Store read/writes1327 /// 1 writes1328 pub fn toggle_allowlist(1329 collection: &CollectionHandle<T>,1330 sender: &T::CrossAccountId,1331 user: &T::CrossAccountId,1332 allowed: bool,1333 ) -> DispatchResult {1334 collection.check_is_owner_or_admin(sender)?;13351336 // =========13371338 if allowed {1339 <Allowlist<T>>::insert((collection.id, user), true);1340 } else {1341 <Allowlist<T>>::remove((collection.id, user));1342 }13431344 Ok(())1345 }13461347 /// Toggle `user` participation in the `collection`'s admin list.1348 /// #### Store read/writes1349 /// 2 writes1350 pub fn toggle_admin(1351 collection: &CollectionHandle<T>,1352 sender: &T::CrossAccountId,1353 user: &T::CrossAccountId,1354 admin: bool,1355 ) -> DispatchResult {1356 collection.check_is_owner(sender)?;13571358 let was_admin = <IsAdmin<T>>::get((collection.id, user));1359 if was_admin == admin {1360 return Ok(());1361 }1362 let amount = <AdminAmount<T>>::get(collection.id);13631364 if admin {1365 let amount = amount1366 .checked_add(1)1367 .ok_or(<Error<T>>::CollectionAdminCountExceeded)?;1368 ensure!(1369 amount <= Self::collection_admins_limit(),1370 <Error<T>>::CollectionAdminCountExceeded,1371 );13721373 // =========13741375 <AdminAmount<T>>::insert(collection.id, amount);1376 <IsAdmin<T>>::insert((collection.id, user), true);1377 } else {1378 <AdminAmount<T>>::insert(collection.id, amount.saturating_sub(1));1379 <IsAdmin<T>>::remove((collection.id, user));1380 }13811382 Ok(())1383 }13841385 /// Merge set fields from `new_limit` to `old_limit`.1386 pub fn clamp_limits(1387 mode: CollectionMode,1388 old_limit: &CollectionLimits,1389 mut new_limit: CollectionLimits,1390 ) -> Result<CollectionLimits, DispatchError> {1391 let limits = old_limit;1392 limit_default!(old_limit, new_limit,1393 account_token_ownership_limit => ensure!(1394 new_limit <= MAX_TOKEN_OWNERSHIP,1395 <Error<T>>::CollectionLimitBoundsExceeded,1396 ),1397 sponsored_data_size => ensure!(1398 new_limit <= CUSTOM_DATA_LIMIT,1399 <Error<T>>::CollectionLimitBoundsExceeded,1400 ),14011402 sponsored_data_rate_limit => {},1403 token_limit => ensure!(1404 old_limit >= new_limit && new_limit > 0,1405 <Error<T>>::CollectionTokenLimitExceeded1406 ),14071408 sponsor_transfer_timeout(match mode {1409 CollectionMode::NFT => NFT_SPONSOR_TRANSFER_TIMEOUT,1410 CollectionMode::Fungible(_) => FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,1411 CollectionMode::ReFungible => REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,1412 }) => ensure!(1413 new_limit <= MAX_SPONSOR_TIMEOUT,1414 <Error<T>>::CollectionLimitBoundsExceeded,1415 ),1416 sponsor_approve_timeout => {},1417 owner_can_transfer => ensure!(1418 !limits.owner_can_transfer_instaled() ||1419 old_limit || !new_limit,1420 <Error<T>>::OwnerPermissionsCantBeReverted,1421 ),1422 owner_can_destroy => ensure!(1423 old_limit || !new_limit,1424 <Error<T>>::OwnerPermissionsCantBeReverted,1425 ),1426 transfers_enabled => {},1427 );1428 Ok(new_limit)1429 }14301431 /// Merge set fields from `new_permission` to `old_permission`.1432 pub fn clamp_permissions(1433 _mode: CollectionMode,1434 old_permission: &CollectionPermissions,1435 mut new_permission: CollectionPermissions,1436 ) -> Result<CollectionPermissions, DispatchError> {1437 limit_default_clone!(old_permission, new_permission,1438 access => {},1439 mint_mode => {},1440 nesting => { /* todo check for permissive, if only it gets out of benchmarks */ },1441 );1442 Ok(new_permission)1443 }1444}14451446/// Indicates unsupported methods by returning [Error::UnsupportedOperation].1447#[macro_export]1448macro_rules! unsupported {1449 ($runtime:path) => {1450 Err($crate::Error::<$runtime>::UnsupportedOperation.into())1451 };1452}14531454/// Return weights for various worst-case operations.1455pub trait CommonWeightInfo<CrossAccountId> {1456 /// Weight of item creation.1457 fn create_item() -> Weight;14581459 /// Weight of items creation.1460 fn create_multiple_items(amount: &[CreateItemData]) -> Weight;14611462 /// Weight of items creation.1463 fn create_multiple_items_ex(cost: &CreateItemExData<CrossAccountId>) -> Weight;14641465 /// The weight of the burning item.1466 fn burn_item() -> Weight;14671468 /// Property setting weight.1469 ///1470 /// * `amount`- The number of properties to set.1471 fn set_collection_properties(amount: u32) -> Weight;14721473 /// Collection property deletion weight.1474 ///1475 /// * `amount`- The number of properties to set.1476 fn delete_collection_properties(amount: u32) -> Weight;14771478 /// Token property setting weight.1479 ///1480 /// * `amount`- The number of properties to set.1481 fn set_token_properties(amount: u32) -> Weight;14821483 /// Token property deletion weight.1484 ///1485 /// * `amount`- The number of properties to delete.1486 fn delete_token_properties(amount: u32) -> Weight;14871488 /// Token property permissions set weight.1489 ///1490 /// * `amount`- The number of property permissions to set.1491 fn set_token_property_permissions(amount: u32) -> Weight;14921493 /// Transfer price of the token or its parts.1494 fn transfer() -> Weight;14951496 /// The price of setting the permission of the operation from another user.1497 fn approve() -> Weight;14981499 /// Transfer price from another user.1500 fn transfer_from() -> Weight;15011502 /// The price of burning a token from another user.1503 fn burn_from() -> Weight;15041505 /// Differs from burn_item in case of Fungible and Refungible, as it should burn1506 /// whole users's balance.1507 ///1508 /// This method shouldn't be used directly, as it doesn't count breadth price, use [burn_recursively](CommonWeightInfo::burn_recursively) instead1509 fn burn_recursively_self_raw() -> Weight;15101511 /// Cost of iterating over `amount` children while burning, without counting child burning itself.1512 ///1513 /// This method shouldn't be used directly, as it doesn't count depth price, use [burn_recursively](CommonWeightInfo::burn_recursively) instead1514 fn burn_recursively_breadth_raw(amount: u32) -> Weight;15151516 /// The price of recursive burning a token.1517 ///1518 /// `max_selfs` - The maximum burning weight of the token itself.1519 /// `max_breadth` - The maximum number of nested tokens to burn.1520 fn burn_recursively(max_selfs: u32, max_breadth: u32) -> Weight {1521 Self::burn_recursively_self_raw()1522 .saturating_mul(max_selfs.max(1) as u64)1523 .saturating_add(Self::burn_recursively_breadth_raw(max_breadth))1524 }15251526 /// The price of retrieving token owner1527 fn token_owner() -> Weight;1528}15291530/// Weight info extension trait for refungible pallet.1531pub trait RefungibleExtensionsWeightInfo {1532 /// Weight of token repartition.1533 fn repartition() -> Weight;1534}15351536/// Common collection operations.1537///1538/// It wraps methods in Fungible, Nonfungible and Refungible pallets1539/// and adds weight info.1540pub trait CommonCollectionOperations<T: Config> {1541 /// Create token.1542 ///1543 /// * `sender` - The user who mint the token and pays for the transaction.1544 /// * `to` - The user who will own the token.1545 /// * `data` - Token data.1546 /// * `nesting_budget` - A budget that can be spent on nesting tokens.1547 fn create_item(1548 &self,1549 sender: T::CrossAccountId,1550 to: T::CrossAccountId,1551 data: CreateItemData,1552 nesting_budget: &dyn Budget,1553 ) -> DispatchResultWithPostInfo;15541555 /// Create multiple tokens.1556 ///1557 /// * `sender` - The user who mint the token and pays for the transaction.1558 /// * `to` - The user who will own the token.1559 /// * `data` - Token data.1560 /// * `nesting_budget` - A budget that can be spent on nesting tokens.1561 fn create_multiple_items(1562 &self,1563 sender: T::CrossAccountId,1564 to: T::CrossAccountId,1565 data: Vec<CreateItemData>,1566 nesting_budget: &dyn Budget,1567 ) -> DispatchResultWithPostInfo;15681569 /// Create multiple tokens.1570 ///1571 /// * `sender` - The user who mint the token and pays for the transaction.1572 /// * `to` - The user who will own the token.1573 /// * `data` - Token data.1574 /// * `nesting_budget` - A budget that can be spent on nesting tokens.1575 fn create_multiple_items_ex(1576 &self,1577 sender: T::CrossAccountId,1578 data: CreateItemExData<T::CrossAccountId>,1579 nesting_budget: &dyn Budget,1580 ) -> DispatchResultWithPostInfo;15811582 /// Burn token.1583 ///1584 /// * `sender` - The user who owns the token.1585 /// * `token` - Token id that will burned.1586 /// * `amount` - The number of parts of the token that will be burned.1587 fn burn_item(1588 &self,1589 sender: T::CrossAccountId,1590 token: TokenId,1591 amount: u128,1592 ) -> DispatchResultWithPostInfo;15931594 /// Burn token and all nested tokens recursievly.1595 ///1596 /// * `sender` - The user who owns the token.1597 /// * `token` - Token id that will burned.1598 /// * `self_budget` - The budget that can be spent on burning tokens.1599 /// * `breadth_budget` - The budget that can be spent on burning nested tokens.1600 fn burn_item_recursively(1601 &self,1602 sender: T::CrossAccountId,1603 token: TokenId,1604 self_budget: &dyn Budget,1605 breadth_budget: &dyn Budget,1606 ) -> DispatchResultWithPostInfo;16071608 /// Set collection properties.1609 ///1610 /// * `sender` - Must be either the owner of the collection or its admin.1611 /// * `properties` - Properties to be set.1612 fn set_collection_properties(1613 &self,1614 sender: T::CrossAccountId,1615 properties: Vec<Property>,1616 ) -> DispatchResultWithPostInfo;16171618 /// Delete collection properties.1619 ///1620 /// * `sender` - Must be either the owner of the collection or its admin.1621 /// * `properties` - The properties to be removed.1622 fn delete_collection_properties(1623 &self,1624 sender: &T::CrossAccountId,1625 property_keys: Vec<PropertyKey>,1626 ) -> DispatchResultWithPostInfo;16271628 /// Set token properties.1629 ///1630 /// The appropriate [`PropertyPermission`] for the token property1631 /// must be set with [`Self::set_token_property_permissions`].1632 ///1633 /// * `sender` - Must be either the owner of the token or its admin.1634 /// * `token_id` - The token for which the properties are being set.1635 /// * `properties` - Properties to be set.1636 /// * `budget` - Budget for setting properties.1637 fn set_token_properties(1638 &self,1639 sender: T::CrossAccountId,1640 token_id: TokenId,1641 properties: Vec<Property>,1642 budget: &dyn Budget,1643 ) -> DispatchResultWithPostInfo;16441645 /// Remove token properties.1646 ///1647 /// The appropriate [`PropertyPermission`] for the token property1648 /// must be set with [`Self::set_token_property_permissions`].1649 ///1650 /// * `sender` - Must be either the owner of the token or its admin.1651 /// * `token_id` - The token for which the properties are being remove.1652 /// * `property_keys` - Keys to remove corresponding properties.1653 /// * `budget` - Budget for removing properties.1654 fn delete_token_properties(1655 &self,1656 sender: T::CrossAccountId,1657 token_id: TokenId,1658 property_keys: Vec<PropertyKey>,1659 budget: &dyn Budget,1660 ) -> DispatchResultWithPostInfo;16611662 /// Set token property permissions.1663 ///1664 /// * `sender` - Must be either the owner of the token or its admin.1665 /// * `token_id` - The token for which the properties are being set.1666 /// * `property_permissions` - Property permissions to be set.1667 /// * `budget` - Budget for setting properties.1668 fn set_token_property_permissions(1669 &self,1670 sender: &T::CrossAccountId,1671 property_permissions: Vec<PropertyKeyPermission>,1672 ) -> DispatchResultWithPostInfo;16731674 /// Transfer amount of token pieces.1675 ///1676 /// * `sender` - Donor user.1677 /// * `to` - Recepient user.1678 /// * `token` - The token of which parts are being sent.1679 /// * `amount` - The number of parts of the token that will be transferred.1680 /// * `budget` - The maximum budget that can be spent on the transfer.1681 fn transfer(1682 &self,1683 sender: T::CrossAccountId,1684 to: T::CrossAccountId,1685 token: TokenId,1686 amount: u128,1687 budget: &dyn Budget,1688 ) -> DispatchResultWithPostInfo;16891690 /// Grant access to another account to transfer parts of the token owned by the calling user via [Self::transfer_from].1691 ///1692 /// * `sender` - The user who grants access to the token.1693 /// * `spender` - The user to whom the rights are granted.1694 /// * `token` - The token to which access is granted.1695 /// * `amount` - The amount of pieces that another user can dispose of.1696 fn approve(1697 &self,1698 sender: T::CrossAccountId,1699 spender: T::CrossAccountId,1700 token: TokenId,1701 amount: u128,1702 ) -> DispatchResultWithPostInfo;17031704 /// Send parts of a token owned by another user.1705 ///1706 /// Before calling this method, you must grant rights to the calling user via [`Self::approve`].1707 ///1708 /// * `sender` - The user who must have access to the token (see [`Self::approve`]).1709 /// * `from` - The user who owns the token.1710 /// * `to` - Recepient user.1711 /// * `token` - The token of which parts are being sent.1712 /// * `amount` - The number of parts of the token that will be transferred.1713 /// * `budget` - The maximum budget that can be spent on the transfer.1714 fn transfer_from(1715 &self,1716 sender: T::CrossAccountId,1717 from: T::CrossAccountId,1718 to: T::CrossAccountId,1719 token: TokenId,1720 amount: u128,1721 budget: &dyn Budget,1722 ) -> DispatchResultWithPostInfo;17231724 /// Burn parts of a token owned by another user.1725 ///1726 /// Before calling this method, you must grant rights to the calling user via [`Self::approve`].1727 ///1728 /// * `sender` - The user who must have access to the token (see [`Self::approve`]).1729 /// * `from` - The user who owns the token.1730 /// * `token` - The token of which parts are being sent.1731 /// * `amount` - The number of parts of the token that will be transferred.1732 /// * `budget` - The maximum budget that can be spent on the burn.1733 fn burn_from(1734 &self,1735 sender: T::CrossAccountId,1736 from: T::CrossAccountId,1737 token: TokenId,1738 amount: u128,1739 budget: &dyn Budget,1740 ) -> DispatchResultWithPostInfo;17411742 /// Check permission to nest token.1743 ///1744 /// * `sender` - The user who initiated the check.1745 /// * `from` - The token that is checked for embedding.1746 /// * `under` - Token under which to check.1747 /// * `budget` - The maximum budget that can be spent on the check.1748 fn check_nesting(1749 &self,1750 sender: T::CrossAccountId,1751 from: (CollectionId, TokenId),1752 under: TokenId,1753 budget: &dyn Budget,1754 ) -> DispatchResult;17551756 /// Nest one token into another.1757 ///1758 /// * `under` - Token holder.1759 /// * `to_nest` - Nested token.1760 fn nest(&self, under: TokenId, to_nest: (CollectionId, TokenId));17611762 /// Unnest token.1763 ///1764 /// * `under` - Token holder.1765 /// * `to_nest` - Token to unnest.1766 fn unnest(&self, under: TokenId, to_nest: (CollectionId, TokenId));17671768 /// Get all user tokens.1769 ///1770 /// * `account` - Account for which you need to get tokens.1771 fn account_tokens(&self, account: T::CrossAccountId) -> Vec<TokenId>;17721773 /// Get all the tokens in the collection.1774 fn collection_tokens(&self) -> Vec<TokenId>;17751776 /// Check if the token exists.1777 ///1778 /// * `token` - Id token to check.1779 fn token_exists(&self, token: TokenId) -> bool;17801781 /// Get the id of the last minted token.1782 fn last_token_id(&self) -> TokenId;17831784 /// Get the owner of the token.1785 ///1786 /// * `token` - The token for which you need to find out the owner.1787 fn token_owner(&self, token: TokenId) -> Option<T::CrossAccountId>;17881789 /// Returns 10 tokens owners in no particular order.1790 ///1791 /// * `token` - The token for which you need to find out the owners.1792 fn token_owners(&self, token: TokenId) -> Vec<T::CrossAccountId>;17931794 /// Get the value of the token property by key.1795 ///1796 /// * `token` - Token with the property to get.1797 /// * `key` - Property name.1798 fn token_property(&self, token_id: TokenId, key: &PropertyKey) -> Option<PropertyValue>;17991800 /// Get a set of token properties by key vector.1801 ///1802 /// * `token` - Token with the property to get.1803 /// * `keys` - Vector of property keys. If this parameter is [None](sp_std::result::Result),1804 /// then all properties are returned.1805 fn token_properties(&self, token: TokenId, keys: Option<Vec<PropertyKey>>) -> Vec<Property>;18061807 /// Amount of unique collection tokens1808 fn total_supply(&self) -> u32;18091810 /// Amount of different tokens account has.1811 ///1812 /// * `account` - The account for which need to get the balance.1813 fn account_balance(&self, account: T::CrossAccountId) -> u32;18141815 /// Amount of specific token account have.1816 fn balance(&self, account: T::CrossAccountId, token: TokenId) -> u128;18171818 /// Amount of token pieces1819 fn total_pieces(&self, token: TokenId) -> Option<u128>;18201821 /// Get the number of parts of the token that a trusted user can manage.1822 ///1823 /// * `sender` - Trusted user.1824 /// * `spender` - Owner of the token.1825 /// * `token` - The token for which to get the value.1826 fn allowance(1827 &self,1828 sender: T::CrossAccountId,1829 spender: T::CrossAccountId,1830 token: TokenId,1831 ) -> u128;18321833 /// Get extension for RFT collection.1834 fn refungible_extensions(&self) -> Option<&dyn RefungibleExtensions<T>>;1835}18361837/// Extension for RFT collection.1838pub trait RefungibleExtensions<T>1839where1840 T: Config,1841{1842 /// Change the number of parts of the token.1843 ///1844 /// When the value changes down, this function is equivalent to burning parts of the token.1845 ///1846 /// * `sender` - The user calling the repartition operation. Must be the owner of the token.1847 /// * `token` - The token for which you want to change the number of parts.1848 /// * `amount` - The new value of the parts of the token.1849 fn repartition(1850 &self,1851 sender: &T::CrossAccountId,1852 token: TokenId,1853 amount: u128,1854 ) -> DispatchResultWithPostInfo;1855}18561857/// Merge [`DispatchResult`] with [`Weight`] into [`DispatchResultWithPostInfo`].1858///1859/// Used for [`CommonCollectionOperations`] implementations and flexible enough to do so.1860pub fn with_weight(res: DispatchResult, weight: Weight) -> DispatchResultWithPostInfo {1861 let post_info = PostDispatchInfo {1862 actual_weight: Some(weight),1863 pays_fee: Pays::Yes,1864 };1865 match res {1866 Ok(()) => Ok(post_info),1867 Err(error) => Err(DispatchErrorWithPostInfo { post_info, error }),1868 }1869}18701871impl<T: Config> From<PropertiesError> for Error<T> {1872 fn from(error: PropertiesError) -> Self {1873 match error {1874 PropertiesError::NoSpaceForProperty => Self::NoSpaceForProperty,1875 PropertiesError::PropertyLimitReached => Self::PropertyLimitReached,1876 PropertiesError::InvalidCharacterInPropertyKey => Self::InvalidCharacterInPropertyKey,1877 PropertiesError::PropertyKeyIsTooLong => Self::PropertyKeyIsTooLong,1878 PropertiesError::EmptyPropertyKey => Self::EmptyPropertyKey,1879 }1880 }1881}pallets/evm-contract-helpers/src/stubs/ContractHelpers.rawdiffbeforeafterbothbinary blob — no preview
pallets/fungible/src/erc.rsdiffbeforeafterboth--- 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<T: Config> FungibleHandle<T> {
+impl<T: Config> FungibleHandle<T>
+where
+ T::AccountId: From<[u8; 32]>,
+{
+ #[weight(<SelfWeightOf<T>>::approve())]
+ fn approve_cross(
+ &mut self,
+ caller: caller,
+ spender: (address, uint256),
+ amount: uint256,
+ ) -> Result<bool> {
+ let caller = T::CrossAccountId::from_eth(caller);
+ let spender = convert_tuple_to_cross_account::<T>(spender)?;
+ let amount = amount.try_into().map_err(|_| "amount overflow")?;
+
+ <Pallet<T>>::set_allowance(self, &caller, &spender, amount)
+ .map_err(dispatch_to_evm::<T>)?;
+ 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(<SelfWeightOf<T>>::burn_from())]
+ fn burn_from_cross(
+ &mut self,
+ caller: caller,
+ from: (address, uint256),
+ amount: uint256,
+ ) -> Result<bool> {
+ let caller = T::CrossAccountId::from_eth(caller);
+ let from = convert_tuple_to_cross_account::<T>(from)?;
+ let amount = amount.try_into().map_err(|_| "amount overflow")?;
+ let budget = self
+ .recorder
+ .weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+ <Pallet<T>>::burn_from(self, &caller, &from, amount, &budget)
+ .map_err(dispatch_to_evm::<T>)?;
+ Ok(true)
+ }
+
/// Mint tokens for multiple accounts.
/// @param amounts array of pairs of account address and amount
#[weight(<SelfWeightOf<T>>::create_multiple_items_ex(amounts.len() as u32))]
@@ -191,6 +235,27 @@
.map_err(dispatch_to_evm::<T>)?;
Ok(true)
}
+
+ #[weight(<SelfWeightOf<T>>::transfer_from())]
+ fn transfer_from_cross(
+ &mut self,
+ caller: caller,
+ from: (address, uint256),
+ to: (address, uint256),
+ amount: uint256,
+ ) -> Result<bool> {
+ let caller = T::CrossAccountId::from_eth(caller);
+ let from = convert_tuple_to_cross_account::<T>(from)?;
+ let to = convert_tuple_to_cross_account::<T>(to)?;
+ let amount = amount.try_into().map_err(|_| "amount overflow")?;
+ let budget = self
+ .recorder
+ .weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+ <Pallet<T>>::transfer_from(self, &caller, &from, &to, amount, &budget)
+ .map_err(dispatch_to_evm::<T>)?;
+ Ok(true)
+ }
}
#[solidity_interface(
pallets/fungible/src/stubs/UniqueFungible.rawdiffbeforeafterbothbinary blob — no preview
pallets/fungible/src/stubs/UniqueFungible.soldiffbeforeafterboth--- 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
pallets/nonfungible/CHANGELOG.mddiffbeforeafterboth--- 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.
+<!-- bureaucrate goes here -->
+
+## [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`.
-<!-- bureaucrate goes here -->
+- 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.
pallets/nonfungible/Cargo.tomldiffbeforeafterboth--- 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"
pallets/nonfungible/src/erc.rsdiffbeforeafterboth--- 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 = <Vec<u8>>::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::<T>)
}
+ /// @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(<SelfWeightOf<T>>::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(<StructureWeight<T>>::find_parent());
+
+ let properties = properties
+ .into_iter()
+ .map(|(key, value)| {
+ let key = <Vec<u8>>::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::<Result<Vec<_>>>()?;
+
+ <Pallet<T>>::set_token_properties(
+ self,
+ &caller,
+ TokenId(token_id),
+ properties.into_iter(),
+ <Pallet<T>>::token_exists(&self, TokenId(token_id)),
+ &nesting_budget,
+ )
+ .map_err(dispatch_to_evm::<T>)
+ }
+
/// @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 = <TokenProperties<T>>::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<T: Config> NonfungibleHandle<T> {
+impl<T: Config> NonfungibleHandle<T>
+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<T: Config> NonfungibleHandle<T> {
+impl<T: Config> NonfungibleHandle<T>
+where
+ T::AccountId: From<[u8; 32]>,
+{
/// @notice A descriptive name for a collection of NFTs in this contract
fn name(&self) -> Result<string> {
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(<SelfWeightOf<T>>::approve())]
+ fn approve_cross(
+ &mut self,
+ caller: caller,
+ approved: (address, uint256),
+ token_id: uint256,
+ ) -> Result<void> {
+ let caller = T::CrossAccountId::from_eth(caller);
+ let approved = convert_tuple_to_cross_account::<T>(approved)?;
+ let token = token_id.try_into()?;
+
+ <Pallet<T>>::set_allowance(self, &caller, token, Some(&approved))
+ .map_err(dispatch_to_evm::<T>)?;
+ 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(<SelfWeightOf<T>>::transfer())]
+ fn transfer_from_cross(
+ &mut self,
+ caller: caller,
+ from: (address, uint256),
+ to: (address, uint256),
+ token_id: uint256,
+ ) -> Result<void> {
+ let caller = T::CrossAccountId::from_eth(caller);
+ let from = convert_tuple_to_cross_account::<T>(from)?;
+ let to = convert_tuple_to_cross_account::<T>(to)?;
+ let token_id = token_id.try_into()?;
+ let budget = self
+ .recorder
+ .weight_calls_budget(<StructureWeight<T>>::find_parent());
+ Pallet::<T>::transfer_from(self, &caller, &from, &to, token_id, &budget)
+ .map_err(dispatch_to_evm::<T>)?;
+ 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(<SelfWeightOf<T>>::burn_from())]
+ fn burn_from_cross(
+ &mut self,
+ caller: caller,
+ from: (address, uint256),
+ token_id: uint256,
+ ) -> Result<void> {
+ let caller = T::CrossAccountId::from_eth(caller);
+ let from = convert_tuple_to_cross_account::<T>(from)?;
+ let token = token_id.try_into()?;
+ let budget = self
+ .recorder
+ .weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+ <Pallet<T>>::burn_from(self, &caller, &from, token, &budget)
+ .map_err(dispatch_to_evm::<T>)?;
+ Ok(())
+ }
+
/// @notice Returns next free NFT ID.
fn next_token_id(&self) -> Result<uint256> {
self.consume_store_reads(1)?;
pallets/nonfungible/src/stubs/UniqueNFT.rawdiffbeforeafterbothbinary blob — no preview
pallets/nonfungible/src/stubs/UniqueNFT.soldiffbeforeafterboth--- 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
pallets/refungible/CHANGELOG.mddiffbeforeafterboth--- 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`.
+
<!-- bureaucrate goes here -->
+
## [v0.2.3] 2022-08-16
### Other changes
pallets/refungible/Cargo.tomldiffbeforeafterboth--- 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"
pallets/refungible/src/erc.rsdiffbeforeafterboth--- 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 = <Vec<u8>>::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::<T>)
}
+ /// @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(<StructureWeight<T>>::find_parent());
+
+ let properties = properties
+ .into_iter()
+ .map(|(key, value)| {
+ let key = <Vec<u8>>::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::<Result<Vec<_>>>()?;
+
+ <Pallet<T>>::set_token_properties(
+ self,
+ &caller,
+ TokenId(token_id),
+ properties.into_iter(),
+ <Pallet<T>>::token_exists(&self, TokenId(token_id)),
+ &nesting_budget,
+ )
+ .map_err(dispatch_to_evm::<T>)
+ }
+
/// @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 = <TokenProperties<T>>::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<T: Config> RefungibleHandle<T> {
+impl<T: Config> RefungibleHandle<T>
+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<T: Config> RefungibleHandle<T> {
+impl<T: Config> RefungibleHandle<T>
+where
+ T::AccountId: From<[u8; 32]>,
+{
/// @notice A descriptive name for a collection of NFTs in this contract
fn name(&self) -> Result<string> {
Ok(decode_utf16(self.name.iter().copied())
@@ -659,14 +707,44 @@
.recorder
.weight_calls_budget(<StructureWeight<T>>::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)?;
<Pallet<T>>::transfer(self, &caller, &to, token, balance, &budget)
.map_err(dispatch_to_evm::<T>)?;
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(<SelfWeightOf<T>>::transfer_creating_removing())]
+ fn transfer_from_cross(
+ &mut self,
+ caller: caller,
+ from: (address, uint256),
+ to: (address, uint256),
+ token_id: uint256,
+ ) -> Result<void> {
+ let caller = T::CrossAccountId::from_eth(caller);
+ let from = convert_tuple_to_cross_account::<T>(from)?;
+ let to = convert_tuple_to_cross_account::<T>(to)?;
+ let token_id = token_id.try_into()?;
+ let budget = self
+ .recorder
+ .weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+ let balance = balance(self, token_id, &from)?;
+ ensure_single_owner(self, token_id, balance)?;
+
+ Pallet::<T>::transfer_from(self, &caller, &from, &to, token_id, balance, &budget)
+ .map_err(dispatch_to_evm::<T>)?;
+ 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(<StructureWeight<T>>::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)?;
+
+ <Pallet<T>>::burn_from(self, &caller, &from, token, balance, &budget)
+ .map_err(dispatch_to_evm::<T>)?;
+ 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(<SelfWeightOf<T>>::burn_from())]
+ fn burn_from_cross(
+ &mut self,
+ caller: caller,
+ from: (address, uint256),
+ token_id: uint256,
+ ) -> Result<void> {
+ let caller = T::CrossAccountId::from_eth(caller);
+ let from = convert_tuple_to_cross_account::<T>(from)?;
+ let token = token_id.try_into()?;
+ let budget = self
+ .recorder
+ .weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+ let balance = balance(self, token, &from)?;
+ ensure_single_owner(self, token, balance)?;
<Pallet<T>>::burn_from(self, &caller, &from, token, balance, &budget)
.map_err(dispatch_to_evm::<T>)?;
pallets/refungible/src/stubs/UniqueRefungible.rawdiffbeforeafterbothbinary blob — no preview
pallets/refungible/src/stubs/UniqueRefungible.soldiffbeforeafterboth--- 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
pallets/refungible/src/stubs/UniqueRefungibleToken.rawdiffbeforeafterbothbinary blob — no preview
pallets/unique/CHANGELOG.mddiffbeforeafterboth--- a/pallets/unique/CHANGELOG.md
+++ b/pallets/unique/CHANGELOG.md
@@ -4,33 +4,40 @@
<!-- bureaucrate goes here -->
+## [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.
pallets/unique/Cargo.tomldiffbeforeafterboth--- 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']
pallets/unique/src/eth/mod.rsdiffbeforeafterboth--- 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<T: Config>(SubstrateRecorder<T>);
@@ -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<T: Config>(
caller: caller,
value: value,
name: string,
+ collection_mode: CollectionMode,
description: string,
token_prefix: string,
) -> Result<address> {
@@ -97,7 +97,7 @@
convert_data::<T>(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<address> {
- self.create_nft_collection(caller, value, name, description, token_prefix)
+ create_collection_internal::<T>(
+ caller,
+ value,
+ name,
+ CollectionMode::NFT,
+ description,
+ token_prefix,
+ )
}
#[weight(<SelfWeightOf<T>>::create_collection())]
@@ -206,7 +213,35 @@
description: string,
token_prefix: string,
) -> Result<address> {
- create_refungible_collection_internal::<T>(caller, value, name, description, token_prefix)
+ create_collection_internal::<T>(
+ caller,
+ value,
+ name,
+ CollectionMode::ReFungible,
+ description,
+ token_prefix,
+ )
+ }
+
+ #[weight(<SelfWeightOf<T>>::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<address> {
+ create_collection_internal::<T>(
+ caller,
+ value,
+ name,
+ CollectionMode::Fungible(decimals),
+ description,
+ token_prefix,
+ )
}
#[solidity(rename_selector = "makeCollectionERC721MetadataCompatible")]
@@ -296,6 +331,16 @@
Ok(())
}
+ #[weight(<SelfWeightOf<T>>::destroy_collection())]
+ fn destroy_collection(&mut self, caller: caller, collection_address: address) -> Result<void> {
+ 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")?;
+ <Pallet<T>>::destroy_collection_internal(caller, collection_id)
+ .map_err(pallet_evm_coder_substrate::dispatch_to_evm::<T>)
+ }
+
/// Check if a collection exists
/// @param collectionAddress Address of the collection in question
/// @return bool Does the collection exist?
pallets/unique/src/eth/stubs/CollectionHelpers.rawdiffbeforeafterbothbinary blob — no preview
pallets/unique/src/eth/stubs/CollectionHelpers.soldiffbeforeafterboth--- 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?
pallets/unique/src/lib.rsdiffbeforeafterboth--- a/pallets/unique/src/lib.rs
+++ b/pallets/unique/src/lib.rs
@@ -362,25 +362,8 @@
#[weight = <SelfWeightOf<T>>::destroy_collection()]
pub fn destroy_collection(origin, collection_id: CollectionId) -> DispatchResult {
let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);
- let collection = <CollectionHandle<T>>::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 _ = <NftTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
- let _ = <FungibleTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
- let _ = <ReFungibleTransferBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);
-
- let _ = <NftApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
- let _ = <FungibleApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
- let _ = <RefungibleApproveBasket<T>>::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 = <CollectionHandle<T>>::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 _ = <NftTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
+ let _ = <FungibleTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
+ let _ = <ReFungibleTransferBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);
+
+ let _ = <NftApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
+ let _ = <FungibleApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
+ let _ = <RefungibleApproveBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);
+
+ Ok(())
+ }
}
tests/.vscode/settings.jsondiffbeforeafterboth--- 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"
+ }
}
tests/src/eth/allowlist.test.tsdiffbeforeafterboth--- 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;
+ //});
});
tests/src/eth/api/CollectionHelpers.soldiffbeforeafterboth--- 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?
tests/src/eth/api/UniqueFungible.soldiffbeforeafterboth--- 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
tests/src/eth/api/UniqueNFT.soldiffbeforeafterboth--- 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
tests/src/eth/api/UniqueRefungible.soldiffbeforeafterboth--- 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
tests/src/eth/base.test.tsdiffbeforeafterboth--- 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}) => {
tests/src/eth/collectionAdmin.test.tsdiffbeforeafterboth--- 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');
tests/src/eth/collectionHelpersAbi.jsondiffbeforeafterboth--- 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",
tests/src/eth/collectionProperties.test.tsdiffbeforeafterboth--- 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');
+ });
+
+});
tests/src/eth/createFTCollection.test.tsdiffbeforeafterboth--- /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 <http://www.gnu.org/licenses/>.
+
+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"');
+ });
+});
tests/src/eth/createNFTCollection.test.tsdiffbeforeafterboth--- 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
tests/src/eth/createRFTCollection.test.tsdiffbeforeafterboth--- 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;
+ });
});
tests/src/eth/destroyCollection.test.tsdiffbeforeafterboth--- /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 <http://www.gnu.org/licenses/>.
+
+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;
+ });
+});
tests/src/eth/fractionalizer/Fractionalizer.soldiffbeforeafterboth--- 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);
}
tests/src/eth/fungible.test.tsdiffbeforeafterboth--- 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',
+ },
+ }});
+ });
});
tests/src/eth/fungibleAbi.jsondiffbeforeafterboth--- 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" }],
tests/src/eth/nonFungible.test.tsdiffbeforeafterboth--- 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();
tests/src/eth/nonFungibleAbi.jsondiffbeforeafterboth--- 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" }],
tests/src/eth/reFungible.test.tsdiffbeforeafterboth--- 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();
tests/src/eth/reFungibleAbi.jsondiffbeforeafterboth--- 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" }],
tests/src/eth/tokenProperties.test.tsdiffbeforeafterboth--- 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);
tests/src/eth/util/playgrounds/types.tsdiffbeforeafterboth--- 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[];
+
tests/src/eth/util/playgrounds/unique.dev.tsdiffbeforeafterboth--- a/tests/src/eth/util/playgrounds/unique.dev.ts
+++ b/tests/src/eth/util/playgrounds/unique.dev.ts
@@ -18,7 +18,7 @@
import {DevUniqueHelper} from '../../../util/playgrounds/unique.dev';
-import {ContractImports, CompiledContract, NormalizedEvent} from './types';
+import {ContractImports, CompiledContract, TEthCrossAccount, NormalizedEvent, EthProperty} from './types';
// Native contracts ABI
import collectionHelpersAbi from '../../collectionHelpersAbi.json';
@@ -28,6 +28,7 @@
import refungibleTokenAbi from '../../reFungibleTokenAbi.json';
import contractHelpersAbi from './../contractHelpersAbi.json';
import {ICrossAccountId, TEthereumAccount} from '../../../util/playgrounds/types';
+import {TCollectionMode} from '../../../util/playgrounds/types';
class EthGroupBase {
helper: EthUniqueHelper;
@@ -107,7 +108,7 @@
return new web3.eth.Contract(collectionHelpersAbi as any, '0x6c4e9fe1ae37a41e93cee429e8e1881abdcbb54f', {from: caller, gas: this.helper.eth.DEFAULT_GAS});
}
- collection(address: string, mode: 'nft' | 'rft' | 'ft', caller?: string): Contract {
+ collection(address: string, mode: TCollectionMode, caller?: string): Contract {
const abi = {
'nft': nonFungibleAbi,
'rft': refungibleAbi,
@@ -173,49 +174,59 @@
async callEVM(signer: TEthereumAccount, contractAddress: string, abi: string) {
return await this.helper.callRpc('api.rpc.eth.call', [{from: signer, to: contractAddress, data: abi}]);
}
-
- async createNFTCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{collectionId: number, collectionAddress: string}> {
+
+ async createCollecion(functionName: string, signer: string, name: string, description: string, tokenPrefix: string): Promise<{ collectionId: number, collectionAddress: string, events: NormalizedEvent[] }> {
const collectionCreationPrice = this.helper.balance.getCollectionCreationPrice();
const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer);
+
+ const result = await collectionHelper.methods[functionName](name, description, tokenPrefix).send({value: Number(collectionCreationPrice)});
- const result = await collectionHelper.methods.createNFTCollection(name, description, tokenPrefix).send({value: Number(collectionCreationPrice)});
-
const collectionAddress = this.helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId);
const collectionId = this.helper.ethAddress.extractCollectionId(collectionAddress);
-
- return {collectionId, collectionAddress};
+ const events = this.helper.eth.normalizeEvents(result.events);
+
+ return {collectionId, collectionAddress, events};
+ }
+
+ async createNFTCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{ collectionId: number, collectionAddress: string, events: NormalizedEvent[] }> {
+ return this.createCollecion('createNFTCollection', signer, name, description, tokenPrefix);
}
- async createERC721MetadataCompatibleNFTCollection(signer: string, name: string, description: string, tokenPrefix: string, baseUri: string): Promise<{collectionId: number, collectionAddress: string}> {
+ async createERC721MetadataCompatibleNFTCollection(signer: string, name: string, description: string, tokenPrefix: string, baseUri: string): Promise<{collectionId: number, collectionAddress: string, events: NormalizedEvent[] }> {
const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer);
- const {collectionId, collectionAddress} = await this.createNFTCollection(signer, name, description, tokenPrefix);
+ const {collectionId, collectionAddress, events} = await this.createCollecion('createNFTCollection', signer, name, description, tokenPrefix);
await collectionHelper.methods.makeCollectionERC721MetadataCompatible(collectionAddress, baseUri).send();
- return {collectionId, collectionAddress};
+ return {collectionId, collectionAddress, events};
}
- async createRFTCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{collectionId: number, collectionAddress: string}> {
+ async createRFTCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{collectionId: number, collectionAddress: string, events: NormalizedEvent[]}> {
+ return this.createCollecion('createRFTCollection', signer, name, description, tokenPrefix);
+ }
+
+ async createFungibleCollection(signer: string, name: string, decimals: number, description: string, tokenPrefix: string): Promise<{ collectionId: number, collectionAddress: string, events: NormalizedEvent[]}> {
const collectionCreationPrice = this.helper.balance.getCollectionCreationPrice();
const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer);
-
- const result = await collectionHelper.methods.createRFTCollection(name, description, tokenPrefix).send({value: Number(collectionCreationPrice)});
-
+
+ const result = await collectionHelper.methods.createFTCollection(name, decimals, description, tokenPrefix).send({value: Number(collectionCreationPrice)});
const collectionAddress = this.helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId);
const collectionId = this.helper.ethAddress.extractCollectionId(collectionAddress);
-
- return {collectionId, collectionAddress};
+
+ const events = this.helper.eth.normalizeEvents(result.events);
+
+ return {collectionId, collectionAddress, events};
}
-
- async createERC721MetadataCompatibleRFTCollection(signer: string, name: string, description: string, tokenPrefix: string, baseUri: string): Promise<{collectionId: number, collectionAddress: string}> {
+
+ async createERC721MetadataCompatibleRFTCollection(signer: string, name: string, description: string, tokenPrefix: string, baseUri: string): Promise<{collectionId: number, collectionAddress: string, events: NormalizedEvent[] }> {
const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer);
- const {collectionId, collectionAddress} = await this.createRFTCollection(signer, name, description, tokenPrefix);
+ const {collectionId, collectionAddress, events} = await this.createCollecion('createRFTCollection', signer, name, description, tokenPrefix);
await collectionHelper.methods.makeCollectionERC721MetadataCompatible(collectionAddress, baseUri).send();
- return {collectionId, collectionAddress};
+ return {collectionId, collectionAddress, events};
}
async deployCollectorContract(signer: string): Promise<Contract> {
@@ -339,10 +350,38 @@
normalizeAddress(address: string): string {
return '0x' + address.substring(address.length - 40);
}
+}
+
+export class EthPropertyGroup extends EthGroupBase {
+ property(key: string, value: string): EthProperty {
+ return [
+ key,
+ '0x'+Buffer.from(value).toString('hex'),
+ ];
+ }
}
-
export type EthUniqueHelperConstructor = new (...args: any[]) => EthUniqueHelper;
+export class EthCrossAccountGroup extends EthGroupBase {
+ fromAddress(address: TEthereumAccount): TEthCrossAccount {
+ return {
+ 0: address,
+ 1: '0',
+ field_0: address,
+ field_1: '0',
+ };
+ }
+
+ fromKeyringPair(keyring: IKeyringPair): TEthCrossAccount {
+ return {
+ 0: '0x0000000000000000000000000000000000000000',
+ 1: keyring.addressRaw,
+ field_0: '0x0000000000000000000000000000000000000000',
+ field_1: keyring.addressRaw,
+ };
+ }
+}
+
export class EthUniqueHelper extends DevUniqueHelper {
web3: Web3 | null = null;
web3Provider: WebsocketProvider | null = null;
@@ -351,6 +390,8 @@
ethAddress: EthAddressGroup;
ethNativeContract: NativeContractGroup;
ethContract: ContractGroup;
+ ethCrossAccount: EthCrossAccountGroup;
+ ethProperty: EthPropertyGroup;
constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {
options.helperBase = options.helperBase ?? EthUniqueHelper;
@@ -358,8 +399,10 @@
super(logger, options);
this.eth = new EthGroup(this);
this.ethAddress = new EthAddressGroup(this);
+ this.ethCrossAccount = new EthCrossAccountGroup(this);
this.ethNativeContract = new NativeContractGroup(this);
this.ethContract = new ContractGroup(this);
+ this.ethProperty = new EthPropertyGroup(this);
}
getWeb3(): Web3 {
tests/src/util/playgrounds/types.tsdiffbeforeafterboth--- 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';
tests/src/util/playgrounds/unique.tsdiffbeforeafterboth--- a/tests/src/util/playgrounds/unique.ts
+++ b/tests/src/util/playgrounds/unique.ts
@@ -9,7 +9,36 @@
import {ApiInterfaceEvents, SignerOptions} from '@polkadot/api/types';
import {encodeAddress, decodeAddress, keccakAsHex, evmToAddress, addressToEvm} from '@polkadot/util-crypto';
import {IKeyringPair} from '@polkadot/types/types';
-import {IApiListeners, IBlock, IEvent, IChainProperties, ICollectionCreationOptions, ICollectionLimits, ICollectionPermissions, ICrossAccountId, ICrossAccountIdLower, ILogger, INestingPermissions, IProperty, IStakingInfo, ISchedulerOptions, ISubstrateBalance, IToken, ITokenPropertyPermission, ITransactionResult, IUniqueHelperLog, TApiAllowedListeners, TEthereumAccount, TSigner, TSubstrateAccount, IForeignAssetMetadata, TNetworks, MoonbeamAssetInfo, DemocracyStandardAccountVote, AcalaAssetMetadata} from './types';
+import {
+ IApiListeners,
+ IBlock,
+ IEvent,
+ IChainProperties,
+ ICollectionCreationOptions,
+ ICollectionLimits,
+ ICollectionPermissions,
+ ICrossAccountId,
+ ICrossAccountIdLower,
+ ILogger,
+ INestingPermissions,
+ IProperty,
+ IStakingInfo,
+ ISchedulerOptions,
+ ISubstrateBalance,
+ IToken,
+ ITokenPropertyPermission,
+ ITransactionResult,
+ IUniqueHelperLog,
+ TApiAllowedListeners,
+ TEthereumAccount,
+ TSigner,
+ TSubstrateAccount,
+ TNetworks,
+ IForeignAssetMetadata,
+ AcalaAssetMetadata,
+ MoonbeamAssetInfo,
+ DemocracyStandardAccountVote,
+} from './types';
export class CrossAccountId implements ICrossAccountId {
Substrate?: TSubstrateAccount;