From d8467f134b10e8f94dba2ed92af6df67f58ac3b5 Mon Sep 17 00:00:00 2001 From: Max Andreev Date: Tue, 17 Jan 2023 05:36:29 +0000 Subject: [PATCH] Merge branch 'develop' into tests/generalization --- --- a/.docker/Dockerfile-chain-dev +++ b/.docker/Dockerfile-chain-dev @@ -1,4 +1,4 @@ -FROM ubuntu:20.04 +FROM uniquenetwork/services:latest ARG RUST_TOOLCHAIN ARG NETWORK @@ -10,10 +10,6 @@ ENV PATH="/cargo-home/bin:$PATH" RUN echo "$NETWORK\n" && echo "$RUST_TOOLCHAIN\n" - -RUN apt-get update && apt-get install -y git curl libssl-dev llvm pkg-config libclang-dev clang git make cmake protobuf-compiler - -RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain none RUN rustup toolchain uninstall $(rustup toolchain list) && \ rustup toolchain install $RUST_TOOLCHAIN && \ --- a/.docker/Dockerfile-chain-dev-unit +++ b/.docker/Dockerfile-chain-dev-unit @@ -1,14 +1,8 @@ -FROM ubuntu:20.04 +FROM uniquenetwork/services:latest ENV DEBIAN_FRONTEND=noninteractive -ENV TZ=Etc/UTC - -RUN apt-get update && apt-get install -y git curl libssl-dev llvm pkg-config libclang-dev clang git make cmake protobuf-compiler - ENV CARGO_HOME="/cargo-home" ENV PATH="/cargo-home/bin:$PATH" - -RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain none ARG RUST_TOOLCHAIN ARG NETWORK --- a/.docker/Dockerfile-parachain +++ b/.docker/Dockerfile-parachain @@ -2,24 +2,13 @@ FROM uniquenetwork/builder-polkadot:${POLKADOT_BUILD_BRANCH} as polkadot # ===== Rust builder ===== -FROM ubuntu:20.04 as rust-builder -LABEL maintainer="Unique.Network" - +FROM uniquenetwork/services:latest as rust-builder ARG RUST_TOOLCHAIN= ENV RUST_TOOLCHAIN $RUST_TOOLCHAIN ENV CARGO_HOME="/cargo-home" ENV PATH="/cargo-home/bin:$PATH" ENV TZ=UTC -RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone - -RUN apt-get update && \ - apt-get install -y curl cmake pkg-config libssl-dev git clang protobuf-compiler && \ - apt-get clean && \ - rm -r /var/lib/apt/lists/* - -RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain none - RUN rustup toolchain uninstall $(rustup toolchain list) && \ rustup toolchain install $RUST_TOOLCHAIN && \ rustup default $RUST_TOOLCHAIN && \ --- a/.docker/Dockerfile-parachain-node-only +++ b/.docker/Dockerfile-parachain-node-only @@ -2,24 +2,13 @@ FROM uniquenetwork/builder-polkadot:${POLKADOT_BUILD_BRANCH} as polkadot # ===== Rust builder ===== -FROM ubuntu:20.04 as rust-builder -LABEL maintainer="Unique.Network" +FROM uniquenetwork/services:latest as rust-builder ARG RUST_TOOLCHAIN= - ENV RUST_TOOLCHAIN $RUST_TOOLCHAIN ENV CARGO_HOME="/cargo-home" ENV PATH="/cargo-home/bin:$PATH" ENV TZ=UTC -RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone - -RUN apt-get update && \ - apt-get install -y curl cmake pkg-config libssl-dev git clang protobuf-compiler && \ - apt-get clean && \ - rm -r /var/lib/apt/lists/* - -RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain none - RUN rustup toolchain uninstall $(rustup toolchain list) && \ rustup toolchain install $RUST_TOOLCHAIN && \ rustup default $RUST_TOOLCHAIN && \ --- a/.docker/Dockerfile-parachain-upgrade +++ b/.docker/Dockerfile-parachain-upgrade @@ -2,8 +2,7 @@ FROM uniquenetwork/builder-polkadot:${POLKADOT_BUILD_BRANCH} as polkadot # ===== Rust builder ===== -FROM ubuntu:20.04 as rust-builder -LABEL maintainer="Unique.Network" +FROM uniquenetwork/services:latest as rust-builder ARG RUST_TOOLCHAIN= @@ -11,15 +10,6 @@ ENV CARGO_HOME="/cargo-home" ENV PATH="/cargo-home/bin:$PATH" ENV TZ=UTC -RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone - -RUN apt-get update && \ - apt-get install -y curl cmake pkg-config libssl-dev git clang protobuf-compiler && \ - apt-get clean && \ - rm -r /var/lib/apt/lists/* - -RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain none - RUN rustup toolchain uninstall $(rustup toolchain list) && \ rustup toolchain install $RUST_TOOLCHAIN && \ rustup default $RUST_TOOLCHAIN && \ --- a/.docker/Dockerfile-parachain-upgrade-data +++ b/.docker/Dockerfile-parachain-upgrade-data @@ -2,23 +2,13 @@ FROM uniquenetwork/builder-polkadot:${POLKADOT_BUILD_BRANCH} as polkadot # ===== Rust builder ===== -FROM ubuntu:20.04 as rust-builder -LABEL maintainer="Unique.Network" - +FROM uniquenetwork/services:latest as rust-builder ARG RUST_TOOLCHAIN= ENV RUST_TOOLCHAIN $RUST_TOOLCHAIN ENV CARGO_HOME="/cargo-home" ENV PATH="/cargo-home/bin:$PATH" ENV TZ=UTC -RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone - -RUN apt-get update && \ - apt-get install -y curl cmake pkg-config libssl-dev git clang protobuf-compiler && \ - apt-get clean && \ - rm -r /var/lib/apt/lists/* - -RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain none RUN rustup toolchain uninstall $(rustup toolchain list) && \ rustup toolchain install $RUST_TOOLCHAIN && \ --- a/.docker/Dockerfile-testnet.j2 +++ b/.docker/Dockerfile-testnet.j2 @@ -1,18 +1,11 @@ # ===== Rust builder ===== -FROM ubuntu:20.04 as rust-builder -LABEL maintainer="Unique.Network" +FROM uniquenetwork/services:latest as rust-builder +ARG RUST_TOOLCHAIN= +ENV RUST_TOOLCHAIN $RUST_TOOLCHAIN ENV CARGO_HOME="/cargo-home" ENV PATH="/cargo-home/bin:$PATH" ENV TZ=UTC -RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone - -RUN apt-get update && \ - apt-get install -y curl cmake pkg-config libssl-dev git clang llvm libudev-dev protobuf-compiler && \ - apt-get clean && \ - rm -r /var/lib/apt/lists/* - -RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain none RUN rustup toolchain uninstall $(rustup toolchain list) && \ rustup toolchain install {{ RUST_TOOLCHAIN }} && \ --- a/.docker/Dockerfile-try-runtime +++ b/.docker/Dockerfile-try-runtime @@ -1,6 +1,5 @@ # ===== Rust builder ===== -FROM ubuntu:20.04 as rust-builder -LABEL maintainer="Unique.Network" +FROM uniquenetwork/services:latest as rust-builder ARG RUST_TOOLCHAIN @@ -8,15 +7,6 @@ ENV CARGO_HOME="/cargo-home" ENV PATH="/cargo-home/bin:$PATH" ENV TZ=UTC -RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone - - -RUN apt-get update && \ - apt-get install -y curl cmake pkg-config libssl-dev git clang protobuf-compiler && \ - apt-get clean && \ - rm -r /var/lib/apt/lists/* - -RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain none RUN rustup toolchain uninstall $(rustup toolchain list) && \ rustup toolchain install $RUST_TOOLCHAIN && \ --- a/.docker/Dockerfile-xcm.j2 +++ b/.docker/Dockerfile-xcm.j2 @@ -1,18 +1,8 @@ # ===== Rust builder ===== -FROM ubuntu:20.04 as rust-builder -LABEL maintainer="Unique.Network" - +FROM uniquenetwork/services:latest as rust-builder ENV CARGO_HOME="/cargo-home" ENV PATH="/cargo-home/bin:$PATH" ENV TZ=UTC -RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone - -RUN apt-get update && \ - apt-get install -y curl cmake pkg-config libssl-dev git clang llvm libudev-dev protobuf-compiler && \ - apt-get clean && \ - rm -r /var/lib/apt/lists/* - -RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain none RUN rustup toolchain uninstall $(rustup toolchain list) && \ rustup toolchain install {{ RUST_TOOLCHAIN }} && \ --- a/Cargo.lock +++ b/Cargo.lock @@ -2340,7 +2340,7 @@ [[package]] name = "evm-coder-procedural" -version = "0.2.2" +version = "0.2.3" dependencies = [ "Inflector", "hex", @@ -2467,7 +2467,7 @@ [[package]] name = "fc-consensus" version = "2.0.0-dev" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.36#cf1894629c7df1c4dafe58aa773627a3d940da14" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.36#ef8d2809ed05da0664e9b43cb87436451295b634" dependencies = [ "async-trait", "fc-db", @@ -2486,7 +2486,7 @@ [[package]] name = "fc-db" version = "2.0.0-dev" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.36#cf1894629c7df1c4dafe58aa773627a3d940da14" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.36#ef8d2809ed05da0664e9b43cb87436451295b634" dependencies = [ "fp-storage", "kvdb-rocksdb", @@ -2505,7 +2505,7 @@ [[package]] name = "fc-mapping-sync" version = "2.0.0-dev" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.36#cf1894629c7df1c4dafe58aa773627a3d940da14" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.36#ef8d2809ed05da0664e9b43cb87436451295b634" dependencies = [ "fc-db", "fp-consensus", @@ -2522,7 +2522,7 @@ [[package]] name = "fc-rpc" version = "2.0.0-dev" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.36#cf1894629c7df1c4dafe58aa773627a3d940da14" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.36#ef8d2809ed05da0664e9b43cb87436451295b634" dependencies = [ "ethereum 0.14.0", "ethereum-types 0.14.1", @@ -2565,7 +2565,7 @@ [[package]] name = "fc-rpc-core" version = "1.1.0-dev" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.36#cf1894629c7df1c4dafe58aa773627a3d940da14" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.36#ef8d2809ed05da0664e9b43cb87436451295b634" dependencies = [ "ethereum 0.14.0", "ethereum-types 0.14.1", @@ -2730,7 +2730,7 @@ [[package]] name = "fp-consensus" version = "2.0.0-dev" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.36#cf1894629c7df1c4dafe58aa773627a3d940da14" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.36#ef8d2809ed05da0664e9b43cb87436451295b634" dependencies = [ "ethereum 0.14.0", "parity-scale-codec 3.2.1", @@ -2742,7 +2742,7 @@ [[package]] name = "fp-ethereum" version = "1.0.0-dev" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.36#cf1894629c7df1c4dafe58aa773627a3d940da14" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.36#ef8d2809ed05da0664e9b43cb87436451295b634" dependencies = [ "ethereum 0.14.0", "ethereum-types 0.14.1", @@ -2757,7 +2757,7 @@ [[package]] name = "fp-evm" version = "3.0.0-dev" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.36#cf1894629c7df1c4dafe58aa773627a3d940da14" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.36#ef8d2809ed05da0664e9b43cb87436451295b634" dependencies = [ "evm", "frame-support", @@ -2771,7 +2771,7 @@ [[package]] name = "fp-evm-mapping" version = "0.1.0" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.36#cf1894629c7df1c4dafe58aa773627a3d940da14" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.36#ef8d2809ed05da0664e9b43cb87436451295b634" dependencies = [ "frame-support", "sp-core", @@ -2780,7 +2780,7 @@ [[package]] name = "fp-rpc" version = "3.0.0-dev" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.36#cf1894629c7df1c4dafe58aa773627a3d940da14" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.36#ef8d2809ed05da0664e9b43cb87436451295b634" dependencies = [ "ethereum 0.14.0", "ethereum-types 0.14.1", @@ -2797,7 +2797,7 @@ [[package]] name = "fp-self-contained" version = "1.0.0-dev" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.36#cf1894629c7df1c4dafe58aa773627a3d940da14" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.36#ef8d2809ed05da0664e9b43cb87436451295b634" dependencies = [ "ethereum 0.14.0", "frame-support", @@ -2810,7 +2810,7 @@ [[package]] name = "fp-storage" version = "2.0.0" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.36#cf1894629c7df1c4dafe58aa773627a3d940da14" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.36#ef8d2809ed05da0664e9b43cb87436451295b634" dependencies = [ "parity-scale-codec 3.2.1", "serde", @@ -5687,7 +5687,7 @@ [[package]] name = "pallet-base-fee" version = "1.0.0" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.36#cf1894629c7df1c4dafe58aa773627a3d940da14" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.36#ef8d2809ed05da0664e9b43cb87436451295b634" dependencies = [ "fp-evm", "frame-support", @@ -5952,7 +5952,7 @@ [[package]] name = "pallet-ethereum" version = "4.0.0-dev" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.36#cf1894629c7df1c4dafe58aa773627a3d940da14" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.36#ef8d2809ed05da0664e9b43cb87436451295b634" dependencies = [ "ethereum 0.14.0", "ethereum-types 0.14.1", @@ -5980,7 +5980,7 @@ [[package]] name = "pallet-evm" version = "6.0.0-dev" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.36#cf1894629c7df1c4dafe58aa773627a3d940da14" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.36#ef8d2809ed05da0664e9b43cb87436451295b634" dependencies = [ "environmental", "evm", @@ -6066,7 +6066,7 @@ [[package]] name = "pallet-evm-precompile-simple" version = "2.0.0-dev" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.36#cf1894629c7df1c4dafe58aa773627a3d940da14" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.36#ef8d2809ed05da0664e9b43cb87436451295b634" dependencies = [ "fp-evm", "ripemd", --- a/crates/evm-coder/procedural/Cargo.toml +++ b/crates/evm-coder/procedural/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "evm-coder-procedural" -version = "0.2.2" +version = "0.2.3" license = "GPLv3" edition = "2021" --- a/crates/evm-coder/procedural/src/solidity_interface.rs +++ b/crates/evm-coder/procedural/src/solidity_interface.rs @@ -811,6 +811,13 @@ let list = generics_list(gen); quote! { <#list> } } +fn generics_stub(gen: &Generics) -> proc_macro2::TokenStream { + if gen.params.is_empty() { + return quote! {}; + } + let params = (0..gen.params.len()).map(|_| quote! {()}); + quote! {<#(#params,)*>} +} fn generics_data(gen: &Generics) -> proc_macro2::TokenStream { let list = generics_list(gen); if gen.params.len() == 1 { @@ -866,6 +873,7 @@ let generics = self.generics; let gen_ref = generics_reference(&generics); let gen_data = generics_data(&generics); + let gen_stub = generics_stub(&generics); let gen_where = &generics.where_clause; let call_sub = self @@ -936,6 +944,12 @@ let solidity_events_idents = self.info.events.0.iter().map(|is| is.name.clone()); let docs = &self.docs; + let expect_selector = self.info.expect_selector.map(|s| { + quote! { + const _: () = assert!(#s == u32::from_be_bytes(<#call_name #gen_stub>::interface_id()), "selector mismatch, review contained function selectors"); + } + }); + quote! { #( const _: ::core::marker::PhantomData<#solidity_events_idents> = ::core::marker::PhantomData; @@ -952,12 +966,15 @@ #call_sub, )* } + + #expect_selector + impl #gen_ref #call_name #gen_ref { #( #consts )* /// Return this call ERC165 selector - pub fn interface_id() -> ::evm_coder::types::bytes4 { + pub const fn interface_id() -> ::evm_coder::types::bytes4 { let mut interface_id = 0; #(#interface_id)* #(#inline_interface_id)* --- a/crates/evm-coder/procedural/src/to_log.rs +++ b/crates/evm-coder/procedural/src/to_log.rs @@ -16,7 +16,6 @@ use inflector::cases; use syn::{Data, DeriveInput, Field, Fields, Ident, Variant, spanned::Spanned}; -use std::fmt::Write; use quote::quote; use crate::{parse_ident_from_path, parse_ident_from_type, snake_ident_to_screaming}; @@ -61,13 +60,13 @@ name: Ident, name_screaming: Ident, fields: Vec, - selector: [u8; 32], - selector_str: String, + selector: proc_macro2::TokenStream, } impl Event { fn try_from(variant: &Variant) -> syn::Result { let name = &variant.ident; + let name_lit = proc_macro2::Literal::string(name.to_string().as_str()); let name_screaming = snake_ident_to_screaming(name); let named = match &variant.fields { @@ -89,22 +88,40 @@ "events can have at most 4 indexed fields (1 indexed field is reserved for event signature)" )); } - let mut selector_str = format!("{}(", name); - for (i, arg) in fields.iter().enumerate() { - if i != 0 { - write!(selector_str, ",").unwrap(); + + let args = fields.iter().map(|f| { + let ty = &f.ty; + quote! {nameof(<#ty as ::evm_coder::abi::AbiType>::SIGNATURE) fixed(",")} + }); + // Remove trailing comma + let shift = (!fields.is_empty()).then(|| quote! {shift_left(1)}); + + let signature = quote! { ::evm_coder::make_signature!(new fixed(#name_lit) fixed("(") #(#args)* #shift fixed(")")) }; + let selector = quote! { + { + let signature = #signature; + let mut sum = ::evm_coder::sha3_const::Keccak256::new(); + let mut pos = 0; + while pos < signature.len { + sum = sum.update(&[signature.data[pos]; 1]); + pos += 1; + } + let a = sum.finalize(); + let mut selector_bytes = [0; 32]; + let mut i = 0; + while i != 32 { + selector_bytes[i] = a[i]; + i += 1; + } + selector_bytes } - write!(selector_str, "{}", arg.ty).unwrap(); - } - selector_str.push(')'); - let selector = crate::event_selector_str(&selector_str); + }; Ok(Self { name: name.to_owned(), name_screaming, fields, selector, - selector_str, }) } @@ -133,14 +150,10 @@ fn expand_consts(&self) -> proc_macro2::TokenStream { let name_screaming = &self.name_screaming; - let selector_str = &self.selector_str; let selector = &self.selector; quote! { - #[doc = #selector_str] - const #name_screaming: [u8; 32] = [#( - #selector, - )*]; + const #name_screaming: [u8; 32] = #selector; } } --- a/node/cli/src/chain_spec.rs +++ b/node/cli/src/chain_spec.rs @@ -143,7 +143,7 @@ AccountPublic::from(get_from_seed::(seed)).into_account() } -#[cfg(feature = "quartz-runtime")] +#[cfg(not(feature = "unique-runtime"))] macro_rules! testnet_genesis { ( $runtime:path, @@ -207,7 +207,7 @@ }}; } -#[cfg(not(feature = "quartz-runtime"))] +#[cfg(feature = "unique-runtime")] macro_rules! testnet_genesis { ( $runtime:path, --- a/pallets/common/src/benchmarking.rs +++ b/pallets/common/src/benchmarking.rs @@ -176,7 +176,7 @@ key: property_key(p as usize), value: property_value(), }).collect::>(); - }: {>::set_collection_properties(&collection, &owner, props)?} + }: {>::set_collection_properties(&collection, &owner, props.into_iter())?} delete_collection_properties { let b in 0..MAX_PROPERTIES_PER_ITEM; @@ -188,7 +188,7 @@ key: property_key(p as usize), value: property_value(), }).collect::>(); - >::set_collection_properties(&collection, &owner, props)?; + >::set_collection_properties(&collection, &owner, props.into_iter())?; let to_delete = (0..b).map(|p| property_key(p as usize)).collect::>(); - }: {>::delete_collection_properties(&collection, &owner, to_delete)?} + }: {>::delete_collection_properties(&collection, &owner, to_delete.into_iter())?} } --- a/pallets/common/src/erc.rs +++ b/pallets/common/src/erc.rs @@ -125,7 +125,7 @@ .map(eth::Property::try_into) .collect::>>()?; - >::set_collection_properties(self, &caller, properties) + >::set_collection_properties(self, &caller, properties.into_iter()) .map_err(dispatch_to_evm::) } @@ -158,7 +158,8 @@ }) .collect::>>()?; - >::delete_collection_properties(self, &caller, keys).map_err(dispatch_to_evm::) + >::delete_collection_properties(self, &caller, keys.into_iter()) + .map_err(dispatch_to_evm::) } /// Get collection property. --- a/pallets/common/src/lib.rs +++ b/pallets/common/src/lib.rs @@ -390,8 +390,10 @@ Ok(()) } - /// Return **true** if `user` was not allowed to have tokens, and he can ignore such restrictions. - pub fn ignores_allowance(&self, user: &T::CrossAccountId) -> bool { + /// Returns **true** if + /// * the `user`is a collection owner or admin + /// * the collection limits allow the owner/admins to transfer/burn any collection token + pub fn ignores_token_restrictions(&self, user: &T::CrossAccountId) -> bool { self.limits.owner_can_transfer() && self.is_owner_or_admin(user) } @@ -1198,6 +1200,58 @@ Ok(()) } + /// This function sets or removes a collection properties according to + /// `properties_updates` contents: + /// * sets a property under the with the value provided `(, Some())` + /// * removes a property under the if the value is `None` `(, None)`. + /// + /// This function fires an event for each property change. + /// In case of an error, all the changes (including the events) will be reverted + /// since the function is transactional. + #[transactional] + fn modify_collection_properties( + collection: &CollectionHandle, + sender: &T::CrossAccountId, + properties_updates: impl Iterator)>, + ) -> DispatchResult { + collection.check_is_owner_or_admin(sender)?; + + let mut stored_properties = >::get(collection.id); + + for (key, value) in properties_updates { + match value { + Some(value) => { + stored_properties + .try_set(key.clone(), value) + .map_err(>::from)?; + + Self::deposit_event(Event::CollectionPropertySet(collection.id, key)); + >::deposit_log( + erc::CollectionHelpersEvents::CollectionChanged { + collection_id: eth::collection_id_to_address(collection.id), + } + .to_log(T::ContractAddress::get()), + ); + } + None => { + stored_properties.remove(&key).map_err(>::from)?; + + Self::deposit_event(Event::CollectionPropertyDeleted(collection.id, key)); + >::deposit_log( + erc::CollectionHelpersEvents::CollectionChanged { + collection_id: eth::collection_id_to_address(collection.id), + } + .to_log(T::ContractAddress::get()), + ); + } + } + } + + >::set(collection.id, stored_properties); + + Ok(()) + } + /// Set collection property. /// /// * `collection` - Collection handler. @@ -1208,23 +1262,7 @@ sender: &T::CrossAccountId, property: Property, ) -> DispatchResult { - collection.check_is_owner_or_admin(sender)?; - - CollectionProperties::::try_mutate(collection.id, |properties| { - let property = property.clone(); - properties.try_set(property.key, property.value) - }) - .map_err(>::from)?; - - Self::deposit_event(Event::CollectionPropertySet(collection.id, property.key)); - >::deposit_log( - erc::CollectionHelpersEvents::CollectionChanged { - collection_id: eth::collection_id_to_address(collection.id), - } - .to_log(T::ContractAddress::get()), - ); - - Ok(()) + Self::set_collection_properties(collection, sender, [property].into_iter()) } /// Set a scoped collection property, where the scope is a special prefix @@ -1270,17 +1308,16 @@ /// * `collection` - Collection handler. /// * `sender` - The owner or administrator of the collection. /// * `properties` - The properties to set. - #[transactional] pub fn set_collection_properties( collection: &CollectionHandle, sender: &T::CrossAccountId, - properties: Vec, + properties: impl Iterator, ) -> DispatchResult { - for property in properties { - Self::set_collection_property(collection, sender, property)?; - } - - Ok(()) + Self::modify_collection_properties( + collection, + sender, + properties.map(|property| (property.key, Some(property.value))), + ) } /// Delete collection property. @@ -1293,25 +1330,7 @@ sender: &T::CrossAccountId, property_key: PropertyKey, ) -> DispatchResult { - collection.check_is_owner_or_admin(sender)?; - - CollectionProperties::::try_mutate(collection.id, |properties| { - properties.remove(&property_key) - }) - .map_err(>::from)?; - - Self::deposit_event(Event::CollectionPropertyDeleted( - collection.id, - property_key, - )); - >::deposit_log( - erc::CollectionHelpersEvents::CollectionChanged { - collection_id: eth::collection_id_to_address(collection.id), - } - .to_log(T::ContractAddress::get()), - ); - - Ok(()) + Self::delete_collection_properties(collection, sender, [property_key].into_iter()) } /// Delete collection properties. @@ -1319,17 +1338,12 @@ /// * `collection` - Collection handler. /// * `sender` - The owner or administrator of the collection. /// * `properties` - The properties to delete. - #[transactional] pub fn delete_collection_properties( collection: &CollectionHandle, sender: &T::CrossAccountId, - property_keys: Vec, + property_keys: impl Iterator, ) -> DispatchResult { - for key in property_keys { - Self::delete_collection_property(collection, sender, key)?; - } - - Ok(()) + Self::modify_collection_properties(collection, sender, property_keys.map(|key| (key, None))) } /// Set collection propetry permission without any checks. --- a/pallets/fungible/src/lib.rs +++ b/pallets/fungible/src/lib.rs @@ -266,7 +266,7 @@ sender: &T::CrossAccountId, properties: Vec, ) -> DispatchResult { - >::set_collection_properties(collection, sender, properties) + >::set_collection_properties(collection, sender, properties.into_iter()) } /// Delete properties of the collection, associated with the provided keys. @@ -275,7 +275,11 @@ sender: &T::CrossAccountId, property_keys: Vec, ) -> DispatchResult { - >::delete_collection_properties(collection, sender, property_keys) + >::delete_collection_properties( + collection, + sender, + property_keys.into_iter(), + ) } /// Checks if collection has tokens. Return `true` if it has. @@ -673,8 +677,14 @@ // `from`, `to` checked in [`transfer`] collection.check_allowlist(spender)?; } + + if collection.ignores_token_restrictions(spender) { + return Ok(Self::compute_allowance_decrease( + collection, from, spender, amount, + )); + } + if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) { - // TODO: should collection owner be allowed to perform this transfer? ensure!( >::check_indirectly_owned( spender.clone(), @@ -687,17 +697,24 @@ ); return Ok(None); } - let allowance = >::get((collection.id, from, spender)).checked_sub(amount); - if allowance.is_none() { - ensure!( - collection.ignores_allowance(spender), - >::ApprovedValueTooLow - ); - } + let allowance = Self::compute_allowance_decrease(collection, from, spender, amount); + ensure!(allowance.is_some(), >::ApprovedValueTooLow); + Ok(allowance) } + /// Returns `Some(amount)` if the `spender` have allowance to spend this amount. + /// Otherwise, it returns `None`. + fn compute_allowance_decrease( + collection: &FungibleHandle, + from: &T::CrossAccountId, + spender: &T::CrossAccountId, + amount: u128, + ) -> Option { + >::get((collection.id, from, spender)).checked_sub(amount) + } + /// Transfer fungible tokens from one account to another. /// Same as the [`transfer`][`Pallet::transfer`] but spender doesn't needs to be an owner of the token pieces. /// The owner should set allowance for the spender to transfer pieces. --- a/pallets/nonfungible/src/erc.rs +++ b/pallets/nonfungible/src/erc.rs @@ -524,11 +524,6 @@ Ok(>::allowance_for_all(self, &owner, &operator)) } - - /// @notice Returns collection helper contract address - fn collection_helper_address(&self) -> Result
{ - Ok(T::ContractAddress::get()) - } } /// @title ERC721 Token that can be irreversibly burned (destroyed). @@ -1058,6 +1053,11 @@ Ok(token_id.into()) } + + /// @notice Returns collection helper contract address + fn collection_helper_address(&self) -> Result
{ + Ok(T::ContractAddress::get()) + } } #[solidity_interface( --- a/pallets/nonfungible/src/lib.rs +++ b/pallets/nonfungible/src/lib.rs @@ -577,20 +577,29 @@ }) } - /// Batch operation to add, edit or remove properties for the token - /// - /// All affected properties should have mutable permission and sender should have - /// permission to edit those properties. + /// A batch operation to add, edit or remove properties for a token. + /// It sets or removes a token's properties according to + /// `properties_updates` contents: + /// * sets a property under the with the value provided `(, Some())` + /// * removes a property under the if the value is `None` `(, None)`. /// - /// - `nesting_budget`: Limit for searching parents in depth to check ownership. + /// - `nesting_budget`: Limit for searching parents in-depth to check ownership. /// - `is_token_create`: Indicates that method is called during token initialization. /// Allows to bypass ownership check. + /// + /// All affected properties should have `mutable` permission + /// to be **deleted** or to be **set more than once**, + /// and the sender should have permission to edit those properties. + /// + /// This function fires an event for each property change. + /// In case of an error, all the changes (including the events) will be reverted + /// since the function is transactional. #[transactional] fn modify_token_properties( collection: &NonfungibleHandle, sender: &T::CrossAccountId, token_id: TokenId, - properties: impl Iterator)>, + properties_updates: impl Iterator)>, is_token_create: bool, nesting_budget: &dyn Budget, ) -> DispatchResult { @@ -614,15 +623,16 @@ }) }; - for (key, value) in properties { - let permission = >::property_permissions(collection.id) + let mut stored_properties = >::get((collection.id, token_id)); + let permissions = >::property_permissions(collection.id); + + for (key, value) in properties_updates { + let permission = permissions .get(&key) .cloned() .unwrap_or_else(PropertyPermission::none); - let is_property_exists = TokenProperties::::get((collection.id, token_id)) - .get(&key) - .is_some(); + let is_property_exists = stored_properties.get(&key).is_some(); match permission { PropertyPermission { mutable: false, .. } if is_property_exists => { @@ -649,10 +659,9 @@ match value { Some(value) => { - >::try_mutate((collection.id, token_id), |properties| { - properties.try_set(key.clone(), value) - }) - .map_err(>::from)?; + stored_properties + .try_set(key.clone(), value) + .map_err(>::from)?; >::deposit_event(CommonEvent::TokenPropertySet( collection.id, @@ -661,10 +670,9 @@ )); } None => { - >::try_mutate((collection.id, token_id), |properties| { - properties.remove(&key) - }) - .map_err(>::from)?; + stored_properties + .remove(&key) + .map_err(>::from)?; >::deposit_event(CommonEvent::TokenPropertyDeleted( collection.id, @@ -683,6 +691,8 @@ ); } + >::set((collection.id, token_id), stored_properties); + Ok(()) } @@ -784,7 +794,7 @@ sender: &T::CrossAccountId, properties: Vec, ) -> DispatchResult { - >::set_collection_properties(collection, sender, properties) + >::set_collection_properties(collection, sender, properties.into_iter()) } /// Remove properties from the collection @@ -793,7 +803,11 @@ sender: &T::CrossAccountId, property_keys: Vec, ) -> DispatchResult { - >::delete_collection_properties(collection, sender, property_keys) + >::delete_collection_properties( + collection, + sender, + property_keys.into_iter(), + ) } /// Set property permissions for the token. @@ -1232,7 +1246,7 @@ collection.check_allowlist(spender)?; } - if collection.limits.owner_can_transfer() && collection.is_owner_or_admin(spender) { + if collection.ignores_token_restrictions(spender) { return Ok(()); } @@ -1255,11 +1269,8 @@ if >::get((collection.id, from, spender)) { return Ok(()); } - ensure!( - collection.ignores_allowance(spender), - >::ApprovedValueTooLow - ); - Ok(()) + + Err(>::ApprovedValueTooLow.into()) } /// Transfer NFT token from one account to another. --- a/pallets/nonfungible/src/stubs/UniqueNFT.sol +++ b/pallets/nonfungible/src/stubs/UniqueNFT.sol @@ -785,7 +785,7 @@ } /// @title Unique extensions for ERC721. -/// @dev the ERC-165 identifier for this interface is 0x0e48fdb4 +/// @dev the ERC-165 identifier for this interface is 0x16de3152 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, @@ -986,6 +986,15 @@ dummy = 0; return 0; } + + /// @notice Returns collection helper contract address + /// @dev EVM selector for this function is: 0x1896cce6, + /// or in textual repr: collectionHelperAddress() + function collectionHelperAddress() public view returns (address) { + require(false, stub_error); + dummy; + return 0x0000000000000000000000000000000000000000; + } } /// @dev anonymous struct @@ -1043,7 +1052,7 @@ /// @title ERC-721 Non-Fungible Token Standard /// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md -/// @dev the ERC-165 identifier for this interface is 0x983a942b +/// @dev the ERC-165 identifier for this interface is 0x80ac58cd contract ERC721 is Dummy, ERC165, ERC721Events { /// @notice Count all NFTs assigned to an owner /// @dev NFTs assigned to the zero address are considered invalid, and this @@ -1175,15 +1184,6 @@ operator; dummy; return false; - } - - /// @notice Returns collection helper contract address - /// @dev EVM selector for this function is: 0x1896cce6, - /// or in textual repr: collectionHelperAddress() - function collectionHelperAddress() public view returns (address) { - require(false, stub_error); - dummy; - return 0x0000000000000000000000000000000000000000; } } --- a/pallets/nonfungible/src/weights.rs +++ b/pallets/nonfungible/src/weights.rs @@ -184,21 +184,21 @@ .saturating_add(T::DbWeight::get().reads(1 as u64)) .saturating_add(T::DbWeight::get().writes(1 as u64)) } + // Storage: Nonfungible TokenProperties (r:1 w:1) // Storage: Common CollectionPropertyPermissions (r:1 w:0) - // Storage: Nonfungible TokenProperties (r:1 w:1) fn set_token_properties(b: u32, ) -> Weight { - Weight::from_ref_time(4_361_000 as u64) - // Standard Error: 5_349_868 - .saturating_add(Weight::from_ref_time(637_246_356 as u64).saturating_mul(b as u64)) + Weight::from_ref_time(31_850_484 as u64) + // Standard Error: 9_618 + .saturating_add(Weight::from_ref_time(4_721_947 as u64).saturating_mul(b as u64)) .saturating_add(T::DbWeight::get().reads(2 as u64)) .saturating_add(T::DbWeight::get().writes(1 as u64)) } + // Storage: Nonfungible TokenProperties (r:1 w:1) // Storage: Common CollectionPropertyPermissions (r:1 w:0) - // Storage: Nonfungible TokenProperties (r:1 w:1) fn delete_token_properties(b: u32, ) -> Weight { - Weight::from_ref_time(4_489_000 as u64) - // Standard Error: 5_738_954 - .saturating_add(Weight::from_ref_time(689_912_822 as u64).saturating_mul(b as u64)) + Weight::from_ref_time(13_795_000 as u64) + // Standard Error: 28_239 + .saturating_add(Weight::from_ref_time(12_840_446 as u64).saturating_mul(b as u64)) .saturating_add(T::DbWeight::get().reads(2 as u64)) .saturating_add(T::DbWeight::get().writes(1 as u64)) } @@ -354,21 +354,21 @@ .saturating_add(RocksDbWeight::get().reads(1 as u64)) .saturating_add(RocksDbWeight::get().writes(1 as u64)) } + // Storage: Nonfungible TokenProperties (r:1 w:1) // Storage: Common CollectionPropertyPermissions (r:1 w:0) - // Storage: Nonfungible TokenProperties (r:1 w:1) fn set_token_properties(b: u32, ) -> Weight { - Weight::from_ref_time(4_361_000 as u64) - // Standard Error: 5_349_868 - .saturating_add(Weight::from_ref_time(637_246_356 as u64).saturating_mul(b as u64)) + Weight::from_ref_time(31_850_484 as u64) + // Standard Error: 9_618 + .saturating_add(Weight::from_ref_time(4_721_947 as u64).saturating_mul(b as u64)) .saturating_add(RocksDbWeight::get().reads(2 as u64)) .saturating_add(RocksDbWeight::get().writes(1 as u64)) } + // Storage: Nonfungible TokenProperties (r:1 w:1) // Storage: Common CollectionPropertyPermissions (r:1 w:0) - // Storage: Nonfungible TokenProperties (r:1 w:1) fn delete_token_properties(b: u32, ) -> Weight { - Weight::from_ref_time(4_489_000 as u64) - // Standard Error: 5_738_954 - .saturating_add(Weight::from_ref_time(689_912_822 as u64).saturating_mul(b as u64)) + Weight::from_ref_time(13_795_000 as u64) + // Standard Error: 28_239 + .saturating_add(Weight::from_ref_time(12_840_446 as u64).saturating_mul(b as u64)) .saturating_add(RocksDbWeight::get().reads(2 as u64)) .saturating_add(RocksDbWeight::get().writes(1 as u64)) } --- a/pallets/refungible/src/erc.rs +++ b/pallets/refungible/src/erc.rs @@ -295,7 +295,9 @@ MintingFinished {}, } -#[solidity_interface(name = ERC721Metadata)] +/// @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 RefungibleHandle where T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>, @@ -364,7 +366,7 @@ /// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension /// @dev See https://eips.ethereum.org/EIPS/eip-721 -#[solidity_interface(name = ERC721Enumerable)] +#[solidity_interface(name = ERC721Enumerable, expect_selector = 0x780e9d63)] impl RefungibleHandle { /// @notice Enumerate valid RFTs /// @param index A counter less than `totalSupply()` @@ -391,7 +393,7 @@ /// @title ERC-721 Non-Fungible Token Standard /// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md -#[solidity_interface(name = ERC721, events(ERC721Events))] +#[solidity_interface(name = ERC721, events(ERC721Events), expect_selector = 0x80ac58cd)] impl RefungibleHandle { /// @notice Count all RFTs assigned to an owner /// @dev RFTs assigned to the zero address are considered invalid, and this @@ -422,6 +424,7 @@ } /// @dev Not implemented + #[solidity(rename_selector = "safeTransferFrom")] fn safe_transfer_from_with_data( &mut self, _from: address, @@ -434,6 +437,7 @@ } /// @dev Not implemented + #[solidity(rename_selector = "safeTransferFrom")] fn safe_transfer_from( &mut self, _from: address, @@ -517,11 +521,6 @@ Ok(>::allowance_for_all(self, &owner, &operator)) } - - /// @notice Returns collection helper contract address - fn collection_helper_address(&self) -> Result
{ - Ok(T::ContractAddress::get()) - } } /// Returns amount of pieces of `token` that `owner` have @@ -1116,6 +1115,11 @@ token.try_into().map_err(|_| "token id overflow")?, )) } + + /// @notice Returns collection helper contract address + fn collection_helper_address(&self) -> Result
{ + Ok(T::ContractAddress::get()) + } } #[solidity_interface( --- a/pallets/refungible/src/lib.rs +++ b/pallets/refungible/src/lib.rs @@ -515,12 +515,29 @@ Ok(()) } + /// A batch operation to add, edit or remove properties for a token. + /// It sets or removes a token's properties according to + /// `properties_updates` contents: + /// * sets a property under the with the value provided `(, Some())` + /// * removes a property under the if the value is `None` `(, None)`. + /// + /// - `nesting_budget`: Limit for searching parents in-depth to check ownership. + /// - `is_token_create`: Indicates that method is called during token initialization. + /// Allows to bypass ownership check. + /// + /// All affected properties should have `mutable` permission + /// to be **deleted** or to be **set more than once**, + /// and the sender should have permission to edit those properties. + /// + /// This function fires an event for each property change. + /// In case of an error, all the changes (including the events) will be reverted + /// since the function is transactional. #[transactional] fn modify_token_properties( collection: &RefungibleHandle, sender: &T::CrossAccountId, token_id: TokenId, - properties: impl Iterator)>, + properties_updates: impl Iterator)>, is_token_create: bool, nesting_budget: &dyn Budget, ) -> DispatchResult { @@ -544,15 +561,16 @@ Ok(is_bundle_owner) }; - for (key, value) in properties { - let permission = >::property_permissions(collection.id) + let mut stored_properties = >::get((collection.id, token_id)); + let permissions = >::property_permissions(collection.id); + + for (key, value) in properties_updates { + let permission = permissions .get(&key) .cloned() .unwrap_or_else(PropertyPermission::none); - let is_property_exists = TokenProperties::::get((collection.id, token_id)) - .get(&key) - .is_some(); + let is_property_exists = stored_properties.get(&key).is_some(); match permission { PropertyPermission { mutable: false, .. } if is_property_exists => { @@ -578,10 +596,9 @@ match value { Some(value) => { - >::try_mutate((collection.id, token_id), |properties| { - properties.try_set(key.clone(), value) - }) - .map_err(>::from)?; + stored_properties + .try_set(key.clone(), value) + .map_err(>::from)?; >::deposit_event(CommonEvent::TokenPropertySet( collection.id, @@ -590,10 +607,9 @@ )); } None => { - >::try_mutate((collection.id, token_id), |properties| { - properties.remove(&key) - }) - .map_err(>::from)?; + stored_properties + .remove(&key) + .map_err(>::from)?; >::deposit_event(CommonEvent::TokenPropertyDeleted( collection.id, @@ -612,6 +628,8 @@ ); } + >::set((collection.id, token_id), stored_properties); + Ok(()) } @@ -1159,6 +1177,13 @@ // `from`, `to` checked in [`transfer`] collection.check_allowlist(spender)?; } + + if collection.ignores_token_restrictions(spender) { + return Ok(Self::compute_allowance_decrease( + collection, token, from, &spender, amount, + )); + } + if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) { // TODO: should collection owner be allowed to perform this transfer? ensure!( @@ -1173,21 +1198,30 @@ ); return Ok(None); } - let allowance = - >::get((collection.id, token, from, &spender)).checked_sub(amount); + let allowance = Self::compute_allowance_decrease(collection, token, from, &spender, amount); + if allowance.is_some() { + return Ok(allowance); + } + // Allowance (if any) would be reduced if spender is also wallet operator if >::get((collection.id, from, spender)) { return Ok(allowance); } - if allowance.is_none() { - ensure!( - collection.ignores_allowance(spender), - >::ApprovedValueTooLow - ); - } - Ok(allowance) + Err(>::ApprovedValueTooLow.into()) + } + + /// Returns `Some(amount)` if the `spender` have allowance to spend this amount. + /// Otherwise, it returns `None`. + fn compute_allowance_decrease( + collection: &RefungibleHandle, + token: TokenId, + from: &T::CrossAccountId, + spender: &T::CrossAccountId, + amount: u128, + ) -> Option { + >::get((collection.id, token, from, spender)).checked_sub(amount) } /// Transfer RFT token pieces from one account to another. @@ -1353,7 +1387,7 @@ sender: &T::CrossAccountId, properties: Vec, ) -> DispatchResult { - >::set_collection_properties(collection, sender, properties) + >::set_collection_properties(collection, sender, properties.into_iter()) } pub fn delete_collection_properties( @@ -1361,7 +1395,11 @@ sender: &T::CrossAccountId, property_keys: Vec, ) -> DispatchResult { - >::delete_collection_properties(collection, sender, property_keys) + >::delete_collection_properties( + collection, + sender, + property_keys.into_iter(), + ) } pub fn set_token_property_permissions( --- a/pallets/refungible/src/stubs/UniqueRefungible.sol +++ b/pallets/refungible/src/stubs/UniqueRefungible.sol @@ -641,6 +641,8 @@ TransferEnabled } +/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension +/// @dev See https://eips.ethereum.org/EIPS/eip-721 /// @dev the ERC-165 identifier for this interface is 0x5b5e139f contract ERC721Metadata is Dummy, ERC165 { // /// @notice A descriptive name for a collection of NFTs in this contract @@ -783,7 +785,7 @@ } /// @title Unique extensions for ERC721. -/// @dev the ERC-165 identifier for this interface is 0xabf30dc2 +/// @dev the ERC-165 identifier for this interface is 0xb365c124 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, @@ -985,6 +987,15 @@ dummy; return 0x0000000000000000000000000000000000000000; } + + /// @notice Returns collection helper contract address + /// @dev EVM selector for this function is: 0x1896cce6, + /// or in textual repr: collectionHelperAddress() + function collectionHelperAddress() public view returns (address) { + require(false, stub_error); + dummy; + return 0x0000000000000000000000000000000000000000; + } } /// @dev anonymous struct @@ -1042,7 +1053,7 @@ /// @title ERC-721 Non-Fungible Token Standard /// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md -/// @dev the ERC-165 identifier for this interface is 0x4016cd87 +/// @dev the ERC-165 identifier for this interface is 0x80ac58cd contract ERC721 is Dummy, ERC165, ERC721Events { /// @notice Count all RFTs assigned to an owner /// @dev RFTs assigned to the zero address are considered invalid, and this @@ -1075,9 +1086,9 @@ } /// @dev Not implemented - /// @dev EVM selector for this function is: 0x60a11672, - /// or in textual repr: safeTransferFromWithData(address,address,uint256,bytes) - function safeTransferFromWithData( + /// @dev EVM selector for this function is: 0xb88d4fde, + /// or in textual repr: safeTransferFrom(address,address,uint256,bytes) + function safeTransferFrom( address from, address to, uint256 tokenId, @@ -1172,15 +1183,6 @@ operator; dummy; return false; - } - - /// @notice Returns collection helper contract address - /// @dev EVM selector for this function is: 0x1896cce6, - /// or in textual repr: collectionHelperAddress() - function collectionHelperAddress() public view returns (address) { - require(false, stub_error); - dummy; - return 0x0000000000000000000000000000000000000000; } } --- a/pallets/refungible/src/weights.rs +++ b/pallets/refungible/src/weights.rs @@ -246,21 +246,21 @@ .saturating_add(T::DbWeight::get().reads(1 as u64)) .saturating_add(T::DbWeight::get().writes(1 as u64)) } + // Storage: Refungible TokenProperties (r:1 w:1) // Storage: Common CollectionPropertyPermissions (r:1 w:0) - // Storage: Refungible TokenProperties (r:1 w:1) fn set_token_properties(b: u32, ) -> Weight { - Weight::from_ref_time(4_578_000 as u64) - // Standard Error: 5_396_287 - .saturating_add(Weight::from_ref_time(633_314_546 as u64).saturating_mul(b as u64)) + Weight::from_ref_time(25_518_267 as u64) + // Standard Error: 20_451 + .saturating_add(Weight::from_ref_time(5_041_089 as u64).saturating_mul(b as u64)) .saturating_add(T::DbWeight::get().reads(2 as u64)) .saturating_add(T::DbWeight::get().writes(1 as u64)) } + // Storage: Refungible TokenProperties (r:1 w:1) // Storage: Common CollectionPropertyPermissions (r:1 w:0) - // Storage: Refungible TokenProperties (r:1 w:1) fn delete_token_properties(b: u32, ) -> Weight { - Weight::from_ref_time(4_583_000 as u64) - // Standard Error: 5_762_380 - .saturating_add(Weight::from_ref_time(696_007_076 as u64).saturating_mul(b as u64)) + Weight::from_ref_time(13_715_000 as u64) + // Standard Error: 28_323 + .saturating_add(Weight::from_ref_time(13_113_351 as u64).saturating_mul(b as u64)) .saturating_add(T::DbWeight::get().reads(2 as u64)) .saturating_add(T::DbWeight::get().writes(1 as u64)) } @@ -478,21 +478,21 @@ .saturating_add(RocksDbWeight::get().reads(1 as u64)) .saturating_add(RocksDbWeight::get().writes(1 as u64)) } + // Storage: Refungible TokenProperties (r:1 w:1) // Storage: Common CollectionPropertyPermissions (r:1 w:0) - // Storage: Refungible TokenProperties (r:1 w:1) fn set_token_properties(b: u32, ) -> Weight { - Weight::from_ref_time(4_578_000 as u64) - // Standard Error: 5_396_287 - .saturating_add(Weight::from_ref_time(633_314_546 as u64).saturating_mul(b as u64)) + Weight::from_ref_time(25_518_267 as u64) + // Standard Error: 20_451 + .saturating_add(Weight::from_ref_time(5_041_089 as u64).saturating_mul(b as u64)) .saturating_add(RocksDbWeight::get().reads(2 as u64)) .saturating_add(RocksDbWeight::get().writes(1 as u64)) } + // Storage: Refungible TokenProperties (r:1 w:1) // Storage: Common CollectionPropertyPermissions (r:1 w:0) - // Storage: Refungible TokenProperties (r:1 w:1) fn delete_token_properties(b: u32, ) -> Weight { - Weight::from_ref_time(4_583_000 as u64) - // Standard Error: 5_762_380 - .saturating_add(Weight::from_ref_time(696_007_076 as u64).saturating_mul(b as u64)) + Weight::from_ref_time(13_715_000 as u64) + // Standard Error: 28_323 + .saturating_add(Weight::from_ref_time(13_113_351 as u64).saturating_mul(b as u64)) .saturating_add(RocksDbWeight::get().reads(2 as u64)) .saturating_add(RocksDbWeight::get().writes(1 as u64)) } --- a/pallets/unique/src/eth/mod.rs +++ b/pallets/unique/src/eth/mod.rs @@ -33,7 +33,6 @@ }; use pallet_evm::{account::CrossAccountId, OnMethodCall, PrecompileHandle, PrecompileResult}; use pallet_evm_coder_substrate::{dispatch_to_evm, SubstrateRecorder, WithRecorder}; -use sp_std::vec; use up_data_structs::{ CollectionDescription, CollectionMode, CollectionName, CollectionTokenPrefix, CreateCollectionData, @@ -316,13 +315,14 @@ >::set_collection_properties( &collection, &caller, - vec![up_data_structs::Property { + [up_data_structs::Property { key: key::base_uri(), value: base_uri .into_bytes() .try_into() .map_err(|_| "base uri is too large")?, - }], + }] + .into_iter(), ) .map_err(dispatch_to_evm::)?; } --- a/runtime/common/ethereum/sponsoring/refungible.rs +++ b/runtime/common/ethereum/sponsoring/refungible.rs @@ -139,8 +139,7 @@ | BalanceOf { .. } | OwnerOf { .. } | GetApproved { .. } - | IsApprovedForAll { .. } - | CollectionHelperAddress => None, + | IsApprovedForAll { .. } => None, // Not sponsored SafeTransferFromWithData { .. } @@ -230,7 +229,8 @@ | CrossOwnerOf { .. } | Properties { .. } | NextTokenId - | TokenContractAddress { .. } => None, + | TokenContractAddress { .. } + | CollectionHelperAddress => None, // Not sponsored BurnFrom { .. } --- a/runtime/common/identity.rs +++ b/runtime/common/identity.rs @@ -24,6 +24,9 @@ transaction_validity::{TransactionValidity, ValidTransaction, TransactionValidityError}, }; +#[cfg(feature = "collator-selection")] +use sp_runtime::transaction_validity::InvalidTransaction; + #[derive(Debug, Encode, Decode, PartialEq, Eq, Clone, TypeInfo)] pub struct DisableIdentityCalls; --- a/runtime/common/runtime_apis.rs +++ b/runtime/common/runtime_apis.rs @@ -451,7 +451,9 @@ } fn account_code_at(address: H160) -> Vec { - EVM::account_codes(address) + use pallet_evm::OnMethodCall; + ::OnMethodCall::get_code(&address) + .unwrap_or_else(|| EVM::account_codes(address)) } fn author() -> H160 { --- a/runtime/opal/Cargo.toml +++ b/runtime/opal/Cargo.toml @@ -193,6 +193,7 @@ 'refungible', 'rmrk', 'app-promotion', + 'collator-selection', 'foreign-assets', 'pallet-test-utils', ] --- a/tests/src/eth/abi/nonFungible.json +++ b/tests/src/eth/abi/nonFungible.json @@ -51,6 +51,12 @@ }, { "anonymous": false, + "inputs": [], + "name": "MintingFinished", + "type": "event" + }, + { + "anonymous": false, "inputs": [ { "indexed": true, @@ -417,6 +423,13 @@ "type": "function" }, { + "inputs": [], + "name": "finishMinting", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { "inputs": [ { "internalType": "uint256", "name": "tokenId", "type": "uint256" } ], @@ -504,6 +517,13 @@ }, { "inputs": [], + "name": "mintingFinished", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], "name": "name", "outputs": [{ "internalType": "string", "name": "", "type": "string" }], "stateMutability": "view", --- a/tests/src/eth/abi/reFungible.json +++ b/tests/src/eth/abi/reFungible.json @@ -51,6 +51,12 @@ }, { "anonymous": false, + "inputs": [], + "name": "MintingFinished", + "type": "event" + }, + { + "anonymous": false, "inputs": [ { "indexed": true, @@ -399,6 +405,13 @@ "type": "function" }, { + "inputs": [], + "name": "finishMinting", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { "inputs": [ { "internalType": "uint256", "name": "tokenId", "type": "uint256" } ], @@ -486,6 +499,13 @@ }, { "inputs": [], + "name": "mintingFinished", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], "name": "name", "outputs": [{ "internalType": "string", "name": "", "type": "string" }], "stateMutability": "view", @@ -596,7 +616,7 @@ { "internalType": "uint256", "name": "tokenId", "type": "uint256" }, { "internalType": "bytes", "name": "data", "type": "bytes" } ], - "name": "safeTransferFromWithData", + "name": "safeTransferFrom", "outputs": [], "stateMutability": "nonpayable", "type": "function" --- a/tests/src/eth/abi/reFungibleToken.json +++ b/tests/src/eth/abi/reFungibleToken.json @@ -98,6 +98,16 @@ }, { "inputs": [ + { "internalType": "address", "name": "from", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "burnFrom", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ { "components": [ { "internalType": "address", "name": "eth", "type": "address" }, --- a/tests/src/eth/api/UniqueNFT.sol +++ b/tests/src/eth/api/UniqueNFT.sol @@ -536,7 +536,7 @@ } /// @title Unique extensions for ERC721. -/// @dev the ERC-165 identifier for this interface is 0x0e48fdb4 +/// @dev the ERC-165 identifier for this interface is 0x16de3152 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, @@ -661,6 +661,11 @@ /// @dev EVM selector for this function is: 0xb904db03, /// or in textual repr: mintCross((address,uint256),(string,bytes)[]) function mintCross(CrossAddress memory to, Property[] memory properties) external returns (uint256); + + /// @notice Returns collection helper contract address + /// @dev EVM selector for this function is: 0x1896cce6, + /// or in textual repr: collectionHelperAddress() + function collectionHelperAddress() external view returns (address); } /// @dev anonymous struct @@ -703,7 +708,7 @@ /// @title ERC-721 Non-Fungible Token Standard /// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md -/// @dev the ERC-165 identifier for this interface is 0x983a942b +/// @dev the ERC-165 identifier for this interface is 0x80ac58cd interface ERC721 is Dummy, ERC165, ERC721Events { /// @notice Count all NFTs assigned to an owner /// @dev NFTs assigned to the zero address are considered invalid, and this @@ -786,11 +791,6 @@ /// @dev EVM selector for this function is: 0xe985e9c5, /// or in textual repr: isApprovedForAll(address,address) function isApprovedForAll(address owner, address operator) external view returns (bool); - - /// @notice Returns collection helper contract address - /// @dev EVM selector for this function is: 0x1896cce6, - /// or in textual repr: collectionHelperAddress() - function collectionHelperAddress() external view returns (address); } interface UniqueNFT is --- a/tests/src/eth/api/UniqueRefungible.sol +++ b/tests/src/eth/api/UniqueRefungible.sol @@ -441,6 +441,8 @@ TransferEnabled } +/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension +/// @dev See https://eips.ethereum.org/EIPS/eip-721 /// @dev the ERC-165 identifier for this interface is 0x5b5e139f interface ERC721Metadata is Dummy, ERC165 { // /// @notice A descriptive name for a collection of NFTs in this contract @@ -534,7 +536,7 @@ } /// @title Unique extensions for ERC721. -/// @dev the ERC-165 identifier for this interface is 0xabf30dc2 +/// @dev the ERC-165 identifier for this interface is 0xb365c124 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, @@ -660,6 +662,11 @@ /// @dev EVM selector for this function is: 0xab76fac6, /// or in textual repr: tokenContractAddress(uint256) function tokenContractAddress(uint256 token) external view returns (address); + + /// @notice Returns collection helper contract address + /// @dev EVM selector for this function is: 0x1896cce6, + /// or in textual repr: collectionHelperAddress() + function collectionHelperAddress() external view returns (address); } /// @dev anonymous struct @@ -702,7 +709,7 @@ /// @title ERC-721 Non-Fungible Token Standard /// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md -/// @dev the ERC-165 identifier for this interface is 0x4016cd87 +/// @dev the ERC-165 identifier for this interface is 0x80ac58cd interface ERC721 is Dummy, ERC165, ERC721Events { /// @notice Count all RFTs assigned to an owner /// @dev RFTs assigned to the zero address are considered invalid, and this @@ -725,9 +732,9 @@ function ownerOf(uint256 tokenId) external view returns (address); /// @dev Not implemented - /// @dev EVM selector for this function is: 0x60a11672, - /// or in textual repr: safeTransferFromWithData(address,address,uint256,bytes) - function safeTransferFromWithData( + /// @dev EVM selector for this function is: 0xb88d4fde, + /// or in textual repr: safeTransferFrom(address,address,uint256,bytes) + function safeTransferFrom( address from, address to, uint256 tokenId, @@ -783,11 +790,6 @@ /// @dev EVM selector for this function is: 0xe985e9c5, /// or in textual repr: isApprovedForAll(address,address) function isApprovedForAll(address owner, address operator) external view returns (bool); - - /// @notice Returns collection helper contract address - /// @dev EVM selector for this function is: 0x1896cce6, - /// or in textual repr: collectionHelperAddress() - function collectionHelperAddress() external view returns (address); } interface UniqueRefungible is --- a/tests/src/nesting/unnest.test.ts +++ b/tests/src/nesting/unnest.test.ts @@ -16,14 +16,17 @@ import {IKeyringPair} from '@polkadot/types/types'; import {expect, itSub, Pallets, usingPlaygrounds} from '../util'; +import {UniqueFTCollection, UniqueNFToken, UniqueRFToken} from '../util/playgrounds/unique'; describe('Integration Test: Unnesting', () => { let alice: IKeyringPair; + let bob: IKeyringPair; + let charlie: IKeyringPair; before(async () => { await usingPlaygrounds(async (helper, privateKey) => { const donor = await privateKey({filename: __filename}); - [alice] = await helper.arrange.createAccounts([50n], donor); + [alice, bob, charlie] = await helper.arrange.createAccounts([200n, 50n, 50n], donor); }); }); @@ -63,6 +66,208 @@ expect(await collectionFT.getBalance(targetToken.nestingAccount())).to.be.equal(0n); expect(await targetToken.getChildren()).to.be.length(0); }); + + itSub.ifWithPallets('ReFungible: allows the owner to successfully unnest a token', [Pallets.ReFungible], async ({helper}) => { + const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}}); + const targetToken = await collection.mintToken(alice); + + const collectionRFT = await helper.rft.mintCollection(alice); + + // Nest and unnest + const token = await collectionRFT.mintToken(alice, 10n, targetToken.nestingAccount()); + await expect(token.transferFrom(alice, targetToken.nestingAccount(), {Substrate: alice.address}, 9n), 'while unnesting').to.be.fulfilled; + expect(await token.getBalance({Substrate: alice.address})).to.be.equal(9n); + expect(await token.getBalance(targetToken.nestingAccount())).to.be.equal(1n); + + // Nest and burn + await token.transfer(alice, targetToken.nestingAccount(), 5n); + await expect(token.burnFrom(alice, targetToken.nestingAccount(), 6n), 'while burning').to.be.fulfilled; + expect(await token.getBalance({Substrate: alice.address})).to.be.equal(4n); + expect(await token.getBalance(targetToken.nestingAccount())).to.be.equal(0n); + expect(await targetToken.getChildren()).to.be.length(0); + }); + + async function checkNestedAmountState({ + expectedBalance, + childrenShouldPresent, + nested, + targetNft, + }: { + expectedBalance: bigint, + childrenShouldPresent: boolean, + nested: UniqueFTCollection | UniqueRFToken, + targetNft: UniqueNFToken, + }) { + const balance = await nested.getBalance(targetNft.nestingAccount()); + expect(balance).to.be.equal(expectedBalance); + + const children = await targetNft.getChildren(); + + if (childrenShouldPresent) { + expect(children[0]).to.be.deep.equal({ + collectionId: nested.collectionId, + tokenId: (nested instanceof UniqueFTCollection) ? 0 : nested.tokenId, + }); + } else { + expect(children.length).to.be.equal(0); + } + } + + function ownerOrAdminUnnestCases(modes: ('ft' | 'nft' | 'rft')[]): { + mode: 'ft' | 'nft' | 'rft', + sender: string, + op: 'transfer' | 'burn', + requiredPallets: Pallets[], + }[] { + const senders = ['owner', 'admin']; + const ops = ['transfer', 'burn']; + + const cases = []; + for (const mode of modes) { + const requiredPallets = (mode === 'rft') + ? [Pallets.ReFungible] + : []; + + for (const sender of senders) { + for (const op of ops) { + cases.push({ + mode: mode as 'ft' | 'nft' | 'rft', + sender, + op: op as 'transfer' | 'burn', + requiredPallets, + }); + } + } + } + + return cases; + } + + ownerOrAdminUnnestCases(['ft', 'rft']).map(testCase => + itSub.ifWithPallets(`[${testCase.mode}]: allows a collection ${testCase.sender} to ${testCase.op} nested token`, testCase.requiredPallets, async({helper}) => { + const owner = alice; + const admin = bob; + + const unnester = (testCase.sender === 'owner') + ? owner + : admin; + + const collectionNFT = await helper.nft.mintCollection(owner); + await collectionNFT.setPermissions(owner, {nesting: {tokenOwner: true}}); + + const collectionNested = await helper[testCase.mode as 'ft' | 'rft'].mintCollection(owner, { + limits: { + ownerCanTransfer: true, + }, + }); + await collectionNested.addAdmin(owner, {Substrate: admin.address}); + + const targetNft = await collectionNFT.mintToken(owner, {Substrate: charlie.address}); + + let nested: UniqueFTCollection | UniqueRFToken; + const totalAmount = 5n; + const firstUnnestAmount = 2n; + const restUnnestAmount = totalAmount - firstUnnestAmount; + + if (collectionNested instanceof UniqueFTCollection) { + await collectionNested.mint(owner, totalAmount, {Substrate: charlie.address}); + nested = collectionNested; + } else { + nested = await collectionNested.mintToken(owner, totalAmount, {Substrate: charlie.address}); + } + + // transfer/burn `amount` of nested assets by `unnester`. + const doOperationAndCheck = async ({ + amount, + shouldBeNestedAfterOp, + }: { + amount: bigint, + shouldBeNestedAfterOp: boolean, + }) => { + const nestedBalanceBeforeOp = await nested.getBalance(targetNft.nestingAccount()); + + if (testCase.op === 'transfer') { + const bobBalanceBeforeOp = await nested.getBalance({Substrate: bob.address}); + + await nested.transferFrom(unnester, targetNft.nestingAccount(), {Substrate: bob.address}, amount); + expect(await nested.getBalance({Substrate: bob.address})).to.be.equal(bobBalanceBeforeOp + amount); + } else { + if (nested instanceof UniqueFTCollection) { + await nested.burnTokensFrom(unnester, targetNft.nestingAccount(), amount); + } else { + await nested.burnFrom(unnester, targetNft.nestingAccount(), amount); + } + } + + await checkNestedAmountState({ + expectedBalance: nestedBalanceBeforeOp - amount, + childrenShouldPresent: shouldBeNestedAfterOp, + nested, + targetNft, + }); + }; + + // Initial setup: nest (fungibles/rft parts). + // Check NFT's balance of nested assets and NFT's children. + await nested.transfer(charlie, targetNft.nestingAccount(), totalAmount); + await checkNestedAmountState({ + expectedBalance: totalAmount, + childrenShouldPresent: true, + nested, + targetNft, + }); + + // Transfer/burn only a part of nested assets. + // Check that NFT's balance of the nested assets correctly decreased and NFT's children are not changed. + await doOperationAndCheck({ + amount: firstUnnestAmount, + shouldBeNestedAfterOp: true, + }); + + // Transfer/burn all remaining nested assets. + // Check that NFT's balance of the nested assets is 0 and NFT has no more children. + await doOperationAndCheck({ + amount: restUnnestAmount, + shouldBeNestedAfterOp: false, + }); + })); + + ownerOrAdminUnnestCases(['nft']).map(testCase => + itSub(`[nft]: allows a collection ${testCase.sender} to ${testCase.op} nested token`, async ({helper}) => { + const owner = alice; + const admin = bob; + + const unnester = (testCase.sender === 'owner') + ? owner + : admin; + + const collectionNFT = await helper.nft.mintCollection(owner); + await collectionNFT.setPermissions(owner, {nesting: {tokenOwner: true}}); + + const collectionNested = await helper.nft.mintCollection(owner, { + limits: { + ownerCanTransfer: true, + }, + }); + await collectionNested.addAdmin(owner, {Substrate: admin.address}); + + const targetNft = await collectionNFT.mintToken(owner, {Substrate: charlie.address}); + const nested = await collectionNested.mintToken(owner, {Substrate: charlie.address}); + + await nested.transfer(charlie, targetNft.nestingAccount()); + expect(await targetNft.getChildren()).to.be.deep.equal([{ + collectionId: nested.collectionId, + tokenId: nested.tokenId, + }]); + + if (testCase.op === 'transfer') { + await nested.transferFrom(unnester, targetNft.nestingAccount(), {Substrate: bob.address}); + } else { + await nested.burnFrom(unnester, targetNft.nestingAccount()); + } + + expect((await targetNft.getChildren()).length).to.be.equal(0); + })); }); describe('Negative Test: Unnesting', () => { --- a/tests/src/pallet-presence.test.ts +++ b/tests/src/pallet-presence.test.ts @@ -76,6 +76,7 @@ appPromotion, testUtils, ...rmrkPallets, + ...collatorSelection, ); } else if (chain.eq('QUARTZ by UNIQUE') || chain.eq('SAPPHIRE by UNIQUE')) { requiredPallets.push( --- a/tests/src/util/playgrounds/unique.ts +++ b/tests/src/util/playgrounds/unique.ts @@ -651,6 +651,9 @@ try { result = await this.signTransaction(sender, this.constructApiCall(extrinsic, params), options, extrinsic) as ITransactionResult; events = this.eventHelper.extractEvents(result.result.events); + const errorEvent = events.find((event) => event.method == 'ExecutedFailed' || event.method == 'CreatedFailed'); + if (errorEvent) + throw Error(errorEvent.method + ': ' + extrinsic); } catch(e) { if(!(e as object).hasOwnProperty('status')) throw e; -- gitstuff