difftreelog
Merge branch 'develop' into feature/CORE-37
in: master
21 files changed
crates/evm-coder-macros/src/solidity_interface.rsdiffbeforeafterboth--- a/crates/evm-coder-macros/src/solidity_interface.rs
+++ b/crates/evm-coder-macros/src/solidity_interface.rs
@@ -1,16 +1,16 @@
#![allow(dead_code)]
use quote::quote;
-use darling::FromMeta;
+use darling::{FromMeta, ToTokens};
use inflector::cases;
use std::fmt::Write;
use syn::{
- FnArg, Generics, Ident, ImplItem, ImplItemMethod, ItemImpl, Meta, NestedMeta, PatType, Path,
- ReturnType, Type, spanned::Spanned,
+ Expr, FnArg, GenericArgument, Generics, Ident, ImplItem, ImplItemMethod, ItemImpl, Lit, Meta,
+ NestedMeta, PatType, Path, PathArguments, ReturnType, Type, spanned::Spanned,
};
use crate::{
- fn_selector_str, parse_ident_from_pat, parse_ident_from_path, parse_ident_from_type,
+ fn_selector_str, parse_ident_from_pat, parse_ident_from_path, parse_path, parse_path_segment,
parse_result_ok, pascal_ident_to_call, pascal_ident_to_snake_call, snake_ident_to_pascal,
snake_ident_to_screaming,
};
@@ -77,14 +77,14 @@
fn expand_generator(&self) -> proc_macro2::TokenStream {
let pascal_call_name = &self.pascal_call_name;
quote! {
- #pascal_call_name::generate_solidity_interface(out_set, is_impl);
+ #pascal_call_name::generate_solidity_interface(tc, is_impl);
}
}
fn expand_event_generator(&self) -> proc_macro2::TokenStream {
let name = &self.name;
quote! {
- #name::generate_solidity_interface(out_set, is_impl);
+ #name::generate_solidity_interface(tc, is_impl);
}
}
}
@@ -121,10 +121,161 @@
rename_selector: Option<String>,
}
+enum AbiType {
+ // type
+ Plain(Ident),
+ // (type1,type2)
+ Tuple(Vec<AbiType>),
+ // type[]
+ Vec(Box<AbiType>),
+ // type[20]
+ Array(Box<AbiType>, usize),
+}
+impl AbiType {
+ fn try_from(value: &Type) -> syn::Result<Self> {
+ let value = Self::try_maybe_special_from(value)?;
+ if value.is_special() {
+ return Err(syn::Error::new(value.span(), "unexpected special type"));
+ }
+ Ok(value)
+ }
+ fn try_maybe_special_from(value: &Type) -> syn::Result<Self> {
+ match value {
+ Type::Array(arr) => {
+ let wrapped = AbiType::try_from(&arr.elem)?;
+ match &arr.len {
+ Expr::Lit(l) => match &l.lit {
+ Lit::Int(i) => {
+ let num = i.base10_parse::<usize>()?;
+ Ok(AbiType::Array(Box::new(wrapped), num as usize))
+ }
+ _ => Err(syn::Error::new(arr.len.span(), "should be int literal")),
+ },
+ _ => Err(syn::Error::new(arr.len.span(), "should be literal")),
+ }
+ }
+ Type::Path(_) => {
+ let path = parse_path(value)?;
+ let segment = parse_path_segment(path)?;
+ if segment.ident == "Vec" {
+ let args = match &segment.arguments {
+ PathArguments::AngleBracketed(e) => e,
+ _ => {
+ return Err(syn::Error::new(
+ segment.arguments.span(),
+ "missing Vec generic",
+ ))
+ }
+ };
+ let args = &args.args;
+ if args.len() != 1 {
+ return Err(syn::Error::new(
+ args.span(),
+ "expected only one generic for vec",
+ ));
+ }
+ let arg = args.first().unwrap();
+
+ let ty = match arg {
+ GenericArgument::Type(ty) => ty,
+ _ => {
+ return Err(syn::Error::new(
+ arg.span(),
+ "expected first generic to be type",
+ ))
+ }
+ };
+
+ let wrapped = AbiType::try_from(ty)?;
+ Ok(Self::Vec(Box::new(wrapped)))
+ } else {
+ if !segment.arguments.is_empty() {
+ return Err(syn::Error::new(
+ segment.arguments.span(),
+ "unexpected generic arguments for non-vec type",
+ ));
+ }
+ Ok(Self::Plain(segment.ident.clone()))
+ }
+ }
+ Type::Tuple(t) => {
+ let mut out = Vec::with_capacity(t.elems.len());
+ for el in t.elems.iter() {
+ out.push(AbiType::try_from(el)?)
+ }
+ Ok(Self::Tuple(out))
+ }
+ _ => Err(syn::Error::new(
+ value.span(),
+ "unexpected type, only arrays, plain types and tuples are supported",
+ )),
+ }
+ }
+ fn is_value(&self) -> bool {
+ match self {
+ Self::Plain(v) if v == "value" => true,
+ _ => false,
+ }
+ }
+ fn is_caller(&self) -> bool {
+ match self {
+ Self::Plain(v) if v == "caller" => true,
+ _ => false,
+ }
+ }
+ fn is_special(&self) -> bool {
+ self.is_caller() || self.is_value()
+ }
+ fn selector_ty_buf(&self, buf: &mut String) -> std::fmt::Result {
+ match self {
+ AbiType::Plain(t) => {
+ write!(buf, "{}", t)
+ }
+ AbiType::Tuple(t) => {
+ write!(buf, "(")?;
+ for (i, t) in t.iter().enumerate() {
+ if i != 0 {
+ write!(buf, ",")?;
+ }
+ t.selector_ty_buf(buf)?;
+ }
+ write!(buf, ")")
+ }
+ AbiType::Vec(v) => {
+ v.selector_ty_buf(buf)?;
+ write!(buf, "[]")
+ }
+ AbiType::Array(v, len) => {
+ v.selector_ty_buf(buf)?;
+ write!(buf, "[{}]", len)
+ }
+ }
+ }
+ fn selector_ty(&self) -> String {
+ let mut out = String::new();
+ self.selector_ty_buf(&mut out).expect("no fmt error");
+ out
+ }
+}
+impl ToTokens for AbiType {
+ fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
+ match self {
+ AbiType::Plain(t) => tokens.extend(quote! {#t}),
+ AbiType::Tuple(t) => {
+ tokens.extend(quote! {(
+ #(#t),*
+ )});
+ }
+ AbiType::Vec(v) => tokens.extend(quote! {Vec<#v>}),
+ AbiType::Array(v, l) => tokens.extend(quote! {[#v; #l]}),
+ }
+ }
+}
+
struct MethodArg {
name: Ident,
camel_name: String,
- ty: Ident,
+ ty: AbiType,
}
impl MethodArg {
fn try_from(value: &PatType) -> syn::Result<Self> {
@@ -132,21 +283,21 @@
Ok(Self {
camel_name: cases::camelcase::to_camel_case(&name.to_string()),
name,
- ty: parse_ident_from_type(&value.ty, false)?.clone(),
+ ty: AbiType::try_maybe_special_from(&value.ty)?,
})
}
fn is_value(&self) -> bool {
- self.ty == "value"
+ self.ty.is_value()
}
fn is_caller(&self) -> bool {
- self.ty == "caller"
+ self.ty.is_caller()
}
fn is_special(&self) -> bool {
- self.is_value() || self.is_caller()
+ self.ty.is_special()
}
- fn selector_ty(&self) -> &Ident {
+ fn selector_ty(&self) -> String {
assert!(!self.is_special());
- &self.ty
+ self.ty.selector_ty()
}
fn expand_call_def(&self) -> proc_macro2::TokenStream {
@@ -419,9 +570,11 @@
.iter()
.filter(|a| !a.is_special())
.map(MethodArg::expand_solidity_argument);
+ let selector = format!("{} {:0>8x}", self.selector_str, self.selector);
quote! {
SolidityFunction {
+ selector: #selector,
name: #camel_name,
mutability: #mutability,
args: (
@@ -544,7 +697,7 @@
)*
)
}
- pub fn generate_solidity_interface(out_set: &mut sp_std::collections::btree_set::BTreeSet<string>, is_impl: bool) {
+ pub fn generate_solidity_interface(tc: &evm_coder::solidity::TypeCollector, is_impl: bool) {
use evm_coder::solidity::*;
use core::fmt::Write;
let interface = SolidityInterface {
@@ -559,9 +712,9 @@
)*),
};
if is_impl {
- out_set.insert("// Common stubs holder\ncontract Dummy {\n\tuint8 dummy;\n\tstring stub_error = \"this contract is implemented in native\";\n}\n".into());
+ tc.collect("// Common stubs holder\ncontract Dummy {\n\tuint8 dummy;\n\tstring stub_error = \"this contract is implemented in native\";\n}\n".into());
} else {
- out_set.insert("// Common stubs holder\ninterface Dummy {\n}\n".into());
+ tc.collect("// Common stubs holder\ninterface Dummy {\n}\n".into());
}
#(
#solidity_generators
@@ -576,8 +729,8 @@
if #solidity_name.starts_with("Inline") {
out.push_str("// Inline\n");
}
- let _ = interface.format(is_impl, &mut out);
- out_set.insert(out);
+ let _ = interface.format(is_impl, &mut out, tc);
+ tc.collect(out);
}
}
impl ::evm_coder::Call for #call_name {
crates/evm-coder-macros/src/to_log.rsdiffbeforeafterboth--- a/crates/evm-coder-macros/src/to_log.rs
+++ b/crates/evm-coder-macros/src/to_log.rs
@@ -179,7 +179,7 @@
#consts
)*
- pub fn generate_solidity_interface(out_set: &mut sp_std::collections::btree_set::BTreeSet<string>, is_impl: bool) {
+ pub fn generate_solidity_interface(tc: &evm_coder::solidity::TypeCollector, is_impl: bool) {
use evm_coder::solidity::*;
use core::fmt::Write;
let interface = SolidityInterface {
@@ -191,8 +191,8 @@
};
let mut out = string::new();
out.push_str("// Inline\n");
- let _ = interface.format(is_impl, &mut out);
- out_set.insert(out);
+ let _ = interface.format(is_impl, &mut out, tc);
+ tc.collect(out);
}
}
crates/evm-coder/src/abi.rsdiffbeforeafterboth--- a/crates/evm-coder/src/abi.rs
+++ b/crates/evm-coder/src/abi.rs
@@ -211,6 +211,10 @@
self.memory(value.as_bytes())
}
+ pub fn bytes(&mut self, value: &[u8]) {
+ self.memory(value)
+ }
+
pub fn finish(mut self) -> Vec<u8> {
for (static_offset, part) in self.dynamic_part {
let part_offset = self.static_part.len();
@@ -247,6 +251,59 @@
impl_abi_readable!(bool, bool);
impl_abi_readable!(string, string);
+mod sealed {
+ /// Not all types can be placed in vec, i.e `Vec<u8>` is restricted, `bytes` should be used instead
+ pub trait CanBePlacedInVec {}
+}
+
+impl sealed::CanBePlacedInVec for U256 {}
+impl sealed::CanBePlacedInVec for string {}
+impl sealed::CanBePlacedInVec for H160 {}
+
+impl<R: sealed::CanBePlacedInVec> AbiRead<Vec<R>> for AbiReader<'_>
+where
+ Self: AbiRead<R>,
+{
+ fn abi_read(&mut self) -> Result<Vec<R>> {
+ let mut sub = self.subresult()?;
+ let size = sub.read_usize()?;
+ sub.subresult_offset = sub.offset;
+ let mut out = Vec::with_capacity(size);
+ for _ in 0..size {
+ out.push(<Self as AbiRead<R>>::abi_read(&mut sub)?);
+ }
+ Ok(out)
+ }
+}
+
+macro_rules! impl_tuples {
+ ($($ident:ident)+) => {
+ impl<$($ident),+> sealed::CanBePlacedInVec for ($($ident,)+) {}
+ impl<$($ident),+> AbiRead<($($ident,)+)> for AbiReader<'_>
+ where
+ $(Self: AbiRead<$ident>),+
+ {
+ fn abi_read(&mut self) -> Result<($($ident,)+)> {
+ let mut subresult = self.subresult()?;
+ Ok((
+ $(<Self as AbiRead<$ident>>::abi_read(&mut subresult)?,)+
+ ))
+ }
+ }
+ };
+}
+
+impl_tuples! {A}
+impl_tuples! {A B}
+impl_tuples! {A B C}
+impl_tuples! {A B C D}
+impl_tuples! {A B C D E}
+impl_tuples! {A B C D E F}
+impl_tuples! {A B C D E F G}
+impl_tuples! {A B C D E F G H}
+impl_tuples! {A B C D E F G H I}
+impl_tuples! {A B C D E F G H I J}
+
pub trait AbiWrite {
fn abi_write(&self, writer: &mut AbiWriter);
}
@@ -273,6 +330,11 @@
writer.string(self)
}
}
+impl AbiWrite for &Vec<u8> {
+ fn abi_write(&self, writer: &mut AbiWriter) {
+ writer.bytes(self)
+ }
+}
impl AbiWrite for () {
fn abi_write(&self, _writer: &mut AbiWriter) {}
@@ -308,6 +370,11 @@
#[cfg(test)]
pub mod test {
+ use crate::{
+ abi::AbiRead,
+ types::{string, uint256},
+ };
+
use super::{AbiReader, AbiWriter};
use hex_literal::hex;
@@ -355,4 +422,48 @@
assert_eq!(decoder.uint32().unwrap(), 1);
assert_eq!(decoder.string().unwrap(), "Test URI");
}
+
+ #[test]
+ fn mint_bulk() {
+ let (call, mut decoder) = AbiReader::new_call(&hex!(
+ "
+ 36543006
+ 00000000000000000000000053744e6da587ba10b32a2554d2efdcd985bc27a3 // address
+ 0000000000000000000000000000000000000000000000000000000000000040 // 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
+ "
+ ))
+ .unwrap();
+ assert_eq!(call, 0x36543006);
+ let _ = 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())
+ ]
+ );
+ }
}
crates/evm-coder/src/lib.rsdiffbeforeafterboth--- a/crates/evm-coder/src/lib.rs
+++ b/crates/evm-coder/src/lib.rs
@@ -67,8 +67,8 @@
#[test]
#[ignore]
fn $name() {
- use sp_std::collections::btree_set::BTreeSet;
- let mut out = BTreeSet::new();
+ use evm_coder::solidity::TypeCollector;
+ let mut out = TypeCollector::new();
$decl::generate_solidity_interface(&mut out, $is_impl);
println!("=== SNIP START ===");
println!("// SPDX-License-Identifier: OTHER");
@@ -76,7 +76,7 @@
println!();
println!("pragma solidity >=0.8.0 <0.9.0;");
println!();
- for b in out {
+ for b in out.finish() {
println!("{}", b);
}
println!("=== SNIP END ===");
crates/evm-coder/src/solidity.rsdiffbeforeafterboth--- a/crates/evm-coder/src/solidity.rs
+++ b/crates/evm-coder/src/solidity.rs
@@ -1,25 +1,79 @@
#[cfg(not(feature = "std"))]
-use alloc::{string::String};
-use core::{fmt, marker::PhantomData};
+use alloc::{
+ string::String,
+ vec::Vec,
+ collections::{BTreeSet, BTreeMap},
+ format,
+};
+#[cfg(feature = "std")]
+use std::collections::{BTreeSet, BTreeMap};
+use core::{
+ fmt::{self, Write},
+ marker::PhantomData,
+ cell::{Cell, RefCell},
+};
use impl_trait_for_tuples::impl_for_tuples;
use crate::types::*;
+#[derive(Default)]
+pub struct TypeCollector {
+ structs: RefCell<BTreeSet<string>>,
+ anonymous: RefCell<BTreeMap<Vec<string>, usize>>,
+ id: Cell<usize>,
+}
+impl TypeCollector {
+ pub fn new() -> Self {
+ Self::default()
+ }
+ pub fn collect(&self, item: string) {
+ self.structs.borrow_mut().insert(item);
+ }
+ pub fn next_id(&self) -> usize {
+ let v = self.id.get();
+ self.id.set(v + 1);
+ v
+ }
+ pub fn collect_tuple<T: SolidityTupleType>(&self) -> String {
+ let names = T::names(self);
+ if let Some(id) = self.anonymous.borrow().get(&names).cloned() {
+ return format!("Tuple{}", id);
+ }
+ let id = self.next_id();
+ let mut str = String::new();
+ writeln!(str, "// Anonymous struct").unwrap();
+ writeln!(str, "struct Tuple{} {{", id).unwrap();
+ for (i, name) in names.iter().enumerate() {
+ writeln!(str, "\t{} field_{};", name, i).unwrap();
+ }
+ writeln!(str, "}}").unwrap();
+ self.collect(str);
+ self.anonymous.borrow_mut().insert(names, id);
+ format!("Tuple{}", id)
+ }
+ pub fn finish(self) -> BTreeSet<string> {
+ self.structs.into_inner()
+ }
+}
+
pub trait SolidityTypeName: 'static {
- fn solidity_name(writer: &mut impl fmt::Write) -> fmt::Result;
- fn solidity_default(writer: &mut impl fmt::Write) -> fmt::Result;
+ fn solidity_name(writer: &mut impl fmt::Write, tc: &TypeCollector) -> fmt::Result;
+ fn is_simple() -> bool;
+ fn solidity_default(writer: &mut impl fmt::Write, tc: &TypeCollector) -> fmt::Result;
fn is_void() -> bool {
false
}
}
-
macro_rules! solidity_type_name {
- ($($ty:ident => $name:literal = $default:literal),* $(,)?) => {
+ ($($ty:ty => $name:literal $simple:literal = $default:literal),* $(,)?) => {
$(
impl SolidityTypeName for $ty {
- fn solidity_name(writer: &mut impl core::fmt::Write) -> core::fmt::Result {
+ fn solidity_name(writer: &mut impl core::fmt::Write, _tc: &TypeCollector) -> core::fmt::Result {
write!(writer, $name)
}
- fn solidity_default(writer: &mut impl core::fmt::Write) -> core::fmt::Result {
+ fn is_simple() -> bool {
+ $simple
+ }
+ fn solidity_default(writer: &mut impl core::fmt::Write, _tc: &TypeCollector) -> core::fmt::Result {
write!(writer, $default)
}
}
@@ -28,20 +82,23 @@
}
solidity_type_name! {
- uint8 => "uint8" = "0",
- uint32 => "uint32" = "0",
- uint128 => "uint128" = "0",
- uint256 => "uint256" = "0",
- address => "address" = "0x0000000000000000000000000000000000000000",
- string => "string memory" = "\"\"",
- bytes => "bytes memory" = "hex\"\"",
- bool => "bool" = "false",
+ uint8 => "uint8" true = "0",
+ uint32 => "uint32" true = "0",
+ uint128 => "uint128" true = "0",
+ uint256 => "uint256" true = "0",
+ address => "address" true = "0x0000000000000000000000000000000000000000",
+ string => "string" false = "\"\"",
+ bytes => "bytes" false = "hex\"\"",
+ bool => "bool" true = "false",
}
impl SolidityTypeName for void {
- fn solidity_name(_writer: &mut impl fmt::Write) -> fmt::Result {
+ fn solidity_name(_writer: &mut impl fmt::Write, _tc: &TypeCollector) -> fmt::Result {
Ok(())
}
- fn solidity_default(_writer: &mut impl fmt::Write) -> fmt::Result {
+ fn is_simple() -> bool {
+ true
+ }
+ fn solidity_default(_writer: &mut impl fmt::Write, _tc: &TypeCollector) -> fmt::Result {
Ok(())
}
fn is_void() -> bool {
@@ -49,10 +106,88 @@
}
}
+mod sealed {
+ pub trait CanBePlacedInVec {}
+}
+
+impl sealed::CanBePlacedInVec for uint256 {}
+impl sealed::CanBePlacedInVec for string {}
+impl sealed::CanBePlacedInVec for address {}
+
+impl<T: SolidityTypeName + sealed::CanBePlacedInVec> SolidityTypeName for Vec<T> {
+ fn solidity_name(writer: &mut impl fmt::Write, tc: &TypeCollector) -> fmt::Result {
+ T::solidity_name(writer, tc)?;
+ write!(writer, "[]")
+ }
+ fn is_simple() -> bool {
+ false
+ }
+ fn solidity_default(writer: &mut impl fmt::Write, _tc: &TypeCollector) -> fmt::Result {
+ write!(writer, "[]")
+ }
+}
+
+pub trait SolidityTupleType {
+ fn names(tc: &TypeCollector) -> Vec<String>;
+ fn len() -> usize;
+}
+
+macro_rules! count {
+ () => (0usize);
+ ( $x:tt $($xs:tt)* ) => (1usize + count!($($xs)*));
+}
+
+macro_rules! impl_tuples {
+ ($($ident:ident)+) => {
+ impl<$($ident),+> sealed::CanBePlacedInVec for ($($ident,)+) {}
+ impl<$($ident: SolidityTypeName + 'static),+> SolidityTupleType for ($($ident,)+) {
+ fn names(tc: &TypeCollector) -> Vec<string> {
+ let mut collected = Vec::with_capacity(Self::len());
+ $({
+ let mut out = string::new();
+ $ident::solidity_name(&mut out, tc).expect("no fmt error");
+ collected.push(out);
+ })*;
+ collected
+ }
+
+ fn len() -> usize {
+ count!($($ident)*)
+ }
+ }
+ impl<$($ident: SolidityTypeName + 'static),+> SolidityTypeName for ($($ident,)+) {
+ fn solidity_name(writer: &mut impl fmt::Write, tc: &TypeCollector) -> fmt::Result {
+ write!(writer, "{}", tc.collect_tuple::<Self>())
+ }
+ fn is_simple() -> bool {
+ false
+ }
+ fn solidity_default(writer: &mut impl fmt::Write, tc: &TypeCollector) -> fmt::Result {
+ write!(writer, "{}(", tc.collect_tuple::<Self>())?;
+ $(
+ <$ident>::solidity_default(writer, tc)?;
+ )*
+ write!(writer, ")")
+ }
+ }
+ };
+}
+
+impl_tuples! {A}
+impl_tuples! {A B}
+impl_tuples! {A B C}
+impl_tuples! {A B C D}
+impl_tuples! {A B C D E}
+impl_tuples! {A B C D E F}
+impl_tuples! {A B C D E F G}
+impl_tuples! {A B C D E F G H}
+impl_tuples! {A B C D E F G H I}
+impl_tuples! {A B C D E F G H I J}
+
pub trait SolidityArguments {
- fn solidity_name(&self, writer: &mut impl fmt::Write) -> fmt::Result;
+ fn solidity_name(&self, writer: &mut impl fmt::Write, tc: &TypeCollector) -> fmt::Result;
fn solidity_get(&self, writer: &mut impl fmt::Write) -> fmt::Result;
- fn solidity_default(&self, writer: &mut impl fmt::Write) -> fmt::Result;
+ fn solidity_default(&self, writer: &mut impl fmt::Write, tc: &TypeCollector) -> fmt::Result;
fn is_empty(&self) -> bool {
self.len() == 0
}
@@ -63,9 +198,13 @@
pub struct UnnamedArgument<T>(PhantomData<*const T>);
impl<T: SolidityTypeName> SolidityArguments for UnnamedArgument<T> {
- fn solidity_name(&self, writer: &mut impl fmt::Write) -> fmt::Result {
+ fn solidity_name(&self, writer: &mut impl fmt::Write, tc: &TypeCollector) -> fmt::Result {
if !T::is_void() {
- T::solidity_name(writer)
+ T::solidity_name(writer, tc)?;
+ if !T::is_simple() {
+ write!(writer, " memory")?;
+ }
+ Ok(())
} else {
Ok(())
}
@@ -73,8 +212,8 @@
fn solidity_get(&self, _writer: &mut impl fmt::Write) -> fmt::Result {
Ok(())
}
- fn solidity_default(&self, writer: &mut impl fmt::Write) -> fmt::Result {
- T::solidity_default(writer)
+ fn solidity_default(&self, writer: &mut impl fmt::Write, tc: &TypeCollector) -> fmt::Result {
+ T::solidity_default(writer, tc)
}
fn len(&self) -> usize {
if T::is_void() {
@@ -94,9 +233,12 @@
}
impl<T: SolidityTypeName> SolidityArguments for NamedArgument<T> {
- fn solidity_name(&self, writer: &mut impl fmt::Write) -> fmt::Result {
+ fn solidity_name(&self, writer: &mut impl fmt::Write, tc: &TypeCollector) -> fmt::Result {
if !T::is_void() {
- T::solidity_name(writer)?;
+ T::solidity_name(writer, tc)?;
+ if !T::is_simple() {
+ write!(writer, " memory")?;
+ }
write!(writer, " {}", self.0)
} else {
Ok(())
@@ -105,8 +247,8 @@
fn solidity_get(&self, writer: &mut impl fmt::Write) -> fmt::Result {
writeln!(writer, "\t\t{};", self.0)
}
- fn solidity_default(&self, writer: &mut impl fmt::Write) -> fmt::Result {
- T::solidity_default(writer)
+ fn solidity_default(&self, writer: &mut impl fmt::Write, tc: &TypeCollector) -> fmt::Result {
+ T::solidity_default(writer, tc)
}
fn len(&self) -> usize {
if T::is_void() {
@@ -126,9 +268,9 @@
}
impl<T: SolidityTypeName> SolidityArguments for SolidityEventArgument<T> {
- fn solidity_name(&self, writer: &mut impl fmt::Write) -> fmt::Result {
+ fn solidity_name(&self, writer: &mut impl fmt::Write, tc: &TypeCollector) -> fmt::Result {
if !T::is_void() {
- T::solidity_name(writer)?;
+ T::solidity_name(writer, tc)?;
if self.0 {
write!(writer, " indexed")?;
}
@@ -140,8 +282,8 @@
fn solidity_get(&self, writer: &mut impl fmt::Write) -> fmt::Result {
writeln!(writer, "\t\t{};", self.1)
}
- fn solidity_default(&self, writer: &mut impl fmt::Write) -> fmt::Result {
- T::solidity_default(writer)
+ fn solidity_default(&self, writer: &mut impl fmt::Write, tc: &TypeCollector) -> fmt::Result {
+ T::solidity_default(writer, tc)
}
fn len(&self) -> usize {
if T::is_void() {
@@ -153,13 +295,13 @@
}
impl SolidityArguments for () {
- fn solidity_name(&self, _writer: &mut impl fmt::Write) -> fmt::Result {
+ fn solidity_name(&self, _writer: &mut impl fmt::Write, _tc: &TypeCollector) -> fmt::Result {
Ok(())
}
fn solidity_get(&self, _writer: &mut impl fmt::Write) -> fmt::Result {
Ok(())
}
- fn solidity_default(&self, _writer: &mut impl fmt::Write) -> fmt::Result {
+ fn solidity_default(&self, _writer: &mut impl fmt::Write, _tc: &TypeCollector) -> fmt::Result {
Ok(())
}
fn len(&self) -> usize {
@@ -171,7 +313,7 @@
impl SolidityArguments for Tuple {
for_tuples!( where #( Tuple: SolidityArguments ),* );
- fn solidity_name(&self, writer: &mut impl fmt::Write) -> fmt::Result {
+ fn solidity_name(&self, writer: &mut impl fmt::Write, tc: &TypeCollector) -> fmt::Result {
let mut first = true;
for_tuples!( #(
if !Tuple.is_empty() {
@@ -179,7 +321,7 @@
write!(writer, ", ")?;
}
first = false;
- Tuple.solidity_name(writer)?;
+ Tuple.solidity_name(writer, tc)?;
}
)* );
Ok(())
@@ -190,12 +332,12 @@
)* );
Ok(())
}
- fn solidity_default(&self, writer: &mut impl fmt::Write) -> fmt::Result {
+ fn solidity_default(&self, writer: &mut impl fmt::Write, tc: &TypeCollector) -> fmt::Result {
if self.is_empty() {
Ok(())
} else if self.len() == 1 {
for_tuples!( #(
- Tuple.solidity_default(writer)?;
+ Tuple.solidity_default(writer, tc)?;
)* );
Ok(())
} else {
@@ -207,7 +349,7 @@
write!(writer, ", ")?;
}
first = false;
- Tuple.solidity_name(writer)?;
+ Tuple.solidity_default(writer, tc)?;
}
)* );
write!(writer, ")")?;
@@ -220,7 +362,12 @@
}
pub trait SolidityFunctions {
- fn solidity_name(&self, is_impl: bool, writer: &mut impl fmt::Write) -> fmt::Result;
+ fn solidity_name(
+ &self,
+ is_impl: bool,
+ writer: &mut impl fmt::Write,
+ tc: &TypeCollector,
+ ) -> fmt::Result;
}
pub enum SolidityMutability {
@@ -229,15 +376,22 @@
Mutable,
}
pub struct SolidityFunction<A, R> {
+ pub selector: &'static str,
pub name: &'static str,
pub args: A,
pub result: R,
pub mutability: SolidityMutability,
}
impl<A: SolidityArguments, R: SolidityArguments> SolidityFunctions for SolidityFunction<A, R> {
- fn solidity_name(&self, is_impl: bool, writer: &mut impl fmt::Write) -> fmt::Result {
+ fn solidity_name(
+ &self,
+ is_impl: bool,
+ writer: &mut impl fmt::Write,
+ tc: &TypeCollector,
+ ) -> fmt::Result {
+ writeln!(writer, "\t// Selector: {}", self.selector)?;
write!(writer, "\tfunction {}(", self.name)?;
- self.args.solidity_name(writer)?;
+ self.args.solidity_name(writer, tc)?;
write!(writer, ")")?;
if is_impl {
write!(writer, " public")?;
@@ -251,7 +405,7 @@
}
if !self.result.is_empty() {
write!(writer, " returns (")?;
- self.result.solidity_name(writer)?;
+ self.result.solidity_name(writer, tc)?;
write!(writer, ")")?;
}
if is_impl {
@@ -265,7 +419,7 @@
}
if !self.result.is_empty() {
write!(writer, "\t\treturn ")?;
- self.result.solidity_default(writer)?;
+ self.result.solidity_default(writer, tc)?;
writeln!(writer, ";")?;
}
writeln!(writer, "\t}}")?;
@@ -280,10 +434,15 @@
impl SolidityFunctions for Tuple {
for_tuples!( where #( Tuple: SolidityFunctions ),* );
- fn solidity_name(&self, is_impl: bool, writer: &mut impl fmt::Write) -> fmt::Result {
+ fn solidity_name(
+ &self,
+ is_impl: bool,
+ writer: &mut impl fmt::Write,
+ tc: &TypeCollector,
+ ) -> fmt::Result {
let mut first = false;
for_tuples!( #(
- Tuple.solidity_name(is_impl, writer)?;
+ Tuple.solidity_name(is_impl, writer, tc)?;
)* );
Ok(())
}
@@ -296,7 +455,12 @@
}
impl<F: SolidityFunctions> SolidityInterface<F> {
- pub fn format(&self, is_impl: bool, out: &mut impl fmt::Write) -> fmt::Result {
+ pub fn format(
+ &self,
+ is_impl: bool,
+ out: &mut impl fmt::Write,
+ tc: &TypeCollector,
+ ) -> fmt::Result {
if is_impl {
write!(out, "contract ")?;
} else {
@@ -313,7 +477,7 @@
}
}
writeln!(out, " {{")?;
- self.functions.solidity_name(is_impl, out)?;
+ self.functions.solidity_name(is_impl, out, tc)?;
writeln!(out, "}}")?;
Ok(())
}
@@ -325,9 +489,14 @@
}
impl<A: SolidityArguments> SolidityFunctions for SolidityEvent<A> {
- fn solidity_name(&self, _is_impl: bool, writer: &mut impl fmt::Write) -> fmt::Result {
+ fn solidity_name(
+ &self,
+ _is_impl: bool,
+ writer: &mut impl fmt::Write,
+ tc: &TypeCollector,
+ ) -> fmt::Result {
write!(writer, "\tevent {}(", self.name)?;
- self.args.solidity_name(writer)?;
+ self.args.solidity_name(writer, tc)?;
writeln!(writer, ");")
}
}
pallets/evm-contract-helpers/src/stubs/ContractHelpers.soldiffbeforeafterboth--- a/pallets/evm-contract-helpers/src/stubs/ContractHelpers.sol
+++ b/pallets/evm-contract-helpers/src/stubs/ContractHelpers.sol
@@ -10,6 +10,7 @@
}
contract ContractHelpers is Dummy {
+ // Selector: contractOwner(address) 5152b14c
function contractOwner(address contractAddress)
public
view
@@ -21,6 +22,7 @@
return 0x0000000000000000000000000000000000000000;
}
+ // Selector: sponsoringEnabled(address) 6027dc61
function sponsoringEnabled(address contractAddress)
public
view
@@ -32,6 +34,7 @@
return false;
}
+ // Selector: toggleSponsoring(address,bool) fcac6d86
function toggleSponsoring(address contractAddress, bool enabled) public {
require(false, stub_error);
contractAddress;
@@ -39,6 +42,7 @@
dummy = 0;
}
+ // Selector: setSponsoringRateLimit(address,uint32) 77b6c908
function setSponsoringRateLimit(address contractAddress, uint32 rateLimit)
public
{
@@ -48,6 +52,7 @@
dummy = 0;
}
+ // Selector: allowed(address,address) 5c658165
function allowed(address contractAddress, address user)
public
view
@@ -60,6 +65,7 @@
return false;
}
+ // Selector: allowlistEnabled(address) c772ef6c
function allowlistEnabled(address contractAddress)
public
view
@@ -71,6 +77,7 @@
return false;
}
+ // Selector: toggleAllowlist(address,bool) 36de20f5
function toggleAllowlist(address contractAddress, bool enabled) public {
require(false, stub_error);
contractAddress;
@@ -78,6 +85,7 @@
dummy = 0;
}
+ // Selector: toggleAllowed(address,address,bool) 4706cc1c
function toggleAllowed(
address contractAddress,
address user,
pallets/nft/src/eth/erc.rsdiffbeforeafterboth--- a/pallets/nft/src/eth/erc.rs
+++ b/pallets/nft/src/eth/erc.rs
@@ -8,6 +8,7 @@
};
use frame_support::storage::{StorageMap, StorageDoubleMap};
use pallet_evm::AddressMapping;
+use pallet_evm_coder_substrate::dispatch_to_evm;
use super::account::CrossAccountId;
use sp_std::{vec, vec::Vec};
@@ -299,6 +300,90 @@
.ok_or("item id overflow")?
.into())
}
+
+ fn set_variable_metadata(
+ &mut self,
+ caller: caller,
+ token_id: uint256,
+ data: bytes,
+ ) -> Result<void> {
+ let caller = T::CrossAccountId::from_eth(caller);
+ let token_id = token_id.try_into().map_err(|_| "token id overflow")?;
+
+ <Module<T>>::set_variable_meta_data_internal(&caller, self, token_id, data)
+ .map_err(dispatch_to_evm::<T>)?;
+ Ok(())
+ }
+
+ fn get_variable_metadata(&self, token_id: uint256) -> Result<bytes> {
+ let token_id = token_id.try_into().map_err(|_| "token id overflow")?;
+
+ <Module<T>>::get_variable_metadata(self, token_id).map_err(dispatch_to_evm::<T>)
+ }
+
+ fn mint_bulk(&mut self, caller: caller, to: address, token_ids: Vec<uint256>) -> Result<bool> {
+ let caller = T::CrossAccountId::from_eth(caller);
+ let to = T::CrossAccountId::from_eth(to);
+ let mut expected_index = <ItemListIndex>::get(self.id)
+ .checked_add(1)
+ .ok_or("item id overflow")?;
+
+ let total_tokens = token_ids.len();
+ for id in token_ids.into_iter() {
+ let id: u32 = id.try_into().map_err(|_| "token id overflow")?;
+ if id != expected_index {
+ return Err("item id should be next".into());
+ }
+ expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;
+ }
+
+ let data = (0..total_tokens)
+ .map(|_| {
+ CreateItemData::NFT(CreateNftData {
+ const_data: vec![].try_into().unwrap(),
+ variable_data: vec![].try_into().unwrap(),
+ })
+ })
+ .collect();
+
+ <Module<T>>::create_multiple_items_internal(&caller, self, &to, data)
+ .map_err(dispatch_to_evm::<T>)?;
+ Ok(true)
+ }
+
+ #[solidity(rename_selector = "mintBulkWithTokenURI")]
+ fn mint_bulk_with_token_uri(
+ &mut self,
+ caller: caller,
+ to: address,
+ tokens: Vec<(uint256, string)>,
+ ) -> Result<bool> {
+ let caller = T::CrossAccountId::from_eth(caller);
+ let to = T::CrossAccountId::from_eth(to);
+ let mut expected_index = <ItemListIndex>::get(self.id)
+ .checked_add(1)
+ .ok_or("item id overflow")?;
+
+ let mut data = Vec::with_capacity(tokens.len());
+ for (id, token_uri) in tokens {
+ let id: u32 = id.try_into().map_err(|_| "token id overflow")?;
+ if id != expected_index {
+ panic!("item id should be next ({}) but got {}", expected_index, id);
+ }
+ expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;
+
+ data.push(CreateItemData::NFT(CreateNftData {
+ const_data: Vec::<u8>::from(token_uri)
+ .try_into()
+ .map_err(|_| "token uri is too long")?,
+ variable_data: vec![].try_into().unwrap(),
+ }));
+ }
+
+ <Module<T>>::create_multiple_items_internal(&caller, self, &to, data)
+ .map_err(dispatch_to_evm::<T>)?;
+ Ok(true)
+ }
}
#[solidity_interface(
pallets/nft/src/eth/stubs/UniqueFungible.soldiffbeforeafterboth--- a/pallets/nft/src/eth/stubs/UniqueFungible.sol
+++ b/pallets/nft/src/eth/stubs/UniqueFungible.sol
@@ -21,12 +21,14 @@
// Inline
contract InlineNameSymbol is Dummy {
+ // Selector: name() 06fdde03
function name() public view returns (string memory) {
require(false, stub_error);
dummy;
return "";
}
+ // Selector: symbol() 95d89b41
function symbol() public view returns (string memory) {
require(false, stub_error);
dummy;
@@ -36,6 +38,7 @@
// Inline
contract InlineTotalSupply is Dummy {
+ // Selector: totalSupply() 18160ddd
function totalSupply() public view returns (uint256) {
require(false, stub_error);
dummy;
@@ -44,6 +47,7 @@
}
contract ERC165 is Dummy {
+ // Selector: supportsInterface(bytes4) 01ffc9a7
function supportsInterface(uint32 interfaceId) public view returns (bool) {
require(false, stub_error);
interfaceId;
@@ -53,12 +57,14 @@
}
contract ERC20 is Dummy, InlineNameSymbol, InlineTotalSupply, ERC20Events {
+ // Selector: decimals() 313ce567
function decimals() public view returns (uint8) {
require(false, stub_error);
dummy;
return 0;
}
+ // Selector: balanceOf(address) 70a08231
function balanceOf(address owner) public view returns (uint256) {
require(false, stub_error);
owner;
@@ -66,6 +72,7 @@
return 0;
}
+ // Selector: transfer(address,uint256) a9059cbb
function transfer(address to, uint256 amount) public returns (bool) {
require(false, stub_error);
to;
@@ -74,6 +81,7 @@
return false;
}
+ // Selector: transferFrom(address,address,uint256) 23b872dd
function transferFrom(
address from,
address to,
@@ -87,6 +95,7 @@
return false;
}
+ // Selector: approve(address,uint256) 095ea7b3
function approve(address spender, uint256 amount) public returns (bool) {
require(false, stub_error);
spender;
@@ -95,6 +104,7 @@
return false;
}
+ // Selector: allowance(address,address) dd62ed3e
function allowance(address owner, address spender)
public
view
pallets/nft/src/eth/stubs/UniqueNFT.rawdiffbeforeafterbothbinary blob — no preview
pallets/nft/src/eth/stubs/UniqueNFT.soldiffbeforeafterboth--- a/pallets/nft/src/eth/stubs/UniqueNFT.sol
+++ b/pallets/nft/src/eth/stubs/UniqueNFT.sol
@@ -3,6 +3,12 @@
pragma solidity >=0.8.0 <0.9.0;
+// Anonymous struct
+struct Tuple0 {
+ uint256 field_0;
+ string field_1;
+}
+
// Common stubs holder
contract Dummy {
uint8 dummy;
@@ -35,12 +41,14 @@
// Inline
contract InlineNameSymbol is Dummy {
+ // Selector: name() 06fdde03
function name() public view returns (string memory) {
require(false, stub_error);
dummy;
return "";
}
+ // Selector: symbol() 95d89b41
function symbol() public view returns (string memory) {
require(false, stub_error);
dummy;
@@ -50,6 +58,7 @@
// Inline
contract InlineTotalSupply is Dummy {
+ // Selector: totalSupply() 18160ddd
function totalSupply() public view returns (uint256) {
require(false, stub_error);
dummy;
@@ -58,6 +67,7 @@
}
contract ERC165 is Dummy {
+ // Selector: supportsInterface(bytes4) 01ffc9a7
function supportsInterface(uint32 interfaceId) public view returns (bool) {
require(false, stub_error);
interfaceId;
@@ -67,6 +77,7 @@
}
contract ERC721 is Dummy, ERC165, ERC721Events {
+ // Selector: balanceOf(address) 70a08231
function balanceOf(address owner) public view returns (uint256) {
require(false, stub_error);
owner;
@@ -74,6 +85,7 @@
return 0;
}
+ // Selector: ownerOf(uint256) 6352211e
function ownerOf(uint256 tokenId) public view returns (address) {
require(false, stub_error);
tokenId;
@@ -81,6 +93,7 @@
return 0x0000000000000000000000000000000000000000;
}
+ // Selector: safeTransferFromWithData(address,address,uint256,bytes) 60a11672
function safeTransferFromWithData(
address from,
address to,
@@ -95,6 +108,7 @@
dummy = 0;
}
+ // Selector: safeTransferFrom(address,address,uint256) 42842e0e
function safeTransferFrom(
address from,
address to,
@@ -107,6 +121,7 @@
dummy = 0;
}
+ // Selector: transferFrom(address,address,uint256) 23b872dd
function transferFrom(
address from,
address to,
@@ -119,6 +134,7 @@
dummy = 0;
}
+ // Selector: approve(address,uint256) 095ea7b3
function approve(address approved, uint256 tokenId) public {
require(false, stub_error);
approved;
@@ -126,6 +142,7 @@
dummy = 0;
}
+ // Selector: setApprovalForAll(address,bool) a22cb465
function setApprovalForAll(address operator, bool approved) public {
require(false, stub_error);
operator;
@@ -133,6 +150,7 @@
dummy = 0;
}
+ // Selector: getApproved(uint256) 081812fc
function getApproved(uint256 tokenId) public view returns (address) {
require(false, stub_error);
tokenId;
@@ -140,6 +158,7 @@
return 0x0000000000000000000000000000000000000000;
}
+ // Selector: isApprovedForAll(address,address) e985e9c5
function isApprovedForAll(address owner, address operator)
public
view
@@ -154,6 +173,7 @@
}
contract ERC721Burnable is Dummy {
+ // Selector: burn(uint256) 42966c68
function burn(uint256 tokenId) public {
require(false, stub_error);
tokenId;
@@ -162,6 +182,7 @@
}
contract ERC721Enumerable is Dummy, InlineTotalSupply {
+ // Selector: tokenByIndex(uint256) 4f6ccce7
function tokenByIndex(uint256 index) public view returns (uint256) {
require(false, stub_error);
index;
@@ -169,6 +190,7 @@
return 0;
}
+ // Selector: tokenOfOwnerByIndex(address,uint256) 2f745c59
function tokenOfOwnerByIndex(address owner, uint256 index)
public
view
@@ -183,6 +205,7 @@
}
contract ERC721Metadata is Dummy, InlineNameSymbol {
+ // Selector: tokenURI(uint256) c87b56dd
function tokenURI(uint256 tokenId) public view returns (string memory) {
require(false, stub_error);
tokenId;
@@ -192,12 +215,14 @@
}
contract ERC721Mintable is Dummy, ERC721MintableEvents {
+ // Selector: mintingFinished() 05d2035b
function mintingFinished() public view returns (bool) {
require(false, stub_error);
dummy;
return false;
}
+ // Selector: mint(address,uint256) 40c10f19
function mint(address to, uint256 tokenId) public returns (bool) {
require(false, stub_error);
to;
@@ -206,6 +231,7 @@
return false;
}
+ // Selector: mintWithTokenURI(address,uint256,string) 50bb4e7f
function mintWithTokenURI(
address to,
uint256 tokenId,
@@ -219,6 +245,7 @@
return false;
}
+ // Selector: finishMinting() 7d64bcb4
function finishMinting() public returns (bool) {
require(false, stub_error);
dummy = 0;
@@ -227,6 +254,7 @@
}
contract ERC721UniqueExtensions is Dummy {
+ // Selector: transfer(address,uint256) a9059cbb
function transfer(address to, uint256 tokenId) public {
require(false, stub_error);
to;
@@ -234,11 +262,56 @@
dummy = 0;
}
+ // Selector: nextTokenId() 75794a3c
function nextTokenId() public view returns (uint256) {
require(false, stub_error);
dummy;
return 0;
}
+
+ // Selector: setVariableMetadata(uint256,bytes) d4eac26d
+ function setVariableMetadata(uint256 tokenId, bytes memory data) public {
+ require(false, stub_error);
+ tokenId;
+ data;
+ dummy = 0;
+ }
+
+ // Selector: getVariableMetadata(uint256) e6c5ce6f
+ function getVariableMetadata(uint256 tokenId)
+ public
+ view
+ returns (bytes memory)
+ {
+ require(false, stub_error);
+ tokenId;
+ dummy;
+ return hex"";
+ }
+
+ // Selector: mintBulk(address,uint256[]) 44a9945e
+ function mintBulk(address to, uint256[] memory tokenIds)
+ public
+ returns (bool)
+ {
+ require(false, stub_error);
+ to;
+ tokenIds;
+ dummy = 0;
+ return false;
+ }
+
+ // Selector: mintBulkWithTokenURI(address,(uint256,string)[]) 36543006
+ function mintBulkWithTokenURI(address to, Tuple0[] memory tokens)
+ public
+ returns (bool)
+ {
+ require(false, stub_error);
+ to;
+ tokens;
+ dummy = 0;
+ return false;
+ }
}
contract UniqueNFT is
pallets/nft/src/lib.rsdiffbeforeafterboth1//2// This file is subject to the terms and conditions defined in3// file 'LICENSE', which is part of this source code package.4//56#![recursion_limit = "1024"]7#![cfg_attr(not(feature = "std"), no_std)]8#![allow(9 clippy::too_many_arguments,10 clippy::unnecessary_mut_passed,11 clippy::unused_unit12)]1314extern crate alloc;1516pub use serde::{Serialize, Deserialize};1718pub use frame_support::{19 construct_runtime, decl_event, decl_module, decl_storage, decl_error,20 dispatch::DispatchResult,21 ensure, fail, parameter_types,22 traits::{23 Currency, ExistenceRequirement, Get, Imbalance, KeyOwnerProofSystem, OnUnbalanced,24 Randomness, IsSubType, WithdrawReasons,25 },26 weights::{27 constants::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight, WEIGHT_PER_SECOND},28 DispatchInfo, GetDispatchInfo, IdentityFee, Pays, PostDispatchInfo, Weight,29 WeightToFeePolynomial, DispatchClass,30 },31 StorageValue, transactional,32};3334use frame_system::{self as system, ensure_signed};35use sp_core::H160;36use sp_std::vec;37use sp_runtime::{DispatchError, sp_std::prelude::Vec};38use core::ops::{Deref, DerefMut};39use nft_data_structs::{40 MAX_DECIMAL_POINTS, MAX_SPONSOR_TIMEOUT, MAX_TOKEN_OWNERSHIP, MAX_REFUNGIBLE_PIECES,41 CUSTOM_DATA_LIMIT, COLLECTION_NUMBER_LIMIT, ACCOUNT_TOKEN_OWNERSHIP_LIMIT,42 VARIABLE_ON_CHAIN_SCHEMA_LIMIT, CONST_ON_CHAIN_SCHEMA_LIMIT, COLLECTION_ADMINS_LIMIT,43 OFFCHAIN_SCHEMA_LIMIT, MAX_TOKEN_PREFIX_LENGTH, MAX_COLLECTION_NAME_LENGTH,44 MAX_COLLECTION_DESCRIPTION_LENGTH, AccessMode, Collection, CreateItemData, CollectionLimits,45 CollectionId, CollectionMode, TokenId, SchemaVersion, SponsorshipState, Ownership, NftItemType,46 MetaUpdatePermission, FungibleItemType, ReFungibleItemType,47};4849#[cfg(test)]50mod mock;5152#[cfg(test)]53mod tests;5455mod eth;56mod sponsorship;57pub use sponsorship::NftSponsorshipHandler;58pub use eth::sponsoring::NftEthSponsorshipHandler;5960pub use eth::NftErcSupport;61pub use eth::account::*;62use eth::erc::{ERC20Events, ERC721Events};6364#[cfg(feature = "runtime-benchmarks")]65mod benchmarking;66pub mod weights;67use weights::WeightInfo;6869decl_error! {70 /// Error for non-fungible-token module.71 pub enum Error for Module<T: Config> {72 /// Total collections bound exceeded.73 TotalCollectionsLimitExceeded,74 /// Decimal_points parameter must be lower than MAX_DECIMAL_POINTS constant, currently it is 30.75 CollectionDecimalPointLimitExceeded,76 /// Collection name can not be longer than 63 char.77 CollectionNameLimitExceeded,78 /// Collection description can not be longer than 255 char.79 CollectionDescriptionLimitExceeded,80 /// Token prefix can not be longer than 15 char.81 CollectionTokenPrefixLimitExceeded,82 /// This collection does not exist.83 CollectionNotFound,84 /// Item not exists.85 TokenNotFound,86 /// Admin not found87 AdminNotFound,88 /// Arithmetic calculation overflow.89 NumOverflow,90 /// Account already has admin role.91 AlreadyAdmin,92 /// You do not own this collection.93 NoPermission,94 /// This address is not set as sponsor, use setCollectionSponsor first.95 ConfirmUnsetSponsorFail,96 /// Collection is not in mint mode.97 PublicMintingNotAllowed,98 /// Sender parameter and item owner must be equal.99 MustBeTokenOwner,100 /// Item balance not enough.101 TokenValueTooLow,102 /// Size of item is too large.103 NftSizeLimitExceeded,104 /// No approve found105 ApproveNotFound,106 /// Requested value more than approved.107 TokenValueNotEnough,108 /// Only approved addresses can call this method.109 ApproveRequired,110 /// Address is not in white list.111 AddresNotInWhiteList,112 /// Number of collection admins bound exceeded.113 CollectionAdminsLimitExceeded,114 /// Owned tokens by a single address bound exceeded.115 AddressOwnershipLimitExceeded,116 /// Length of items properties must be greater than 0.117 EmptyArgument,118 /// const_data exceeded data limit.119 TokenConstDataLimitExceeded,120 /// variable_data exceeded data limit.121 TokenVariableDataLimitExceeded,122 /// Not NFT item data used to mint in NFT collection.123 NotNftDataUsedToMintNftCollectionToken,124 /// Not Fungible item data used to mint in Fungible collection.125 NotFungibleDataUsedToMintFungibleCollectionToken,126 /// Not Re Fungible item data used to mint in Re Fungible collection.127 NotReFungibleDataUsedToMintReFungibleCollectionToken,128 /// Unexpected collection type.129 UnexpectedCollectionType,130 /// Can't store metadata in fungible tokens.131 CantStoreMetadataInFungibleTokens,132 /// Collection token limit exceeded133 CollectionTokenLimitExceeded,134 /// Account token limit exceeded per collection135 AccountTokenLimitExceeded,136 /// Collection limit bounds per collection exceeded137 CollectionLimitBoundsExceeded,138 /// Tried to enable permissions which are only permitted to be disabled139 OwnerPermissionsCantBeReverted,140 /// Schema data size limit bound exceeded141 SchemaDataLimitExceeded,142 /// Maximum refungibility exceeded143 WrongRefungiblePieces,144 /// createRefungible should be called with one owner145 BadCreateRefungibleCall,146 /// Gas limit exceeded147 OutOfGas,148 /// Metadata update denied by collection settings149 MetadataUpdateDenied,150 /// Metadata update flag become unmutable with None option151 MetadataFlagFrozen,152 /// Collection settings not allowing items transferring153 TransferNotAllowed,154 /// Can't transfer tokens to ethereum zero address155 AddressIsZero,156 }157}158159#[must_use = "Should call submit_logs or save, otherwise some data will be lost for evm side"]160pub struct CollectionHandle<T: Config> {161 pub id: CollectionId,162 collection: Collection<T>,163 recorder: pallet_evm_coder_substrate::SubstrateRecorder<T>,164}165impl<T: Config> CollectionHandle<T> {166 pub fn get_with_gas_limit(id: CollectionId, gas_limit: u64) -> Option<Self> {167 <CollectionById<T>>::get(id).map(|collection| Self {168 id,169 collection,170 recorder: pallet_evm_coder_substrate::SubstrateRecorder::new(171 eth::collection_id_to_address(id),172 gas_limit,173 ),174 })175 }176 pub fn get(id: CollectionId) -> Option<Self> {177 Self::get_with_gas_limit(id, u64::MAX)178 }179 pub fn log(&self, log: impl evm_coder::ToLog) -> DispatchResult {180 self.recorder.log_sub(log)181 }182 #[allow(dead_code)]183 fn consume_gas(&self, gas: u64) -> DispatchResult {184 self.recorder.consume_gas_sub(gas)185 }186 fn consume_sload(&self) -> DispatchResult {187 self.recorder.consume_sload_sub()188 }189 fn consume_sstore(&self) -> DispatchResult {190 self.recorder.consume_sstore_sub()191 }192 pub fn submit_logs(self) -> DispatchResult {193 self.recorder.submit_logs()194 }195 pub fn save(self) -> DispatchResult {196 self.recorder.submit_logs()?;197 <CollectionById<T>>::insert(self.id, self.collection);198 Ok(())199 }200}201impl<T: Config> Deref for CollectionHandle<T> {202 type Target = Collection<T>;203204 fn deref(&self) -> &Self::Target {205 &self.collection206 }207}208209impl<T: Config> DerefMut for CollectionHandle<T> {210 fn deref_mut(&mut self) -> &mut Self::Target {211 &mut self.collection212 }213}214215pub trait Config: system::Config + pallet_evm_coder_substrate::Config + Sized {216 type Event: From<Event<Self>> + Into<<Self as system::Config>::Event>;217218 /// Weight information for extrinsics in this pallet.219 type WeightInfo: WeightInfo;220221 type EvmAddressMapping: pallet_evm::AddressMapping<Self::AccountId>;222 type EvmBackwardsAddressMapping: EvmBackwardsAddressMapping<Self::AccountId>;223224 type CrossAccountId: CrossAccountId<Self::AccountId>;225 type Currency: Currency<Self::AccountId>;226 type CollectionCreationPrice: Get<227 <<Self as Config>::Currency as Currency<Self::AccountId>>::Balance,228 >;229 type TreasuryAccountId: Get<Self::AccountId>;230}231232type SelfWeightOf<T> = <T as Config>::WeightInfo;233234trait WeightInfoHelpers: WeightInfo {235 fn transfer() -> Weight {236 Self::transfer_nft()237 .max(Self::transfer_fungible())238 .max(Self::transfer_refungible())239 }240 fn transfer_from() -> Weight {241 Self::transfer_from_nft()242 .max(Self::transfer_from_fungible())243 .max(Self::transfer_from_refungible())244 }245 fn approve() -> Weight {246 // TODO: refungible, fungible247 Self::approve_nft()248 }249 fn set_variable_meta_data(data: u32) -> Weight {250 // TODO: refungible251 Self::set_variable_meta_data_nft(data)252 }253 fn create_item(data: u32) -> Weight {254 Self::create_item_nft(data)255 .max(Self::create_item_fungible())256 .max(Self::create_item_refungible(data))257 }258 fn create_multiple_items(amount: u32) -> Weight {259 Self::create_multiple_items_nft(amount)260 .max(Self::create_multiple_items_fungible(amount))261 .max(Self::create_multiple_items_refungible(amount))262 }263 fn burn_item() -> Weight {264 // TODO: refungible, fungible265 Self::burn_item_nft()266 }267}268impl<T: WeightInfo> WeightInfoHelpers for T {}269270// # Used definitions271//272// ## User control levels273//274// chain-controlled - key is uncontrolled by user275// i.e autoincrementing index276// can use non-cryptographic hash277// real - key is controlled by user278// but it is hard to generate enough colliding values, i.e owner of signed txs279// can use non-cryptographic hash280// controlled - key is completly controlled by users281// i.e maps with mutable keys282// should use cryptographic hash283//284// ## User control level downgrade reasons285//286// ?1 - chain-controlled -> controlled287// collections/tokens can be destroyed, resulting in massive holes288// ?2 - chain-controlled -> controlled289// same as ?1, but can be only added, resulting in easier exploitation290// ?3 - real -> controlled291// no confirmation required, so addresses can be easily generated292decl_storage! {293 trait Store for Module<T: Config> as Nft {294295 //#region Private members296 /// Id of next collection297 CreatedCollectionCount: u32;298 /// Used for migrations299 ChainVersion: u64;300 /// Id of last collection token301 /// Collection id (controlled?1)302 ItemListIndex: map hasher(blake2_128_concat) CollectionId => TokenId;303 //#endregion304305 //#region Bound counters306 /// Amount of collections destroyed, used for total amount tracking with307 /// CreatedCollectionCount308 DestroyedCollectionCount: u32;309 /// Total amount of account owned tokens (NFTs + RFTs + unique fungibles)310 /// Account id (real)311 pub AccountItemCount get(fn account_item_count): map hasher(twox_64_concat) T::AccountId => u32;312 //#endregion313314 //#region Basic collections315 /// Collection info316 /// Collection id (controlled?1)317 pub CollectionById get(fn collection_id) config(): map hasher(blake2_128_concat) CollectionId => Option<Collection<T>> = None;318 /// List of collection admins319 /// Collection id (controlled?2)320 pub AdminList get(fn admin_list_collection): map hasher(blake2_128_concat) CollectionId => Vec<T::CrossAccountId>;321 /// Whitelisted collection users322 /// Collection id (controlled?2), user id (controlled?3)323 pub WhiteList get(fn white_list): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) T::AccountId => bool;324 //#endregion325326 /// How many of collection items user have327 /// Collection id (controlled?2), account id (real)328 pub Balance get(fn balance_count): double_map hasher(blake2_128_concat) CollectionId, hasher(twox_64_concat) T::AccountId => u128;329330 /// Amount of items which spender can transfer out of owners account (via transferFrom)331 /// Collection id (controlled?2), (token id (controlled ?2) + owner account id (real) + spender account id (controlled?3))332 /// TODO: Off chain worker should remove from this map when token gets removed333 pub Allowances get(fn approved): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) (TokenId, T::AccountId, T::AccountId) => u128;334335 //#region Item collections336 /// Collection id (controlled?2), token id (controlled?1)337 pub NftItemList get(fn nft_item_id) config(): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<NftItemType<T::CrossAccountId>>;338 /// Collection id (controlled?2), owner (controlled?2)339 pub FungibleItemList get(fn fungible_item_id) config(): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) T::AccountId => FungibleItemType;340 /// Collection id (controlled?2), token id (controlled?1)341 pub ReFungibleItemList get(fn refungible_item_id) config(): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<ReFungibleItemType<T::CrossAccountId>>;342 //#endregion343344 //#region Index list345 /// Collection id (controlled?2), tokens owner (controlled?2)346 pub AddressTokens get(fn address_tokens): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) T::AccountId => Vec<TokenId>;347 //#endregion348349 //#region Tokens transfer rate limit baskets350 /// (Collection id (controlled?2), who created (real))351 /// TODO: Off chain worker should remove from this map when collection gets removed352 pub CreateItemBasket get(fn create_item_basket): map hasher(blake2_128_concat) (CollectionId, T::AccountId) => T::BlockNumber;353 /// Collection id (controlled?2), token id (controlled?2)354 pub NftTransferBasket get(fn nft_transfer_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => T::BlockNumber;355 /// Collection id (controlled?2), owning user (real)356 pub FungibleTransferBasket get(fn fungible_transfer_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(twox_64_concat) T::AccountId => T::BlockNumber;357 /// Collection id (controlled?2), token id (controlled?2)358 pub ReFungibleTransferBasket get(fn refungible_transfer_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => T::BlockNumber;359 //#endregion360361 /// Variable metadata sponsoring362 /// Collection id (controlled?2), token id (controlled?2)363 pub VariableMetaDataBasket get(fn variable_meta_data_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber> = None;364 }365 add_extra_genesis {366 build(|config: &GenesisConfig<T>| {367 // Modification of storage368 for (_num, _c) in &config.collection_id {369 <Module<T>>::init_collection(_c);370 }371372 for (_num, _c, _i) in &config.nft_item_id {373 <Module<T>>::init_nft_token(*_c, _i);374 }375376 for (collection_id, account_id, fungible_item) in &config.fungible_item_id {377 <Module<T>>::init_fungible_token(*collection_id, &T::CrossAccountId::from_sub(account_id.clone()), fungible_item);378 }379380 for (_num, _c, _i) in &config.refungible_item_id {381 <Module<T>>::init_refungible_token(*_c, _i);382 }383 })384 }385}386387decl_event!(388 pub enum Event<T>389 where390 AccountId = <T as frame_system::Config>::AccountId,391 CrossAccountId = <T as Config>::CrossAccountId,392 {393 /// New collection was created394 ///395 /// # Arguments396 ///397 /// * collection_id: Globally unique identifier of newly created collection.398 ///399 /// * mode: [CollectionMode] converted into u8.400 ///401 /// * account_id: Collection owner.402 CollectionCreated(CollectionId, u8, AccountId),403404 /// New item was created.405 ///406 /// # Arguments407 ///408 /// * collection_id: Id of the collection where item was created.409 ///410 /// * item_id: Id of an item. Unique within the collection.411 ///412 /// * recipient: Owner of newly created item413 ItemCreated(CollectionId, TokenId, CrossAccountId),414415 /// Collection item was burned.416 ///417 /// # Arguments418 ///419 /// collection_id.420 ///421 /// item_id: Identifier of burned NFT.422 ItemDestroyed(CollectionId, TokenId),423424 /// Item was transferred425 ///426 /// * collection_id: Id of collection to which item is belong427 ///428 /// * item_id: Id of an item429 ///430 /// * sender: Original owner of item431 ///432 /// * recipient: New owner of item433 ///434 /// * amount: Always 1 for NFT435 Transfer(CollectionId, TokenId, CrossAccountId, CrossAccountId, u128),436437 /// * collection_id438 ///439 /// * item_id440 ///441 /// * sender442 ///443 /// * spender444 ///445 /// * amount446 Approved(CollectionId, TokenId, CrossAccountId, CrossAccountId, u128),447 }448);449450decl_module! {451 pub struct Module<T: Config> for enum Call452 where453 origin: T::Origin454 {455 fn deposit_event() = default;456 const CollectionAdminsLimit: u64 = COLLECTION_ADMINS_LIMIT;457 type Error = Error<T>;458459 fn on_initialize(_now: T::BlockNumber) -> Weight {460 0461 }462463 /// This method creates a Collection of NFTs. Each Token may have multiple properties encoded as an array of bytes of certain length. The initial owner and admin of the collection are set to the address that signed the transaction. Both addresses can be changed later.464 ///465 /// # Permissions466 ///467 /// * Anyone.468 ///469 /// # Arguments470 ///471 /// * collection_name: UTF-16 string with collection name (limit 64 characters), will be stored as zero-terminated.472 ///473 /// * collection_description: UTF-16 string with collection description (limit 256 characters), will be stored as zero-terminated.474 ///475 /// * token_prefix: UTF-8 string with token prefix.476 ///477 /// * mode: [CollectionMode] collection type and type dependent data.478 // returns collection ID479 #[weight = <SelfWeightOf<T>>::create_collection()]480 #[transactional]481 pub fn create_collection(origin,482 collection_name: Vec<u16>,483 collection_description: Vec<u16>,484 token_prefix: Vec<u8>,485 mode: CollectionMode) -> DispatchResult {486487 // Anyone can create a collection488 let who = ensure_signed(origin)?;489490 // Take a (non-refundable) deposit of collection creation491 let mut imbalance = <<<T as Config>::Currency as Currency<T::AccountId>>::PositiveImbalance>::zero();492 imbalance.subsume(<<T as Config>::Currency as Currency<T::AccountId>>::deposit_creating(493 &T::TreasuryAccountId::get(),494 T::CollectionCreationPrice::get(),495 ));496 <T as Config>::Currency::settle(497 &who,498 imbalance,499 WithdrawReasons::TRANSFER,500 ExistenceRequirement::KeepAlive,501 ).map_err(|_| Error::<T>::NoPermission)?;502503 let decimal_points = match mode {504 CollectionMode::Fungible(points) => points,505 _ => 0506 };507508 let created_count = CreatedCollectionCount::get();509 let destroyed_count = DestroyedCollectionCount::get();510511 // bound Total number of collections512 ensure!(created_count - destroyed_count < COLLECTION_NUMBER_LIMIT, Error::<T>::TotalCollectionsLimitExceeded);513514 // check params515 ensure!(decimal_points <= MAX_DECIMAL_POINTS, Error::<T>::CollectionDecimalPointLimitExceeded);516 ensure!(collection_name.len() <= MAX_COLLECTION_NAME_LENGTH, Error::<T>::CollectionNameLimitExceeded);517 ensure!(collection_description.len() <= MAX_COLLECTION_DESCRIPTION_LENGTH, Error::<T>::CollectionDescriptionLimitExceeded);518 ensure!(token_prefix.len() <= MAX_TOKEN_PREFIX_LENGTH, Error::<T>::CollectionTokenPrefixLimitExceeded);519520 // Generate next collection ID521 let next_id = created_count522 .checked_add(1)523 .ok_or(Error::<T>::NumOverflow)?;524525 CreatedCollectionCount::put(next_id);526527 let limits = CollectionLimits {528 sponsored_data_size: CUSTOM_DATA_LIMIT,529 ..Default::default()530 };531532 // Create new collection533 let new_collection = Collection {534 owner: who.clone(),535 name: collection_name,536 mode: mode.clone(),537 mint_mode: false,538 access: AccessMode::Normal,539 description: collection_description,540 decimal_points,541 token_prefix,542 offchain_schema: Vec::new(),543 schema_version: SchemaVersion::ImageURL,544 sponsorship: SponsorshipState::Disabled,545 variable_on_chain_schema: Vec::new(),546 const_on_chain_schema: Vec::new(),547 limits,548 meta_update_permission: MetaUpdatePermission::default(),549 transfers_enabled: true,550 };551552 // Add new collection to map553 <CollectionById<T>>::insert(next_id, new_collection);554555 // call event556 Self::deposit_event(RawEvent::CollectionCreated(next_id, mode.id(), who));557558 Ok(())559 }560561 /// **DANGEROUS**: Destroys collection and all NFTs within this collection. Users irrecoverably lose their assets and may lose real money.562 ///563 /// # Permissions564 ///565 /// * Collection Owner.566 ///567 /// # Arguments568 ///569 /// * collection_id: collection to destroy.570 #[weight = <SelfWeightOf<T>>::destroy_collection()]571 #[transactional]572 pub fn destroy_collection(origin, collection_id: CollectionId) -> DispatchResult {573574 let sender = ensure_signed(origin)?;575 let collection = Self::get_collection(collection_id)?;576 Self::check_owner_permissions(&collection, &sender)?;577 if !collection.limits.owner_can_destroy {578 fail!(Error::<T>::NoPermission);579 }580581 <AddressTokens<T>>::remove_prefix(collection_id, None);582 <Allowances<T>>::remove_prefix(collection_id, None);583 <Balance<T>>::remove_prefix(collection_id, None);584 <ItemListIndex>::remove(collection_id);585 <AdminList<T>>::remove(collection_id);586 <CollectionById<T>>::remove(collection_id);587 <WhiteList<T>>::remove_prefix(collection_id, None);588589 <NftItemList<T>>::remove_prefix(collection_id, None);590 <FungibleItemList<T>>::remove_prefix(collection_id, None);591 <ReFungibleItemList<T>>::remove_prefix(collection_id, None);592593 <NftTransferBasket<T>>::remove_prefix(collection_id, None);594 <FungibleTransferBasket<T>>::remove_prefix(collection_id, None);595 <ReFungibleTransferBasket<T>>::remove_prefix(collection_id, None);596597 <VariableMetaDataBasket<T>>::remove_prefix(collection_id, None);598599 DestroyedCollectionCount::put(DestroyedCollectionCount::get()600 .checked_add(1)601 .ok_or(Error::<T>::NumOverflow)?);602603 Ok(())604 }605606 /// Add an address to white list.607 ///608 /// # Permissions609 ///610 /// * Collection Owner611 /// * Collection Admin612 ///613 /// # Arguments614 ///615 /// * collection_id.616 ///617 /// * address.618 #[weight = <SelfWeightOf<T>>::add_to_white_list()]619 #[transactional]620 pub fn add_to_white_list(origin, collection_id: CollectionId, address: T::CrossAccountId) -> DispatchResult{621622 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);623 let collection = Self::get_collection(collection_id)?;624625 Self::toggle_white_list_internal(626 &sender,627 &collection,628 &address,629 true,630 )?;631632 Ok(())633 }634635 /// Remove an address from white list.636 ///637 /// # Permissions638 ///639 /// * Collection Owner640 /// * Collection Admin641 ///642 /// # Arguments643 ///644 /// * collection_id.645 ///646 /// * address.647 #[weight = <SelfWeightOf<T>>::remove_from_white_list()]648 #[transactional]649 pub fn remove_from_white_list(origin, collection_id: CollectionId, address: T::CrossAccountId) -> DispatchResult{650651 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);652 let collection = Self::get_collection(collection_id)?;653654 Self::toggle_white_list_internal(655 &sender,656 &collection,657 &address,658 false,659 )?;660661 Ok(())662 }663664 /// Toggle between normal and white list access for the methods with access for `Anyone`.665 ///666 /// # Permissions667 ///668 /// * Collection Owner.669 ///670 /// # Arguments671 ///672 /// * collection_id.673 ///674 /// * mode: [AccessMode]675 #[weight = <SelfWeightOf<T>>::set_public_access_mode()]676 #[transactional]677 pub fn set_public_access_mode(origin, collection_id: CollectionId, mode: AccessMode) -> DispatchResult678 {679 let sender = ensure_signed(origin)?;680681 let mut target_collection = Self::get_collection(collection_id)?;682 Self::check_owner_permissions(&target_collection, &sender)?;683 target_collection.access = mode;684 target_collection.save()685 }686687 /// Allows Anyone to create tokens if:688 /// * White List is enabled, and689 /// * Address is added to white list, and690 /// * This method was called with True parameter691 ///692 /// # Permissions693 /// * Collection Owner694 ///695 /// # Arguments696 ///697 /// * collection_id.698 ///699 /// * mint_permission: Boolean parameter. If True, allows minting to Anyone with conditions above.700 #[weight = <SelfWeightOf<T>>::set_mint_permission()]701 #[transactional]702 pub fn set_mint_permission(origin, collection_id: CollectionId, mint_permission: bool) -> DispatchResult703 {704 let sender = ensure_signed(origin)?;705706 let mut target_collection = Self::get_collection(collection_id)?;707 Self::check_owner_permissions(&target_collection, &sender)?;708 target_collection.mint_mode = mint_permission;709 target_collection.save()710 }711712 /// Change the owner of the collection.713 ///714 /// # Permissions715 ///716 /// * Collection Owner.717 ///718 /// # Arguments719 ///720 /// * collection_id.721 ///722 /// * new_owner.723 #[weight = <SelfWeightOf<T>>::change_collection_owner()]724 #[transactional]725 pub fn change_collection_owner(origin, collection_id: CollectionId, new_owner: T::AccountId) -> DispatchResult {726727 let sender = ensure_signed(origin)?;728 let mut target_collection = Self::get_collection(collection_id)?;729 Self::check_owner_permissions(&target_collection, &sender)?;730 target_collection.owner = new_owner;731 target_collection.save()732 }733734 /// Adds an admin of the Collection.735 /// NFT Collection can be controlled by multiple admin addresses (some which can also be servers, for example). Admins can issue and burn NFTs, as well as add and remove other admins, but cannot change NFT or Collection ownership.736 ///737 /// # Permissions738 ///739 /// * Collection Owner.740 /// * Collection Admin.741 ///742 /// # Arguments743 ///744 /// * collection_id: ID of the Collection to add admin for.745 ///746 /// * new_admin_id: Address of new admin to add.747 #[weight = <SelfWeightOf<T>>::add_collection_admin()]748 #[transactional]749 pub fn add_collection_admin(origin, collection_id: CollectionId, new_admin_id: T::CrossAccountId) -> DispatchResult {750 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);751 let collection = Self::get_collection(collection_id)?;752 Self::check_owner_or_admin_permissions(&collection, &sender)?;753 let mut admin_arr = <AdminList<T>>::get(collection_id);754755 match admin_arr.binary_search(&new_admin_id) {756 Ok(_) => {},757 Err(idx) => {758 ensure!(admin_arr.len() < COLLECTION_ADMINS_LIMIT as usize, Error::<T>::CollectionAdminsLimitExceeded);759 admin_arr.insert(idx, new_admin_id);760 <AdminList<T>>::insert(collection_id, admin_arr);761 }762 }763 Ok(())764 }765766 /// Remove admin address of the Collection. An admin address can remove itself. List of admins may become empty, in which case only Collection Owner will be able to add an Admin.767 ///768 /// # Permissions769 ///770 /// * Collection Owner.771 /// * Collection Admin.772 ///773 /// # Arguments774 ///775 /// * collection_id: ID of the Collection to remove admin for.776 ///777 /// * account_id: Address of admin to remove.778 #[weight = <SelfWeightOf<T>>::remove_collection_admin()]779 #[transactional]780 pub fn remove_collection_admin(origin, collection_id: CollectionId, account_id: T::CrossAccountId) -> DispatchResult {781 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);782 let collection = Self::get_collection(collection_id)?;783 Self::check_owner_or_admin_permissions(&collection, &sender)?;784 let mut admin_arr = <AdminList<T>>::get(collection_id);785786 if let Ok(idx) = admin_arr.binary_search(&account_id) {787 admin_arr.remove(idx);788 <AdminList<T>>::insert(collection_id, admin_arr);789 }790 Ok(())791 }792793 /// # Permissions794 ///795 /// * Collection Owner796 ///797 /// # Arguments798 ///799 /// * collection_id.800 ///801 /// * new_sponsor.802 #[weight = <SelfWeightOf<T>>::set_collection_sponsor()]803 #[transactional]804 pub fn set_collection_sponsor(origin, collection_id: CollectionId, new_sponsor: T::AccountId) -> DispatchResult {805 let sender = ensure_signed(origin)?;806 let mut target_collection = Self::get_collection(collection_id)?;807 Self::check_owner_permissions(&target_collection, &sender)?;808809 target_collection.sponsorship = SponsorshipState::Unconfirmed(new_sponsor);810 target_collection.save()811 }812813 /// # Permissions814 ///815 /// * Sponsor.816 ///817 /// # Arguments818 ///819 /// * collection_id.820 #[weight = <SelfWeightOf<T>>::confirm_sponsorship()]821 #[transactional]822 pub fn confirm_sponsorship(origin, collection_id: CollectionId) -> DispatchResult {823 let sender = ensure_signed(origin)?;824825 let mut target_collection = Self::get_collection(collection_id)?;826 ensure!(827 target_collection.sponsorship.pending_sponsor() == Some(&sender),828 Error::<T>::ConfirmUnsetSponsorFail829 );830831 target_collection.sponsorship = SponsorshipState::Confirmed(sender);832 target_collection.save()833 }834835 /// Switch back to pay-per-own-transaction model.836 ///837 /// # Permissions838 ///839 /// * Collection owner.840 ///841 /// # Arguments842 ///843 /// * collection_id.844 #[weight = <SelfWeightOf<T>>::remove_collection_sponsor()]845 #[transactional]846 pub fn remove_collection_sponsor(origin, collection_id: CollectionId) -> DispatchResult {847 let sender = ensure_signed(origin)?;848849 let mut target_collection = Self::get_collection(collection_id)?;850 Self::check_owner_permissions(&target_collection, &sender)?;851852 target_collection.sponsorship = SponsorshipState::Disabled;853 target_collection.save()854 }855856 /// This method creates a concrete instance of NFT Collection created with CreateCollection method.857 ///858 /// # Permissions859 ///860 /// * Collection Owner.861 /// * Collection Admin.862 /// * Anyone if863 /// * White List is enabled, and864 /// * Address is added to white list, and865 /// * MintPermission is enabled (see SetMintPermission method)866 ///867 /// # Arguments868 ///869 /// * collection_id: ID of the collection.870 ///871 /// * owner: Address, initial owner of the NFT.872 ///873 /// * data: Token data to store on chain.874 // #[weight =875 // (130_000_000 as Weight)876 // .saturating_add((2135 as Weight).saturating_mul((properties.len() as u64) as Weight))877 // .saturating_add(RocksDbWeight::get().reads(10 as Weight))878 // .saturating_add(RocksDbWeight::get().writes(8 as Weight))]879880 #[weight = <SelfWeightOf<T>>::create_item(data.data_size() as u32)]881 #[transactional]882 pub fn create_item(origin, collection_id: CollectionId, owner: T::CrossAccountId, data: CreateItemData) -> DispatchResult {883 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);884 let collection = Self::get_collection(collection_id)?;885886 Self::create_item_internal(&sender, &collection, &owner, data)?;887888 collection.submit_logs()889 }890891 /// This method creates multiple items in a collection created with CreateCollection method.892 ///893 /// # Permissions894 ///895 /// * Collection Owner.896 /// * Collection Admin.897 /// * Anyone if898 /// * White List is enabled, and899 /// * Address is added to white list, and900 /// * MintPermission is enabled (see SetMintPermission method)901 ///902 /// # Arguments903 ///904 /// * collection_id: ID of the collection.905 ///906 /// * itemsData: Array items properties. Each property is an array of bytes itself, see [create_item].907 ///908 /// * owner: Address, initial owner of the NFT.909 #[weight = <SelfWeightOf<T>>::create_multiple_items(items_data.len() as u32)]910 #[transactional]911 pub fn create_multiple_items(origin, collection_id: CollectionId, owner: T::CrossAccountId, items_data: Vec<CreateItemData>) -> DispatchResult {912913 ensure!(!items_data.is_empty(), Error::<T>::EmptyArgument);914 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);915 let collection = Self::get_collection(collection_id)?;916917 Self::create_multiple_items_internal(&sender, &collection, &owner, items_data)?;918919 collection.submit_logs()920 }921922 // TODO! transaction weight923924 /// Set transfers_enabled value for particular collection925 ///926 /// # Permissions927 ///928 /// * Collection Owner.929 ///930 /// # Arguments931 ///932 /// * collection_id: ID of the collection.933 ///934 /// * value: New flag value.935 #[weight = <SelfWeightOf<T>>::set_transfers_enabled_flag()]936 #[transactional]937 pub fn set_transfers_enabled_flag(origin, collection_id: CollectionId, value: bool) -> DispatchResult {938939 let sender = ensure_signed(origin)?;940 let mut target_collection = Self::get_collection(collection_id)?;941942 Self::check_owner_permissions(&target_collection, &sender)?;943944 target_collection.transfers_enabled = value;945 target_collection.save()946 }947948 // TODO! transaction weight949 /// Set meta_update_permission value for particular collection950 ///951 /// # Permissions952 ///953 /// * Collection Owner.954 ///955 /// # Arguments956 ///957 /// * collection_id: ID of the collection.958 ///959 /// * value: New flag value.960 #[weight = <T as Config>::WeightInfo::burn_item()]961 #[transactional]962 pub fn set_meta_update_permission_flag(origin, collection_id: CollectionId, value: MetaUpdatePermission) -> DispatchResult {963964 let sender = ensure_signed(origin)?;965 let mut target_collection = Self::get_collection(collection_id)?;966967 ensure!(968 target_collection.meta_update_permission != MetaUpdatePermission::None,969 Error::<T>::MetadataFlagFrozen970 );971 Self::check_owner_permissions(&target_collection, &sender)?;972973 target_collection.meta_update_permission = value;974975 target_collection.save()976 }977978 /// Destroys a concrete instance of NFT.979 ///980 /// # Permissions981 ///982 /// * Collection Owner.983 /// * Collection Admin.984 /// * Current NFT Owner.985 ///986 /// # Arguments987 ///988 /// * collection_id: ID of the collection.989 ///990 /// * item_id: ID of NFT to burn.991 #[weight = <SelfWeightOf<T>>::burn_item()]992 #[transactional]993 pub fn burn_item(origin, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResult {994995 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);996 let target_collection = Self::get_collection(collection_id)?;997998 Self::burn_item_internal(&sender, &target_collection, item_id, value)?;9991000 target_collection.submit_logs()1001 }10021003 /// Change ownership of the token.1004 ///1005 /// # Permissions1006 ///1007 /// * Collection Owner1008 /// * Collection Admin1009 /// * Current NFT owner1010 ///1011 /// # Arguments1012 ///1013 /// * recipient: Address of token recipient.1014 ///1015 /// * collection_id.1016 ///1017 /// * item_id: ID of the item1018 /// * Non-Fungible Mode: Required.1019 /// * Fungible Mode: Ignored.1020 /// * Re-Fungible Mode: Required.1021 ///1022 /// * value: Amount to transfer.1023 /// * Non-Fungible Mode: Ignored1024 /// * Fungible Mode: Must specify transferred amount1025 /// * Re-Fungible Mode: Must specify transferred portion (between 0 and 1)1026 #[weight = <SelfWeightOf<T>>::transfer()]1027 #[transactional]1028 pub fn transfer(origin, recipient: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResult {1029 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1030 let collection = Self::get_collection(collection_id)?;10311032 Self::transfer_internal(&sender, &recipient, &collection, item_id, value)?;10331034 collection.submit_logs()1035 }10361037 /// Set, change, or remove approved address to transfer the ownership of the NFT.1038 ///1039 /// # Permissions1040 ///1041 /// * Collection Owner1042 /// * Collection Admin1043 /// * Current NFT owner1044 ///1045 /// # Arguments1046 ///1047 /// * approved: Address that is approved to transfer this NFT or zero (if needed to remove approval).1048 ///1049 /// * collection_id.1050 ///1051 /// * item_id: ID of the item.1052 #[weight = <SelfWeightOf<T>>::approve()]1053 #[transactional]1054 pub fn approve(origin, spender: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, amount: u128) -> DispatchResult {1055 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1056 let collection = Self::get_collection(collection_id)?;10571058 Self::approve_internal(&sender, &spender, &collection, item_id, amount)?;10591060 collection.submit_logs()1061 }10621063 /// Change ownership of a NFT on behalf of the owner. See Approve method for additional information. After this method executes, the approval is removed so that the approved address will not be able to transfer this NFT again from this owner.1064 ///1065 /// # Permissions1066 /// * Collection Owner1067 /// * Collection Admin1068 /// * Current NFT owner1069 /// * Address approved by current NFT owner1070 ///1071 /// # Arguments1072 ///1073 /// * from: Address that owns token.1074 ///1075 /// * recipient: Address of token recipient.1076 ///1077 /// * collection_id.1078 ///1079 /// * item_id: ID of the item.1080 ///1081 /// * value: Amount to transfer.1082 #[weight = <SelfWeightOf<T>>::transfer_from()]1083 #[transactional]1084 pub fn transfer_from(origin, from: T::CrossAccountId, recipient: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, value: u128 ) -> DispatchResult {1085 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1086 let collection = Self::get_collection(collection_id)?;10871088 Self::transfer_from_internal(&sender, &from, &recipient, &collection, item_id, value)?;10891090 collection.submit_logs()1091 }1092 // #[weight = 0]1093 // // let no_perm_mes = "You do not have permissions to modify this collection";1094 // // ensure!(<ApprovedList<T>>::contains_key((collection_id, item_id)), no_perm_mes);1095 // // let list_itm = <ApprovedList<T>>::get((collection_id, item_id));1096 // // ensure!(list_itm.contains(&new_owner.clone()), no_perm_mes);10971098 // // // on_nft_received call10991100 // // Self::transfer(origin, collection_id, item_id, new_owner)?;11011102 // Ok(())1103 // }11041105 /// Set off-chain data schema.1106 ///1107 /// # Permissions1108 ///1109 /// * Collection Owner1110 /// * Collection Admin1111 ///1112 /// # Arguments1113 ///1114 /// * collection_id.1115 ///1116 /// * schema: String representing the offchain data schema.1117 #[weight = <SelfWeightOf<T>>::set_variable_meta_data(data.len() as u32)]1118 #[transactional]1119 pub fn set_variable_meta_data (1120 origin,1121 collection_id: CollectionId,1122 item_id: TokenId,1123 data: Vec<u8>1124 ) -> DispatchResult {1125 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);11261127 let collection = Self::get_collection(collection_id)?;11281129 Self::set_variable_meta_data_internal(&sender, &collection, item_id, data)?;11301131 Ok(())1132 }11331134 /// Set schema standard1135 /// ImageURL1136 /// Unique1137 ///1138 /// # Permissions1139 ///1140 /// * Collection Owner1141 /// * Collection Admin1142 ///1143 /// # Arguments1144 ///1145 /// * collection_id.1146 ///1147 /// * schema: SchemaVersion: enum1148 #[weight = <SelfWeightOf<T>>::set_schema_version()]1149 #[transactional]1150 pub fn set_schema_version(1151 origin,1152 collection_id: CollectionId,1153 version: SchemaVersion1154 ) -> DispatchResult {1155 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1156 let mut target_collection = Self::get_collection(collection_id)?;1157 Self::check_owner_or_admin_permissions(&target_collection, &sender)?;1158 target_collection.schema_version = version;1159 target_collection.save()1160 }11611162 /// Set off-chain data schema.1163 ///1164 /// # Permissions1165 ///1166 /// * Collection Owner1167 /// * Collection Admin1168 ///1169 /// # Arguments1170 ///1171 /// * collection_id.1172 ///1173 /// * schema: String representing the offchain data schema.1174 #[weight = <SelfWeightOf<T>>::set_offchain_schema(schema.len() as u32)]1175 #[transactional]1176 pub fn set_offchain_schema(1177 origin,1178 collection_id: CollectionId,1179 schema: Vec<u8>1180 ) -> DispatchResult {1181 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1182 let mut target_collection = Self::get_collection(collection_id)?;1183 Self::check_owner_or_admin_permissions(&target_collection, &sender)?;11841185 // check schema limit1186 ensure!(schema.len() as u32 <= OFFCHAIN_SCHEMA_LIMIT, "");11871188 target_collection.offchain_schema = schema;1189 target_collection.save()1190 }11911192 /// Set const on-chain data schema.1193 ///1194 /// # Permissions1195 ///1196 /// * Collection Owner1197 /// * Collection Admin1198 ///1199 /// # Arguments1200 ///1201 /// * collection_id.1202 ///1203 /// * schema: String representing the const on-chain data schema.1204 #[weight = <SelfWeightOf<T>>::set_const_on_chain_schema(schema.len() as u32)]1205 #[transactional]1206 pub fn set_const_on_chain_schema (1207 origin,1208 collection_id: CollectionId,1209 schema: Vec<u8>1210 ) -> DispatchResult {1211 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1212 let mut target_collection = Self::get_collection(collection_id)?;1213 Self::check_owner_or_admin_permissions(&target_collection, &sender)?;12141215 // check schema limit1216 ensure!(schema.len() as u32 <= CONST_ON_CHAIN_SCHEMA_LIMIT, "");12171218 target_collection.const_on_chain_schema = schema;1219 target_collection.save()1220 }12211222 /// Set variable on-chain data schema.1223 ///1224 /// # Permissions1225 ///1226 /// * Collection Owner1227 /// * Collection Admin1228 ///1229 /// # Arguments1230 ///1231 /// * collection_id.1232 ///1233 /// * schema: String representing the variable on-chain data schema.1234 #[weight = <SelfWeightOf<T>>::set_const_on_chain_schema(schema.len() as u32)]1235 #[transactional]1236 pub fn set_variable_on_chain_schema (1237 origin,1238 collection_id: CollectionId,1239 schema: Vec<u8>1240 ) -> DispatchResult {1241 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1242 let mut target_collection = Self::get_collection(collection_id)?;1243 Self::check_owner_or_admin_permissions(&target_collection, &sender)?;12441245 // check schema limit1246 ensure!(schema.len() as u32 <= VARIABLE_ON_CHAIN_SCHEMA_LIMIT, "");12471248 target_collection.variable_on_chain_schema = schema;1249 target_collection.save()1250 }12511252 #[weight = <SelfWeightOf<T>>::set_collection_limits()]1253 #[transactional]1254 pub fn set_collection_limits(1255 origin,1256 collection_id: u32,1257 new_limits: CollectionLimits<T::BlockNumber>,1258 ) -> DispatchResult {1259 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1260 let mut target_collection = Self::get_collection(collection_id)?;1261 Self::check_owner_permissions(&target_collection, sender.as_sub())?;1262 let old_limits = &target_collection.limits;12631264 // collection bounds1265 ensure!(new_limits.sponsor_transfer_timeout <= MAX_SPONSOR_TIMEOUT &&1266 new_limits.account_token_ownership_limit <= MAX_TOKEN_OWNERSHIP &&1267 new_limits.sponsored_data_size <= CUSTOM_DATA_LIMIT,1268 Error::<T>::CollectionLimitBoundsExceeded);12691270 // token_limit check prev1271 ensure!(old_limits.token_limit >= new_limits.token_limit, Error::<T>::CollectionTokenLimitExceeded);1272 ensure!(new_limits.token_limit > 0, Error::<T>::CollectionTokenLimitExceeded);12731274 ensure!(1275 (old_limits.owner_can_transfer || !new_limits.owner_can_transfer) &&1276 (old_limits.owner_can_destroy || !new_limits.owner_can_destroy),1277 Error::<T>::OwnerPermissionsCantBeReverted,1278 );12791280 target_collection.limits = new_limits;12811282 target_collection.save()1283 }1284 }1285}12861287impl<T: Config> Module<T> {1288 pub fn create_item_internal(1289 sender: &T::CrossAccountId,1290 collection: &CollectionHandle<T>,1291 owner: &T::CrossAccountId,1292 data: CreateItemData,1293 ) -> DispatchResult {1294 ensure!(1295 owner != &T::CrossAccountId::from_eth(H160([0; 20])),1296 Error::<T>::AddressIsZero1297 );12981299 Self::can_create_items_in_collection(collection, sender, owner, 1)?;1300 Self::validate_create_item_args(collection, &data)?;1301 Self::create_item_no_validation(collection, owner, data)?;13021303 Ok(())1304 }13051306 pub fn transfer_internal(1307 sender: &T::CrossAccountId,1308 recipient: &T::CrossAccountId,1309 target_collection: &CollectionHandle<T>,1310 item_id: TokenId,1311 value: u128,1312 ) -> DispatchResult {1313 ensure!(1314 recipient != &T::CrossAccountId::from_eth(H160([0; 20])),1315 Error::<T>::AddressIsZero1316 );13171318 // Limits check1319 Self::is_correct_transfer(target_collection, recipient)?;13201321 // Transfer permissions check1322 ensure!(1323 Self::is_item_owner(sender, target_collection, item_id)?1324 || Self::is_owner_or_admin_permissions(target_collection, sender)?,1325 Error::<T>::NoPermission1326 );13271328 if target_collection.access == AccessMode::WhiteList {1329 Self::check_white_list(target_collection, sender)?;1330 Self::check_white_list(target_collection, recipient)?;1331 }13321333 match target_collection.mode {1334 CollectionMode::NFT => Self::transfer_nft(1335 target_collection,1336 item_id,1337 sender.clone(),1338 recipient.clone(),1339 )?,1340 CollectionMode::Fungible(_) => {1341 Self::transfer_fungible(target_collection, value, sender, recipient)?1342 }1343 CollectionMode::ReFungible => Self::transfer_refungible(1344 target_collection,1345 item_id,1346 value,1347 sender.clone(),1348 recipient.clone(),1349 )?,1350 _ => (),1351 };13521353 Self::deposit_event(RawEvent::Transfer(1354 target_collection.id,1355 item_id,1356 sender.clone(),1357 recipient.clone(),1358 value,1359 ));13601361 Ok(())1362 }13631364 pub fn approve_internal(1365 sender: &T::CrossAccountId,1366 spender: &T::CrossAccountId,1367 collection: &CollectionHandle<T>,1368 item_id: TokenId,1369 amount: u128,1370 ) -> DispatchResult {1371 Self::token_exists(collection, item_id)?;13721373 // Transfer permissions check1374 let bypasses_limits = collection.limits.owner_can_transfer1375 && Self::is_owner_or_admin_permissions(collection, sender)?;13761377 let allowance_limit = if bypasses_limits {1378 None1379 } else if let Some(amount) = Self::owned_amount(sender, collection, item_id)? {1380 Some(amount)1381 } else {1382 fail!(Error::<T>::NoPermission);1383 };13841385 if collection.access == AccessMode::WhiteList {1386 Self::check_white_list(collection, sender)?;1387 Self::check_white_list(collection, spender)?;1388 }13891390 collection.consume_sload()?;1391 let allowance: u128 = amount1392 .checked_add(<Allowances<T>>::get(1393 collection.id,1394 (item_id, sender.as_sub(), spender.as_sub()),1395 ))1396 .ok_or(Error::<T>::NumOverflow)?;1397 if let Some(limit) = allowance_limit {1398 ensure!(limit >= allowance, Error::<T>::TokenValueTooLow);1399 }1400 collection.consume_sstore()?;1401 <Allowances<T>>::insert(1402 collection.id,1403 (item_id, sender.as_sub(), spender.as_sub()),1404 allowance,1405 );14061407 if matches!(collection.mode, CollectionMode::NFT) {1408 // TODO: NFT: only one owner may exist for token in ERC7211409 collection.log(ERC721Events::Approval {1410 owner: *sender.as_eth(),1411 approved: *spender.as_eth(),1412 token_id: item_id.into(),1413 })?;1414 }14151416 if matches!(collection.mode, CollectionMode::Fungible(_)) {1417 // TODO: NFT: only one owner may exist for token in ERC201418 collection.log(ERC20Events::Approval {1419 owner: *sender.as_eth(),1420 spender: *spender.as_eth(),1421 value: allowance.into(),1422 })?;1423 }14241425 Self::deposit_event(RawEvent::Approved(1426 collection.id,1427 item_id,1428 sender.clone(),1429 spender.clone(),1430 allowance,1431 ));1432 Ok(())1433 }14341435 pub fn transfer_from_internal(1436 sender: &T::CrossAccountId,1437 from: &T::CrossAccountId,1438 recipient: &T::CrossAccountId,1439 collection: &CollectionHandle<T>,1440 item_id: TokenId,1441 amount: u128,1442 ) -> DispatchResult {1443 if sender == from {1444 // Transfer by `from`, because it is either equal to sender, or derived from him1445 return Self::transfer_internal(from, recipient, collection, item_id, amount);1446 }14471448 // Check approval1449 collection.consume_sload()?;1450 let approval: u128 =1451 <Allowances<T>>::get(collection.id, (item_id, from.as_sub(), sender.as_sub()));14521453 // Limits check1454 Self::is_correct_transfer(collection, recipient)?;14551456 // Transfer permissions check1457 ensure!(1458 approval >= amount1459 || (collection.limits.owner_can_transfer1460 && Self::is_owner_or_admin_permissions(collection, sender)?),1461 Error::<T>::NoPermission1462 );14631464 if collection.access == AccessMode::WhiteList {1465 Self::check_white_list(collection, sender)?;1466 Self::check_white_list(collection, recipient)?;1467 }14681469 // Reduce approval by transferred amount or remove if remaining approval drops to 01470 let allowance = approval.saturating_sub(amount);1471 collection.consume_sstore()?;1472 if allowance > 0 {1473 <Allowances<T>>::insert(1474 collection.id,1475 (item_id, from.as_sub(), sender.as_sub()),1476 allowance,1477 );1478 } else {1479 <Allowances<T>>::remove(collection.id, (item_id, from.as_sub(), sender.as_sub()));1480 }14811482 match collection.mode {1483 CollectionMode::NFT => {1484 Self::transfer_nft(collection, item_id, from.clone(), recipient.clone())?1485 }1486 CollectionMode::Fungible(_) => {1487 Self::transfer_fungible(collection, amount, from, recipient)?1488 }1489 CollectionMode::ReFungible => Self::transfer_refungible(1490 collection,1491 item_id,1492 amount,1493 from.clone(),1494 recipient.clone(),1495 )?,1496 _ => (),1497 };14981499 if matches!(collection.mode, CollectionMode::Fungible(_)) {1500 collection.log(ERC20Events::Approval {1501 owner: *from.as_eth(),1502 spender: *sender.as_eth(),1503 value: allowance.into(),1504 })?;1505 }15061507 Ok(())1508 }15091510 pub fn set_variable_meta_data_internal(1511 sender: &T::CrossAccountId,1512 collection: &CollectionHandle<T>,1513 item_id: TokenId,1514 data: Vec<u8>,1515 ) -> DispatchResult {1516 Self::token_exists(collection, item_id)?;15171518 ensure!(1519 CUSTOM_DATA_LIMIT >= data.len() as u32,1520 Error::<T>::TokenVariableDataLimitExceeded1521 );15221523 // Modify permissions check1524 ensure!(1525 Self::is_item_owner(sender, collection, item_id)?1526 || Self::is_owner_or_admin_permissions(collection, sender)?,1527 Error::<T>::NoPermission1528 );15291530 match collection.mode {1531 CollectionMode::NFT => Self::set_nft_variable_data(collection, item_id, data)?,1532 CollectionMode::ReFungible => {1533 Self::set_re_fungible_variable_data(collection, item_id, data)?1534 }1535 CollectionMode::Fungible(_) => fail!(Error::<T>::CantStoreMetadataInFungibleTokens),1536 _ => fail!(Error::<T>::UnexpectedCollectionType),1537 };15381539 Ok(())1540 }15411542 pub fn meta_update_check(1543 sender: &T::CrossAccountId,1544 collection: &CollectionHandle<T>,1545 item_id: TokenId,1546 ) -> DispatchResult {1547 match collection.meta_update_permission {1548 MetaUpdatePermission::ItemOwner => ensure!(1549 Self::is_item_owner(sender, collection, item_id)?,1550 Error::<T>::NoPermission1551 ),1552 MetaUpdatePermission::Admin => ensure!(1553 Self::is_owner_or_admin_permissions(collection, sender)?,1554 Error::<T>::NoPermission1555 ),1556 MetaUpdatePermission::None => fail!(Error::<T>::MetadataUpdateDenied),1557 }15581559 Ok(())1560 }15611562 pub fn create_multiple_items_internal(1563 sender: &T::CrossAccountId,1564 collection: &CollectionHandle<T>,1565 owner: &T::CrossAccountId,1566 items_data: Vec<CreateItemData>,1567 ) -> DispatchResult {1568 Self::can_create_items_in_collection(collection, sender, owner, items_data.len() as u32)?;15691570 for data in &items_data {1571 Self::validate_create_item_args(collection, data)?;1572 }1573 for data in &items_data {1574 Self::create_item_no_validation(collection, owner, data.clone())?;1575 }15761577 Ok(())1578 }15791580 pub fn burn_item_internal(1581 sender: &T::CrossAccountId,1582 collection: &CollectionHandle<T>,1583 item_id: TokenId,1584 value: u128,1585 ) -> DispatchResult {1586 ensure!(1587 Self::is_item_owner(sender, collection, item_id)?1588 || (collection.limits.owner_can_transfer1589 && Self::is_owner_or_admin_permissions(collection, sender)?),1590 Error::<T>::NoPermission1591 );15921593 if collection.access == AccessMode::WhiteList {1594 Self::check_white_list(collection, sender)?;1595 }15961597 match collection.mode {1598 CollectionMode::NFT => Self::burn_nft_item(collection, item_id)?,1599 CollectionMode::Fungible(_) => Self::burn_fungible_item(sender, collection, value)?,1600 CollectionMode::ReFungible => Self::burn_refungible_item(collection, item_id, sender)?,1601 _ => (),1602 };16031604 Ok(())1605 }16061607 pub fn toggle_white_list_internal(1608 sender: &T::CrossAccountId,1609 collection: &CollectionHandle<T>,1610 address: &T::CrossAccountId,1611 whitelisted: bool,1612 ) -> DispatchResult {1613 Self::check_owner_or_admin_permissions(collection, sender)?;16141615 if whitelisted {1616 <WhiteList<T>>::insert(collection.id, address.as_sub(), true);1617 } else {1618 <WhiteList<T>>::remove(collection.id, address.as_sub());1619 }16201621 Ok(())1622 }16231624 fn is_correct_transfer(1625 collection: &CollectionHandle<T>,1626 recipient: &T::CrossAccountId,1627 ) -> DispatchResult {1628 let collection_id = collection.id;16291630 // check token limit and account token limit1631 collection.consume_sload()?;1632 let account_items: u32 =1633 <AddressTokens<T>>::get(collection_id, recipient.as_sub()).len() as u32;1634 ensure!(1635 collection.limits.account_token_ownership_limit > account_items,1636 Error::<T>::AccountTokenLimitExceeded1637 );16381639 // preliminary transfer check1640 ensure!(collection.transfers_enabled, Error::<T>::TransferNotAllowed);16411642 Ok(())1643 }16441645 fn can_create_items_in_collection(1646 collection: &CollectionHandle<T>,1647 sender: &T::CrossAccountId,1648 owner: &T::CrossAccountId,1649 amount: u32,1650 ) -> DispatchResult {1651 let collection_id = collection.id;16521653 // check token limit and account token limit1654 let total_items: u32 = ItemListIndex::get(collection_id)1655 .checked_add(amount)1656 .ok_or(Error::<T>::CollectionTokenLimitExceeded)?;1657 let account_items: u32 = (<AddressTokens<T>>::get(collection_id, owner.as_sub()).len()1658 as u32)1659 .checked_add(amount)1660 .ok_or(Error::<T>::AccountTokenLimitExceeded)?;1661 ensure!(1662 collection.limits.token_limit >= total_items,1663 Error::<T>::CollectionTokenLimitExceeded1664 );1665 ensure!(1666 collection.limits.account_token_ownership_limit >= account_items,1667 Error::<T>::AccountTokenLimitExceeded1668 );16691670 if !Self::is_owner_or_admin_permissions(collection, sender)? {1671 ensure!(collection.mint_mode, Error::<T>::PublicMintingNotAllowed);1672 Self::check_white_list(collection, owner)?;1673 Self::check_white_list(collection, sender)?;1674 }16751676 Ok(())1677 }16781679 fn validate_create_item_args(1680 target_collection: &CollectionHandle<T>,1681 data: &CreateItemData,1682 ) -> DispatchResult {1683 match target_collection.mode {1684 CollectionMode::NFT => {1685 if !matches!(data, CreateItemData::NFT(_)) {1686 fail!(Error::<T>::NotNftDataUsedToMintNftCollectionToken);1687 }1688 }1689 CollectionMode::Fungible(_) => {1690 if !matches!(data, CreateItemData::Fungible(_)) {1691 fail!(Error::<T>::NotFungibleDataUsedToMintFungibleCollectionToken);1692 }1693 }1694 CollectionMode::ReFungible => {1695 if let CreateItemData::ReFungible(data) = data {1696 // Check refungibility limits1697 ensure!(1698 data.pieces <= MAX_REFUNGIBLE_PIECES,1699 Error::<T>::WrongRefungiblePieces1700 );1701 ensure!(data.pieces > 0, Error::<T>::WrongRefungiblePieces);1702 } else {1703 fail!(Error::<T>::NotReFungibleDataUsedToMintReFungibleCollectionToken);1704 }1705 }1706 _ => {1707 fail!(Error::<T>::UnexpectedCollectionType);1708 }1709 };17101711 Ok(())1712 }17131714 fn create_item_no_validation(1715 collection: &CollectionHandle<T>,1716 owner: &T::CrossAccountId,1717 data: CreateItemData,1718 ) -> DispatchResult {1719 match data {1720 CreateItemData::NFT(data) => {1721 let item = NftItemType {1722 owner: owner.clone(),1723 const_data: data.const_data.into_inner(),1724 variable_data: data.variable_data.into_inner(),1725 };17261727 Self::add_nft_item(collection, item)?;1728 }1729 CreateItemData::Fungible(data) => {1730 Self::add_fungible_item(collection, owner, data.value)?;1731 }1732 CreateItemData::ReFungible(data) => {1733 let owner_list = vec![Ownership {1734 owner: owner.clone(),1735 fraction: data.pieces,1736 }];17371738 let item = ReFungibleItemType {1739 owner: owner_list,1740 const_data: data.const_data.into_inner(),1741 variable_data: data.variable_data.into_inner(),1742 };17431744 Self::add_refungible_item(collection, item)?;1745 }1746 };17471748 Ok(())1749 }17501751 fn add_fungible_item(1752 collection: &CollectionHandle<T>,1753 owner: &T::CrossAccountId,1754 value: u128,1755 ) -> DispatchResult {1756 let collection_id = collection.id;17571758 // Does new owner already have an account?1759 collection.consume_sload()?;1760 let balance: u128 = <FungibleItemList<T>>::get(collection_id, owner.as_sub()).value;17611762 // Mint1763 let item = FungibleItemType {1764 value: balance.checked_add(value).ok_or(Error::<T>::NumOverflow)?,1765 };1766 collection.consume_sstore()?;1767 <FungibleItemList<T>>::insert(collection_id, owner.as_sub(), item);17681769 // Update balance1770 collection.consume_sload()?;1771 let new_balance = <Balance<T>>::get(collection_id, owner.as_sub())1772 .checked_add(value)1773 .ok_or(Error::<T>::NumOverflow)?;1774 collection.consume_sstore()?;1775 <Balance<T>>::insert(collection_id, owner.as_sub(), new_balance);17761777 collection.log(ERC20Events::Transfer {1778 from: H160::default(),1779 to: *owner.as_eth(),1780 value: value.into(),1781 })?;1782 Self::deposit_event(RawEvent::ItemCreated(collection_id, 0, owner.clone()));1783 Ok(())1784 }17851786 fn add_refungible_item(1787 collection: &CollectionHandle<T>,1788 item: ReFungibleItemType<T::CrossAccountId>,1789 ) -> DispatchResult {1790 let collection_id = collection.id;17911792 let current_index = <ItemListIndex>::get(collection_id)1793 .checked_add(1)1794 .ok_or(Error::<T>::NumOverflow)?;1795 let itemcopy = item.clone();17961797 ensure!(item.owner.len() == 1, Error::<T>::BadCreateRefungibleCall,);1798 let item_owner = item.owner.first().expect("only one owner is defined");17991800 let value = item_owner.fraction;1801 let owner = item_owner.owner.clone();18021803 Self::add_token_index(collection, current_index, &owner)?;18041805 <ItemListIndex>::insert(collection_id, current_index);1806 <ReFungibleItemList<T>>::insert(collection_id, current_index, itemcopy);18071808 // Update balance1809 let new_balance = <Balance<T>>::get(collection_id, owner.as_sub())1810 .checked_add(value)1811 .ok_or(Error::<T>::NumOverflow)?;1812 <Balance<T>>::insert(collection_id, owner.as_sub(), new_balance);18131814 Self::deposit_event(RawEvent::ItemCreated(collection_id, current_index, owner));1815 Ok(())1816 }18171818 fn add_nft_item(1819 collection: &CollectionHandle<T>,1820 item: NftItemType<T::CrossAccountId>,1821 ) -> DispatchResult {1822 let collection_id = collection.id;18231824 let current_index = <ItemListIndex>::get(collection_id)1825 .checked_add(1)1826 .ok_or(Error::<T>::NumOverflow)?;18271828 let item_owner = item.owner.clone();1829 Self::add_token_index(collection, current_index, &item.owner)?;18301831 <ItemListIndex>::insert(collection_id, current_index);1832 <NftItemList<T>>::insert(collection_id, current_index, item);18331834 // Update balance1835 let new_balance = <Balance<T>>::get(collection_id, item_owner.as_sub())1836 .checked_add(1)1837 .ok_or(Error::<T>::NumOverflow)?;1838 <Balance<T>>::insert(collection_id, item_owner.as_sub(), new_balance);18391840 collection.log(ERC721Events::Transfer {1841 from: H160::default(),1842 to: *item_owner.as_eth(),1843 token_id: current_index.into(),1844 })?;1845 Self::deposit_event(RawEvent::ItemCreated(1846 collection_id,1847 current_index,1848 item_owner,1849 ));1850 Ok(())1851 }18521853 fn burn_refungible_item(1854 collection: &CollectionHandle<T>,1855 item_id: TokenId,1856 owner: &T::CrossAccountId,1857 ) -> DispatchResult {1858 let collection_id = collection.id;18591860 let mut token = <ReFungibleItemList<T>>::get(collection_id, item_id)1861 .ok_or(Error::<T>::TokenNotFound)?;1862 let rft_balance = token1863 .owner1864 .iter()1865 .find(|&i| i.owner == *owner)1866 .ok_or(Error::<T>::TokenNotFound)?;1867 Self::remove_token_index(collection, item_id, owner)?;18681869 // update balance1870 let new_balance = <Balance<T>>::get(collection_id, rft_balance.owner.as_sub())1871 .checked_sub(rft_balance.fraction)1872 .ok_or(Error::<T>::NumOverflow)?;1873 <Balance<T>>::insert(collection_id, rft_balance.owner.as_sub(), new_balance);18741875 // Re-create owners list with sender removed1876 let index = token1877 .owner1878 .iter()1879 .position(|i| i.owner == *owner)1880 .expect("owned item is exists");1881 token.owner.remove(index);1882 let owner_count = token.owner.len();18831884 // Burn the token completely if this was the last (only) owner1885 if owner_count == 0 {1886 <ReFungibleItemList<T>>::remove(collection_id, item_id);1887 <VariableMetaDataBasket<T>>::remove(collection_id, item_id);1888 } else {1889 <ReFungibleItemList<T>>::insert(collection_id, item_id, token);1890 }18911892 Ok(())1893 }18941895 fn burn_nft_item(collection: &CollectionHandle<T>, item_id: TokenId) -> DispatchResult {1896 let collection_id = collection.id;18971898 let item =1899 <NftItemList<T>>::get(collection_id, item_id).ok_or(Error::<T>::TokenNotFound)?;1900 Self::remove_token_index(collection, item_id, &item.owner)?;19011902 // update balance1903 let new_balance = <Balance<T>>::get(collection_id, item.owner.as_sub())1904 .checked_sub(1)1905 .ok_or(Error::<T>::NumOverflow)?;1906 <Balance<T>>::insert(collection_id, item.owner.as_sub(), new_balance);1907 <NftItemList<T>>::remove(collection_id, item_id);1908 <VariableMetaDataBasket<T>>::remove(collection_id, item_id);19091910 collection.log(ERC721Events::Transfer {1911 from: *item.owner.as_eth(),1912 to: H160::default(),1913 token_id: item_id.into(),1914 })?;1915 Self::deposit_event(RawEvent::ItemDestroyed(collection.id, item_id));1916 Ok(())1917 }19181919 fn burn_fungible_item(1920 owner: &T::CrossAccountId,1921 collection: &CollectionHandle<T>,1922 value: u128,1923 ) -> DispatchResult {1924 let collection_id = collection.id;19251926 let mut balance = <FungibleItemList<T>>::get(collection_id, owner.as_sub());1927 ensure!(balance.value >= value, Error::<T>::TokenValueNotEnough);19281929 // update balance1930 let new_balance = <Balance<T>>::get(collection_id, owner.as_sub())1931 .checked_sub(value)1932 .ok_or(Error::<T>::NumOverflow)?;1933 <Balance<T>>::insert(collection_id, owner.as_sub(), new_balance);19341935 if balance.value - value > 0 {1936 balance.value -= value;1937 <FungibleItemList<T>>::insert(collection_id, owner.as_sub(), balance);1938 } else {1939 <FungibleItemList<T>>::remove(collection_id, owner.as_sub());1940 }19411942 collection.log(ERC20Events::Transfer {1943 from: *owner.as_eth(),1944 to: H160::default(),1945 value: value.into(),1946 })?;1947 Ok(())1948 }19491950 pub fn get_collection(1951 collection_id: CollectionId,1952 ) -> Result<CollectionHandle<T>, sp_runtime::DispatchError> {1953 Ok(<CollectionHandle<T>>::get(collection_id).ok_or(Error::<T>::CollectionNotFound)?)1954 }19551956 fn check_owner_permissions(1957 target_collection: &CollectionHandle<T>,1958 subject: &T::AccountId,1959 ) -> DispatchResult {1960 ensure!(1961 *subject == target_collection.owner,1962 Error::<T>::NoPermission1963 );19641965 Ok(())1966 }19671968 fn is_owner_or_admin_permissions(1969 collection: &CollectionHandle<T>,1970 subject: &T::CrossAccountId,1971 ) -> Result<bool, DispatchError> {1972 collection.consume_sload()?;1973 Ok(*subject.as_sub() == collection.owner1974 || <AdminList<T>>::get(collection.id).contains(subject))1975 }19761977 fn check_owner_or_admin_permissions(1978 collection: &CollectionHandle<T>,1979 subject: &T::CrossAccountId,1980 ) -> DispatchResult {1981 ensure!(1982 Self::is_owner_or_admin_permissions(collection, subject)?,1983 Error::<T>::NoPermission1984 );19851986 Ok(())1987 }19881989 fn owned_amount(1990 subject: &T::CrossAccountId,1991 collection: &CollectionHandle<T>,1992 item_id: TokenId,1993 ) -> Result<Option<u128>, DispatchError> {1994 collection.consume_sload()?;1995 Ok(Self::owned_amount_unchecked(subject, collection, item_id))1996 }19971998 fn owned_amount_unchecked(1999 subject: &T::CrossAccountId,2000 target_collection: &CollectionHandle<T>,2001 item_id: TokenId,2002 ) -> Option<u128> {2003 let collection_id = target_collection.id;20042005 match target_collection.mode {2006 CollectionMode::NFT => {2007 (<NftItemList<T>>::get(collection_id, item_id)?.owner == *subject).then(|| 1)2008 }2009 CollectionMode::Fungible(_) => {2010 Some(<FungibleItemList<T>>::get(collection_id, &subject.as_sub()).value)2011 }2012 CollectionMode::ReFungible => <ReFungibleItemList<T>>::get(collection_id, item_id)?2013 .owner2014 .iter()2015 .find(|i| i.owner == *subject)2016 .map(|i| i.fraction),2017 CollectionMode::Invalid => None,2018 }2019 }20202021 fn is_item_owner(2022 subject: &T::CrossAccountId,2023 target_collection: &CollectionHandle<T>,2024 item_id: TokenId,2025 ) -> Result<bool, DispatchError> {2026 Ok(match target_collection.mode {2027 CollectionMode::Fungible(_) => true,2028 _ => Self::owned_amount(subject, target_collection, item_id)?.is_some(),2029 })2030 }20312032 fn check_white_list(2033 collection: &CollectionHandle<T>,2034 address: &T::CrossAccountId,2035 ) -> DispatchResult {2036 collection.consume_sload()?;2037 ensure!(2038 <WhiteList<T>>::contains_key(collection.id, address.as_sub()),2039 Error::<T>::AddresNotInWhiteList,2040 );2041 Ok(())2042 }20432044 /// Check if token exists. In case of Fungible, check if there is an entry for2045 /// the owner in fungible balances double map2046 fn token_exists(target_collection: &CollectionHandle<T>, item_id: TokenId) -> DispatchResult {2047 let collection_id = target_collection.id;2048 let exists = match target_collection.mode {2049 CollectionMode::NFT => <NftItemList<T>>::contains_key(collection_id, item_id),2050 CollectionMode::Fungible(_) => true,2051 CollectionMode::ReFungible => {2052 <ReFungibleItemList<T>>::contains_key(collection_id, item_id)2053 }2054 _ => false,2055 };20562057 ensure!(exists, Error::<T>::TokenNotFound);2058 Ok(())2059 }20602061 fn transfer_fungible(2062 collection: &CollectionHandle<T>,2063 value: u128,2064 owner: &T::CrossAccountId,2065 recipient: &T::CrossAccountId,2066 ) -> DispatchResult {2067 let collection_id = collection.id;20682069 collection.consume_sload()?;2070 collection.consume_sload()?;2071 let mut recipient_balance = <FungibleItemList<T>>::get(collection_id, recipient.as_sub());2072 let mut balance = <FungibleItemList<T>>::get(collection_id, owner.as_sub());20732074 recipient_balance.value = recipient_balance2075 .value2076 .checked_add(value)2077 .ok_or(Error::<T>::NumOverflow)?;2078 balance.value = balance2079 .value2080 .checked_sub(value)2081 .ok_or(Error::<T>::TokenValueTooLow)?;20822083 // update balanceOf2084 collection.consume_sstore()?;2085 collection.consume_sstore()?;2086 if balance.value != 0 {2087 <Balance<T>>::insert(collection_id, owner.as_sub(), balance.value);2088 } else {2089 <Balance<T>>::remove(collection_id, owner.as_sub());2090 }2091 <Balance<T>>::insert(collection_id, recipient.as_sub(), recipient_balance.value);20922093 // Reduce or remove sender2094 collection.consume_sstore()?;2095 collection.consume_sstore()?;2096 if balance.value != 0 {2097 <FungibleItemList<T>>::insert(collection_id, owner.as_sub(), balance);2098 } else {2099 <FungibleItemList<T>>::remove(collection_id, owner.as_sub());2100 }2101 <FungibleItemList<T>>::insert(collection_id, recipient.as_sub(), recipient_balance);21022103 collection.log(ERC20Events::Transfer {2104 from: *owner.as_eth(),2105 to: *recipient.as_eth(),2106 value: value.into(),2107 })?;2108 Self::deposit_event(RawEvent::Transfer(2109 collection.id,2110 1,2111 owner.clone(),2112 recipient.clone(),2113 value,2114 ));21152116 Ok(())2117 }21182119 fn transfer_refungible(2120 collection: &CollectionHandle<T>,2121 item_id: TokenId,2122 value: u128,2123 owner: T::CrossAccountId,2124 new_owner: T::CrossAccountId,2125 ) -> DispatchResult {2126 let collection_id = collection.id;2127 collection.consume_sload()?;2128 let full_item = <ReFungibleItemList<T>>::get(collection_id, item_id)2129 .ok_or(Error::<T>::TokenNotFound)?;21302131 let item = full_item2132 .owner2133 .iter()2134 .find(|i| i.owner == owner)2135 .ok_or(Error::<T>::TokenNotFound)?;2136 let amount = item.fraction;21372138 ensure!(amount >= value, Error::<T>::TokenValueTooLow);21392140 collection.consume_sload()?;2141 // update balance2142 let balance_old_owner = <Balance<T>>::get(collection_id, item.owner.as_sub())2143 .checked_sub(value)2144 .ok_or(Error::<T>::NumOverflow)?;2145 collection.consume_sstore()?;2146 <Balance<T>>::insert(collection_id, item.owner.as_sub(), balance_old_owner);21472148 collection.consume_sload()?;2149 let balance_new_owner = <Balance<T>>::get(collection_id, new_owner.as_sub())2150 .checked_add(value)2151 .ok_or(Error::<T>::NumOverflow)?;2152 collection.consume_sstore()?;2153 <Balance<T>>::insert(collection_id, new_owner.as_sub(), balance_new_owner);21542155 let old_owner = item.owner.clone();2156 let new_owner_has_account = full_item.owner.iter().any(|i| i.owner == new_owner);21572158 let mut new_full_item = full_item.clone();2159 // transfer2160 if amount == value && !new_owner_has_account {2161 // change owner2162 // new owner do not have account2163 new_full_item2164 .owner2165 .iter_mut()2166 .find(|i| i.owner == owner)2167 .expect("old owner does present in refungible")2168 .owner = new_owner.clone();2169 collection.consume_sstore()?;2170 <ReFungibleItemList<T>>::insert(collection_id, item_id, new_full_item);21712172 // update index collection2173 Self::move_token_index(collection, item_id, &old_owner, &new_owner)?;2174 } else {2175 new_full_item2176 .owner2177 .iter_mut()2178 .find(|i| i.owner == owner)2179 .expect("old owner does present in refungible")2180 .fraction -= value;21812182 // separate amount2183 if new_owner_has_account {2184 // new owner has account2185 new_full_item2186 .owner2187 .iter_mut()2188 .find(|i| i.owner == new_owner)2189 .expect("new owner has account")2190 .fraction += value;2191 } else {2192 // new owner do not have account2193 new_full_item.owner.push(Ownership {2194 owner: new_owner.clone(),2195 fraction: value,2196 });2197 Self::add_token_index(collection, item_id, &new_owner)?;2198 }21992200 collection.consume_sstore()?;2201 <ReFungibleItemList<T>>::insert(collection_id, item_id, new_full_item);2202 }22032204 Self::deposit_event(RawEvent::Transfer(2205 collection.id,2206 item_id,2207 owner,2208 new_owner,2209 amount,2210 ));22112212 Ok(())2213 }22142215 fn transfer_nft(2216 collection: &CollectionHandle<T>,2217 item_id: TokenId,2218 sender: T::CrossAccountId,2219 new_owner: T::CrossAccountId,2220 ) -> DispatchResult {2221 let collection_id = collection.id;2222 collection.consume_sload()?;2223 let mut item =2224 <NftItemList<T>>::get(collection_id, item_id).ok_or(Error::<T>::TokenNotFound)?;22252226 ensure!(sender == item.owner, Error::<T>::MustBeTokenOwner);22272228 collection.consume_sload()?;2229 // update balance2230 let balance_old_owner = <Balance<T>>::get(collection_id, item.owner.as_sub())2231 .checked_sub(1)2232 .ok_or(Error::<T>::NumOverflow)?;2233 collection.consume_sstore()?;2234 <Balance<T>>::insert(collection_id, item.owner.as_sub(), balance_old_owner);22352236 collection.consume_sload()?;2237 let balance_new_owner = <Balance<T>>::get(collection_id, new_owner.as_sub())2238 .checked_add(1)2239 .ok_or(Error::<T>::NumOverflow)?;2240 collection.consume_sstore()?;2241 <Balance<T>>::insert(collection_id, new_owner.as_sub(), balance_new_owner);22422243 // change owner2244 let old_owner = item.owner.clone();2245 item.owner = new_owner.clone();2246 collection.consume_sstore()?;2247 <NftItemList<T>>::insert(collection_id, item_id, item);22482249 // update index collection2250 Self::move_token_index(collection, item_id, &old_owner, &new_owner)?;22512252 collection.log(ERC721Events::Transfer {2253 from: *sender.as_eth(),2254 to: *new_owner.as_eth(),2255 token_id: item_id.into(),2256 })?;2257 Self::deposit_event(RawEvent::Transfer(2258 collection.id,2259 item_id,2260 sender,2261 new_owner,2262 1,2263 ));22642265 Ok(())2266 }22672268 fn set_re_fungible_variable_data(2269 collection: &CollectionHandle<T>,2270 item_id: TokenId,2271 data: Vec<u8>,2272 ) -> DispatchResult {2273 let collection_id = collection.id;2274 let mut item = <ReFungibleItemList<T>>::get(collection_id, item_id)2275 .ok_or(Error::<T>::TokenNotFound)?;22762277 item.variable_data = data;22782279 <ReFungibleItemList<T>>::insert(collection_id, item_id, item);22802281 Ok(())2282 }22832284 fn set_nft_variable_data(2285 collection: &CollectionHandle<T>,2286 item_id: TokenId,2287 data: Vec<u8>,2288 ) -> DispatchResult {2289 let collection_id = collection.id;2290 let mut item =2291 <NftItemList<T>>::get(collection_id, item_id).ok_or(Error::<T>::TokenNotFound)?;22922293 item.variable_data = data;22942295 <NftItemList<T>>::insert(collection_id, item_id, item);22962297 Ok(())2298 }22992300 #[allow(dead_code)]2301 fn init_collection(item: &Collection<T>) {2302 // check params2303 assert!(2304 item.decimal_points <= MAX_DECIMAL_POINTS,2305 "decimal_points parameter must be lower than MAX_DECIMAL_POINTS"2306 );2307 assert!(2308 item.name.len() <= 64,2309 "Collection name can not be longer than 63 char"2310 );2311 assert!(2312 item.name.len() <= 256,2313 "Collection description can not be longer than 255 char"2314 );2315 assert!(2316 item.token_prefix.len() <= 16,2317 "Token prefix can not be longer than 15 char"2318 );23192320 // Generate next collection ID2321 let next_id = CreatedCollectionCount::get().checked_add(1).unwrap();23222323 CreatedCollectionCount::put(next_id);2324 }23252326 #[allow(dead_code)]2327 fn init_nft_token(collection_id: CollectionId, item: &NftItemType<T::CrossAccountId>) {2328 let current_index = <ItemListIndex>::get(collection_id).checked_add(1).unwrap();23292330 Self::add_token_index(2331 &CollectionHandle::get(collection_id).unwrap(),2332 current_index,2333 &item.owner,2334 )2335 .unwrap();23362337 <ItemListIndex>::insert(collection_id, current_index);23382339 // Update balance2340 let new_balance = <Balance<T>>::get(collection_id, item.owner.as_sub())2341 .checked_add(1)2342 .unwrap();2343 <Balance<T>>::insert(collection_id, item.owner.as_sub(), new_balance);2344 }23452346 #[allow(dead_code)]2347 fn init_fungible_token(2348 collection_id: CollectionId,2349 owner: &T::CrossAccountId,2350 item: &FungibleItemType,2351 ) {2352 let current_index = <ItemListIndex>::get(collection_id).checked_add(1).unwrap();23532354 Self::add_token_index(2355 &CollectionHandle::get(collection_id).unwrap(),2356 current_index,2357 owner,2358 )2359 .unwrap();23602361 <ItemListIndex>::insert(collection_id, current_index);23622363 // Update balance2364 let new_balance = <Balance<T>>::get(collection_id, owner.as_sub())2365 .checked_add(item.value)2366 .unwrap();2367 <Balance<T>>::insert(collection_id, owner.as_sub(), new_balance);2368 }23692370 #[allow(dead_code)]2371 fn init_refungible_token(2372 collection_id: CollectionId,2373 item: &ReFungibleItemType<T::CrossAccountId>,2374 ) {2375 let current_index = <ItemListIndex>::get(collection_id).checked_add(1).unwrap();23762377 let value = item.owner.first().unwrap().fraction;2378 let owner = item.owner.first().unwrap().owner.clone();23792380 Self::add_token_index(2381 &CollectionHandle::get(collection_id).unwrap(),2382 current_index,2383 &owner,2384 )2385 .unwrap();23862387 <ItemListIndex>::insert(collection_id, current_index);23882389 // Update balance2390 let new_balance = <Balance<T>>::get(collection_id, &owner.as_sub())2391 .checked_add(value)2392 .unwrap();2393 <Balance<T>>::insert(collection_id, owner.as_sub(), new_balance);2394 }23952396 fn add_token_index(2397 collection: &CollectionHandle<T>,2398 item_index: TokenId,2399 owner: &T::CrossAccountId,2400 ) -> DispatchResult {2401 // add to account limit2402 collection.consume_sload()?;2403 if <AccountItemCount<T>>::contains_key(owner.as_sub()) {2404 // bound Owned tokens by a single address2405 collection.consume_sload()?;2406 let count = <AccountItemCount<T>>::get(owner.as_sub());2407 ensure!(2408 count < ACCOUNT_TOKEN_OWNERSHIP_LIMIT,2409 Error::<T>::AddressOwnershipLimitExceeded2410 );24112412 collection.consume_sstore()?;2413 <AccountItemCount<T>>::insert(2414 owner.as_sub(),2415 count.checked_add(1).ok_or(Error::<T>::NumOverflow)?,2416 );2417 } else {2418 collection.consume_sstore()?;2419 <AccountItemCount<T>>::insert(owner.as_sub(), 1);2420 }24212422 collection.consume_sload()?;2423 let list_exists = <AddressTokens<T>>::contains_key(collection.id, owner.as_sub());2424 if list_exists {2425 collection.consume_sload()?;2426 let mut list = <AddressTokens<T>>::get(collection.id, owner.as_sub());2427 let item_contains = list.contains(&item_index.clone());24282429 if !item_contains {2430 list.push(item_index);2431 }24322433 collection.consume_sstore()?;2434 <AddressTokens<T>>::insert(collection.id, owner.as_sub(), list);2435 } else {2436 let itm = vec![item_index];2437 collection.consume_sstore()?;2438 <AddressTokens<T>>::insert(collection.id, owner.as_sub(), itm);2439 }24402441 Ok(())2442 }24432444 fn remove_token_index(2445 collection: &CollectionHandle<T>,2446 item_index: TokenId,2447 owner: &T::CrossAccountId,2448 ) -> DispatchResult {2449 // update counter2450 collection.consume_sload()?;2451 collection.consume_sstore()?;2452 <AccountItemCount<T>>::insert(2453 owner.as_sub(),2454 <AccountItemCount<T>>::get(owner.as_sub())2455 .checked_sub(1)2456 .ok_or(Error::<T>::NumOverflow)?,2457 );24582459 collection.consume_sload()?;2460 let list_exists = <AddressTokens<T>>::contains_key(collection.id, owner.as_sub());2461 if list_exists {2462 collection.consume_sload()?;2463 let mut list = <AddressTokens<T>>::get(collection.id, owner.as_sub());2464 let item_contains = list.contains(&item_index.clone());24652466 if item_contains {2467 list.retain(|&item| item != item_index);2468 collection.consume_sstore()?;2469 <AddressTokens<T>>::insert(collection.id, owner.as_sub(), list);2470 }2471 }24722473 Ok(())2474 }24752476 fn move_token_index(2477 collection: &CollectionHandle<T>,2478 item_index: TokenId,2479 old_owner: &T::CrossAccountId,2480 new_owner: &T::CrossAccountId,2481 ) -> DispatchResult {2482 Self::remove_token_index(collection, item_index, old_owner)?;2483 Self::add_token_index(collection, item_index, new_owner)?;24842485 Ok(())2486 }2487}24882489sp_api::decl_runtime_apis! {2490 pub trait NftApi {2491 /// Used for ethereum integration2492 fn eth_contract_code(account: H160) -> Option<Vec<u8>>;2493 }2494}1//2// This file is subject to the terms and conditions defined in3// file 'LICENSE', which is part of this source code package.4//56#![recursion_limit = "1024"]7#![cfg_attr(not(feature = "std"), no_std)]8#![allow(9 clippy::too_many_arguments,10 clippy::unnecessary_mut_passed,11 clippy::unused_unit12)]1314extern crate alloc;1516pub use serde::{Serialize, Deserialize};1718pub use frame_support::{19 construct_runtime, decl_event, decl_module, decl_storage, decl_error,20 dispatch::DispatchResult,21 ensure, fail, parameter_types,22 traits::{23 Currency, ExistenceRequirement, Get, Imbalance, KeyOwnerProofSystem, OnUnbalanced,24 Randomness, IsSubType, WithdrawReasons,25 },26 weights::{27 constants::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight, WEIGHT_PER_SECOND},28 DispatchInfo, GetDispatchInfo, IdentityFee, Pays, PostDispatchInfo, Weight,29 WeightToFeePolynomial, DispatchClass,30 },31 StorageValue, transactional,32};3334use frame_system::{self as system, ensure_signed};35use sp_core::H160;36use sp_std::vec;37use sp_runtime::{DispatchError, sp_std::prelude::Vec};38use core::ops::{Deref, DerefMut};39use nft_data_structs::{40 MAX_DECIMAL_POINTS, MAX_SPONSOR_TIMEOUT, MAX_TOKEN_OWNERSHIP, MAX_REFUNGIBLE_PIECES,41 CUSTOM_DATA_LIMIT, COLLECTION_NUMBER_LIMIT, ACCOUNT_TOKEN_OWNERSHIP_LIMIT,42 VARIABLE_ON_CHAIN_SCHEMA_LIMIT, CONST_ON_CHAIN_SCHEMA_LIMIT, COLLECTION_ADMINS_LIMIT,43 OFFCHAIN_SCHEMA_LIMIT, MAX_TOKEN_PREFIX_LENGTH, MAX_COLLECTION_NAME_LENGTH,44 MAX_COLLECTION_DESCRIPTION_LENGTH, AccessMode, Collection, CreateItemData, CollectionLimits,45 CollectionId, CollectionMode, TokenId, SchemaVersion, SponsorshipState, Ownership, NftItemType,46 MetaUpdatePermission, FungibleItemType, ReFungibleItemType,47};4849#[cfg(test)]50mod mock;5152#[cfg(test)]53mod tests;5455mod eth;56mod sponsorship;57pub use sponsorship::NftSponsorshipHandler;58pub use eth::sponsoring::NftEthSponsorshipHandler;5960pub use eth::NftErcSupport;61pub use eth::account::*;62use eth::erc::{ERC20Events, ERC721Events};6364#[cfg(feature = "runtime-benchmarks")]65mod benchmarking;66pub mod weights;67use weights::WeightInfo;6869decl_error! {70 /// Error for non-fungible-token module.71 pub enum Error for Module<T: Config> {72 /// Total collections bound exceeded.73 TotalCollectionsLimitExceeded,74 /// Decimal_points parameter must be lower than MAX_DECIMAL_POINTS constant, currently it is 30.75 CollectionDecimalPointLimitExceeded,76 /// Collection name can not be longer than 63 char.77 CollectionNameLimitExceeded,78 /// Collection description can not be longer than 255 char.79 CollectionDescriptionLimitExceeded,80 /// Token prefix can not be longer than 15 char.81 CollectionTokenPrefixLimitExceeded,82 /// This collection does not exist.83 CollectionNotFound,84 /// Item not exists.85 TokenNotFound,86 /// Admin not found87 AdminNotFound,88 /// Arithmetic calculation overflow.89 NumOverflow,90 /// Account already has admin role.91 AlreadyAdmin,92 /// You do not own this collection.93 NoPermission,94 /// This address is not set as sponsor, use setCollectionSponsor first.95 ConfirmUnsetSponsorFail,96 /// Collection is not in mint mode.97 PublicMintingNotAllowed,98 /// Sender parameter and item owner must be equal.99 MustBeTokenOwner,100 /// Item balance not enough.101 TokenValueTooLow,102 /// Size of item is too large.103 NftSizeLimitExceeded,104 /// No approve found105 ApproveNotFound,106 /// Requested value more than approved.107 TokenValueNotEnough,108 /// Only approved addresses can call this method.109 ApproveRequired,110 /// Address is not in white list.111 AddresNotInWhiteList,112 /// Number of collection admins bound exceeded.113 CollectionAdminsLimitExceeded,114 /// Owned tokens by a single address bound exceeded.115 AddressOwnershipLimitExceeded,116 /// Length of items properties must be greater than 0.117 EmptyArgument,118 /// const_data exceeded data limit.119 TokenConstDataLimitExceeded,120 /// variable_data exceeded data limit.121 TokenVariableDataLimitExceeded,122 /// Not NFT item data used to mint in NFT collection.123 NotNftDataUsedToMintNftCollectionToken,124 /// Not Fungible item data used to mint in Fungible collection.125 NotFungibleDataUsedToMintFungibleCollectionToken,126 /// Not Re Fungible item data used to mint in Re Fungible collection.127 NotReFungibleDataUsedToMintReFungibleCollectionToken,128 /// Unexpected collection type.129 UnexpectedCollectionType,130 /// Can't store metadata in fungible tokens.131 CantStoreMetadataInFungibleTokens,132 /// Collection token limit exceeded133 CollectionTokenLimitExceeded,134 /// Account token limit exceeded per collection135 AccountTokenLimitExceeded,136 /// Collection limit bounds per collection exceeded137 CollectionLimitBoundsExceeded,138 /// Tried to enable permissions which are only permitted to be disabled139 OwnerPermissionsCantBeReverted,140 /// Schema data size limit bound exceeded141 SchemaDataLimitExceeded,142 /// Maximum refungibility exceeded143 WrongRefungiblePieces,144 /// createRefungible should be called with one owner145 BadCreateRefungibleCall,146 /// Gas limit exceeded147 OutOfGas,148 /// Metadata update denied by collection settings149 MetadataUpdateDenied,150 /// Metadata update flag become unmutable with None option151 MetadataFlagFrozen,152 /// Collection settings not allowing items transferring153 TransferNotAllowed,154 /// Can't transfer tokens to ethereum zero address155 AddressIsZero,156 }157}158159#[must_use = "Should call submit_logs or save, otherwise some data will be lost for evm side"]160pub struct CollectionHandle<T: Config> {161 pub id: CollectionId,162 collection: Collection<T>,163 recorder: pallet_evm_coder_substrate::SubstrateRecorder<T>,164}165impl<T: Config> CollectionHandle<T> {166 pub fn get_with_gas_limit(id: CollectionId, gas_limit: u64) -> Option<Self> {167 <CollectionById<T>>::get(id).map(|collection| Self {168 id,169 collection,170 recorder: pallet_evm_coder_substrate::SubstrateRecorder::new(171 eth::collection_id_to_address(id),172 gas_limit,173 ),174 })175 }176 pub fn get(id: CollectionId) -> Option<Self> {177 Self::get_with_gas_limit(id, u64::MAX)178 }179 pub fn log(&self, log: impl evm_coder::ToLog) -> DispatchResult {180 self.recorder.log_sub(log)181 }182 #[allow(dead_code)]183 fn consume_gas(&self, gas: u64) -> DispatchResult {184 self.recorder.consume_gas_sub(gas)185 }186 fn consume_sload(&self) -> DispatchResult {187 self.recorder.consume_sload_sub()188 }189 fn consume_sstore(&self) -> DispatchResult {190 self.recorder.consume_sstore_sub()191 }192 pub fn submit_logs(self) -> DispatchResult {193 self.recorder.submit_logs()194 }195 pub fn save(self) -> DispatchResult {196 self.recorder.submit_logs()?;197 <CollectionById<T>>::insert(self.id, self.collection);198 Ok(())199 }200}201impl<T: Config> Deref for CollectionHandle<T> {202 type Target = Collection<T>;203204 fn deref(&self) -> &Self::Target {205 &self.collection206 }207}208209impl<T: Config> DerefMut for CollectionHandle<T> {210 fn deref_mut(&mut self) -> &mut Self::Target {211 &mut self.collection212 }213}214215pub trait Config: system::Config + pallet_evm_coder_substrate::Config + Sized {216 type Event: From<Event<Self>> + Into<<Self as system::Config>::Event>;217218 /// Weight information for extrinsics in this pallet.219 type WeightInfo: WeightInfo;220221 type EvmAddressMapping: pallet_evm::AddressMapping<Self::AccountId>;222 type EvmBackwardsAddressMapping: EvmBackwardsAddressMapping<Self::AccountId>;223224 type CrossAccountId: CrossAccountId<Self::AccountId>;225 type Currency: Currency<Self::AccountId>;226 type CollectionCreationPrice: Get<227 <<Self as Config>::Currency as Currency<Self::AccountId>>::Balance,228 >;229 type TreasuryAccountId: Get<Self::AccountId>;230}231232type SelfWeightOf<T> = <T as Config>::WeightInfo;233234trait WeightInfoHelpers: WeightInfo {235 fn transfer() -> Weight {236 Self::transfer_nft()237 .max(Self::transfer_fungible())238 .max(Self::transfer_refungible())239 }240 fn transfer_from() -> Weight {241 Self::transfer_from_nft()242 .max(Self::transfer_from_fungible())243 .max(Self::transfer_from_refungible())244 }245 fn approve() -> Weight {246 // TODO: refungible, fungible247 Self::approve_nft()248 }249 fn set_variable_meta_data(data: u32) -> Weight {250 // TODO: refungible251 Self::set_variable_meta_data_nft(data)252 }253 fn create_item(data: u32) -> Weight {254 Self::create_item_nft(data)255 .max(Self::create_item_fungible())256 .max(Self::create_item_refungible(data))257 }258 fn create_multiple_items(amount: u32) -> Weight {259 Self::create_multiple_items_nft(amount)260 .max(Self::create_multiple_items_fungible(amount))261 .max(Self::create_multiple_items_refungible(amount))262 }263 fn burn_item() -> Weight {264 // TODO: refungible, fungible265 Self::burn_item_nft()266 }267}268impl<T: WeightInfo> WeightInfoHelpers for T {}269270// # Used definitions271//272// ## User control levels273//274// chain-controlled - key is uncontrolled by user275// i.e autoincrementing index276// can use non-cryptographic hash277// real - key is controlled by user278// but it is hard to generate enough colliding values, i.e owner of signed txs279// can use non-cryptographic hash280// controlled - key is completly controlled by users281// i.e maps with mutable keys282// should use cryptographic hash283//284// ## User control level downgrade reasons285//286// ?1 - chain-controlled -> controlled287// collections/tokens can be destroyed, resulting in massive holes288// ?2 - chain-controlled -> controlled289// same as ?1, but can be only added, resulting in easier exploitation290// ?3 - real -> controlled291// no confirmation required, so addresses can be easily generated292decl_storage! {293 trait Store for Module<T: Config> as Nft {294295 //#region Private members296 /// Id of next collection297 CreatedCollectionCount: u32;298 /// Used for migrations299 ChainVersion: u64;300 /// Id of last collection token301 /// Collection id (controlled?1)302 ItemListIndex: map hasher(blake2_128_concat) CollectionId => TokenId;303 //#endregion304305 //#region Bound counters306 /// Amount of collections destroyed, used for total amount tracking with307 /// CreatedCollectionCount308 DestroyedCollectionCount: u32;309 /// Total amount of account owned tokens (NFTs + RFTs + unique fungibles)310 /// Account id (real)311 pub AccountItemCount get(fn account_item_count): map hasher(twox_64_concat) T::AccountId => u32;312 //#endregion313314 //#region Basic collections315 /// Collection info316 /// Collection id (controlled?1)317 pub CollectionById get(fn collection_id) config(): map hasher(blake2_128_concat) CollectionId => Option<Collection<T>> = None;318 /// List of collection admins319 /// Collection id (controlled?2)320 pub AdminList get(fn admin_list_collection): map hasher(blake2_128_concat) CollectionId => Vec<T::CrossAccountId>;321 /// Whitelisted collection users322 /// Collection id (controlled?2), user id (controlled?3)323 pub WhiteList get(fn white_list): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) T::AccountId => bool;324 //#endregion325326 /// How many of collection items user have327 /// Collection id (controlled?2), account id (real)328 pub Balance get(fn balance_count): double_map hasher(blake2_128_concat) CollectionId, hasher(twox_64_concat) T::AccountId => u128;329330 /// Amount of items which spender can transfer out of owners account (via transferFrom)331 /// Collection id (controlled?2), (token id (controlled ?2) + owner account id (real) + spender account id (controlled?3))332 /// TODO: Off chain worker should remove from this map when token gets removed333 pub Allowances get(fn approved): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) (TokenId, T::AccountId, T::AccountId) => u128;334335 //#region Item collections336 /// Collection id (controlled?2), token id (controlled?1)337 pub NftItemList get(fn nft_item_id) config(): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<NftItemType<T::CrossAccountId>>;338 /// Collection id (controlled?2), owner (controlled?2)339 pub FungibleItemList get(fn fungible_item_id) config(): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) T::AccountId => FungibleItemType;340 /// Collection id (controlled?2), token id (controlled?1)341 pub ReFungibleItemList get(fn refungible_item_id) config(): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<ReFungibleItemType<T::CrossAccountId>>;342 //#endregion343344 //#region Index list345 /// Collection id (controlled?2), tokens owner (controlled?2)346 pub AddressTokens get(fn address_tokens): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) T::AccountId => Vec<TokenId>;347 //#endregion348349 //#region Tokens transfer rate limit baskets350 /// (Collection id (controlled?2), who created (real))351 /// TODO: Off chain worker should remove from this map when collection gets removed352 pub CreateItemBasket get(fn create_item_basket): map hasher(blake2_128_concat) (CollectionId, T::AccountId) => T::BlockNumber;353 /// Collection id (controlled?2), token id (controlled?2)354 pub NftTransferBasket get(fn nft_transfer_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => T::BlockNumber;355 /// Collection id (controlled?2), owning user (real)356 pub FungibleTransferBasket get(fn fungible_transfer_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(twox_64_concat) T::AccountId => T::BlockNumber;357 /// Collection id (controlled?2), token id (controlled?2)358 pub ReFungibleTransferBasket get(fn refungible_transfer_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => T::BlockNumber;359 //#endregion360361 /// Variable metadata sponsoring362 /// Collection id (controlled?2), token id (controlled?2)363 pub VariableMetaDataBasket get(fn variable_meta_data_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber> = None;364 }365 add_extra_genesis {366 build(|config: &GenesisConfig<T>| {367 // Modification of storage368 for (_num, _c) in &config.collection_id {369 <Module<T>>::init_collection(_c);370 }371372 for (_num, _c, _i) in &config.nft_item_id {373 <Module<T>>::init_nft_token(*_c, _i);374 }375376 for (collection_id, account_id, fungible_item) in &config.fungible_item_id {377 <Module<T>>::init_fungible_token(*collection_id, &T::CrossAccountId::from_sub(account_id.clone()), fungible_item);378 }379380 for (_num, _c, _i) in &config.refungible_item_id {381 <Module<T>>::init_refungible_token(*_c, _i);382 }383 })384 }385}386387decl_event!(388 pub enum Event<T>389 where390 AccountId = <T as frame_system::Config>::AccountId,391 CrossAccountId = <T as Config>::CrossAccountId,392 {393 /// New collection was created394 ///395 /// # Arguments396 ///397 /// * collection_id: Globally unique identifier of newly created collection.398 ///399 /// * mode: [CollectionMode] converted into u8.400 ///401 /// * account_id: Collection owner.402 CollectionCreated(CollectionId, u8, AccountId),403404 /// New item was created.405 ///406 /// # Arguments407 ///408 /// * collection_id: Id of the collection where item was created.409 ///410 /// * item_id: Id of an item. Unique within the collection.411 ///412 /// * recipient: Owner of newly created item413 ItemCreated(CollectionId, TokenId, CrossAccountId),414415 /// Collection item was burned.416 ///417 /// # Arguments418 ///419 /// collection_id.420 ///421 /// item_id: Identifier of burned NFT.422 ItemDestroyed(CollectionId, TokenId),423424 /// Item was transferred425 ///426 /// * collection_id: Id of collection to which item is belong427 ///428 /// * item_id: Id of an item429 ///430 /// * sender: Original owner of item431 ///432 /// * recipient: New owner of item433 ///434 /// * amount: Always 1 for NFT435 Transfer(CollectionId, TokenId, CrossAccountId, CrossAccountId, u128),436437 /// * collection_id438 ///439 /// * item_id440 ///441 /// * sender442 ///443 /// * spender444 ///445 /// * amount446 Approved(CollectionId, TokenId, CrossAccountId, CrossAccountId, u128),447 }448);449450decl_module! {451 pub struct Module<T: Config> for enum Call452 where453 origin: T::Origin454 {455 fn deposit_event() = default;456 const CollectionAdminsLimit: u64 = COLLECTION_ADMINS_LIMIT;457 type Error = Error<T>;458459 fn on_initialize(_now: T::BlockNumber) -> Weight {460 0461 }462463 /// This method creates a Collection of NFTs. Each Token may have multiple properties encoded as an array of bytes of certain length. The initial owner and admin of the collection are set to the address that signed the transaction. Both addresses can be changed later.464 ///465 /// # Permissions466 ///467 /// * Anyone.468 ///469 /// # Arguments470 ///471 /// * collection_name: UTF-16 string with collection name (limit 64 characters), will be stored as zero-terminated.472 ///473 /// * collection_description: UTF-16 string with collection description (limit 256 characters), will be stored as zero-terminated.474 ///475 /// * token_prefix: UTF-8 string with token prefix.476 ///477 /// * mode: [CollectionMode] collection type and type dependent data.478 // returns collection ID479 #[weight = <SelfWeightOf<T>>::create_collection()]480 #[transactional]481 pub fn create_collection(origin,482 collection_name: Vec<u16>,483 collection_description: Vec<u16>,484 token_prefix: Vec<u8>,485 mode: CollectionMode) -> DispatchResult {486487 // Anyone can create a collection488 let who = ensure_signed(origin)?;489490 // Take a (non-refundable) deposit of collection creation491 let mut imbalance = <<<T as Config>::Currency as Currency<T::AccountId>>::PositiveImbalance>::zero();492 imbalance.subsume(<<T as Config>::Currency as Currency<T::AccountId>>::deposit_creating(493 &T::TreasuryAccountId::get(),494 T::CollectionCreationPrice::get(),495 ));496 <T as Config>::Currency::settle(497 &who,498 imbalance,499 WithdrawReasons::TRANSFER,500 ExistenceRequirement::KeepAlive,501 ).map_err(|_| Error::<T>::NoPermission)?;502503 let decimal_points = match mode {504 CollectionMode::Fungible(points) => points,505 _ => 0506 };507508 let created_count = CreatedCollectionCount::get();509 let destroyed_count = DestroyedCollectionCount::get();510511 // bound Total number of collections512 ensure!(created_count - destroyed_count < COLLECTION_NUMBER_LIMIT, Error::<T>::TotalCollectionsLimitExceeded);513514 // check params515 ensure!(decimal_points <= MAX_DECIMAL_POINTS, Error::<T>::CollectionDecimalPointLimitExceeded);516 ensure!(collection_name.len() <= MAX_COLLECTION_NAME_LENGTH, Error::<T>::CollectionNameLimitExceeded);517 ensure!(collection_description.len() <= MAX_COLLECTION_DESCRIPTION_LENGTH, Error::<T>::CollectionDescriptionLimitExceeded);518 ensure!(token_prefix.len() <= MAX_TOKEN_PREFIX_LENGTH, Error::<T>::CollectionTokenPrefixLimitExceeded);519520 // Generate next collection ID521 let next_id = created_count522 .checked_add(1)523 .ok_or(Error::<T>::NumOverflow)?;524525 CreatedCollectionCount::put(next_id);526527 let limits = CollectionLimits {528 sponsored_data_size: CUSTOM_DATA_LIMIT,529 ..Default::default()530 };531532 // Create new collection533 let new_collection = Collection {534 owner: who.clone(),535 name: collection_name,536 mode: mode.clone(),537 mint_mode: false,538 access: AccessMode::Normal,539 description: collection_description,540 decimal_points,541 token_prefix,542 offchain_schema: Vec::new(),543 schema_version: SchemaVersion::ImageURL,544 sponsorship: SponsorshipState::Disabled,545 variable_on_chain_schema: Vec::new(),546 const_on_chain_schema: Vec::new(),547 limits,548 meta_update_permission: MetaUpdatePermission::default(),549 transfers_enabled: true,550 };551552 // Add new collection to map553 <CollectionById<T>>::insert(next_id, new_collection);554555 // call event556 Self::deposit_event(RawEvent::CollectionCreated(next_id, mode.id(), who));557558 Ok(())559 }560561 /// **DANGEROUS**: Destroys collection and all NFTs within this collection. Users irrecoverably lose their assets and may lose real money.562 ///563 /// # Permissions564 ///565 /// * Collection Owner.566 ///567 /// # Arguments568 ///569 /// * collection_id: collection to destroy.570 #[weight = <SelfWeightOf<T>>::destroy_collection()]571 #[transactional]572 pub fn destroy_collection(origin, collection_id: CollectionId) -> DispatchResult {573574 let sender = ensure_signed(origin)?;575 let collection = Self::get_collection(collection_id)?;576 Self::check_owner_permissions(&collection, &sender)?;577 if !collection.limits.owner_can_destroy {578 fail!(Error::<T>::NoPermission);579 }580581 <AddressTokens<T>>::remove_prefix(collection_id, None);582 <Allowances<T>>::remove_prefix(collection_id, None);583 <Balance<T>>::remove_prefix(collection_id, None);584 <ItemListIndex>::remove(collection_id);585 <AdminList<T>>::remove(collection_id);586 <CollectionById<T>>::remove(collection_id);587 <WhiteList<T>>::remove_prefix(collection_id, None);588589 <NftItemList<T>>::remove_prefix(collection_id, None);590 <FungibleItemList<T>>::remove_prefix(collection_id, None);591 <ReFungibleItemList<T>>::remove_prefix(collection_id, None);592593 <NftTransferBasket<T>>::remove_prefix(collection_id, None);594 <FungibleTransferBasket<T>>::remove_prefix(collection_id, None);595 <ReFungibleTransferBasket<T>>::remove_prefix(collection_id, None);596597 <VariableMetaDataBasket<T>>::remove_prefix(collection_id, None);598599 DestroyedCollectionCount::put(DestroyedCollectionCount::get()600 .checked_add(1)601 .ok_or(Error::<T>::NumOverflow)?);602603 Ok(())604 }605606 /// Add an address to white list.607 ///608 /// # Permissions609 ///610 /// * Collection Owner611 /// * Collection Admin612 ///613 /// # Arguments614 ///615 /// * collection_id.616 ///617 /// * address.618 #[weight = <SelfWeightOf<T>>::add_to_white_list()]619 #[transactional]620 pub fn add_to_white_list(origin, collection_id: CollectionId, address: T::CrossAccountId) -> DispatchResult{621622 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);623 let collection = Self::get_collection(collection_id)?;624625 Self::toggle_white_list_internal(626 &sender,627 &collection,628 &address,629 true,630 )?;631632 Ok(())633 }634635 /// Remove an address from white list.636 ///637 /// # Permissions638 ///639 /// * Collection Owner640 /// * Collection Admin641 ///642 /// # Arguments643 ///644 /// * collection_id.645 ///646 /// * address.647 #[weight = <SelfWeightOf<T>>::remove_from_white_list()]648 #[transactional]649 pub fn remove_from_white_list(origin, collection_id: CollectionId, address: T::CrossAccountId) -> DispatchResult{650651 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);652 let collection = Self::get_collection(collection_id)?;653654 Self::toggle_white_list_internal(655 &sender,656 &collection,657 &address,658 false,659 )?;660661 Ok(())662 }663664 /// Toggle between normal and white list access for the methods with access for `Anyone`.665 ///666 /// # Permissions667 ///668 /// * Collection Owner.669 ///670 /// # Arguments671 ///672 /// * collection_id.673 ///674 /// * mode: [AccessMode]675 #[weight = <SelfWeightOf<T>>::set_public_access_mode()]676 #[transactional]677 pub fn set_public_access_mode(origin, collection_id: CollectionId, mode: AccessMode) -> DispatchResult678 {679 let sender = ensure_signed(origin)?;680681 let mut target_collection = Self::get_collection(collection_id)?;682 Self::check_owner_permissions(&target_collection, &sender)?;683 target_collection.access = mode;684 target_collection.save()685 }686687 /// Allows Anyone to create tokens if:688 /// * White List is enabled, and689 /// * Address is added to white list, and690 /// * This method was called with True parameter691 ///692 /// # Permissions693 /// * Collection Owner694 ///695 /// # Arguments696 ///697 /// * collection_id.698 ///699 /// * mint_permission: Boolean parameter. If True, allows minting to Anyone with conditions above.700 #[weight = <SelfWeightOf<T>>::set_mint_permission()]701 #[transactional]702 pub fn set_mint_permission(origin, collection_id: CollectionId, mint_permission: bool) -> DispatchResult703 {704 let sender = ensure_signed(origin)?;705706 let mut target_collection = Self::get_collection(collection_id)?;707 Self::check_owner_permissions(&target_collection, &sender)?;708 target_collection.mint_mode = mint_permission;709 target_collection.save()710 }711712 /// Change the owner of the collection.713 ///714 /// # Permissions715 ///716 /// * Collection Owner.717 ///718 /// # Arguments719 ///720 /// * collection_id.721 ///722 /// * new_owner.723 #[weight = <SelfWeightOf<T>>::change_collection_owner()]724 #[transactional]725 pub fn change_collection_owner(origin, collection_id: CollectionId, new_owner: T::AccountId) -> DispatchResult {726727 let sender = ensure_signed(origin)?;728 let mut target_collection = Self::get_collection(collection_id)?;729 Self::check_owner_permissions(&target_collection, &sender)?;730 target_collection.owner = new_owner;731 target_collection.save()732 }733734 /// Adds an admin of the Collection.735 /// NFT Collection can be controlled by multiple admin addresses (some which can also be servers, for example). Admins can issue and burn NFTs, as well as add and remove other admins, but cannot change NFT or Collection ownership.736 ///737 /// # Permissions738 ///739 /// * Collection Owner.740 /// * Collection Admin.741 ///742 /// # Arguments743 ///744 /// * collection_id: ID of the Collection to add admin for.745 ///746 /// * new_admin_id: Address of new admin to add.747 #[weight = <SelfWeightOf<T>>::add_collection_admin()]748 #[transactional]749 pub fn add_collection_admin(origin, collection_id: CollectionId, new_admin_id: T::CrossAccountId) -> DispatchResult {750 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);751 let collection = Self::get_collection(collection_id)?;752 Self::check_owner_or_admin_permissions(&collection, &sender)?;753 let mut admin_arr = <AdminList<T>>::get(collection_id);754755 match admin_arr.binary_search(&new_admin_id) {756 Ok(_) => {},757 Err(idx) => {758 ensure!(admin_arr.len() < COLLECTION_ADMINS_LIMIT as usize, Error::<T>::CollectionAdminsLimitExceeded);759 admin_arr.insert(idx, new_admin_id);760 <AdminList<T>>::insert(collection_id, admin_arr);761 }762 }763 Ok(())764 }765766 /// Remove admin address of the Collection. An admin address can remove itself. List of admins may become empty, in which case only Collection Owner will be able to add an Admin.767 ///768 /// # Permissions769 ///770 /// * Collection Owner.771 /// * Collection Admin.772 ///773 /// # Arguments774 ///775 /// * collection_id: ID of the Collection to remove admin for.776 ///777 /// * account_id: Address of admin to remove.778 #[weight = <SelfWeightOf<T>>::remove_collection_admin()]779 #[transactional]780 pub fn remove_collection_admin(origin, collection_id: CollectionId, account_id: T::CrossAccountId) -> DispatchResult {781 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);782 let collection = Self::get_collection(collection_id)?;783 Self::check_owner_or_admin_permissions(&collection, &sender)?;784 let mut admin_arr = <AdminList<T>>::get(collection_id);785786 if let Ok(idx) = admin_arr.binary_search(&account_id) {787 admin_arr.remove(idx);788 <AdminList<T>>::insert(collection_id, admin_arr);789 }790 Ok(())791 }792793 /// # Permissions794 ///795 /// * Collection Owner796 ///797 /// # Arguments798 ///799 /// * collection_id.800 ///801 /// * new_sponsor.802 #[weight = <SelfWeightOf<T>>::set_collection_sponsor()]803 #[transactional]804 pub fn set_collection_sponsor(origin, collection_id: CollectionId, new_sponsor: T::AccountId) -> DispatchResult {805 let sender = ensure_signed(origin)?;806 let mut target_collection = Self::get_collection(collection_id)?;807 Self::check_owner_permissions(&target_collection, &sender)?;808809 target_collection.sponsorship = SponsorshipState::Unconfirmed(new_sponsor);810 target_collection.save()811 }812813 /// # Permissions814 ///815 /// * Sponsor.816 ///817 /// # Arguments818 ///819 /// * collection_id.820 #[weight = <SelfWeightOf<T>>::confirm_sponsorship()]821 #[transactional]822 pub fn confirm_sponsorship(origin, collection_id: CollectionId) -> DispatchResult {823 let sender = ensure_signed(origin)?;824825 let mut target_collection = Self::get_collection(collection_id)?;826 ensure!(827 target_collection.sponsorship.pending_sponsor() == Some(&sender),828 Error::<T>::ConfirmUnsetSponsorFail829 );830831 target_collection.sponsorship = SponsorshipState::Confirmed(sender);832 target_collection.save()833 }834835 /// Switch back to pay-per-own-transaction model.836 ///837 /// # Permissions838 ///839 /// * Collection owner.840 ///841 /// # Arguments842 ///843 /// * collection_id.844 #[weight = <SelfWeightOf<T>>::remove_collection_sponsor()]845 #[transactional]846 pub fn remove_collection_sponsor(origin, collection_id: CollectionId) -> DispatchResult {847 let sender = ensure_signed(origin)?;848849 let mut target_collection = Self::get_collection(collection_id)?;850 Self::check_owner_permissions(&target_collection, &sender)?;851852 target_collection.sponsorship = SponsorshipState::Disabled;853 target_collection.save()854 }855856 /// This method creates a concrete instance of NFT Collection created with CreateCollection method.857 ///858 /// # Permissions859 ///860 /// * Collection Owner.861 /// * Collection Admin.862 /// * Anyone if863 /// * White List is enabled, and864 /// * Address is added to white list, and865 /// * MintPermission is enabled (see SetMintPermission method)866 ///867 /// # Arguments868 ///869 /// * collection_id: ID of the collection.870 ///871 /// * owner: Address, initial owner of the NFT.872 ///873 /// * data: Token data to store on chain.874 // #[weight =875 // (130_000_000 as Weight)876 // .saturating_add((2135 as Weight).saturating_mul((properties.len() as u64) as Weight))877 // .saturating_add(RocksDbWeight::get().reads(10 as Weight))878 // .saturating_add(RocksDbWeight::get().writes(8 as Weight))]879880 #[weight = <SelfWeightOf<T>>::create_item(data.data_size() as u32)]881 #[transactional]882 pub fn create_item(origin, collection_id: CollectionId, owner: T::CrossAccountId, data: CreateItemData) -> DispatchResult {883 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);884 let collection = Self::get_collection(collection_id)?;885886 Self::create_item_internal(&sender, &collection, &owner, data)?;887888 collection.submit_logs()889 }890891 /// This method creates multiple items in a collection created with CreateCollection method.892 ///893 /// # Permissions894 ///895 /// * Collection Owner.896 /// * Collection Admin.897 /// * Anyone if898 /// * White List is enabled, and899 /// * Address is added to white list, and900 /// * MintPermission is enabled (see SetMintPermission method)901 ///902 /// # Arguments903 ///904 /// * collection_id: ID of the collection.905 ///906 /// * itemsData: Array items properties. Each property is an array of bytes itself, see [create_item].907 ///908 /// * owner: Address, initial owner of the NFT.909 #[weight = <SelfWeightOf<T>>::create_multiple_items(items_data.len() as u32)]910 #[transactional]911 pub fn create_multiple_items(origin, collection_id: CollectionId, owner: T::CrossAccountId, items_data: Vec<CreateItemData>) -> DispatchResult {912913 ensure!(!items_data.is_empty(), Error::<T>::EmptyArgument);914 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);915 let collection = Self::get_collection(collection_id)?;916917 Self::create_multiple_items_internal(&sender, &collection, &owner, items_data)?;918919 collection.submit_logs()920 }921922 // TODO! transaction weight923924 /// Set transfers_enabled value for particular collection925 ///926 /// # Permissions927 ///928 /// * Collection Owner.929 ///930 /// # Arguments931 ///932 /// * collection_id: ID of the collection.933 ///934 /// * value: New flag value.935 #[weight = <SelfWeightOf<T>>::set_transfers_enabled_flag()]936 #[transactional]937 pub fn set_transfers_enabled_flag(origin, collection_id: CollectionId, value: bool) -> DispatchResult {938939 let sender = ensure_signed(origin)?;940 let mut target_collection = Self::get_collection(collection_id)?;941942 Self::check_owner_permissions(&target_collection, &sender)?;943944 target_collection.transfers_enabled = value;945 target_collection.save()946 }947948 // TODO! transaction weight949 /// Set meta_update_permission value for particular collection950 ///951 /// # Permissions952 ///953 /// * Collection Owner.954 ///955 /// # Arguments956 ///957 /// * collection_id: ID of the collection.958 ///959 /// * value: New flag value.960 #[weight = <T as Config>::WeightInfo::burn_item()]961 #[transactional]962 pub fn set_meta_update_permission_flag(origin, collection_id: CollectionId, value: MetaUpdatePermission) -> DispatchResult {963964 let sender = ensure_signed(origin)?;965 let mut target_collection = Self::get_collection(collection_id)?;966967 ensure!(968 target_collection.meta_update_permission != MetaUpdatePermission::None,969 Error::<T>::MetadataFlagFrozen970 );971 Self::check_owner_permissions(&target_collection, &sender)?;972973 target_collection.meta_update_permission = value;974975 target_collection.save()976 }977978 /// Destroys a concrete instance of NFT.979 ///980 /// # Permissions981 ///982 /// * Collection Owner.983 /// * Collection Admin.984 /// * Current NFT Owner.985 ///986 /// # Arguments987 ///988 /// * collection_id: ID of the collection.989 ///990 /// * item_id: ID of NFT to burn.991 #[weight = <SelfWeightOf<T>>::burn_item()]992 #[transactional]993 pub fn burn_item(origin, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResult {994995 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);996 let target_collection = Self::get_collection(collection_id)?;997998 Self::burn_item_internal(&sender, &target_collection, item_id, value)?;9991000 target_collection.submit_logs()1001 }10021003 /// Change ownership of the token.1004 ///1005 /// # Permissions1006 ///1007 /// * Collection Owner1008 /// * Collection Admin1009 /// * Current NFT owner1010 ///1011 /// # Arguments1012 ///1013 /// * recipient: Address of token recipient.1014 ///1015 /// * collection_id.1016 ///1017 /// * item_id: ID of the item1018 /// * Non-Fungible Mode: Required.1019 /// * Fungible Mode: Ignored.1020 /// * Re-Fungible Mode: Required.1021 ///1022 /// * value: Amount to transfer.1023 /// * Non-Fungible Mode: Ignored1024 /// * Fungible Mode: Must specify transferred amount1025 /// * Re-Fungible Mode: Must specify transferred portion (between 0 and 1)1026 #[weight = <SelfWeightOf<T>>::transfer()]1027 #[transactional]1028 pub fn transfer(origin, recipient: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResult {1029 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1030 let collection = Self::get_collection(collection_id)?;10311032 Self::transfer_internal(&sender, &recipient, &collection, item_id, value)?;10331034 collection.submit_logs()1035 }10361037 /// Set, change, or remove approved address to transfer the ownership of the NFT.1038 ///1039 /// # Permissions1040 ///1041 /// * Collection Owner1042 /// * Collection Admin1043 /// * Current NFT owner1044 ///1045 /// # Arguments1046 ///1047 /// * approved: Address that is approved to transfer this NFT or zero (if needed to remove approval).1048 ///1049 /// * collection_id.1050 ///1051 /// * item_id: ID of the item.1052 #[weight = <SelfWeightOf<T>>::approve()]1053 #[transactional]1054 pub fn approve(origin, spender: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, amount: u128) -> DispatchResult {1055 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1056 let collection = Self::get_collection(collection_id)?;10571058 Self::approve_internal(&sender, &spender, &collection, item_id, amount)?;10591060 collection.submit_logs()1061 }10621063 /// Change ownership of a NFT on behalf of the owner. See Approve method for additional information. After this method executes, the approval is removed so that the approved address will not be able to transfer this NFT again from this owner.1064 ///1065 /// # Permissions1066 /// * Collection Owner1067 /// * Collection Admin1068 /// * Current NFT owner1069 /// * Address approved by current NFT owner1070 ///1071 /// # Arguments1072 ///1073 /// * from: Address that owns token.1074 ///1075 /// * recipient: Address of token recipient.1076 ///1077 /// * collection_id.1078 ///1079 /// * item_id: ID of the item.1080 ///1081 /// * value: Amount to transfer.1082 #[weight = <SelfWeightOf<T>>::transfer_from()]1083 #[transactional]1084 pub fn transfer_from(origin, from: T::CrossAccountId, recipient: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, value: u128 ) -> DispatchResult {1085 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1086 let collection = Self::get_collection(collection_id)?;10871088 Self::transfer_from_internal(&sender, &from, &recipient, &collection, item_id, value)?;10891090 collection.submit_logs()1091 }1092 // #[weight = 0]1093 // // let no_perm_mes = "You do not have permissions to modify this collection";1094 // // ensure!(<ApprovedList<T>>::contains_key((collection_id, item_id)), no_perm_mes);1095 // // let list_itm = <ApprovedList<T>>::get((collection_id, item_id));1096 // // ensure!(list_itm.contains(&new_owner.clone()), no_perm_mes);10971098 // // // on_nft_received call10991100 // // Self::transfer(origin, collection_id, item_id, new_owner)?;11011102 // Ok(())1103 // }11041105 /// Set off-chain data schema.1106 ///1107 /// # Permissions1108 ///1109 /// * Collection Owner1110 /// * Collection Admin1111 ///1112 /// # Arguments1113 ///1114 /// * collection_id.1115 ///1116 /// * schema: String representing the offchain data schema.1117 #[weight = <SelfWeightOf<T>>::set_variable_meta_data(data.len() as u32)]1118 #[transactional]1119 pub fn set_variable_meta_data (1120 origin,1121 collection_id: CollectionId,1122 item_id: TokenId,1123 data: Vec<u8>1124 ) -> DispatchResult {1125 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);11261127 let collection = Self::get_collection(collection_id)?;11281129 Self::set_variable_meta_data_internal(&sender, &collection, item_id, data)?;11301131 Ok(())1132 }11331134 /// Set schema standard1135 /// ImageURL1136 /// Unique1137 ///1138 /// # Permissions1139 ///1140 /// * Collection Owner1141 /// * Collection Admin1142 ///1143 /// # Arguments1144 ///1145 /// * collection_id.1146 ///1147 /// * schema: SchemaVersion: enum1148 #[weight = <SelfWeightOf<T>>::set_schema_version()]1149 #[transactional]1150 pub fn set_schema_version(1151 origin,1152 collection_id: CollectionId,1153 version: SchemaVersion1154 ) -> DispatchResult {1155 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1156 let mut target_collection = Self::get_collection(collection_id)?;1157 Self::check_owner_or_admin_permissions(&target_collection, &sender)?;1158 target_collection.schema_version = version;1159 target_collection.save()1160 }11611162 /// Set off-chain data schema.1163 ///1164 /// # Permissions1165 ///1166 /// * Collection Owner1167 /// * Collection Admin1168 ///1169 /// # Arguments1170 ///1171 /// * collection_id.1172 ///1173 /// * schema: String representing the offchain data schema.1174 #[weight = <SelfWeightOf<T>>::set_offchain_schema(schema.len() as u32)]1175 #[transactional]1176 pub fn set_offchain_schema(1177 origin,1178 collection_id: CollectionId,1179 schema: Vec<u8>1180 ) -> DispatchResult {1181 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1182 let mut target_collection = Self::get_collection(collection_id)?;1183 Self::check_owner_or_admin_permissions(&target_collection, &sender)?;11841185 // check schema limit1186 ensure!(schema.len() as u32 <= OFFCHAIN_SCHEMA_LIMIT, "");11871188 target_collection.offchain_schema = schema;1189 target_collection.save()1190 }11911192 /// Set const on-chain data schema.1193 ///1194 /// # Permissions1195 ///1196 /// * Collection Owner1197 /// * Collection Admin1198 ///1199 /// # Arguments1200 ///1201 /// * collection_id.1202 ///1203 /// * schema: String representing the const on-chain data schema.1204 #[weight = <SelfWeightOf<T>>::set_const_on_chain_schema(schema.len() as u32)]1205 #[transactional]1206 pub fn set_const_on_chain_schema (1207 origin,1208 collection_id: CollectionId,1209 schema: Vec<u8>1210 ) -> DispatchResult {1211 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1212 let mut target_collection = Self::get_collection(collection_id)?;1213 Self::check_owner_or_admin_permissions(&target_collection, &sender)?;12141215 // check schema limit1216 ensure!(schema.len() as u32 <= CONST_ON_CHAIN_SCHEMA_LIMIT, "");12171218 target_collection.const_on_chain_schema = schema;1219 target_collection.save()1220 }12211222 /// Set variable on-chain data schema.1223 ///1224 /// # Permissions1225 ///1226 /// * Collection Owner1227 /// * Collection Admin1228 ///1229 /// # Arguments1230 ///1231 /// * collection_id.1232 ///1233 /// * schema: String representing the variable on-chain data schema.1234 #[weight = <SelfWeightOf<T>>::set_const_on_chain_schema(schema.len() as u32)]1235 #[transactional]1236 pub fn set_variable_on_chain_schema (1237 origin,1238 collection_id: CollectionId,1239 schema: Vec<u8>1240 ) -> DispatchResult {1241 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1242 let mut target_collection = Self::get_collection(collection_id)?;1243 Self::check_owner_or_admin_permissions(&target_collection, &sender)?;12441245 // check schema limit1246 ensure!(schema.len() as u32 <= VARIABLE_ON_CHAIN_SCHEMA_LIMIT, "");12471248 target_collection.variable_on_chain_schema = schema;1249 target_collection.save()1250 }12511252 #[weight = <SelfWeightOf<T>>::set_collection_limits()]1253 #[transactional]1254 pub fn set_collection_limits(1255 origin,1256 collection_id: u32,1257 new_limits: CollectionLimits<T::BlockNumber>,1258 ) -> DispatchResult {1259 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1260 let mut target_collection = Self::get_collection(collection_id)?;1261 Self::check_owner_permissions(&target_collection, sender.as_sub())?;1262 let old_limits = &target_collection.limits;12631264 // collection bounds1265 ensure!(new_limits.sponsor_transfer_timeout <= MAX_SPONSOR_TIMEOUT &&1266 new_limits.account_token_ownership_limit <= MAX_TOKEN_OWNERSHIP &&1267 new_limits.sponsored_data_size <= CUSTOM_DATA_LIMIT,1268 Error::<T>::CollectionLimitBoundsExceeded);12691270 // token_limit check prev1271 ensure!(old_limits.token_limit >= new_limits.token_limit, Error::<T>::CollectionTokenLimitExceeded);1272 ensure!(new_limits.token_limit > 0, Error::<T>::CollectionTokenLimitExceeded);12731274 ensure!(1275 (old_limits.owner_can_transfer || !new_limits.owner_can_transfer) &&1276 (old_limits.owner_can_destroy || !new_limits.owner_can_destroy),1277 Error::<T>::OwnerPermissionsCantBeReverted,1278 );12791280 target_collection.limits = new_limits;12811282 target_collection.save()1283 }1284 }1285}12861287impl<T: Config> Module<T> {1288 pub fn create_item_internal(1289 sender: &T::CrossAccountId,1290 collection: &CollectionHandle<T>,1291 owner: &T::CrossAccountId,1292 data: CreateItemData,1293 ) -> DispatchResult {1294 ensure!(1295 owner != &T::CrossAccountId::from_eth(H160([0; 20])),1296 Error::<T>::AddressIsZero1297 );12981299 Self::can_create_items_in_collection(collection, sender, owner, 1)?;1300 Self::validate_create_item_args(collection, &data)?;1301 Self::create_item_no_validation(collection, owner, data)?;13021303 Ok(())1304 }13051306 pub fn transfer_internal(1307 sender: &T::CrossAccountId,1308 recipient: &T::CrossAccountId,1309 target_collection: &CollectionHandle<T>,1310 item_id: TokenId,1311 value: u128,1312 ) -> DispatchResult {1313 ensure!(1314 recipient != &T::CrossAccountId::from_eth(H160([0; 20])),1315 Error::<T>::AddressIsZero1316 );13171318 // Limits check1319 Self::is_correct_transfer(target_collection, recipient)?;13201321 // Transfer permissions check1322 ensure!(1323 Self::is_item_owner(sender, target_collection, item_id)?1324 || Self::is_owner_or_admin_permissions(target_collection, sender)?,1325 Error::<T>::NoPermission1326 );13271328 if target_collection.access == AccessMode::WhiteList {1329 Self::check_white_list(target_collection, sender)?;1330 Self::check_white_list(target_collection, recipient)?;1331 }13321333 match target_collection.mode {1334 CollectionMode::NFT => Self::transfer_nft(1335 target_collection,1336 item_id,1337 sender.clone(),1338 recipient.clone(),1339 )?,1340 CollectionMode::Fungible(_) => {1341 Self::transfer_fungible(target_collection, value, sender, recipient)?1342 }1343 CollectionMode::ReFungible => Self::transfer_refungible(1344 target_collection,1345 item_id,1346 value,1347 sender.clone(),1348 recipient.clone(),1349 )?,1350 _ => (),1351 };13521353 Self::deposit_event(RawEvent::Transfer(1354 target_collection.id,1355 item_id,1356 sender.clone(),1357 recipient.clone(),1358 value,1359 ));13601361 Ok(())1362 }13631364 pub fn approve_internal(1365 sender: &T::CrossAccountId,1366 spender: &T::CrossAccountId,1367 collection: &CollectionHandle<T>,1368 item_id: TokenId,1369 amount: u128,1370 ) -> DispatchResult {1371 Self::token_exists(collection, item_id)?;13721373 // Transfer permissions check1374 let bypasses_limits = collection.limits.owner_can_transfer1375 && Self::is_owner_or_admin_permissions(collection, sender)?;13761377 let allowance_limit = if bypasses_limits {1378 None1379 } else if let Some(amount) = Self::owned_amount(sender, collection, item_id)? {1380 Some(amount)1381 } else {1382 fail!(Error::<T>::NoPermission);1383 };13841385 if collection.access == AccessMode::WhiteList {1386 Self::check_white_list(collection, sender)?;1387 Self::check_white_list(collection, spender)?;1388 }13891390 collection.consume_sload()?;1391 let allowance: u128 = amount1392 .checked_add(<Allowances<T>>::get(1393 collection.id,1394 (item_id, sender.as_sub(), spender.as_sub()),1395 ))1396 .ok_or(Error::<T>::NumOverflow)?;1397 if let Some(limit) = allowance_limit {1398 ensure!(limit >= allowance, Error::<T>::TokenValueTooLow);1399 }1400 collection.consume_sstore()?;1401 <Allowances<T>>::insert(1402 collection.id,1403 (item_id, sender.as_sub(), spender.as_sub()),1404 allowance,1405 );14061407 if matches!(collection.mode, CollectionMode::NFT) {1408 // TODO: NFT: only one owner may exist for token in ERC7211409 collection.log(ERC721Events::Approval {1410 owner: *sender.as_eth(),1411 approved: *spender.as_eth(),1412 token_id: item_id.into(),1413 })?;1414 }14151416 if matches!(collection.mode, CollectionMode::Fungible(_)) {1417 // TODO: NFT: only one owner may exist for token in ERC201418 collection.log(ERC20Events::Approval {1419 owner: *sender.as_eth(),1420 spender: *spender.as_eth(),1421 value: allowance.into(),1422 })?;1423 }14241425 Self::deposit_event(RawEvent::Approved(1426 collection.id,1427 item_id,1428 sender.clone(),1429 spender.clone(),1430 allowance,1431 ));1432 Ok(())1433 }14341435 pub fn transfer_from_internal(1436 sender: &T::CrossAccountId,1437 from: &T::CrossAccountId,1438 recipient: &T::CrossAccountId,1439 collection: &CollectionHandle<T>,1440 item_id: TokenId,1441 amount: u128,1442 ) -> DispatchResult {1443 if sender == from {1444 // Transfer by `from`, because it is either equal to sender, or derived from him1445 return Self::transfer_internal(from, recipient, collection, item_id, amount);1446 }14471448 // Check approval1449 collection.consume_sload()?;1450 let approval: u128 =1451 <Allowances<T>>::get(collection.id, (item_id, from.as_sub(), sender.as_sub()));14521453 // Limits check1454 Self::is_correct_transfer(collection, recipient)?;14551456 // Transfer permissions check1457 ensure!(1458 approval >= amount1459 || (collection.limits.owner_can_transfer1460 && Self::is_owner_or_admin_permissions(collection, sender)?),1461 Error::<T>::NoPermission1462 );14631464 if collection.access == AccessMode::WhiteList {1465 Self::check_white_list(collection, sender)?;1466 Self::check_white_list(collection, recipient)?;1467 }14681469 // Reduce approval by transferred amount or remove if remaining approval drops to 01470 let allowance = approval.saturating_sub(amount);1471 collection.consume_sstore()?;1472 if allowance > 0 {1473 <Allowances<T>>::insert(1474 collection.id,1475 (item_id, from.as_sub(), sender.as_sub()),1476 allowance,1477 );1478 } else {1479 <Allowances<T>>::remove(collection.id, (item_id, from.as_sub(), sender.as_sub()));1480 }14811482 match collection.mode {1483 CollectionMode::NFT => {1484 Self::transfer_nft(collection, item_id, from.clone(), recipient.clone())?1485 }1486 CollectionMode::Fungible(_) => {1487 Self::transfer_fungible(collection, amount, from, recipient)?1488 }1489 CollectionMode::ReFungible => Self::transfer_refungible(1490 collection,1491 item_id,1492 amount,1493 from.clone(),1494 recipient.clone(),1495 )?,1496 _ => (),1497 };14981499 if matches!(collection.mode, CollectionMode::Fungible(_)) {1500 collection.log(ERC20Events::Approval {1501 owner: *from.as_eth(),1502 spender: *sender.as_eth(),1503 value: allowance.into(),1504 })?;1505 }15061507 Ok(())1508 }15091510 pub fn set_variable_meta_data_internal(1511 sender: &T::CrossAccountId,1512 collection: &CollectionHandle<T>,1513 item_id: TokenId,1514 data: Vec<u8>,1515 ) -> DispatchResult {1516 Self::token_exists(collection, item_id)?;15171518 ensure!(1519 CUSTOM_DATA_LIMIT >= data.len() as u32,1520 Error::<T>::TokenVariableDataLimitExceeded1521 );15221523 // Modify permissions check1524 ensure!(1525 Self::is_item_owner(sender, collection, item_id)?1526 || Self::is_owner_or_admin_permissions(collection, sender)?,1527 Error::<T>::NoPermission1528 );15291530 match collection.mode {1531 CollectionMode::NFT => Self::set_nft_variable_data(collection, item_id, data)?,1532 CollectionMode::ReFungible => {1533 Self::set_re_fungible_variable_data(collection, item_id, data)?1534 }1535 CollectionMode::Fungible(_) => fail!(Error::<T>::CantStoreMetadataInFungibleTokens),1536 _ => fail!(Error::<T>::UnexpectedCollectionType),1537 };15381539 Ok(())1540 }15411542 pub fn meta_update_check(1543 sender: &T::CrossAccountId,1544 collection: &CollectionHandle<T>,1545 item_id: TokenId,1546 ) -> DispatchResult {1547 match collection.meta_update_permission {1548 MetaUpdatePermission::ItemOwner => ensure!(1549 Self::is_item_owner(sender, collection, item_id)?,1550 Error::<T>::NoPermission1551 ),1552 MetaUpdatePermission::Admin => ensure!(1553 Self::is_owner_or_admin_permissions(collection, sender)?,1554 Error::<T>::NoPermission1555 ),1556 MetaUpdatePermission::None => fail!(Error::<T>::MetadataUpdateDenied),1557 }15581559 Ok(())1560 }15611562 pub fn get_variable_metadata(1563 collection: &CollectionHandle<T>,1564 item_id: TokenId,1565 ) -> Result<Vec<u8>, DispatchError> {1566 Ok(match collection.mode {1567 CollectionMode::NFT => {1568 <NftItemList<T>>::get(collection.id, item_id)1569 .ok_or(Error::<T>::TokenNotFound)?1570 .variable_data1571 }1572 CollectionMode::ReFungible => {1573 <ReFungibleItemList<T>>::get(collection.id, item_id)1574 .ok_or(Error::<T>::TokenNotFound)?1575 .variable_data1576 }1577 _ => fail!(Error::<T>::UnexpectedCollectionType),1578 })1579 }15801581 pub fn create_multiple_items_internal(1582 sender: &T::CrossAccountId,1583 collection: &CollectionHandle<T>,1584 owner: &T::CrossAccountId,1585 items_data: Vec<CreateItemData>,1586 ) -> DispatchResult {1587 Self::can_create_items_in_collection(collection, sender, owner, items_data.len() as u32)?;15881589 for data in &items_data {1590 Self::validate_create_item_args(collection, data)?;1591 }1592 for data in &items_data {1593 Self::create_item_no_validation(collection, owner, data.clone())?;1594 }15951596 Ok(())1597 }15981599 pub fn burn_item_internal(1600 sender: &T::CrossAccountId,1601 collection: &CollectionHandle<T>,1602 item_id: TokenId,1603 value: u128,1604 ) -> DispatchResult {1605 ensure!(1606 Self::is_item_owner(sender, collection, item_id)?1607 || (collection.limits.owner_can_transfer1608 && Self::is_owner_or_admin_permissions(collection, sender)?),1609 Error::<T>::NoPermission1610 );16111612 if collection.access == AccessMode::WhiteList {1613 Self::check_white_list(collection, sender)?;1614 }16151616 match collection.mode {1617 CollectionMode::NFT => Self::burn_nft_item(collection, item_id)?,1618 CollectionMode::Fungible(_) => Self::burn_fungible_item(sender, collection, value)?,1619 CollectionMode::ReFungible => Self::burn_refungible_item(collection, item_id, sender)?,1620 _ => (),1621 };16221623 Ok(())1624 }16251626 pub fn toggle_white_list_internal(1627 sender: &T::CrossAccountId,1628 collection: &CollectionHandle<T>,1629 address: &T::CrossAccountId,1630 whitelisted: bool,1631 ) -> DispatchResult {1632 Self::check_owner_or_admin_permissions(collection, sender)?;16331634 if whitelisted {1635 <WhiteList<T>>::insert(collection.id, address.as_sub(), true);1636 } else {1637 <WhiteList<T>>::remove(collection.id, address.as_sub());1638 }16391640 Ok(())1641 }16421643 fn is_correct_transfer(1644 collection: &CollectionHandle<T>,1645 recipient: &T::CrossAccountId,1646 ) -> DispatchResult {1647 let collection_id = collection.id;16481649 // check token limit and account token limit1650 collection.consume_sload()?;1651 let account_items: u32 =1652 <AddressTokens<T>>::get(collection_id, recipient.as_sub()).len() as u32;1653 ensure!(1654 collection.limits.account_token_ownership_limit > account_items,1655 Error::<T>::AccountTokenLimitExceeded1656 );16571658 // preliminary transfer check1659 ensure!(collection.transfers_enabled, Error::<T>::TransferNotAllowed);16601661 Ok(())1662 }16631664 fn can_create_items_in_collection(1665 collection: &CollectionHandle<T>,1666 sender: &T::CrossAccountId,1667 owner: &T::CrossAccountId,1668 amount: u32,1669 ) -> DispatchResult {1670 let collection_id = collection.id;16711672 // check token limit and account token limit1673 let total_items: u32 = ItemListIndex::get(collection_id)1674 .checked_add(amount)1675 .ok_or(Error::<T>::CollectionTokenLimitExceeded)?;1676 let account_items: u32 = (<AddressTokens<T>>::get(collection_id, owner.as_sub()).len()1677 as u32)1678 .checked_add(amount)1679 .ok_or(Error::<T>::AccountTokenLimitExceeded)?;1680 ensure!(1681 collection.limits.token_limit >= total_items,1682 Error::<T>::CollectionTokenLimitExceeded1683 );1684 ensure!(1685 collection.limits.account_token_ownership_limit >= account_items,1686 Error::<T>::AccountTokenLimitExceeded1687 );16881689 if !Self::is_owner_or_admin_permissions(collection, sender)? {1690 ensure!(collection.mint_mode, Error::<T>::PublicMintingNotAllowed);1691 Self::check_white_list(collection, owner)?;1692 Self::check_white_list(collection, sender)?;1693 }16941695 Ok(())1696 }16971698 fn validate_create_item_args(1699 target_collection: &CollectionHandle<T>,1700 data: &CreateItemData,1701 ) -> DispatchResult {1702 match target_collection.mode {1703 CollectionMode::NFT => {1704 if !matches!(data, CreateItemData::NFT(_)) {1705 fail!(Error::<T>::NotNftDataUsedToMintNftCollectionToken);1706 }1707 }1708 CollectionMode::Fungible(_) => {1709 if !matches!(data, CreateItemData::Fungible(_)) {1710 fail!(Error::<T>::NotFungibleDataUsedToMintFungibleCollectionToken);1711 }1712 }1713 CollectionMode::ReFungible => {1714 if let CreateItemData::ReFungible(data) = data {1715 // Check refungibility limits1716 ensure!(1717 data.pieces <= MAX_REFUNGIBLE_PIECES,1718 Error::<T>::WrongRefungiblePieces1719 );1720 ensure!(data.pieces > 0, Error::<T>::WrongRefungiblePieces);1721 } else {1722 fail!(Error::<T>::NotReFungibleDataUsedToMintReFungibleCollectionToken);1723 }1724 }1725 _ => {1726 fail!(Error::<T>::UnexpectedCollectionType);1727 }1728 };17291730 Ok(())1731 }17321733 fn create_item_no_validation(1734 collection: &CollectionHandle<T>,1735 owner: &T::CrossAccountId,1736 data: CreateItemData,1737 ) -> DispatchResult {1738 match data {1739 CreateItemData::NFT(data) => {1740 let item = NftItemType {1741 owner: owner.clone(),1742 const_data: data.const_data.into_inner(),1743 variable_data: data.variable_data.into_inner(),1744 };17451746 Self::add_nft_item(collection, item)?;1747 }1748 CreateItemData::Fungible(data) => {1749 Self::add_fungible_item(collection, owner, data.value)?;1750 }1751 CreateItemData::ReFungible(data) => {1752 let owner_list = vec![Ownership {1753 owner: owner.clone(),1754 fraction: data.pieces,1755 }];17561757 let item = ReFungibleItemType {1758 owner: owner_list,1759 const_data: data.const_data.into_inner(),1760 variable_data: data.variable_data.into_inner(),1761 };17621763 Self::add_refungible_item(collection, item)?;1764 }1765 };17661767 Ok(())1768 }17691770 fn add_fungible_item(1771 collection: &CollectionHandle<T>,1772 owner: &T::CrossAccountId,1773 value: u128,1774 ) -> DispatchResult {1775 let collection_id = collection.id;17761777 // Does new owner already have an account?1778 collection.consume_sload()?;1779 let balance: u128 = <FungibleItemList<T>>::get(collection_id, owner.as_sub()).value;17801781 // Mint1782 let item = FungibleItemType {1783 value: balance.checked_add(value).ok_or(Error::<T>::NumOverflow)?,1784 };1785 collection.consume_sstore()?;1786 <FungibleItemList<T>>::insert(collection_id, owner.as_sub(), item);17871788 // Update balance1789 collection.consume_sload()?;1790 let new_balance = <Balance<T>>::get(collection_id, owner.as_sub())1791 .checked_add(value)1792 .ok_or(Error::<T>::NumOverflow)?;1793 collection.consume_sstore()?;1794 <Balance<T>>::insert(collection_id, owner.as_sub(), new_balance);17951796 collection.log(ERC20Events::Transfer {1797 from: H160::default(),1798 to: *owner.as_eth(),1799 value: value.into(),1800 })?;1801 Self::deposit_event(RawEvent::ItemCreated(collection_id, 0, owner.clone()));1802 Ok(())1803 }18041805 fn add_refungible_item(1806 collection: &CollectionHandle<T>,1807 item: ReFungibleItemType<T::CrossAccountId>,1808 ) -> DispatchResult {1809 let collection_id = collection.id;18101811 let current_index = <ItemListIndex>::get(collection_id)1812 .checked_add(1)1813 .ok_or(Error::<T>::NumOverflow)?;1814 let itemcopy = item.clone();18151816 ensure!(item.owner.len() == 1, Error::<T>::BadCreateRefungibleCall,);1817 let item_owner = item.owner.first().expect("only one owner is defined");18181819 let value = item_owner.fraction;1820 let owner = item_owner.owner.clone();18211822 Self::add_token_index(collection, current_index, &owner)?;18231824 <ItemListIndex>::insert(collection_id, current_index);1825 <ReFungibleItemList<T>>::insert(collection_id, current_index, itemcopy);18261827 // Update balance1828 let new_balance = <Balance<T>>::get(collection_id, owner.as_sub())1829 .checked_add(value)1830 .ok_or(Error::<T>::NumOverflow)?;1831 <Balance<T>>::insert(collection_id, owner.as_sub(), new_balance);18321833 Self::deposit_event(RawEvent::ItemCreated(collection_id, current_index, owner));1834 Ok(())1835 }18361837 fn add_nft_item(1838 collection: &CollectionHandle<T>,1839 item: NftItemType<T::CrossAccountId>,1840 ) -> DispatchResult {1841 let collection_id = collection.id;18421843 let current_index = <ItemListIndex>::get(collection_id)1844 .checked_add(1)1845 .ok_or(Error::<T>::NumOverflow)?;18461847 let item_owner = item.owner.clone();1848 Self::add_token_index(collection, current_index, &item.owner)?;18491850 <ItemListIndex>::insert(collection_id, current_index);1851 <NftItemList<T>>::insert(collection_id, current_index, item);18521853 // Update balance1854 let new_balance = <Balance<T>>::get(collection_id, item_owner.as_sub())1855 .checked_add(1)1856 .ok_or(Error::<T>::NumOverflow)?;1857 <Balance<T>>::insert(collection_id, item_owner.as_sub(), new_balance);18581859 collection.log(ERC721Events::Transfer {1860 from: H160::default(),1861 to: *item_owner.as_eth(),1862 token_id: current_index.into(),1863 })?;1864 Self::deposit_event(RawEvent::ItemCreated(1865 collection_id,1866 current_index,1867 item_owner,1868 ));1869 Ok(())1870 }18711872 fn burn_refungible_item(1873 collection: &CollectionHandle<T>,1874 item_id: TokenId,1875 owner: &T::CrossAccountId,1876 ) -> DispatchResult {1877 let collection_id = collection.id;18781879 let mut token = <ReFungibleItemList<T>>::get(collection_id, item_id)1880 .ok_or(Error::<T>::TokenNotFound)?;1881 let rft_balance = token1882 .owner1883 .iter()1884 .find(|&i| i.owner == *owner)1885 .ok_or(Error::<T>::TokenNotFound)?;1886 Self::remove_token_index(collection, item_id, owner)?;18871888 // update balance1889 let new_balance = <Balance<T>>::get(collection_id, rft_balance.owner.as_sub())1890 .checked_sub(rft_balance.fraction)1891 .ok_or(Error::<T>::NumOverflow)?;1892 <Balance<T>>::insert(collection_id, rft_balance.owner.as_sub(), new_balance);18931894 // Re-create owners list with sender removed1895 let index = token1896 .owner1897 .iter()1898 .position(|i| i.owner == *owner)1899 .expect("owned item is exists");1900 token.owner.remove(index);1901 let owner_count = token.owner.len();19021903 // Burn the token completely if this was the last (only) owner1904 if owner_count == 0 {1905 <ReFungibleItemList<T>>::remove(collection_id, item_id);1906 <VariableMetaDataBasket<T>>::remove(collection_id, item_id);1907 } else {1908 <ReFungibleItemList<T>>::insert(collection_id, item_id, token);1909 }19101911 Ok(())1912 }19131914 fn burn_nft_item(collection: &CollectionHandle<T>, item_id: TokenId) -> DispatchResult {1915 let collection_id = collection.id;19161917 let item =1918 <NftItemList<T>>::get(collection_id, item_id).ok_or(Error::<T>::TokenNotFound)?;1919 Self::remove_token_index(collection, item_id, &item.owner)?;19201921 // update balance1922 let new_balance = <Balance<T>>::get(collection_id, item.owner.as_sub())1923 .checked_sub(1)1924 .ok_or(Error::<T>::NumOverflow)?;1925 <Balance<T>>::insert(collection_id, item.owner.as_sub(), new_balance);1926 <NftItemList<T>>::remove(collection_id, item_id);1927 <VariableMetaDataBasket<T>>::remove(collection_id, item_id);19281929 collection.log(ERC721Events::Transfer {1930 from: *item.owner.as_eth(),1931 to: H160::default(),1932 token_id: item_id.into(),1933 })?;1934 Self::deposit_event(RawEvent::ItemDestroyed(collection.id, item_id));1935 Ok(())1936 }19371938 fn burn_fungible_item(1939 owner: &T::CrossAccountId,1940 collection: &CollectionHandle<T>,1941 value: u128,1942 ) -> DispatchResult {1943 let collection_id = collection.id;19441945 let mut balance = <FungibleItemList<T>>::get(collection_id, owner.as_sub());1946 ensure!(balance.value >= value, Error::<T>::TokenValueNotEnough);19471948 // update balance1949 let new_balance = <Balance<T>>::get(collection_id, owner.as_sub())1950 .checked_sub(value)1951 .ok_or(Error::<T>::NumOverflow)?;1952 <Balance<T>>::insert(collection_id, owner.as_sub(), new_balance);19531954 if balance.value - value > 0 {1955 balance.value -= value;1956 <FungibleItemList<T>>::insert(collection_id, owner.as_sub(), balance);1957 } else {1958 <FungibleItemList<T>>::remove(collection_id, owner.as_sub());1959 }19601961 collection.log(ERC20Events::Transfer {1962 from: *owner.as_eth(),1963 to: H160::default(),1964 value: value.into(),1965 })?;1966 Ok(())1967 }19681969 pub fn get_collection(1970 collection_id: CollectionId,1971 ) -> Result<CollectionHandle<T>, sp_runtime::DispatchError> {1972 Ok(<CollectionHandle<T>>::get(collection_id).ok_or(Error::<T>::CollectionNotFound)?)1973 }19741975 fn check_owner_permissions(1976 target_collection: &CollectionHandle<T>,1977 subject: &T::AccountId,1978 ) -> DispatchResult {1979 ensure!(1980 *subject == target_collection.owner,1981 Error::<T>::NoPermission1982 );19831984 Ok(())1985 }19861987 fn is_owner_or_admin_permissions(1988 collection: &CollectionHandle<T>,1989 subject: &T::CrossAccountId,1990 ) -> Result<bool, DispatchError> {1991 collection.consume_sload()?;1992 Ok(*subject.as_sub() == collection.owner1993 || <AdminList<T>>::get(collection.id).contains(subject))1994 }19951996 fn check_owner_or_admin_permissions(1997 collection: &CollectionHandle<T>,1998 subject: &T::CrossAccountId,1999 ) -> DispatchResult {2000 ensure!(2001 Self::is_owner_or_admin_permissions(collection, subject)?,2002 Error::<T>::NoPermission2003 );20042005 Ok(())2006 }20072008 fn owned_amount(2009 subject: &T::CrossAccountId,2010 collection: &CollectionHandle<T>,2011 item_id: TokenId,2012 ) -> Result<Option<u128>, DispatchError> {2013 collection.consume_sload()?;2014 Ok(Self::owned_amount_unchecked(subject, collection, item_id))2015 }20162017 fn owned_amount_unchecked(2018 subject: &T::CrossAccountId,2019 target_collection: &CollectionHandle<T>,2020 item_id: TokenId,2021 ) -> Option<u128> {2022 let collection_id = target_collection.id;20232024 match target_collection.mode {2025 CollectionMode::NFT => {2026 (<NftItemList<T>>::get(collection_id, item_id)?.owner == *subject).then(|| 1)2027 }2028 CollectionMode::Fungible(_) => {2029 Some(<FungibleItemList<T>>::get(collection_id, &subject.as_sub()).value)2030 }2031 CollectionMode::ReFungible => <ReFungibleItemList<T>>::get(collection_id, item_id)?2032 .owner2033 .iter()2034 .find(|i| i.owner == *subject)2035 .map(|i| i.fraction),2036 CollectionMode::Invalid => None,2037 }2038 }20392040 fn is_item_owner(2041 subject: &T::CrossAccountId,2042 target_collection: &CollectionHandle<T>,2043 item_id: TokenId,2044 ) -> Result<bool, DispatchError> {2045 Ok(match target_collection.mode {2046 CollectionMode::Fungible(_) => true,2047 _ => Self::owned_amount(subject, target_collection, item_id)?.is_some(),2048 })2049 }20502051 fn check_white_list(2052 collection: &CollectionHandle<T>,2053 address: &T::CrossAccountId,2054 ) -> DispatchResult {2055 collection.consume_sload()?;2056 ensure!(2057 <WhiteList<T>>::contains_key(collection.id, address.as_sub()),2058 Error::<T>::AddresNotInWhiteList,2059 );2060 Ok(())2061 }20622063 /// Check if token exists. In case of Fungible, check if there is an entry for2064 /// the owner in fungible balances double map2065 fn token_exists(target_collection: &CollectionHandle<T>, item_id: TokenId) -> DispatchResult {2066 let collection_id = target_collection.id;2067 let exists = match target_collection.mode {2068 CollectionMode::NFT => <NftItemList<T>>::contains_key(collection_id, item_id),2069 CollectionMode::Fungible(_) => true,2070 CollectionMode::ReFungible => {2071 <ReFungibleItemList<T>>::contains_key(collection_id, item_id)2072 }2073 _ => false,2074 };20752076 ensure!(exists, Error::<T>::TokenNotFound);2077 Ok(())2078 }20792080 fn transfer_fungible(2081 collection: &CollectionHandle<T>,2082 value: u128,2083 owner: &T::CrossAccountId,2084 recipient: &T::CrossAccountId,2085 ) -> DispatchResult {2086 let collection_id = collection.id;20872088 collection.consume_sload()?;2089 collection.consume_sload()?;2090 let mut recipient_balance = <FungibleItemList<T>>::get(collection_id, recipient.as_sub());2091 let mut balance = <FungibleItemList<T>>::get(collection_id, owner.as_sub());20922093 recipient_balance.value = recipient_balance2094 .value2095 .checked_add(value)2096 .ok_or(Error::<T>::NumOverflow)?;2097 balance.value = balance2098 .value2099 .checked_sub(value)2100 .ok_or(Error::<T>::TokenValueTooLow)?;21012102 // update balanceOf2103 collection.consume_sstore()?;2104 collection.consume_sstore()?;2105 if balance.value != 0 {2106 <Balance<T>>::insert(collection_id, owner.as_sub(), balance.value);2107 } else {2108 <Balance<T>>::remove(collection_id, owner.as_sub());2109 }2110 <Balance<T>>::insert(collection_id, recipient.as_sub(), recipient_balance.value);21112112 // Reduce or remove sender2113 collection.consume_sstore()?;2114 collection.consume_sstore()?;2115 if balance.value != 0 {2116 <FungibleItemList<T>>::insert(collection_id, owner.as_sub(), balance);2117 } else {2118 <FungibleItemList<T>>::remove(collection_id, owner.as_sub());2119 }2120 <FungibleItemList<T>>::insert(collection_id, recipient.as_sub(), recipient_balance);21212122 collection.log(ERC20Events::Transfer {2123 from: *owner.as_eth(),2124 to: *recipient.as_eth(),2125 value: value.into(),2126 })?;2127 Self::deposit_event(RawEvent::Transfer(2128 collection.id,2129 1,2130 owner.clone(),2131 recipient.clone(),2132 value,2133 ));21342135 Ok(())2136 }21372138 fn transfer_refungible(2139 collection: &CollectionHandle<T>,2140 item_id: TokenId,2141 value: u128,2142 owner: T::CrossAccountId,2143 new_owner: T::CrossAccountId,2144 ) -> DispatchResult {2145 let collection_id = collection.id;2146 collection.consume_sload()?;2147 let full_item = <ReFungibleItemList<T>>::get(collection_id, item_id)2148 .ok_or(Error::<T>::TokenNotFound)?;21492150 let item = full_item2151 .owner2152 .iter()2153 .find(|i| i.owner == owner)2154 .ok_or(Error::<T>::TokenNotFound)?;2155 let amount = item.fraction;21562157 ensure!(amount >= value, Error::<T>::TokenValueTooLow);21582159 collection.consume_sload()?;2160 // update balance2161 let balance_old_owner = <Balance<T>>::get(collection_id, item.owner.as_sub())2162 .checked_sub(value)2163 .ok_or(Error::<T>::NumOverflow)?;2164 collection.consume_sstore()?;2165 <Balance<T>>::insert(collection_id, item.owner.as_sub(), balance_old_owner);21662167 collection.consume_sload()?;2168 let balance_new_owner = <Balance<T>>::get(collection_id, new_owner.as_sub())2169 .checked_add(value)2170 .ok_or(Error::<T>::NumOverflow)?;2171 collection.consume_sstore()?;2172 <Balance<T>>::insert(collection_id, new_owner.as_sub(), balance_new_owner);21732174 let old_owner = item.owner.clone();2175 let new_owner_has_account = full_item.owner.iter().any(|i| i.owner == new_owner);21762177 let mut new_full_item = full_item.clone();2178 // transfer2179 if amount == value && !new_owner_has_account {2180 // change owner2181 // new owner do not have account2182 new_full_item2183 .owner2184 .iter_mut()2185 .find(|i| i.owner == owner)2186 .expect("old owner does present in refungible")2187 .owner = new_owner.clone();2188 collection.consume_sstore()?;2189 <ReFungibleItemList<T>>::insert(collection_id, item_id, new_full_item);21902191 // update index collection2192 Self::move_token_index(collection, item_id, &old_owner, &new_owner)?;2193 } else {2194 new_full_item2195 .owner2196 .iter_mut()2197 .find(|i| i.owner == owner)2198 .expect("old owner does present in refungible")2199 .fraction -= value;22002201 // separate amount2202 if new_owner_has_account {2203 // new owner has account2204 new_full_item2205 .owner2206 .iter_mut()2207 .find(|i| i.owner == new_owner)2208 .expect("new owner has account")2209 .fraction += value;2210 } else {2211 // new owner do not have account2212 new_full_item.owner.push(Ownership {2213 owner: new_owner.clone(),2214 fraction: value,2215 });2216 Self::add_token_index(collection, item_id, &new_owner)?;2217 }22182219 collection.consume_sstore()?;2220 <ReFungibleItemList<T>>::insert(collection_id, item_id, new_full_item);2221 }22222223 Self::deposit_event(RawEvent::Transfer(2224 collection.id,2225 item_id,2226 owner,2227 new_owner,2228 amount,2229 ));22302231 Ok(())2232 }22332234 fn transfer_nft(2235 collection: &CollectionHandle<T>,2236 item_id: TokenId,2237 sender: T::CrossAccountId,2238 new_owner: T::CrossAccountId,2239 ) -> DispatchResult {2240 let collection_id = collection.id;2241 collection.consume_sload()?;2242 let mut item =2243 <NftItemList<T>>::get(collection_id, item_id).ok_or(Error::<T>::TokenNotFound)?;22442245 ensure!(sender == item.owner, Error::<T>::MustBeTokenOwner);22462247 collection.consume_sload()?;2248 // update balance2249 let balance_old_owner = <Balance<T>>::get(collection_id, item.owner.as_sub())2250 .checked_sub(1)2251 .ok_or(Error::<T>::NumOverflow)?;2252 collection.consume_sstore()?;2253 <Balance<T>>::insert(collection_id, item.owner.as_sub(), balance_old_owner);22542255 collection.consume_sload()?;2256 let balance_new_owner = <Balance<T>>::get(collection_id, new_owner.as_sub())2257 .checked_add(1)2258 .ok_or(Error::<T>::NumOverflow)?;2259 collection.consume_sstore()?;2260 <Balance<T>>::insert(collection_id, new_owner.as_sub(), balance_new_owner);22612262 // change owner2263 let old_owner = item.owner.clone();2264 item.owner = new_owner.clone();2265 collection.consume_sstore()?;2266 <NftItemList<T>>::insert(collection_id, item_id, item);22672268 // update index collection2269 Self::move_token_index(collection, item_id, &old_owner, &new_owner)?;22702271 collection.log(ERC721Events::Transfer {2272 from: *sender.as_eth(),2273 to: *new_owner.as_eth(),2274 token_id: item_id.into(),2275 })?;2276 Self::deposit_event(RawEvent::Transfer(2277 collection.id,2278 item_id,2279 sender,2280 new_owner,2281 1,2282 ));22832284 Ok(())2285 }22862287 fn set_re_fungible_variable_data(2288 collection: &CollectionHandle<T>,2289 item_id: TokenId,2290 data: Vec<u8>,2291 ) -> DispatchResult {2292 let collection_id = collection.id;2293 let mut item = <ReFungibleItemList<T>>::get(collection_id, item_id)2294 .ok_or(Error::<T>::TokenNotFound)?;22952296 item.variable_data = data;22972298 <ReFungibleItemList<T>>::insert(collection_id, item_id, item);22992300 Ok(())2301 }23022303 fn set_nft_variable_data(2304 collection: &CollectionHandle<T>,2305 item_id: TokenId,2306 data: Vec<u8>,2307 ) -> DispatchResult {2308 let collection_id = collection.id;2309 let mut item =2310 <NftItemList<T>>::get(collection_id, item_id).ok_or(Error::<T>::TokenNotFound)?;23112312 item.variable_data = data;23132314 <NftItemList<T>>::insert(collection_id, item_id, item);23152316 Ok(())2317 }23182319 #[allow(dead_code)]2320 fn init_collection(item: &Collection<T>) {2321 // check params2322 assert!(2323 item.decimal_points <= MAX_DECIMAL_POINTS,2324 "decimal_points parameter must be lower than MAX_DECIMAL_POINTS"2325 );2326 assert!(2327 item.name.len() <= 64,2328 "Collection name can not be longer than 63 char"2329 );2330 assert!(2331 item.name.len() <= 256,2332 "Collection description can not be longer than 255 char"2333 );2334 assert!(2335 item.token_prefix.len() <= 16,2336 "Token prefix can not be longer than 15 char"2337 );23382339 // Generate next collection ID2340 let next_id = CreatedCollectionCount::get().checked_add(1).unwrap();23412342 CreatedCollectionCount::put(next_id);2343 }23442345 #[allow(dead_code)]2346 fn init_nft_token(collection_id: CollectionId, item: &NftItemType<T::CrossAccountId>) {2347 let current_index = <ItemListIndex>::get(collection_id).checked_add(1).unwrap();23482349 Self::add_token_index(2350 &CollectionHandle::get(collection_id).unwrap(),2351 current_index,2352 &item.owner,2353 )2354 .unwrap();23552356 <ItemListIndex>::insert(collection_id, current_index);23572358 // Update balance2359 let new_balance = <Balance<T>>::get(collection_id, item.owner.as_sub())2360 .checked_add(1)2361 .unwrap();2362 <Balance<T>>::insert(collection_id, item.owner.as_sub(), new_balance);2363 }23642365 #[allow(dead_code)]2366 fn init_fungible_token(2367 collection_id: CollectionId,2368 owner: &T::CrossAccountId,2369 item: &FungibleItemType,2370 ) {2371 let current_index = <ItemListIndex>::get(collection_id).checked_add(1).unwrap();23722373 Self::add_token_index(2374 &CollectionHandle::get(collection_id).unwrap(),2375 current_index,2376 owner,2377 )2378 .unwrap();23792380 <ItemListIndex>::insert(collection_id, current_index);23812382 // Update balance2383 let new_balance = <Balance<T>>::get(collection_id, owner.as_sub())2384 .checked_add(item.value)2385 .unwrap();2386 <Balance<T>>::insert(collection_id, owner.as_sub(), new_balance);2387 }23882389 #[allow(dead_code)]2390 fn init_refungible_token(2391 collection_id: CollectionId,2392 item: &ReFungibleItemType<T::CrossAccountId>,2393 ) {2394 let current_index = <ItemListIndex>::get(collection_id).checked_add(1).unwrap();23952396 let value = item.owner.first().unwrap().fraction;2397 let owner = item.owner.first().unwrap().owner.clone();23982399 Self::add_token_index(2400 &CollectionHandle::get(collection_id).unwrap(),2401 current_index,2402 &owner,2403 )2404 .unwrap();24052406 <ItemListIndex>::insert(collection_id, current_index);24072408 // Update balance2409 let new_balance = <Balance<T>>::get(collection_id, &owner.as_sub())2410 .checked_add(value)2411 .unwrap();2412 <Balance<T>>::insert(collection_id, owner.as_sub(), new_balance);2413 }24142415 fn add_token_index(2416 collection: &CollectionHandle<T>,2417 item_index: TokenId,2418 owner: &T::CrossAccountId,2419 ) -> DispatchResult {2420 // add to account limit2421 collection.consume_sload()?;2422 if <AccountItemCount<T>>::contains_key(owner.as_sub()) {2423 // bound Owned tokens by a single address2424 collection.consume_sload()?;2425 let count = <AccountItemCount<T>>::get(owner.as_sub());2426 ensure!(2427 count < ACCOUNT_TOKEN_OWNERSHIP_LIMIT,2428 Error::<T>::AddressOwnershipLimitExceeded2429 );24302431 collection.consume_sstore()?;2432 <AccountItemCount<T>>::insert(2433 owner.as_sub(),2434 count.checked_add(1).ok_or(Error::<T>::NumOverflow)?,2435 );2436 } else {2437 collection.consume_sstore()?;2438 <AccountItemCount<T>>::insert(owner.as_sub(), 1);2439 }24402441 collection.consume_sload()?;2442 let list_exists = <AddressTokens<T>>::contains_key(collection.id, owner.as_sub());2443 if list_exists {2444 collection.consume_sload()?;2445 let mut list = <AddressTokens<T>>::get(collection.id, owner.as_sub());2446 let item_contains = list.contains(&item_index.clone());24472448 if !item_contains {2449 list.push(item_index);2450 }24512452 collection.consume_sstore()?;2453 <AddressTokens<T>>::insert(collection.id, owner.as_sub(), list);2454 } else {2455 let itm = vec![item_index];2456 collection.consume_sstore()?;2457 <AddressTokens<T>>::insert(collection.id, owner.as_sub(), itm);2458 }24592460 Ok(())2461 }24622463 fn remove_token_index(2464 collection: &CollectionHandle<T>,2465 item_index: TokenId,2466 owner: &T::CrossAccountId,2467 ) -> DispatchResult {2468 // update counter2469 collection.consume_sload()?;2470 collection.consume_sstore()?;2471 <AccountItemCount<T>>::insert(2472 owner.as_sub(),2473 <AccountItemCount<T>>::get(owner.as_sub())2474 .checked_sub(1)2475 .ok_or(Error::<T>::NumOverflow)?,2476 );24772478 collection.consume_sload()?;2479 let list_exists = <AddressTokens<T>>::contains_key(collection.id, owner.as_sub());2480 if list_exists {2481 collection.consume_sload()?;2482 let mut list = <AddressTokens<T>>::get(collection.id, owner.as_sub());2483 let item_contains = list.contains(&item_index.clone());24842485 if item_contains {2486 list.retain(|&item| item != item_index);2487 collection.consume_sstore()?;2488 <AddressTokens<T>>::insert(collection.id, owner.as_sub(), list);2489 }2490 }24912492 Ok(())2493 }24942495 fn move_token_index(2496 collection: &CollectionHandle<T>,2497 item_index: TokenId,2498 old_owner: &T::CrossAccountId,2499 new_owner: &T::CrossAccountId,2500 ) -> DispatchResult {2501 Self::remove_token_index(collection, item_index, old_owner)?;2502 Self::add_token_index(collection, item_index, new_owner)?;25032504 Ok(())2505 }2506}25072508sp_api::decl_runtime_apis! {2509 pub trait NftApi {2510 /// Used for ethereum integration2511 fn eth_contract_code(account: H160) -> Option<Vec<u8>>;2512 }2513}tests/src/eth/api/ContractHelpers.soldiffbeforeafterboth--- a/tests/src/eth/api/ContractHelpers.sol
+++ b/tests/src/eth/api/ContractHelpers.sol
@@ -9,33 +9,41 @@
}
interface ContractHelpers is Dummy {
+ // Selector: contractOwner(address) 5152b14c
function contractOwner(address contractAddress)
external
view
returns (address);
+ // Selector: sponsoringEnabled(address) 6027dc61
function sponsoringEnabled(address contractAddress)
external
view
returns (bool);
+ // Selector: toggleSponsoring(address,bool) fcac6d86
function toggleSponsoring(address contractAddress, bool enabled) external;
+ // Selector: setSponsoringRateLimit(address,uint32) 77b6c908
function setSponsoringRateLimit(address contractAddress, uint32 rateLimit)
external;
+ // Selector: allowed(address,address) 5c658165
function allowed(address contractAddress, address user)
external
view
returns (bool);
+ // Selector: allowlistEnabled(address) c772ef6c
function allowlistEnabled(address contractAddress)
external
view
returns (bool);
+ // Selector: toggleAllowlist(address,bool) 36de20f5
function toggleAllowlist(address contractAddress, bool enabled) external;
+ // Selector: toggleAllowed(address,address,bool) 4706cc1c
function toggleAllowed(
address contractAddress,
address user,
tests/src/eth/api/UniqueFungible.soldiffbeforeafterboth--- a/tests/src/eth/api/UniqueFungible.sol
+++ b/tests/src/eth/api/UniqueFungible.sol
@@ -20,35 +20,45 @@
// Inline
interface InlineNameSymbol is Dummy {
+ // Selector: name() 06fdde03
function name() external view returns (string memory);
+ // Selector: symbol() 95d89b41
function symbol() external view returns (string memory);
}
// Inline
interface InlineTotalSupply is Dummy {
+ // Selector: totalSupply() 18160ddd
function totalSupply() external view returns (uint256);
}
interface ERC165 is Dummy {
+ // Selector: supportsInterface(bytes4) 01ffc9a7
function supportsInterface(uint32 interfaceId) external view returns (bool);
}
interface ERC20 is Dummy, InlineNameSymbol, InlineTotalSupply, ERC20Events {
+ // Selector: decimals() 313ce567
function decimals() external view returns (uint8);
+ // Selector: balanceOf(address) 70a08231
function balanceOf(address owner) external view returns (uint256);
+ // Selector: transfer(address,uint256) a9059cbb
function transfer(address to, uint256 amount) external returns (bool);
+ // Selector: transferFrom(address,address,uint256) 23b872dd
function transferFrom(
address from,
address to,
uint256 amount
) external returns (bool);
+ // Selector: approve(address,uint256) 095ea7b3
function approve(address spender, uint256 amount) external returns (bool);
+ // Selector: allowance(address,address) dd62ed3e
function allowance(address owner, address spender)
external
view
tests/src/eth/api/UniqueNFT.soldiffbeforeafterboth--- a/tests/src/eth/api/UniqueNFT.sol
+++ b/tests/src/eth/api/UniqueNFT.sol
@@ -3,6 +3,12 @@
pragma solidity >=0.8.0 <0.9.0;
+// Anonymous struct
+struct Tuple0 {
+ uint256 field_0;
+ string field_1;
+}
+
// Common stubs holder
interface Dummy {
@@ -34,25 +40,32 @@
// Inline
interface InlineNameSymbol is Dummy {
+ // Selector: name() 06fdde03
function name() external view returns (string memory);
+ // Selector: symbol() 95d89b41
function symbol() external view returns (string memory);
}
// Inline
interface InlineTotalSupply is Dummy {
+ // Selector: totalSupply() 18160ddd
function totalSupply() external view returns (uint256);
}
interface ERC165 is Dummy {
+ // Selector: supportsInterface(bytes4) 01ffc9a7
function supportsInterface(uint32 interfaceId) external view returns (bool);
}
interface ERC721 is Dummy, ERC165, ERC721Events {
+ // Selector: balanceOf(address) 70a08231
function balanceOf(address owner) external view returns (uint256);
+ // Selector: ownerOf(uint256) 6352211e
function ownerOf(uint256 tokenId) external view returns (address);
+ // Selector: safeTransferFromWithData(address,address,uint256,bytes) 60a11672
function safeTransferFromWithData(
address from,
address to,
@@ -60,24 +73,30 @@
bytes memory data
) external;
+ // Selector: safeTransferFrom(address,address,uint256) 42842e0e
function safeTransferFrom(
address from,
address to,
uint256 tokenId
) external;
+ // Selector: transferFrom(address,address,uint256) 23b872dd
function transferFrom(
address from,
address to,
uint256 tokenId
) external;
+ // Selector: approve(address,uint256) 095ea7b3
function approve(address approved, uint256 tokenId) external;
+ // Selector: setApprovalForAll(address,bool) a22cb465
function setApprovalForAll(address operator, bool approved) external;
+ // Selector: getApproved(uint256) 081812fc
function getApproved(uint256 tokenId) external view returns (address);
+ // Selector: isApprovedForAll(address,address) e985e9c5
function isApprovedForAll(address owner, address operator)
external
view
@@ -85,12 +104,15 @@
}
interface ERC721Burnable is Dummy {
+ // Selector: burn(uint256) 42966c68
function burn(uint256 tokenId) external;
}
interface ERC721Enumerable is Dummy, InlineTotalSupply {
+ // Selector: tokenByIndex(uint256) 4f6ccce7
function tokenByIndex(uint256 index) external view returns (uint256);
+ // Selector: tokenOfOwnerByIndex(address,uint256) 2f745c59
function tokenOfOwnerByIndex(address owner, uint256 index)
external
view
@@ -98,27 +120,53 @@
}
interface ERC721Metadata is Dummy, InlineNameSymbol {
+ // Selector: tokenURI(uint256) c87b56dd
function tokenURI(uint256 tokenId) external view returns (string memory);
}
interface ERC721Mintable is Dummy, ERC721MintableEvents {
+ // Selector: mintingFinished() 05d2035b
function mintingFinished() external view returns (bool);
+ // Selector: mint(address,uint256) 40c10f19
function mint(address to, uint256 tokenId) external returns (bool);
+ // Selector: mintWithTokenURI(address,uint256,string) 50bb4e7f
function mintWithTokenURI(
address to,
uint256 tokenId,
string memory tokenUri
) external returns (bool);
+ // Selector: finishMinting() 7d64bcb4
function finishMinting() external returns (bool);
}
interface ERC721UniqueExtensions is Dummy {
+ // Selector: transfer(address,uint256) a9059cbb
function transfer(address to, uint256 tokenId) external;
+ // Selector: nextTokenId() 75794a3c
function nextTokenId() external view returns (uint256);
+
+ // Selector: setVariableMetadata(uint256,bytes) d4eac26d
+ function setVariableMetadata(uint256 tokenId, bytes memory data) external;
+
+ // Selector: getVariableMetadata(uint256) e6c5ce6f
+ function getVariableMetadata(uint256 tokenId)
+ external
+ view
+ returns (bytes memory);
+
+ // Selector: mintBulk(address,uint256[]) 44a9945e
+ function mintBulk(address to, uint256[] memory tokenIds)
+ external
+ returns (bool);
+
+ // Selector: mintBulkWithTokenURI(address,(uint256,string)[]) 36543006
+ function mintBulkWithTokenURI(address to, Tuple0[] memory tokens)
+ external
+ returns (bool);
}
interface UniqueNFT is
tests/src/eth/nonFungible.test.tsdiffbeforeafterboth--- a/tests/src/eth/nonFungible.test.ts
+++ b/tests/src/eth/nonFungible.test.ts
@@ -4,7 +4,7 @@
//
import privateKey from '../substrate/privateKey';
-import { approveExpectSuccess, burnItemExpectSuccess, createCollectionExpectSuccess, createItemExpectSuccess, transferExpectSuccess, transferFromExpectSuccess, UNIQUE } from '../util/helpers';
+import { approveExpectSuccess, burnItemExpectSuccess, createCollectionExpectSuccess, createItemExpectSuccess, setVariableMetaDataExpectSuccess, transferExpectSuccess, transferFromExpectSuccess, UNIQUE } from '../util/helpers';
import { collectionIdToAddress, createEthAccount, createEthAccountWithBalance, GAS_ARGS, itWeb3, normalizeEvents, recordEthFee, recordEvents, subToEth, transferBalanceToEth } from './util/helpers';
import nonFungibleAbi from './nonFungibleAbi.json';
import { expect } from 'chai';
@@ -105,6 +105,69 @@
expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI');
}
});
+ itWeb3('Can perform mintBulk()', async ({ web3, api }) => {
+ const collection = await createCollectionExpectSuccess({
+ mode: { type: 'NFT' },
+ });
+ const alice = privateKey('//Alice');
+
+ const caller = await createEthAccountWithBalance(api, web3);
+ const changeAdminTx = api.tx.nft.addCollectionAdmin(collection, { ethereum: caller });
+ await submitTransactionAsync(alice, changeAdminTx);
+ const receiver = createEthAccount(web3);
+
+ const address = collectionIdToAddress(collection);
+ const contract = new web3.eth.Contract(nonFungibleAbi as any, address, {from: caller, ...GAS_ARGS});
+
+ {
+ const nextTokenId = await contract.methods.nextTokenId().call();
+ expect(nextTokenId).to.be.equal('1');
+ const result = await contract.methods.mintBulkWithTokenURI(
+ receiver,
+ [
+ [nextTokenId, 'Test URI 0'],
+ [+nextTokenId + 1, 'Test URI 1'],
+ [+nextTokenId + 2, 'Test URI 2'],
+ ],
+ ).send({ from: caller });
+ const events = normalizeEvents(result.events);
+
+ expect(events).to.be.deep.equal([
+ {
+ address,
+ event: 'Transfer',
+ args: {
+ from: '0x0000000000000000000000000000000000000000',
+ to: receiver,
+ tokenId: nextTokenId,
+ },
+ },
+ {
+ address,
+ event: 'Transfer',
+ args: {
+ from: '0x0000000000000000000000000000000000000000',
+ to: receiver,
+ tokenId: String(+nextTokenId + 1),
+ },
+ },
+ {
+ address,
+ event: 'Transfer',
+ args: {
+ from: '0x0000000000000000000000000000000000000000',
+ to: receiver,
+ tokenId: String(+nextTokenId + 2),
+ },
+ },
+ ]);
+
+ await waitNewBlocks(api, 1);
+ expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI 0');
+ expect(await contract.methods.tokenURI(+nextTokenId + 1).call()).to.be.equal('Test URI 1');
+ expect(await contract.methods.tokenURI(+nextTokenId + 2).call()).to.be.equal('Test URI 2');
+ }
+ });
itWeb3('Can perform burn()', async ({ web3, api }) => {
const collection = await createCollectionExpectSuccess({
@@ -263,6 +326,41 @@
expect(+balance).to.equal(1);
}
});
+
+ itWeb3('Can perform getVariableMetadata', async ({ web3, api }) => {
+ const collection = await createCollectionExpectSuccess({
+ mode: { type: 'NFT' },
+ });
+ const alice = privateKey('//Alice');
+
+ const owner = await createEthAccountWithBalance(api, web3);
+
+ const item = await createItemExpectSuccess(alice, collection, 'NFT', { ethereum: owner });
+ await setVariableMetaDataExpectSuccess(alice, collection, item, [1, 2, 3]);
+
+ const address = collectionIdToAddress(collection);
+ const contract = new web3.eth.Contract(nonFungibleAbi as any, address, { from: owner, ...GAS_ARGS });
+
+ expect(await contract.methods.getVariableMetadata(item).call()).to.be.equal('0x010203');
+ });
+
+ itWeb3('Can perform setVariableMetadata', async ({ web3, api }) => {
+ const collection = await createCollectionExpectSuccess({
+ mode: { type: 'NFT' },
+ });
+ const alice = privateKey('//Alice');
+
+ const owner = await createEthAccountWithBalance(api, web3);
+
+ const item = await createItemExpectSuccess(alice, collection, 'NFT', { ethereum: owner });
+
+ const address = collectionIdToAddress(collection);
+ const contract = new web3.eth.Contract(nonFungibleAbi as any, address, { from: owner, ...GAS_ARGS });
+
+ expect(await contract.methods.setVariableMetadata(item, '0x010203').send({ from: owner }));
+ await waitNewBlocks(api, 1);
+ expect(await contract.methods.getVariableMetadata(item).call()).to.be.equal('0x010203');
+ });
});
describe('NFT: Fees', () => {
tests/src/eth/nonFungibleAbi.jsondiffbeforeafterboth--- a/tests/src/eth/nonFungibleAbi.json
+++ b/tests/src/eth/nonFungibleAbi.json
@@ -50,7 +50,7 @@
"type": "event"
},
{
- "anonymous": true,
+ "anonymous": false,
"inputs": [],
"name": "MintingFinished",
"type": "event"
@@ -165,6 +165,25 @@
{
"inputs": [
{
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "getVariableMetadata",
+ "outputs": [
+ {
+ "internalType": "bytes",
+ "name": "",
+ "type": "bytes"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
"internalType": "address",
"name": "owner",
"type": "address"
@@ -218,13 +237,73 @@
"type": "address"
},
{
+ "internalType": "uint256[]",
+ "name": "tokenIds",
+ "type": "uint256[]"
+ }
+ ],
+ "name": "mintBulk",
+ "outputs": [
+ {
+ "internalType": "bool",
+ "name": "",
+ "type": "bool"
+ }
+ ],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address"
+ },
+ {
+ "components": [
+ {
+ "internalType": "uint256",
+ "name": "field_0",
+ "type": "uint256"
+ },
+ {
+ "internalType": "string",
+ "name": "field_1",
+ "type": "string"
+ }
+ ],
+ "internalType": "struct Tuple0[]",
+ "name": "tokens",
+ "type": "tuple[]"
+ }
+ ],
+ "name": "mintBulkWithTokenURI",
+ "outputs": [
+ {
+ "internalType": "bool",
+ "name": "",
+ "type": "bool"
+ }
+ ],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address"
+ },
+ {
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "string",
- "name": "tokenURI",
+ "name": "tokenUri",
"type": "string"
}
],
@@ -369,6 +448,24 @@
{
"inputs": [
{
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ },
+ {
+ "internalType": "bytes",
+ "name": "data",
+ "type": "bytes"
+ }
+ ],
+ "name": "setVariableMetadata",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
"internalType": "uint32",
"name": "interfaceId",
"type": "uint32"
@@ -514,4 +611,4 @@
"stateMutability": "nonpayable",
"type": "function"
}
-]
\ No newline at end of file
+]
tests/src/eth/proxy/UniqueNFTProxy.abidiffbeforeafterboth--- a/tests/src/eth/proxy/UniqueNFTProxy.abi
+++ b/tests/src/eth/proxy/UniqueNFTProxy.abi
@@ -1 +1 @@
-[{"inputs":[{"internalType":"address","name":"_proxied","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"approved","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"operator","type":"address"},{"indexed":false,"internalType":"bool","name":"approved","type":"bool"}],"name":"ApprovalForAll","type":"event"},{"anonymous":false,"inputs":[],"name":"MintingFinished","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"approved","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"approve","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"burn","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"finishMinting","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"getApproved","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"operator","type":"address"}],"name":"isApprovedForAll","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"mint","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"string","name":"tokenUri","type":"string"}],"name":"mintWithTokenURI","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"mintingFinished","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"nextTokenId","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"ownerOf","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"safeTransferFromWithData","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"operator","type":"address"},{"internalType":"bool","name":"approved","type":"bool"}],"name":"setApprovalForAll","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint32","name":"interfaceId","type":"uint32"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"index","type":"uint256"}],"name":"tokenByIndex","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint256","name":"index","type":"uint256"}],"name":"tokenOfOwnerByIndex","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"tokenURI","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"transfer","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"transferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"}]
\ No newline at end of file
+[{"inputs":[{"internalType":"address","name":"_proxied","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"approved","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"operator","type":"address"},{"indexed":false,"internalType":"bool","name":"approved","type":"bool"}],"name":"ApprovalForAll","type":"event"},{"anonymous":false,"inputs":[],"name":"MintingFinished","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"approved","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"approve","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"burn","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"finishMinting","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"getApproved","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"getVariableMetadata","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"operator","type":"address"}],"name":"isApprovedForAll","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"mint","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256[]","name":"tokenIds","type":"uint256[]"}],"name":"mintBulk","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"components":[{"internalType":"uint256","name":"field_0","type":"uint256"},{"internalType":"string","name":"field_1","type":"string"}],"internalType":"struct Tuple0[]","name":"tokens","type":"tuple[]"}],"name":"mintBulkWithTokenURI","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"string","name":"tokenUri","type":"string"}],"name":"mintWithTokenURI","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"mintingFinished","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"nextTokenId","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"ownerOf","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"safeTransferFromWithData","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"operator","type":"address"},{"internalType":"bool","name":"approved","type":"bool"}],"name":"setApprovalForAll","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"setVariableMetadata","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint32","name":"interfaceId","type":"uint32"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"index","type":"uint256"}],"name":"tokenByIndex","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint256","name":"index","type":"uint256"}],"name":"tokenOfOwnerByIndex","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"tokenURI","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"transfer","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"transferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"}]
\ No newline at end of file
tests/src/eth/proxy/UniqueNFTProxy.bindiffbeforeafterboth--- a/tests/src/eth/proxy/UniqueNFTProxy.bin
+++ b/tests/src/eth/proxy/UniqueNFTProxy.bin
@@ -1 +1 @@
-608060405234801561001057600080fd5b506040516111e03803806111e083398101604081905261002f91610054565b600080546001600160a01b0319166001600160a01b0392909216919091179055610084565b60006020828403121561006657600080fd5b81516001600160a01b038116811461007d57600080fd5b9392505050565b61114d806100936000396000f3fe608060405234801561001057600080fd5b506004361061014d5760003560e01c806350bb4e7f116100c357806395d89b411161007c57806395d89b41146102a8578063a22cb465146102b0578063a9059cbb146102c3578063c87b56dd146102d6578063e985e9c5146102e9578063f4f4b500146102fc57600080fd5b806350bb4e7f1461024c57806360a116721461025f5780636352211e1461027257806370a082311461028557806375794a3c146102985780637d64bcb4146102a057600080fd5b806323b872dd1161011557806323b872dd146101da5780632f745c59146101ed57806340c10f191461020057806342842e0e1461021357806342966c68146102265780634f6ccce71461023957600080fd5b806305d2035b1461015257806306fdde031461016f578063081812fc14610184578063095ea7b3146101af57806318160ddd146101c4575b600080fd5b61015a61030f565b60405190151581526020015b60405180910390f35b61017761039b565b6040516101669190611043565b610197610192366004610f5b565b61041b565b6040516001600160a01b039091168152602001610166565b6101c26101bd366004610e2e565b61049f565b005b6101cc61050a565b604051908152602001610166565b6101c26101e8366004610d3f565b610591565b6101cc6101fb366004610e2e565b610605565b61015a61020e366004610e2e565b610691565b6101c2610221366004610d3f565b610718565b6101c2610234366004610f5b565b610759565b6101cc610247366004610f5b565b6107ba565b61015a61025a366004610e5a565b610838565b6101c261026d366004610d80565b6108c7565b610197610280366004610f5b565b610936565b6101cc610293366004610ccc565b610968565b6101cc61099b565b61015a6109ea565b610177610a4f565b6101c26102be366004610e00565b610a93565b6101c26102d1366004610e2e565b610acd565b6101776102e4366004610f5b565b610b06565b6101976102f7366004610d06565b610b87565b61015a61030a366004610f8d565b610c0d565b60008060009054906101000a90046001600160a01b03166001600160a01b03166305d2035b6040518163ffffffff1660e01b815260040160206040518083038186803b15801561035e57600080fd5b505afa158015610372573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103969190610ec7565b905090565b60008054604080516306fdde0360e01b815290516060936001600160a01b03909316926306fdde039260048082019391829003018186803b1580156103df57600080fd5b505afa1580156103f3573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526103969190810190610ee4565b6000805460405163020604bf60e21b8152600481018490526001600160a01b039091169063081812fc906024015b60206040518083038186803b15801561046157600080fd5b505afa158015610475573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906104999190610ce9565b92915050565b60005460405163095ea7b360e01b81526001600160a01b038481166004830152602482018490529091169063095ea7b3906044015b600060405180830381600087803b1580156104ee57600080fd5b505af1158015610502573d6000803e3d6000fd5b505050505050565b60008060009054906101000a90046001600160a01b03166001600160a01b03166318160ddd6040518163ffffffff1660e01b815260040160206040518083038186803b15801561055957600080fd5b505afa15801561056d573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103969190610f74565b6000546040516323b872dd60e01b81526001600160a01b038581166004830152848116602483015260448201849052909116906323b872dd906064015b600060405180830381600087803b1580156105e857600080fd5b505af11580156105fc573d6000803e3d6000fd5b50505050505050565b60008054604051632f745c5960e01b81526001600160a01b0385811660048301526024820185905290911690632f745c599060440160206040518083038186803b15801561065257600080fd5b505afa158015610666573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061068a9190610f74565b9392505050565b600080546040516340c10f1960e01b81526001600160a01b03858116600483015260248201859052909116906340c10f1990604401602060405180830381600087803b1580156106e057600080fd5b505af11580156106f4573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061068a9190610ec7565b600054604051632142170760e11b81526001600160a01b038581166004830152848116602483015260448201849052909116906342842e0e906064016105ce565b600054604051630852cd8d60e31b8152600481018390526001600160a01b03909116906342966c6890602401600060405180830381600087803b15801561079f57600080fd5b505af11580156107b3573d6000803e3d6000fd5b5050505050565b60008054604051634f6ccce760e01b8152600481018490526001600160a01b0390911690634f6ccce7906024015b60206040518083038186803b15801561080057600080fd5b505afa158015610814573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906104999190610f74565b600080546040516350bb4e7f60e01b81526001600160a01b03909116906350bb4e7f9061086d9087908790879060040161101c565b602060405180830381600087803b15801561088757600080fd5b505af115801561089b573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906108bf9190610ec7565b949350505050565b6000546040516330508b3960e11b81526001600160a01b03909116906360a11672906108fd908790879087908790600401610fdf565b600060405180830381600087803b15801561091757600080fd5b505af115801561092b573d6000803e3d6000fd5b505050505b50505050565b600080546040516331a9108f60e11b8152600481018490526001600160a01b0390911690636352211e90602401610449565b600080546040516370a0823160e01b81526001600160a01b038481166004830152909116906370a08231906024016107e8565b60008060009054906101000a90046001600160a01b03166001600160a01b03166375794a3c6040518163ffffffff1660e01b815260040160206040518083038186803b15801561055957600080fd5b60008060009054906101000a90046001600160a01b03166001600160a01b0316637d64bcb46040518163ffffffff1660e01b8152600401602060405180830381600087803b158015610a3b57600080fd5b505af1158015610372573d6000803e3d6000fd5b60008054604080516395d89b4160e01b815290516060936001600160a01b03909316926395d89b419260048082019391829003018186803b1580156103df57600080fd5b60005460405163a22cb46560e01b81526001600160a01b03848116600483015283151560248301529091169063a22cb465906044016104d4565b60005460405163a9059cbb60e01b81526001600160a01b038481166004830152602482018490529091169063a9059cbb906044016104d4565b60005460405163c87b56dd60e01b8152600481018390526060916001600160a01b03169063c87b56dd9060240160006040518083038186803b158015610b4b57600080fd5b505afa158015610b5f573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526104999190810190610ee4565b6000805460405163e985e9c560e01b81526001600160a01b03858116600483015284811660248301529091169063e985e9c59060440160206040518083038186803b158015610bd557600080fd5b505afa158015610be9573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061068a9190610ce9565b6000805460405162f4f4b560e81b815263ffffffff841660048201526001600160a01b039091169063f4f4b5009060240160206040518083038186803b158015610c5657600080fd5b505afa158015610c6a573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906104999190610ec7565b6000610ca1610c9c84611087565b611056565b9050828152838383011115610cb557600080fd5b828260208301376000602084830101529392505050565b600060208284031215610cde57600080fd5b813561068a816110f1565b600060208284031215610cfb57600080fd5b815161068a816110f1565b60008060408385031215610d1957600080fd5b8235610d24816110f1565b91506020830135610d34816110f1565b809150509250929050565b600080600060608486031215610d5457600080fd5b8335610d5f816110f1565b92506020840135610d6f816110f1565b929592945050506040919091013590565b60008060008060808587031215610d9657600080fd5b8435610da1816110f1565b93506020850135610db1816110f1565b925060408501359150606085013567ffffffffffffffff811115610dd457600080fd5b8501601f81018713610de557600080fd5b610df487823560208401610c8e565b91505092959194509250565b60008060408385031215610e1357600080fd5b8235610e1e816110f1565b91506020830135610d3481611109565b60008060408385031215610e4157600080fd5b8235610e4c816110f1565b946020939093013593505050565b600080600060608486031215610e6f57600080fd5b8335610e7a816110f1565b925060208401359150604084013567ffffffffffffffff811115610e9d57600080fd5b8401601f81018613610eae57600080fd5b610ebd86823560208401610c8e565b9150509250925092565b600060208284031215610ed957600080fd5b815161068a81611109565b600060208284031215610ef657600080fd5b815167ffffffffffffffff811115610f0d57600080fd5b8201601f81018413610f1e57600080fd5b8051610f2c610c9c82611087565b818152856020838501011115610f4157600080fd5b610f528260208301602086016110af565b95945050505050565b600060208284031215610f6d57600080fd5b5035919050565b600060208284031215610f8657600080fd5b5051919050565b600060208284031215610f9f57600080fd5b813563ffffffff8116811461068a57600080fd5b60008151808452610fcb8160208601602086016110af565b601f01601f19169290920160200192915050565b6001600160a01b038581168252841660208201526040810183905260806060820181905260009061101290830184610fb3565b9695505050505050565b60018060a01b0384168152826020820152606060408201526000610f526060830184610fb3565b60208152600061068a6020830184610fb3565b604051601f8201601f1916810167ffffffffffffffff8111828210171561107f5761107f6110db565b604052919050565b600067ffffffffffffffff8211156110a1576110a16110db565b50601f01601f191660200190565b60005b838110156110ca5781810151838201526020016110b2565b838111156109305750506000910152565b634e487b7160e01b600052604160045260246000fd5b6001600160a01b038116811461110657600080fd5b50565b801515811461110657600080fdfea2646970667358221220cd50bb20b48b73eddb390585af8699a01cd5f49b79ab50aa532df41df639f36764736f6c63430008070033
\ No newline at end of file
+608060405234801561001057600080fd5b5060405161168d38038061168d83398101604081905261002f91610054565b600080546001600160a01b0319166001600160a01b0392909216919091179055610084565b60006020828403121561006657600080fd5b81516001600160a01b038116811461007d57600080fd5b9392505050565b6115fa806100936000396000f3fe608060405234801561001057600080fd5b50600436106101a95760003560e01c806350bb4e7f116100f9578063a22cb46511610097578063d4eac26d11610071578063d4eac26d1461036b578063e6c5ce6f1461037e578063e985e9c514610391578063f4f4b500146103a457600080fd5b8063a22cb46514610332578063a9059cbb14610345578063c87b56dd1461035857600080fd5b806370a08231116100d357806370a082311461030757806375794a3c1461031a5780637d64bcb41461032257806395d89b411461032a57600080fd5b806350bb4e7f146102ce57806360a11672146102e15780636352211e146102f457600080fd5b80632f745c591161016657806342842e0e1161014057806342842e0e1461028257806342966c681461029557806344a9945e146102a85780634f6ccce7146102bb57600080fd5b80632f745c5914610249578063365430061461025c57806340c10f191461026f57600080fd5b806305d2035b146101ae57806306fdde03146101cb578063081812fc146101e0578063095ea7b31461020b57806318160ddd1461022057806323b872dd14610236575b600080fd5b6101b66103b7565b60405190151581526020015b60405180910390f35b6101d3610443565b6040516101c2919061148a565b6101f36101ee366004611277565b6104c3565b6040516001600160a01b0390911681526020016101c2565b61021e61021936600461118c565b610547565b005b6102286105b2565b6040519081526020016101c2565b61021e610244366004610eff565b610639565b61022861025736600461118c565b6106ad565b6101b661026a366004610fac565b610739565b6101b661027d36600461118c565b6107be565b61021e610290366004610eff565b6107f8565b61021e6102a3366004611277565b610839565b6101b66102b63660046110b1565b61089a565b6102286102c9366004611277565b6108cd565b6101b66102dc3660046111b8565b61094b565b61021e6102ef366004610f40565b6109da565b6101f3610302366004611277565b610a49565b610228610315366004610e8c565b610a7b565b610228610aae565b6101b6610afd565b6101d3610b62565b61021e61034036600461115e565b610ba6565b61021e61035336600461118c565b610be0565b6101d3610366366004611277565b610c19565b61021e6103793660046112a9565b610c9b565b6101d361038c366004611277565b610ccd565b6101f361039f366004610ec6565b610cff565b6101b66103b23660046112f0565b610d85565b60008060009054906101000a90046001600160a01b03166001600160a01b03166305d2035b6040518163ffffffff1660e01b815260040160206040518083038186803b15801561040657600080fd5b505afa15801561041a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061043e9190611211565b905090565b60008054604080516306fdde0360e01b815290516060936001600160a01b03909316926306fdde039260048082019391829003018186803b15801561048757600080fd5b505afa15801561049b573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261043e919081019061122e565b6000805460405163020604bf60e21b8152600481018490526001600160a01b039091169063081812fc906024015b60206040518083038186803b15801561050957600080fd5b505afa15801561051d573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906105419190610ea9565b92915050565b60005460405163095ea7b360e01b81526001600160a01b038481166004830152602482018490529091169063095ea7b3906044015b600060405180830381600087803b15801561059657600080fd5b505af11580156105aa573d6000803e3d6000fd5b505050505050565b60008060009054906101000a90046001600160a01b03166001600160a01b03166318160ddd6040518163ffffffff1660e01b815260040160206040518083038186803b15801561060157600080fd5b505afa158015610615573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061043e9190611290565b6000546040516323b872dd60e01b81526001600160a01b038581166004830152848116602483015260448201849052909116906323b872dd906064015b600060405180830381600087803b15801561069057600080fd5b505af11580156106a4573d6000803e3d6000fd5b50505050505050565b60008054604051632f745c5960e01b81526001600160a01b0385811660048301526024820185905290911690632f745c599060440160206040518083038186803b1580156106fa57600080fd5b505afa15801561070e573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906107329190611290565b9392505050565b60008054604051631b2a180360e11b81526001600160a01b039091169063365430069061076c908690869060040161137f565b602060405180830381600087803b15801561078657600080fd5b505af115801561079a573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906107329190611211565b600080546040516340c10f1960e01b81526001600160a01b03858116600483015260248201859052909116906340c10f199060440161076c565b600054604051632142170760e11b81526001600160a01b038581166004830152848116602483015260448201849052909116906342842e0e90606401610676565b600054604051630852cd8d60e31b8152600481018390526001600160a01b03909116906342966c6890602401600060405180830381600087803b15801561087f57600080fd5b505af1158015610893573d6000803e3d6000fd5b5050505050565b60008054604051632254ca2f60e11b81526001600160a01b03909116906344a9945e9061076c9086908690600401611404565b60008054604051634f6ccce760e01b8152600481018490526001600160a01b0390911690634f6ccce7906024015b60206040518083038186803b15801561091357600080fd5b505afa158015610927573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906105419190611290565b600080546040516350bb4e7f60e01b81526001600160a01b03909116906350bb4e7f906109809087908790879060040161145a565b602060405180830381600087803b15801561099a57600080fd5b505af11580156109ae573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109d29190611211565b949350505050565b6000546040516330508b3960e11b81526001600160a01b03909116906360a1167290610a10908790879087908790600401611342565b600060405180830381600087803b158015610a2a57600080fd5b505af1158015610a3e573d6000803e3d6000fd5b505050505b50505050565b600080546040516331a9108f60e11b8152600481018490526001600160a01b0390911690636352211e906024016104f1565b600080546040516370a0823160e01b81526001600160a01b038481166004830152909116906370a08231906024016108fb565b60008060009054906101000a90046001600160a01b03166001600160a01b03166375794a3c6040518163ffffffff1660e01b815260040160206040518083038186803b15801561060157600080fd5b60008060009054906101000a90046001600160a01b03166001600160a01b0316637d64bcb46040518163ffffffff1660e01b8152600401602060405180830381600087803b158015610b4e57600080fd5b505af115801561041a573d6000803e3d6000fd5b60008054604080516395d89b4160e01b815290516060936001600160a01b03909316926395d89b419260048082019391829003018186803b15801561048757600080fd5b60005460405163a22cb46560e01b81526001600160a01b03848116600483015283151560248301529091169063a22cb4659060440161057c565b60005460405163a9059cbb60e01b81526001600160a01b038481166004830152602482018490529091169063a9059cbb9060440161057c565b60005460405163c87b56dd60e01b8152600481018390526060916001600160a01b03169063c87b56dd906024015b60006040518083038186803b158015610c5f57600080fd5b505afa158015610c73573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052610541919081019061122e565b60005460405163d4eac26d60e01b81526001600160a01b039091169063d4eac26d9061057c908590859060040161149d565b60005460405163e6c5ce6f60e01b8152600481018390526060916001600160a01b03169063e6c5ce6f90602401610c47565b6000805460405163e985e9c560e01b81526001600160a01b03858116600483015284811660248301529091169063e985e9c59060440160206040518083038186803b158015610d4d57600080fd5b505afa158015610d61573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906107329190610ea9565b6000805460405162f4f4b560e81b815263ffffffff841660048201526001600160a01b039091169063f4f4b5009060240160206040518083038186803b158015610dce57600080fd5b505afa158015610de2573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906105419190611211565b6000610e19610e1484611534565b6114df565b9050828152838383011115610e2d57600080fd5b61073283602083018461155c565b600082601f830112610e4c57600080fd5b8135610e5a610e1482611534565b818152846020838601011115610e6f57600080fd5b816020850160208301376000918101602001919091529392505050565b600060208284031215610e9e57600080fd5b81356107328161159e565b600060208284031215610ebb57600080fd5b81516107328161159e565b60008060408385031215610ed957600080fd5b8235610ee48161159e565b91506020830135610ef48161159e565b809150509250929050565b600080600060608486031215610f1457600080fd5b8335610f1f8161159e565b92506020840135610f2f8161159e565b929592945050506040919091013590565b60008060008060808587031215610f5657600080fd5b8435610f618161159e565b93506020850135610f718161159e565b925060408501359150606085013567ffffffffffffffff811115610f9457600080fd5b610fa087828801610e3b565b91505092959194509250565b60008060408385031215610fbf57600080fd5b8235610fca8161159e565b915060208381013567ffffffffffffffff80821115610fe857600080fd5b818601915086601f830112610ffc57600080fd5b813561100a610e1482611510565b8082825285820191508585018a878560051b880101111561102a57600080fd5b60005b848110156110a05781358681111561104457600080fd5b87016040818e03601f1901121561105a57600080fd5b6110626114b6565b89820135815260408201358881111561107a57600080fd5b6110888f8c83860101610e3b565b828c015250855250928701929087019060010161102d565b50979a909950975050505050505050565b600080604083850312156110c457600080fd5b82356110cf8161159e565b915060208381013567ffffffffffffffff8111156110ec57600080fd5b8401601f810186136110fd57600080fd5b803561110b610e1482611510565b80828252848201915084840189868560051b870101111561112b57600080fd5b600094505b8385101561114e578035835260019490940193918501918501611130565b5080955050505050509250929050565b6000806040838503121561117157600080fd5b823561117c8161159e565b91506020830135610ef4816115b6565b6000806040838503121561119f57600080fd5b82356111aa8161159e565b946020939093013593505050565b6000806000606084860312156111cd57600080fd5b83356111d88161159e565b925060208401359150604084013567ffffffffffffffff8111156111fb57600080fd5b61120786828701610e3b565b9150509250925092565b60006020828403121561122357600080fd5b8151610732816115b6565b60006020828403121561124057600080fd5b815167ffffffffffffffff81111561125757600080fd5b8201601f8101841361126857600080fd5b6109d284825160208401610e06565b60006020828403121561128957600080fd5b5035919050565b6000602082840312156112a257600080fd5b5051919050565b600080604083850312156112bc57600080fd5b82359150602083013567ffffffffffffffff8111156112da57600080fd5b6112e685828601610e3b565b9150509250929050565b60006020828403121561130257600080fd5b813563ffffffff8116811461073257600080fd5b6000815180845261132e81602086016020860161155c565b601f01601f19169290920160200192915050565b6001600160a01b038581168252841660208201526040810183905260806060820181905260009061137590830184611316565b9695505050505050565b6001600160a01b0383168152604060208083018290528351828401819052600092916060600583901b860181019290860190878301865b828110156113f557888603605f190184528151805187528501518587018890526113e288880182611316565b96505092840192908401906001016113b6565b50939998505050505050505050565b6001600160a01b038316815260406020808301829052835191830182905260009184820191906060850190845b8181101561144d57845183529383019391830191600101611431565b5090979650505050505050565b60018060a01b03841681528260208201526060604082015260006114816060830184611316565b95945050505050565b6020815260006107326020830184611316565b8281526040602082015260006109d26040830184611316565b6040805190810167ffffffffffffffff811182821017156114d9576114d9611588565b60405290565b604051601f8201601f1916810167ffffffffffffffff8111828210171561150857611508611588565b604052919050565b600067ffffffffffffffff82111561152a5761152a611588565b5060051b60200190565b600067ffffffffffffffff82111561154e5761154e611588565b50601f01601f191660200190565b60005b8381101561157757818101518382015260200161155f565b83811115610a435750506000910152565b634e487b7160e01b600052604160045260246000fd5b6001600160a01b03811681146115b357600080fd5b50565b80151581146115b357600080fdfea2646970667358221220669a75a3efcdc6b60606caa5c7e41cab1d727e89a03fd231d1cda27f4de159a064736f6c63430008070033
\ No newline at end of file
tests/src/eth/proxy/UniqueNFTProxy.soldiffbeforeafterboth--- a/tests/src/eth/proxy/UniqueNFTProxy.sol
+++ b/tests/src/eth/proxy/UniqueNFTProxy.sol
@@ -156,4 +156,36 @@
{
return proxied.supportsInterface(interfaceId);
}
+
+ function setVariableMetadata(uint256 tokenId, bytes memory data)
+ external
+ override
+ {
+ return proxied.setVariableMetadata(tokenId, data);
+ }
+
+ function getVariableMetadata(uint256 tokenId)
+ external
+ view
+ override
+ returns (bytes memory)
+ {
+ return proxied.getVariableMetadata(tokenId);
+ }
+
+ function mintBulk(address to, uint256[] memory tokenIds)
+ external
+ override
+ returns (bool)
+ {
+ return proxied.mintBulk(to, tokenIds);
+ }
+
+ function mintBulkWithTokenURI(address to, Tuple0[] memory tokens)
+ external
+ override
+ returns (bool)
+ {
+ return proxied.mintBulkWithTokenURI(to, tokens);
+ }
}
tests/src/eth/proxy/nonFungibleProxy.test.tsdiffbeforeafterboth--- a/tests/src/eth/proxy/nonFungibleProxy.test.ts
+++ b/tests/src/eth/proxy/nonFungibleProxy.test.ts
@@ -4,7 +4,7 @@
//
import privateKey from '../../substrate/privateKey';
-import { createCollectionExpectSuccess, createItemExpectSuccess } from '../../util/helpers';
+import { createCollectionExpectSuccess, createItemExpectSuccess, setVariableMetaDataExpectSuccess } from '../../util/helpers';
import { collectionIdToAddress, createEthAccount, createEthAccountWithBalance, GAS_ARGS, itWeb3, normalizeEvents } from '../util/helpers';
import nonFungibleAbi from '../nonFungibleAbi.json';
import { expect } from 'chai';
@@ -96,13 +96,11 @@
{
const nextTokenId = await contract.methods.nextTokenId().call();
expect(nextTokenId).to.be.equal('1');
- console.log('Before mint');
const result = await contract.methods.mintWithTokenURI(
receiver,
nextTokenId,
'Test URI',
).send({ from: caller });
- console.log('After mint');
const events = normalizeEvents(result.events);
expect(events).to.be.deep.equal([
@@ -121,6 +119,69 @@
expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI');
}
});
+ itWeb3('Can perform mintBulk()', async ({ web3, api }) => {
+ const collection = await createCollectionExpectSuccess({
+ mode: { type: 'NFT' },
+ });
+ const alice = privateKey('//Alice');
+
+ const caller = await createEthAccountWithBalance(api, web3);
+ const receiver = createEthAccount(web3);
+
+ const address = collectionIdToAddress(collection);
+ const contract = await proxyWrap(api, web3, new web3.eth.Contract(nonFungibleAbi as any, address, { from: caller, ...GAS_ARGS }));
+ const changeAdminTx = api.tx.nft.addCollectionAdmin(collection, { ethereum: contract.options.address });
+ await submitTransactionAsync(alice, changeAdminTx);
+
+ {
+ const nextTokenId = await contract.methods.nextTokenId().call();
+ expect(nextTokenId).to.be.equal('1');
+ const result = await contract.methods.mintBulkWithTokenURI(
+ receiver,
+ [
+ [nextTokenId, 'Test URI 0'],
+ [+nextTokenId + 1, 'Test URI 1'],
+ [+nextTokenId + 2, 'Test URI 2'],
+ ],
+ ).send({ from: caller });
+ const events = normalizeEvents(result.events);
+
+ expect(events).to.be.deep.equal([
+ {
+ address,
+ event: 'Transfer',
+ args: {
+ from: '0x0000000000000000000000000000000000000000',
+ to: receiver,
+ tokenId: nextTokenId,
+ },
+ },
+ {
+ address,
+ event: 'Transfer',
+ args: {
+ from: '0x0000000000000000000000000000000000000000',
+ to: receiver,
+ tokenId: String(+nextTokenId + 1),
+ },
+ },
+ {
+ address,
+ event: 'Transfer',
+ args: {
+ from: '0x0000000000000000000000000000000000000000',
+ to: receiver,
+ tokenId: String(+nextTokenId + 2),
+ },
+ },
+ ]);
+
+ await waitNewBlocks(api, 1);
+ expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI 0');
+ expect(await contract.methods.tokenURI(+nextTokenId + 1).call()).to.be.equal('Test URI 1');
+ expect(await contract.methods.tokenURI(+nextTokenId + 2).call()).to.be.equal('Test URI 2');
+ }
+ });
itWeb3('Can perform burn()', async ({ web3, api }) => {
const collection = await createCollectionExpectSuccess({
@@ -267,4 +328,35 @@
expect(+balance).to.equal(1);
}
});
+
+ itWeb3('Can perform getVariableMetadata', async ({ web3, api }) => {
+ const collection = await createCollectionExpectSuccess({
+ mode: { type: 'NFT' },
+ });
+ const alice = privateKey('//Alice');
+ const caller = await createEthAccountWithBalance(api, web3);
+
+ const address = collectionIdToAddress(collection);
+ const contract = await proxyWrap(api, web3, new web3.eth.Contract(nonFungibleAbi as any, address, { from: caller, ...GAS_ARGS }));
+ const item = await createItemExpectSuccess(alice, collection, 'NFT', { ethereum: contract.options.address });
+ await setVariableMetaDataExpectSuccess(alice, collection, item, [1, 2, 3]);
+
+ expect(await contract.methods.getVariableMetadata(item).call()).to.be.equal('0x010203');
+ });
+
+ itWeb3('Can perform setVariableMetadata', async ({ web3, api }) => {
+ const collection = await createCollectionExpectSuccess({
+ mode: { type: 'NFT' },
+ });
+ const alice = privateKey('//Alice');
+ const caller = await createEthAccountWithBalance(api, web3);
+
+ const address = collectionIdToAddress(collection);
+ const contract = await proxyWrap(api, web3, new web3.eth.Contract(nonFungibleAbi as any, address, { from: caller, ...GAS_ARGS }));
+ const item = await createItemExpectSuccess(alice, collection, 'NFT', { ethereum: contract.options.address });
+
+ expect(await contract.methods.setVariableMetadata(item, '0x010203').send({ from: caller }));
+ await waitNewBlocks(api, 1);
+ expect(await contract.methods.getVariableMetadata(item).call()).to.be.equal('0x010203');
+ });
});
tests/src/eth/util/helpers.tsdiffbeforeafterboth--- a/tests/src/eth/util/helpers.ts
+++ b/tests/src/eth/util/helpers.ts
@@ -269,4 +269,4 @@
expect(after < before).to.be.true;
return before - after;
-}
\ No newline at end of file
+}