git.delta.rocks / unique-network / refs/commits / 8e4698bc6eb3

difftreelog

Merge pull request #160 from UniqueNetwork/feature/erc-mintable-burnable

kozyrevdev2021-08-30parents: #da8dccd #ac8c84f.patch.diff
in: master
Implement ERC721Burnable/ERC721Mintable

13 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5852,34 +5852,34 @@
 [[package]]
 name = "pallet-scheduler"
 version = "3.0.0"
+source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.8#74101dc21cfffb4c2d014fcc28edc166d5ca1b16"
 dependencies = [
  "frame-benchmarking",
  "frame-support",
  "frame-system",
  "log",
  "parity-scale-codec",
- "serde",
- "sp-core",
  "sp-io",
  "sp-runtime",
  "sp-std",
- "substrate-test-utils",
- "up-sponsorship",
 ]
 
 [[package]]
 name = "pallet-scheduler"
 version = "3.0.0"
-source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.8#74101dc21cfffb4c2d014fcc28edc166d5ca1b16"
 dependencies = [
  "frame-benchmarking",
  "frame-support",
  "frame-system",
  "log",
  "parity-scale-codec",
+ "serde",
+ "sp-core",
  "sp-io",
  "sp-runtime",
  "sp-std",
+ "substrate-test-utils",
+ "up-sponsorship",
 ]
 
 [[package]]
@@ -10695,13 +10695,13 @@
 [[package]]
 name = "substrate-wasm-builder"
 version = "4.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93a3d51ad6abbc408b03ea962062bfcc959b438a318d7d4bedd181e1effd0610"
+source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.8#74101dc21cfffb4c2d014fcc28edc166d5ca1b16"
 dependencies = [
  "ansi_term 0.12.1",
  "atty",
  "build-helper",
- "cargo_metadata 0.12.3",
+ "cargo_metadata 0.13.1",
+ "sp-maybe-compressed-blob",
  "tempfile",
  "toml",
  "walkdir",
@@ -10711,13 +10711,13 @@
 [[package]]
 name = "substrate-wasm-builder"
 version = "4.0.0"
-source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.8#74101dc21cfffb4c2d014fcc28edc166d5ca1b16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93a3d51ad6abbc408b03ea962062bfcc959b438a318d7d4bedd181e1effd0610"
 dependencies = [
  "ansi_term 0.12.1",
  "atty",
  "build-helper",
- "cargo_metadata 0.13.1",
- "sp-maybe-compressed-blob",
+ "cargo_metadata 0.12.3",
  "tempfile",
  "toml",
  "walkdir",
modifiedcrates/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
@@ -73,6 +73,20 @@
 			}
 		}
 	}
+
+	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);
+		}
+	}
+
+	fn expand_event_generator(&self) -> proc_macro2::TokenStream {
+		let name = &self.name;
+		quote! {
+			#name::generate_solidity_interface(out_set, is_impl);
+		}
+	}
 }
 
 #[derive(Default)]
@@ -109,12 +123,15 @@
 
 struct MethodArg {
 	name: Ident,
+	camel_name: String,
 	ty: Ident,
 }
 impl MethodArg {
 	fn try_from(value: &PatType) -> syn::Result<Self> {
+		let name = parse_ident_from_pat(&value.pat)?.clone();
 		Ok(Self {
-			name: parse_ident_from_pat(&value.pat)?.clone(),
+			camel_name: cases::camelcase::to_camel_case(&name.to_string()),
+			name,
 			ty: parse_ident_from_type(&value.ty, false)?.clone(),
 		})
 	}
@@ -168,10 +185,10 @@
 	}
 
 	fn expand_solidity_argument(&self) -> proc_macro2::TokenStream {
-		let name = &self.name.to_string();
+		let camel_name = &self.camel_name.to_string();
 		let ty = &self.ty;
 		quote! {
-			<NamedArgument<#ty>>::new(#name)
+			<NamedArgument<#ty>>::new(#camel_name)
 		}
 	}
 }
@@ -397,7 +414,11 @@
 		};
 		let result = &self.result;
 
-		let args = self.args.iter().map(MethodArg::expand_solidity_argument);
+		let args = self
+			.args
+			.iter()
+			.filter(|a| !a.is_special())
+			.map(MethodArg::expand_solidity_argument);
 
 		quote! {
 			SolidityFunction {
@@ -475,6 +496,24 @@
 		let call_variants_this = self.methods.iter().map(Method::expand_variant_call);
 		let solidity_functions = self.methods.iter().map(Method::expand_solidity_function);
 
+		// TODO: Inline inline_is
+		let solidity_is = self
+			.info
+			.is
+			.0
+			.iter()
+			.chain(self.info.inline_is.0.iter())
+			.map(|is| is.name.to_string());
+		let solidity_events_is = self.info.events.0.iter().map(|is| is.name.to_string());
+		let solidity_generators = self
+			.info
+			.is
+			.0
+			.iter()
+			.chain(self.info.inline_is.0.iter())
+			.map(Is::expand_generator);
+		let solidity_event_generators = self.info.events.0.iter().map(Is::expand_event_generator);
+
 		// let methods = self.methods.iter().map(Method::solidity_def);
 
 		quote! {
@@ -505,18 +544,40 @@
 						)*
 					)
 				}
-				pub fn generate_solidity_interface() -> string {
+				pub fn generate_solidity_interface(out_set: &mut sp_std::collections::btree_set::BTreeSet<string>, is_impl: bool) {
 					use evm_coder::solidity::*;
 					use core::fmt::Write;
 					let interface = SolidityInterface {
 						name: #solidity_name,
+						is: &["Dummy", #(
+							#solidity_is,
+						)* #(
+							#solidity_events_is,
+						)* ],
 						functions: (#(
 							#solidity_functions,
 						)*),
 					};
+					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());
+					} else {
+						out_set.insert("// Common stubs holder\ninterface Dummy {\n}\n".into());
+					}
+					#(
+						#solidity_generators
+					)*
+					#(
+						#solidity_event_generators
+					)*
+
 					let mut out = string::new();
-					let _ = interface.format(&mut out);
-					out
+					// In solidity interface usage (is) should be preceeded by interface definition
+					// This comment helps to sort it in a set
+					if #solidity_name.starts_with("Inline") {
+						out.push_str("// Inline\n");
+					}
+					let _ = interface.format(is_impl, &mut out);
+					out_set.insert(out);
 				}
 			}
 			impl ::evm_coder::Call for #call_name {
modifiedcrates/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
@@ -1,3 +1,4 @@
+use inflector::cases;
 use syn::{Data, DeriveInput, Field, Fields, Ident, Variant, spanned::Spanned};
 use std::fmt::Write;
 use quote::quote;
@@ -6,6 +7,7 @@
 
 struct EventField {
 	name: Ident,
+	camel_name: String,
 	ty: Ident,
 	indexed: bool,
 }
@@ -24,10 +26,18 @@
 		}
 		Ok(Self {
 			name: name.to_owned(),
+			camel_name: cases::camelcase::to_camel_case(&name.to_string()),
 			ty: ty.to_owned(),
 			indexed,
 		})
 	}
+	fn expand_solidity_argument(&self) -> proc_macro2::TokenStream {
+		let camel_name = &self.camel_name;
+		let ty = &self.ty;
+		quote! {
+			<NamedArgument<#ty>>::new(#camel_name)
+		}
+	}
 }
 
 struct Event {
@@ -116,6 +126,21 @@
 			)*];
 		}
 	}
+
+	fn expand_solidity_function(&self) -> proc_macro2::TokenStream {
+		let name = self.name.to_string();
+		let args = self.fields.iter().map(EventField::expand_solidity_argument);
+		quote! {
+			SolidityEvent {
+				name: #name,
+				args: (
+					#(
+						#args,
+					)*
+				),
+			}
+		}
+	}
 }
 
 pub struct Events {
@@ -144,12 +169,30 @@
 
 		let consts = self.events.iter().map(Event::expand_consts);
 		let serializers = self.events.iter().map(Event::expand_serializers);
+		let solidity_name = self.name.to_string();
+		let solidity_functions = self.events.iter().map(Event::expand_solidity_function);
 
 		quote! {
 			impl #name {
 				#(
 					#consts
 				)*
+
+				pub fn generate_solidity_interface(out_set: &mut sp_std::collections::btree_set::BTreeSet<string>, is_impl: bool) {
+					use evm_coder::solidity::*;
+					use core::fmt::Write;
+					let interface = SolidityInterface {
+						name: #solidity_name,
+						is: &[],
+						functions: (#(
+							#solidity_functions,
+						)*),
+					};
+					let mut out = string::new();
+					out.push_str("// Inline\n");
+					let _ = interface.format(is_impl, &mut out);
+					out_set.insert(out);
+				}
 			}
 
 			#[automatically_derived]
modifiedcrates/evm-coder/src/abi.rsdiffbeforeafterboth
--- a/crates/evm-coder/src/abi.rs
+++ b/crates/evm-coder/src/abi.rs
@@ -19,11 +19,16 @@
 #[derive(Clone)]
 pub struct AbiReader<'i> {
 	buf: &'i [u8],
+	subresult_offset: usize,
 	offset: usize,
 }
 impl<'i> AbiReader<'i> {
 	pub fn new(buf: &'i [u8]) -> Self {
-		Self { buf, offset: 0 }
+		Self {
+			buf,
+			subresult_offset: 0,
+			offset: 0,
+		}
 	}
 	pub fn new_call(buf: &'i [u8]) -> Result<(u32, Self)> {
 		if buf.len() < 4 {
@@ -32,11 +37,18 @@
 		let mut method_id = [0; 4];
 		method_id.copy_from_slice(&buf[0..4]);
 
-		Ok((u32::from_be_bytes(method_id), Self { buf, offset: 4 }))
+		Ok((
+			u32::from_be_bytes(method_id),
+			Self {
+				buf,
+				subresult_offset: 4,
+				offset: 4,
+			},
+		))
 	}
 
 	fn read_padleft<const S: usize>(&mut self) -> Result<[u8; S]> {
-		if self.buf.len() - self.offset < 32 {
+		if self.buf.len() - self.offset < ABI_ALIGNMENT {
 			return Err(Error::Error(ExitError::OutOfOffset));
 		}
 		let mut block = [0; S];
@@ -79,6 +91,9 @@
 		}
 		Ok(subresult.buf[subresult.offset..subresult.offset + length].into())
 	}
+	pub fn string(&mut self) -> Result<string> {
+		string::from_utf8(self.bytes()?).map_err(|_| Error::Error(ExitError::InvalidRange))
+	}
 
 	pub fn uint32(&mut self) -> Result<u32> {
 		Ok(u32::from_be_bytes(self.read_padleft()?))
@@ -103,9 +118,13 @@
 
 	fn subresult(&mut self) -> Result<AbiReader<'i>> {
 		let offset = self.read_usize()?;
+		if offset + self.subresult_offset > self.buf.len() {
+			return Err(Error::Error(ExitError::InvalidRange));
+		}
 		Ok(AbiReader {
 			buf: self.buf,
-			offset: offset + self.offset,
+			subresult_offset: offset + self.subresult_offset,
+			offset: offset + self.subresult_offset,
 		})
 	}
 
@@ -196,7 +215,7 @@
 		for (static_offset, part) in self.dynamic_part {
 			let part_offset = self.static_part.len();
 
-			let encoded_dynamic_offset = usize::to_be_bytes(part_offset - static_offset);
+			let encoded_dynamic_offset = usize::to_be_bytes(part_offset);
 			self.static_part[static_offset + ABI_ALIGNMENT - encoded_dynamic_offset.len()
 				..static_offset + ABI_ALIGNMENT]
 				.copy_from_slice(&encoded_dynamic_offset);
@@ -226,6 +245,7 @@
 impl_abi_readable!(H160, address);
 impl_abi_readable!(Vec<u8>, bytes);
 impl_abi_readable!(bool, bool);
+impl_abi_readable!(string, string);
 
 pub trait AbiWrite {
 	fn abi_write(&self, writer: &mut AbiWriter);
@@ -285,3 +305,54 @@
 		writer
 	}}
 }
+
+#[cfg(test)]
+pub mod test {
+	use super::{AbiReader, AbiWriter};
+	use hex_literal::hex;
+
+	#[test]
+	fn dynamic_after_static() {
+		let mut encoder = AbiWriter::new();
+		encoder.bool(&true);
+		encoder.string("test");
+		let encoded = encoder.finish();
+
+		let mut encoder = AbiWriter::new();
+		encoder.bool(&true);
+		// Offset to subresult
+		encoder.uint32(&(32 * 2));
+		// Len of "test"
+		encoder.uint32(&4);
+		encoder.write_padright(&[b't', b'e', b's', b't']);
+		let alternative_encoded = encoder.finish();
+
+		assert_eq!(encoded, alternative_encoded);
+
+		let mut decoder = AbiReader::new(&encoded);
+		assert_eq!(decoder.bool().unwrap(), true);
+		assert_eq!(decoder.string().unwrap(), "test");
+	}
+
+	#[test]
+	fn mint_sample() {
+		let (call, mut decoder) = AbiReader::new_call(&hex!(
+			"
+				50bb4e7f
+				000000000000000000000000ad2c0954693c2b5404b7e50967d3481bea432374
+				0000000000000000000000000000000000000000000000000000000000000001
+				0000000000000000000000000000000000000000000000000000000000000060
+				0000000000000000000000000000000000000000000000000000000000000008
+				5465737420555249000000000000000000000000000000000000000000000000
+			"
+		))
+		.unwrap();
+		assert_eq!(call, 0x50bb4e7f);
+		assert_eq!(
+			format!("{:?}", decoder.address().unwrap()),
+			"0xad2c0954693c2b5404b7e50967d3481bea432374"
+		);
+		assert_eq!(decoder.uint32().unwrap(), 1);
+		assert_eq!(decoder.string().unwrap(), "Test URI");
+	}
+}
modifiedcrates/evm-coder/src/solidity.rsdiffbeforeafterboth
--- a/crates/evm-coder/src/solidity.rs
+++ b/crates/evm-coder/src/solidity.rs
@@ -6,37 +6,44 @@
 
 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 is_void() -> bool {
 		false
 	}
 }
 
 macro_rules! solidity_type_name {
-    ($($ty:ident => $name:expr),* $(,)?) => {
+    ($($ty:ident => $name:literal = $default:literal),* $(,)?) => {
         $(
             impl SolidityTypeName for $ty {
                 fn solidity_name(writer: &mut impl core::fmt::Write) -> core::fmt::Result {
                     write!(writer, $name)
                 }
+				fn solidity_default(writer: &mut impl core::fmt::Write) -> core::fmt::Result {
+					write!(writer, $default)
+				}
             }
         )*
     };
 }
 
 solidity_type_name! {
-	uint8 => "uint8",
-	uint32 => "uint32",
-	uint128 => "uint128",
-	uint256 => "uint256",
-	address => "address",
-	string => "memory string",
-	bytes => "memory bytes",
-	bool => "bool",
+	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",
 }
 impl SolidityTypeName for void {
 	fn solidity_name(_writer: &mut impl fmt::Write) -> fmt::Result {
 		Ok(())
 	}
+	fn solidity_default(_writer: &mut impl fmt::Write) -> fmt::Result {
+		Ok(())
+	}
 	fn is_void() -> bool {
 		true
 	}
@@ -44,6 +51,8 @@
 
 pub trait SolidityArguments {
 	fn solidity_name(&self, writer: &mut impl fmt::Write) -> 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 is_empty(&self) -> bool {
 		self.len() == 0
 	}
@@ -61,6 +70,12 @@
 			Ok(())
 		}
 	}
+	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 len(&self) -> usize {
 		if T::is_void() {
 			0
@@ -87,6 +102,12 @@
 			Ok(())
 		}
 	}
+	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 len(&self) -> usize {
 		if T::is_void() {
 			0
@@ -100,6 +121,12 @@
 	fn solidity_name(&self, _writer: &mut impl fmt::Write) -> 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 {
+		Ok(())
+	}
 	fn len(&self) -> usize {
 		0
 	}
@@ -122,13 +149,43 @@
         )* );
 		Ok(())
 	}
+	fn solidity_get(&self, writer: &mut impl fmt::Write) -> fmt::Result {
+		for_tuples!( #(
+            Tuple.solidity_get(writer)?;
+        )* );
+		Ok(())
+	}
+	fn solidity_default(&self, writer: &mut impl fmt::Write) -> fmt::Result {
+		if self.is_empty() {
+			Ok(())
+		} else if self.len() == 1 {
+			for_tuples!( #(
+				Tuple.solidity_default(writer)?;
+			)* );
+			Ok(())
+		} else {
+			write!(writer, "(")?;
+			let mut first = true;
+			for_tuples!( #(
+				if !Tuple.is_empty() {
+					if !first {
+						write!(writer, ", ")?;
+					}
+					first = false;
+					Tuple.solidity_name(writer)?;
+				}
+			)* );
+			write!(writer, ")")?;
+			Ok(())
+		}
+	}
 	fn len(&self) -> usize {
 		for_tuples!( #( Tuple.len() )+* )
 	}
 }
 
 pub trait SolidityFunctions {
-	fn solidity_name(&self, writer: &mut impl fmt::Write) -> fmt::Result;
+	fn solidity_name(&self, is_impl: bool, writer: &mut impl fmt::Write) -> fmt::Result;
 }
 
 pub enum SolidityMutability {
@@ -143,10 +200,15 @@
 	pub mutability: SolidityMutability,
 }
 impl<A: SolidityArguments, R: SolidityArguments> SolidityFunctions for SolidityFunction<A, R> {
-	fn solidity_name(&self, writer: &mut impl fmt::Write) -> fmt::Result {
-		write!(writer, "function {}(", self.name)?;
+	fn solidity_name(&self, is_impl: bool, writer: &mut impl fmt::Write) -> fmt::Result {
+		write!(writer, "\tfunction {}(", self.name)?;
 		self.args.solidity_name(writer)?;
-		write!(writer, ") external")?;
+		write!(writer, ")")?;
+		if is_impl {
+			write!(writer, " public")?;
+		} else {
+			write!(writer, " external")?;
+		}
 		match &self.mutability {
 			SolidityMutability::Pure => write!(writer, " pure")?,
 			SolidityMutability::View => write!(writer, " view")?,
@@ -157,7 +219,25 @@
 			self.result.solidity_name(writer)?;
 			write!(writer, ")")?;
 		}
-		writeln!(writer, ";")
+		if is_impl {
+			writeln!(writer, " {{")?;
+			writeln!(writer, "\t\trequire(false, stub_error);")?;
+			self.args.solidity_get(writer)?;
+			match &self.mutability {
+				SolidityMutability::Pure => {}
+				SolidityMutability::View => writeln!(writer, "\t\tdummy;")?,
+				SolidityMutability::Mutable => writeln!(writer, "\t\tdummy = 0;")?,
+			}
+			if !self.result.is_empty() {
+				write!(writer, "\t\treturn ")?;
+				self.result.solidity_default(writer)?;
+				writeln!(writer, ";")?;
+			}
+			writeln!(writer, "\t}}")?;
+		} else {
+			writeln!(writer, ";")?;
+		}
+		Ok(())
 	}
 }
 
@@ -165,10 +245,10 @@
 impl SolidityFunctions for Tuple {
 	for_tuples!( where #( Tuple: SolidityFunctions ),* );
 
-	fn solidity_name(&self, writer: &mut impl fmt::Write) -> fmt::Result {
+	fn solidity_name(&self, is_impl: bool, writer: &mut impl fmt::Write) -> fmt::Result {
 		let mut first = false;
 		for_tuples!( #(
-            Tuple.solidity_name(writer)?;
+            Tuple.solidity_name(is_impl, writer)?;
         )* );
 		Ok(())
 	}
@@ -176,14 +256,43 @@
 
 pub struct SolidityInterface<F: SolidityFunctions> {
 	pub name: &'static str,
+	pub is: &'static [&'static str],
 	pub functions: F,
 }
 
 impl<F: SolidityFunctions> SolidityInterface<F> {
-	pub fn format(&self, out: &mut impl fmt::Write) -> fmt::Result {
-		writeln!(out, "interface {} {{", self.name)?;
-		self.functions.solidity_name(out)?;
+	pub fn format(&self, is_impl: bool, out: &mut impl fmt::Write) -> fmt::Result {
+		if is_impl {
+			write!(out, "contract ")?;
+		} else {
+			write!(out, "interface ")?;
+		}
+		write!(out, "{}", self.name)?;
+		if !self.is.is_empty() {
+			write!(out, " is")?;
+			for (i, n) in self.is.iter().enumerate() {
+				if i != 0 {
+					write!(out, ",")?;
+				}
+				write!(out, " {}", n)?;
+			}
+		}
+		writeln!(out, " {{")?;
+		self.functions.solidity_name(is_impl, out)?;
 		writeln!(out, "}}")?;
 		Ok(())
 	}
 }
+
+pub struct SolidityEvent<A> {
+	pub name: &'static str,
+	pub args: A,
+}
+
+impl<A: SolidityArguments> SolidityFunctions for SolidityEvent<A> {
+	fn solidity_name(&self, _is_impl: bool, writer: &mut impl fmt::Write) -> fmt::Result {
+		write!(writer, "\tevent {}(", self.name)?;
+		self.args.solidity_name(writer)?;
+		writeln!(writer, ");")
+	}
+}
modifiedpallets/nft/src/eth/erc.rsdiffbeforeafterboth
--- a/pallets/nft/src/eth/erc.rs
+++ b/pallets/nft/src/eth/erc.rs
@@ -1,12 +1,15 @@
 use core::char::{decode_utf16, REPLACEMENT_CHARACTER};
 use evm_coder::{ToLog, execution::Result, solidity, solidity_interface, types::*};
+use nft_data_structs::{CreateItemData, CreateNftData};
 use core::convert::TryInto;
-use alloc::format;
-use crate::{Allowances, Module, Balance, CollectionHandle, CollectionMode, Config, NftItemList};
-use frame_support::storage::StorageDoubleMap;
+use crate::{
+	Allowances, Module, Balance, CollectionHandle, CollectionMode, Config, NftItemList,
+	ItemListIndex,
+};
+use frame_support::storage::{StorageMap, StorageDoubleMap};
 use pallet_evm::AddressMapping;
 use super::account::CrossAccountId;
-use sp_std::vec::Vec;
+use sp_std::{vec, vec::Vec};
 
 #[solidity_interface(name = "ERC165")]
 impl<T: Config> CollectionHandle<T> {
@@ -42,9 +45,15 @@
 
 #[solidity_interface(name = "ERC721Metadata", inline_is(InlineNameSymbol))]
 impl<T: Config> CollectionHandle<T> {
+	#[solidity(rename_selector = "tokenURI")]
 	fn token_uri(&self, token_id: uint256) -> Result<string> {
-		// TODO: We should standartize url prefix, maybe via offchain schema?
-		Ok(format!("unique.network/{}/{}", self.id, token_id))
+		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;
+		Ok(string::from_utf8_lossy(
+			&<NftItemList<T>>::get(self.id, token_id)
+				.ok_or("token not found")?
+				.const_data,
+		)
+		.into())
 	}
 }
 
@@ -178,6 +187,93 @@
 	}
 }
 
+#[solidity_interface(name = "ERC721Burnable")]
+impl<T: Config> CollectionHandle<T> {
+	fn burn(&mut self, caller: caller, token_id: uint256) -> Result<void> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let token_id = token_id.try_into().map_err(|_| "amount overflow")?;
+
+		<Module<T>>::burn_item_internal(&caller, &self, token_id, 1).map_err(|_| "burn error")?;
+		Ok(())
+	}
+}
+
+#[derive(ToLog)]
+pub enum ERC721MintableEvents {
+	#[allow(dead_code)]
+	MintingFinished {},
+}
+
+#[solidity_interface(name = "ERC721Mintable", events(ERC721MintableEvents))]
+impl<T: Config> CollectionHandle<T> {
+	fn minting_finished(&self) -> Result<bool> {
+		Ok(false)
+	}
+
+	fn mint(&mut self, caller: caller, to: address, token_id: uint256) -> Result<bool> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let to = T::CrossAccountId::from_eth(to);
+		let token_id: u32 = token_id.try_into().map_err(|_| "amount overflow")?;
+		if <ItemListIndex>::get(self.id)
+			.checked_add(1)
+			.ok_or("item id overflow")?
+			!= token_id
+		{
+			return Err("item id should be next".into());
+		}
+
+		<Module<T>>::create_item_internal(
+			&caller,
+			&self,
+			&to,
+			CreateItemData::NFT(CreateNftData {
+				const_data: vec![].try_into().unwrap(),
+				variable_data: vec![].try_into().unwrap(),
+			}),
+		)
+		.map_err(|_| "mint error")?;
+		Ok(true)
+	}
+
+	#[solidity(rename_selector = "mintWithTokenURI")]
+	fn mint_with_token_uri(
+		&mut self,
+		caller: caller,
+		to: address,
+		token_id: uint256,
+		token_uri: string,
+	) -> Result<bool> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let to = T::CrossAccountId::from_eth(to);
+		let token_id: u32 = token_id.try_into().map_err(|_| "amount overflow")?;
+		if <ItemListIndex>::get(self.id)
+			.checked_add(1)
+			.ok_or("item id overflow")?
+			!= token_id
+		{
+			return Err("item id should be next".into());
+		}
+
+		<Module<T>>::create_item_internal(
+			&caller,
+			&self,
+			&to,
+			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(),
+			}),
+		)
+		.map_err(|_| "mint error")?;
+		Ok(true)
+	}
+
+	fn finish_minting(&mut self, _caller: caller) -> Result<bool> {
+		Err("not implementable".into())
+	}
+}
+
 #[solidity_interface(name = "ERC721UniqueExtensions")]
 impl<T: Config> CollectionHandle<T> {
 	#[solidity(rename_selector = "transfer")]
@@ -196,6 +292,13 @@
 			.map_err(|_| "transfer error")?;
 		Ok(())
 	}
+
+	fn next_token_id(&self) -> Result<uint256> {
+		Ok(ItemListIndex::get(self.id)
+			.checked_add(1)
+			.ok_or("item id overflow")?
+			.into())
+	}
 }
 
 #[solidity_interface(
@@ -205,7 +308,9 @@
 		ERC721,
 		ERC721Metadata,
 		ERC721Enumerable,
-		ERC721UniqueExtensions
+		ERC721UniqueExtensions,
+		ERC721Mintable,
+		ERC721Burnable,
 	)
 )]
 impl<T: Config> CollectionHandle<T> {}
@@ -297,3 +402,32 @@
 
 #[solidity_interface(name = "UniqueFungible", is(ERC165, ERC20))]
 impl<T: Config> CollectionHandle<T> {}
+
+macro_rules! generate_code {
+	($name:ident, $decl:ident, $is_impl:literal) => {
+		#[test]
+		#[ignore]
+		fn $name() {
+			use sp_std::collections::btree_set::BTreeSet;
+			let mut out = BTreeSet::new();
+			$decl::generate_solidity_interface(&mut out, $is_impl);
+			println!("=== SNIP START ===");
+			println!("// SPDX-License-Identifier: OTHER");
+			println!("// This code is automatically generated with `cargo test --package pallet-nft -- eth::erc::{} --exact --nocapture --ignored`", stringify!(name));
+			println!();
+			println!("pragma solidity >=0.8.0 <0.9.0;");
+			println!();
+			for b in out {
+				println!("{}", b);
+			}
+			println!("=== SNIP END ===");
+		}
+	};
+}
+
+// Not a tests, but code generators
+generate_code!(nft_impl, UniqueNFTCall, true);
+generate_code!(nft_iface, UniqueNFTCall, false);
+
+generate_code!(fungible_impl, UniqueFungibleCall, true);
+generate_code!(fungible_iface, UniqueFungibleCall, false);
modifiedpallets/nft/src/eth/stubs/ERC20.bindiffbeforeafterboth

binary blob — no preview

modifiedpallets/nft/src/eth/stubs/ERC20.soldiffbeforeafterboth
--- a/pallets/nft/src/eth/stubs/ERC20.sol
+++ b/pallets/nft/src/eth/stubs/ERC20.sol
@@ -1,69 +1,94 @@
 // SPDX-License-Identifier: OTHER
+// This code is automatically generated with `cargo test --package pallet-nft -- eth::erc::name --exact --nocapture --ignored`
 
 pragma solidity >=0.8.0 <0.9.0;
 
-contract ERC20 {
-	uint8 _dummy = 0;
-	string stub_error = "this contract does not exists, code for collections is implemented at pallet side";
+// Common stubs holder
+contract Dummy {
+	uint8 dummy;
+	string stub_error = "this contract is implemented in native";
+}
 
-	// 0x18160ddd
-	function totalSupply() external view returns (uint256) {
+// Inline
+contract ERC20Events {
+	event Transfer(address from, address to, uint256 value);
+	event Approval(address owner, address spender, uint256 value);
+}
+
+// Inline
+contract InlineNameSymbol is Dummy {
+	function name() public view returns (string memory) {
 		require(false, stub_error);
-		_dummy;
-		return 0;
+		dummy;
+		return "";
 	}
+	function symbol() public view returns (string memory) {
+		require(false, stub_error);
+		dummy;
+		return "";
+	}
+}
 
-	// 0x70a08231
-	function balanceOf(address account) external view returns (uint256) {
+// Inline
+contract InlineTotalSupply is Dummy {
+	function totalSupply() public view returns (uint256) {
 		require(false, stub_error);
-		account;
-		_dummy;
+		dummy;
 		return 0;
 	}
+}
 
-	// 0xa9059cbb
-	function transfer(address recipient, uint256 amount) external returns (bool) {
+contract ERC165 is Dummy {
+	function supportsInterface(uint32 interfaceId) public view returns (bool) {
 		require(false, stub_error);
-		recipient;
-		amount;
-		_dummy = 0;
+		interfaceId;
+		dummy;
 		return false;
 	}
+}
 
-	// 0xdd62ed3e
-	function allowance(address owner, address spender) external view returns (uint256) {
+contract ERC20 is Dummy, InlineNameSymbol, InlineTotalSupply, ERC20Events {
+	function decimals() public view returns (uint8) {
 		require(false, stub_error);
+		dummy;
+		return 0;
+	}
+	function balanceOf(address owner) public view returns (uint256) {
+		require(false, stub_error);
 		owner;
-		spender;
-		return _dummy;
+		dummy;
+		return 0;
+	}
+	function transfer(address to, uint256 amount) public returns (bool) {
+		require(false, stub_error);
+		to;
+		amount;
+		dummy = 0;
+		return false;
 	}
-
-	// 0x095ea7b3
-	function approve(address spender, uint256 amount) external returns (bool) {
+	function transferFrom(address from, address to, uint256 amount) public returns (bool) {
 		require(false, stub_error);
-		spender;
+		from;
+		to;
 		amount;
-		_dummy = 0;
+		dummy = 0;
 		return false;
 	}
-
-	// 0x23b872dd
-	function transferFrom(address sender, address recipient, uint256 amount) external returns (bool) {
+	function approve(address spender, uint256 amount) public returns (bool) {
 		require(false, stub_error);
-		sender;
-		recipient;
+		spender;
 		amount;
-		_dummy = 0;
+		dummy = 0;
 		return false;
 	}
+	function allowance(address owner, address spender) public view returns (uint256) {
+		require(false, stub_error);
+		owner;
+		spender;
+		dummy;
+		return 0;
+	}
+}
 
-	// While ERC165 is not required by spec of ERC20, better implement it
-	// 0x01ffc9a7
-	function supportsInterface(bytes4 interfaceID) public pure returns (bool) {
-		return 
-			// ERC20
-			interfaceID == 0x36372b07 || 
-			// ERC165
-			interfaceID == 0x01ffc9a7;
-	}
+contract UniqueFungible is Dummy, ERC165, ERC20 {
 }
\ No newline at end of file
modifiedpallets/nft/src/eth/stubs/ERC721.bindiffbeforeafterboth

binary blob — no preview

modifiedpallets/nft/src/eth/stubs/ERC721.soldiffbeforeafterboth
--- a/pallets/nft/src/eth/stubs/ERC721.sol
+++ b/pallets/nft/src/eth/stubs/ERC721.sol
@@ -1,163 +1,194 @@
 // SPDX-License-Identifier: OTHER
+// This code is automatically generated with `cargo test --package pallet-nft -- eth::erc::name --exact --nocapture --ignored`
 
 pragma solidity >=0.8.0 <0.9.0;
 
-contract ERC721 {
-    uint8 _dummy = 0;
-    address _dummy_addr = 0x0000000000000000000000000000000000000000;
-    string _dummy_string = "";
-    string stub_error =
-        "this contract does not exists, code for collections is implemented at pallet side";
+// Common stubs holder
+contract Dummy {
+	uint8 dummy;
+	string stub_error = "this contract is implemented in native";
+}
 
-    event Transfer(
-        address indexed from,
-        address indexed to,
-        uint256 indexed tokenId
-    );
+// Inline
+contract ERC721Events {
+	event Transfer(address from, address to, uint256 tokenId);
+	event Approval(address owner, address approved, uint256 tokenId);
+	event ApprovalForAll(address owner, address operator, bool approved);
+}
 
-    event Approval(
-        address indexed owner,
-        address indexed approved,
-        uint256 indexed tokenId
-    );
+// Inline
+contract ERC721MintableEvents {
+	event MintingFinished();
+}
 
-    event ApprovalForAll(
-        address indexed owner,
-        address indexed operator,
-        bool approved
-    );
+// Inline
+contract InlineNameSymbol is Dummy {
+	function name() public view returns (string memory) {
+		require(false, stub_error);
+		dummy;
+		return "";
+	}
+	function symbol() public view returns (string memory) {
+		require(false, stub_error);
+		dummy;
+		return "";
+	}
+}
 
-	// 0x18160ddd
-    function totalSupply() external view returns (uint256) {
-        require(false, stub_error);
-        return 0;
-    }
+// Inline
+contract InlineTotalSupply is Dummy {
+	function totalSupply() public view returns (uint256) {
+		require(false, stub_error);
+		dummy;
+		return 0;
+	}
+}
 
-    function name() external view returns (string memory res_name) {
-        require(false, stub_error);
-        res_name = _dummy_string;
-    }
+contract ERC165 is Dummy {
+	function supportsInterface(uint32 interfaceId) public view returns (bool) {
+		require(false, stub_error);
+		interfaceId;
+		dummy;
+		return false;
+	}
+}
 
-    function symbol() external view returns (string memory res_symbol) {
-        require(false, stub_error);
-        res_symbol = _dummy_string;
-    }
+contract ERC721 is Dummy, ERC165, ERC721Events {
+	function balanceOf(address owner) public view returns (uint256) {
+		require(false, stub_error);
+		owner;
+		dummy;
+		return 0;
+	}
+	function ownerOf(uint256 tokenId) public view returns (address) {
+		require(false, stub_error);
+		tokenId;
+		dummy;
+		return 0x0000000000000000000000000000000000000000;
+	}
+	function safeTransferFromWithData(address from, address to, uint256 tokenId, bytes memory data) public {
+		require(false, stub_error);
+		from;
+		to;
+		tokenId;
+		data;
+		dummy = 0;
+	}
+	function safeTransferFrom(address from, address to, uint256 tokenId) public {
+		require(false, stub_error);
+		from;
+		to;
+		tokenId;
+		dummy = 0;
+	}
+	function transferFrom(address from, address to, uint256 tokenId) public {
+		require(false, stub_error);
+		from;
+		to;
+		tokenId;
+		dummy = 0;
+	}
+	function approve(address approved, uint256 tokenId) public {
+		require(false, stub_error);
+		approved;
+		tokenId;
+		dummy = 0;
+	}
+	function setApprovalForAll(address operator, bool approved) public {
+		require(false, stub_error);
+		operator;
+		approved;
+		dummy = 0;
+	}
+	function getApproved(uint256 tokenId) public view returns (address) {
+		require(false, stub_error);
+		tokenId;
+		dummy;
+		return 0x0000000000000000000000000000000000000000;
+	}
+	function isApprovedForAll(address owner, address operator) public view returns (address) {
+		require(false, stub_error);
+		owner;
+		operator;
+		dummy;
+		return 0x0000000000000000000000000000000000000000;
+	}
+}
 
-    function tokenURI(uint256 tokenId) external view returns (string memory) {
-        require(false, stub_error);
-        tokenId;
-        return _dummy_string;
-    }
+contract ERC721Burnable is Dummy {
+	function burn(uint256 tokenId) public {
+		require(false, stub_error);
+		tokenId;
+		dummy = 0;
+	}
+}
 
-    function tokenByIndex(uint256 index) external view returns (uint256) {
-        require(false, stub_error);
-        index;
+contract ERC721Enumerable is Dummy, InlineTotalSupply {
+	function tokenByIndex(uint256 index) public view returns (uint256) {
+		require(false, stub_error);
+		index;
+		dummy;
 		return 0;
-    }
-
-    function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256) {
-        require(false, stub_error);
+	}
+	function tokenOfOwnerByIndex(address owner, uint256 index) public view returns (uint256) {
+		require(false, stub_error);
 		owner;
 		index;
+		dummy;
 		return 0;
-    }
+	}
+}
 
-    // 0x70a08231
-    function balanceOf(address owner) external view returns (uint256) {
-        require(false, stub_error);
-        owner;
-        return 0;
-    }
+contract ERC721Metadata is Dummy, InlineNameSymbol {
+	function tokenURI(uint256 tokenId) public view returns (string memory) {
+		require(false, stub_error);
+		tokenId;
+		dummy;
+		return "";
+	}
+}
 
-    // 0x6352211e
-    function ownerOf(uint256 tokenId) external view returns (address) {
-        require(false, stub_error);
-        tokenId;
-        return _dummy_addr;
-    }
+contract ERC721Mintable is Dummy, ERC721MintableEvents {
+	function mintingFinished() public view returns (bool) {
+		require(false, stub_error);
+		dummy;
+		return false;
+	}
+	function mint(address to, uint256 tokenId) public returns (bool) {
+		require(false, stub_error);
+		to;
+		tokenId;
+		dummy = 0;
+		return false;
+	}
+	function mintWithTokenURI(address to, uint256 tokenId, string memory tokenUri) public returns (bool) {
+		require(false, stub_error);
+		to;
+		tokenId;
+		tokenUri;
+		dummy = 0;
+		return false;
+	}
+	function finishMinting() public returns (bool) {
+		require(false, stub_error);
+		dummy = 0;
+		return false;
+	}
+}
 
-    // 0xb88d4fde
-    function safeTransferFrom(
-        address from,
-        address to,
-        uint256 tokenId,
-        bytes calldata data
-    ) external payable {
-        require(false, stub_error);
-        from;
-        to;
-        tokenId;
-        data;
-    }
-
-    // 0x42842e0e
-    function safeTransferFrom(
-        address from,
-        address to,
-        uint256 tokenId
-    ) external payable {
-        require(false, stub_error);
-        from;
-        to;
-        tokenId;
-    }
-
-    // 0x23b872dd
-    function transferFrom(
-        address from,
-        address to,
-        uint256 tokenId
-    ) external payable {
-        require(false, stub_error);
-        from;
-        to;
-        tokenId;
-    }
+contract ERC721UniqueExtensions is Dummy {
+	function transfer(address to, uint256 tokenId) public {
+		require(false, stub_error);
+		to;
+		tokenId;
+		dummy = 0;
+	}
+	function nextTokenId() public view returns (uint256) {
+		require(false, stub_error);
+		dummy;
+		return 0;
+	}
+}
 
-    // 0x095ea7b3
-    function approve(address approved, uint256 tokenId) external payable {
-        require(false, stub_error);
-        approved;
-        tokenId;
-    }
-
-    // 0xa22cb465
-    function setApprovalForAll(address operator, bool approved) external {
-        require(false, stub_error);
-        operator;
-        approved;
-        _dummy = 0;
-    }
-
-    // 0x081812fc
-    function getApproved(uint256 tokenId) external view returns (address) {
-        require(false, stub_error);
-        tokenId;
-        return _dummy_addr;
-    }
-
-    // 0xe985e9c5
-    function isApprovedForAll(address owner, address operator)
-        external
-        view
-        returns (bool)
-    {
-        require(false, stub_error);
-        owner;
-        operator;
-        return false;
-    }
-
-    // 0x01ffc9a7
-    function supportsInterface(bytes4 interfaceID) public pure returns (bool) {
-        return
-            // ERC721
-            interfaceID == 0x80ac58cd ||
-            // ERC721Metadata
-            interfaceID == 0x5b5e139f ||
-            // ERC721Enumerable
-            interfaceID == 0x780e9d63 ||
-            // ERC165
-            interfaceID == 0x01ffc9a7;
-    }
-}
+contract UniqueNFT is Dummy, ERC165, ERC721, ERC721Metadata, ERC721Enumerable, ERC721UniqueExtensions, ERC721Mintable, ERC721Burnable {
+}
\ No newline at end of file
modifiedpallets/nft/src/lib.rsdiffbeforeafterboth
--- a/pallets/nft/src/lib.rs
+++ b/pallets/nft/src/lib.rs
@@ -177,6 +177,8 @@
 		OutOfGas,
 		/// Collection settings not allowing items transferring
 		TransferNotAllowed,
+		/// Can't transfer tokens to ethereum zero address
+		AddressIsZero,
 	}
 }
 
@@ -1248,6 +1250,11 @@
 		owner: &T::CrossAccountId,
 		data: CreateItemData,
 	) -> DispatchResult {
+		ensure!(
+			owner != &T::CrossAccountId::from_eth(H160([0; 20])),
+			Error::<T>::AddressIsZero
+		);
+
 		Self::can_create_items_in_collection(collection, sender, owner, 1)?;
 		Self::validate_create_item_args(collection, &data)?;
 		Self::create_item_no_validation(collection, owner, data)?;
@@ -1262,6 +1269,11 @@
 		item_id: TokenId,
 		value: u128,
 	) -> DispatchResult {
+		ensure!(
+			recipient != &T::CrossAccountId::from_eth(H160([0; 20])),
+			Error::<T>::AddressIsZero
+		);
+
 		// Limits check
 		Self::is_correct_transfer(target_collection, recipient)?;
 
@@ -1829,6 +1841,11 @@
 		<NftItemList<T>>::remove(collection_id, item_id);
 		<VariableMetaDataBasket<T>>::remove(collection_id, item_id);
 
+		collection.log(ERC721Events::Transfer {
+			from: *item.owner.as_eth(),
+			to: H160::default(),
+			token_id: item_id.into(),
+		})?;
 		Self::deposit_event(RawEvent::ItemDestroyed(collection.id, item_id));
 		Ok(())
 	}
modifiedtests/src/eth/nonFungible.test.tsdiffbeforeafterboth
--- a/tests/src/eth/nonFungible.test.ts
+++ b/tests/src/eth/nonFungible.test.ts
@@ -4,11 +4,13 @@
 //
 
 import privateKey from '../substrate/privateKey';
-import { approveExpectSuccess, createCollectionExpectSuccess, createItemExpectSuccess, transferExpectSuccess, transferFromExpectSuccess, UNIQUE } from '../util/helpers';
+import { approveExpectSuccess, burnItemExpectSuccess, createCollectionExpectSuccess, createItemExpectSuccess, transferExpectSuccess, transferFromExpectSuccess, UNIQUE } from '../util/helpers';
 import { collectionIdToAddress, createEthAccount, createEthAccountWithBalance, GAS_ARGS, itWeb3, normalizeEvents, recordEthFee, recordEvents, subToEth, transferBalanceToEth } from './util/helpers';
+import { evmToAddress } from '@polkadot/util-crypto';
 import nonFungibleAbi from './nonFungibleAbi.json';
 import { expect } from 'chai';
 import waitNewBlocks from '../substrate/wait-new-blocks';
+import { submitTransactionAsync } from '../substrate/substrate-api';
 
 describe('NFT: Information getting', () => {
   itWeb3('totalSupply', async ({ api, web3 }) => {
@@ -64,6 +66,78 @@
 });
 
 describe('NFT: Plain calls', () => {
+  itWeb3('Can perform mint()', 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.mintWithTokenURI(
+        receiver,
+        nextTokenId,
+        'Test URI',
+      ).send({from: caller});
+      const events = normalizeEvents(result.events);
+
+      expect(events).to.be.deep.equal([
+        {
+          address,
+          event: 'Transfer',
+          args: {
+            from: '0x0000000000000000000000000000000000000000',
+            to: receiver,
+            tokenId: nextTokenId,
+          },
+        },
+      ]);
+
+      await waitNewBlocks(api, 1);
+      expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI');
+    }
+  });
+
+  itWeb3('Can perform burn()', async ({ web3, api }) => {
+    const collection = await createCollectionExpectSuccess({
+      mode: {type: 'NFT'},
+    });
+    const alice = privateKey('//Alice');
+
+    const owner = await createEthAccountWithBalance(api, web3);
+
+    const tokenId = 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});
+
+    {
+      const result = await contract.methods.burn(tokenId).send({ from: owner });
+      const events = normalizeEvents(result.events);
+      
+      expect(events).to.be.deep.equal([
+        {
+          address,
+          event: 'Transfer',
+          args: {
+            from: owner,
+            to: '0x0000000000000000000000000000000000000000',
+            tokenId: tokenId.toString(),
+          },
+        },
+      ]);
+    }
+  });
+
   itWeb3('Can perform approve()', async ({ web3, api }) => {
     const collection = await createCollectionExpectSuccess({
       mode: { type: 'NFT' },
@@ -251,6 +325,60 @@
 });
 
 describe('NFT: Substrate calls', () => {
+  itWeb3('Events emitted for mint()', async ({ web3 }) => {
+    const collection = await createCollectionExpectSuccess({
+      mode: { type: 'NFT' },
+    });
+    const alice = privateKey('//Alice');
+
+    const address = collectionIdToAddress(collection);
+    const contract = new web3.eth.Contract(nonFungibleAbi as any, address);
+
+    let tokenId: number;
+    const events = await recordEvents(contract, async () => {
+      tokenId = await createItemExpectSuccess(alice, collection, 'NFT');
+    });
+
+    expect(events).to.be.deep.equal([
+      {
+        address,
+        event: 'Transfer',
+        args: {
+          from: '0x0000000000000000000000000000000000000000',
+          to: subToEth(alice.address),
+          tokenId: tokenId!.toString(),
+        },
+      },
+    ]);
+  });
+
+  itWeb3('Events emitted for burn()', async ({ web3 }) => {
+    const collection = await createCollectionExpectSuccess({
+      mode: { type: 'NFT' },
+    });
+    const alice = privateKey('//Alice');
+
+    const address = collectionIdToAddress(collection);
+    const contract = new web3.eth.Contract(nonFungibleAbi as any, address);
+
+    const tokenId = await createItemExpectSuccess(alice, collection, 'NFT');
+    const events = await recordEvents(contract, async () => {
+      await burnItemExpectSuccess(alice, collection, tokenId);
+    });
+
+    expect(events).to.be.deep.equal([
+      {
+        address,
+        event: 'Transfer',
+        args: {
+          from: subToEth(alice.address),
+          to: '0x0000000000000000000000000000000000000000',
+          tokenId: tokenId.toString(),
+        },
+      },
+    ]);
+  });
+
   itWeb3('Events emitted for approve()', async ({ web3 }) => {
     const collection = await createCollectionExpectSuccess({
       mode: { type: 'NFT' },
@@ -342,4 +470,4 @@
       },
     ]);
   });
-});
\ No newline at end of file
+});
modifiedtests/src/eth/nonFungibleAbi.jsondiffbeforeafterboth
49 "name": "ApprovalForAll",49 "name": "ApprovalForAll",
50 "type": "event"50 "type": "event"
51 },51 },
52 {
53 "anonymous": true,
54 "inputs": [],
55 "name": "MintingFinished",
56 "type": "event"
57 },
52 {58 {
53 "anonymous": false,59 "anonymous": false,
54 "inputs": [60 "inputs": [
89 ],95 ],
90 "name": "approve",96 "name": "approve",
91 "outputs": [],97 "outputs": [],
92 "stateMutability": "payable",98 "stateMutability": "nonpayable",
93 "type": "function"99 "type": "function"
94 },100 },
95 {101 {
111 "stateMutability": "view",117 "stateMutability": "view",
112 "type": "function"118 "type": "function"
113 },119 },
120 {
121 "inputs": [
122 {
123 "internalType": "uint256",
124 "name": "tokenId",
125 "type": "uint256"
126 }
127 ],
128 "name": "burn",
129 "outputs": [],
130 "stateMutability": "nonpayable",
131 "type": "function"
132 },
133 {
134 "inputs": [],
135 "name": "finishMinting",
136 "outputs": [
137 {
138 "internalType": "bool",
139 "name": "",
140 "type": "bool"
141 }
142 ],
143 "stateMutability": "nonpayable",
144 "type": "function"
145 },
114 {146 {
115 "inputs": [147 "inputs": [
116 {148 {
130 "stateMutability": "view",162 "stateMutability": "view",
131 "type": "function"163 "type": "function"
132 },164 },
133 {165 {
134 "inputs": [166 "inputs": [
135 {167 {
136 "internalType": "address",168 "internalType": "address",
144 }176 }
145 ],177 ],
146 "name": "isApprovedForAll",178 "name": "isApprovedForAll",
179 "outputs": [
180 {
181 "internalType": "address",
182 "name": "",
183 "type": "address"
184 }
185 ],
186 "stateMutability": "view",
187 "type": "function"
188 },
189 {
190 "inputs": [
191 {
192 "internalType": "address",
193 "name": "to",
194 "type": "address"
195 },
196 {
197 "internalType": "uint256",
198 "name": "tokenId",
199 "type": "uint256"
200 }
201 ],
202 "name": "mint",
147 "outputs": [203 "outputs": [
148 {204 {
149 "internalType": "bool",205 "internalType": "bool",
150 "name": "",206 "name": "",
151 "type": "bool"207 "type": "bool"
152 }208 }
153 ],209 ],
210 "stateMutability": "nonpayable",
211 "type": "function"
212 },
213 {
214 "inputs": [
215 {
216 "internalType": "address",
217 "name": "to",
218 "type": "address"
219 },
220 {
221 "internalType": "uint256",
222 "name": "tokenId",
223 "type": "uint256"
224 },
225 {
226 "internalType": "string",
227 "name": "tokenURI",
228 "type": "string"
229 }
230 ],
231 "name": "mintWithTokenURI",
232 "outputs": [
233 {
234 "internalType": "bool",
235 "name": "",
236 "type": "bool"
237 }
238 ],
239 "stateMutability": "nonpayable",
240 "type": "function"
241 },
242 {
243 "inputs": [],
244 "name": "mintingFinished",
245 "outputs": [
246 {
247 "internalType": "bool",
248 "name": "",
249 "type": "bool"
250 }
251 ],
154 "stateMutability": "view",252 "stateMutability": "view",
155 "type": "function"253 "type": "function"
156 },254 },
157 {255 {
158 "inputs": [],256 "inputs": [],
159 "name": "name",257 "name": "name",
160 "outputs": [258 "outputs": [
161 {259 {
162 "internalType": "string",260 "internalType": "string",
163 "name": "res_name",261 "name": "",
164 "type": "string"262 "type": "string"
165 }263 }
166 ],264 ],
167 "stateMutability": "view",265 "stateMutability": "view",
168 "type": "function"266 "type": "function"
169 },267 },
268 {
269 "inputs": [],
270 "name": "nextTokenId",
271 "outputs": [
272 {
273 "internalType": "uint256",
274 "name": "",
275 "type": "uint256"
276 }
277 ],
278 "stateMutability": "view",
279 "type": "function"
280 },
170 {281 {
171 "inputs": [282 "inputs": [
172 {283 {
206 ],317 ],
207 "name": "safeTransferFrom",318 "name": "safeTransferFrom",
208 "outputs": [],319 "outputs": [],
209 "stateMutability": "payable",320 "stateMutability": "nonpayable",
210 "type": "function"321 "type": "function"
211 },322 },
212 {323 {
232 "type": "bytes"343 "type": "bytes"
233 }344 }
234 ],345 ],
235 "name": "safeTransferFrom",346 "name": "safeTransferFromWithData",
236 "outputs": [],347 "outputs": [],
237 "stateMutability": "payable",348 "stateMutability": "nonpayable",
238 "type": "function"349 "type": "function"
239 },350 },
240 {351 {
258 {369 {
259 "inputs": [370 "inputs": [
260 {371 {
261 "internalType": "bytes4",372 "internalType": "uint32",
262 "name": "interfaceID",373 "name": "interfaceId",
263 "type": "bytes4"374 "type": "uint32"
264 }375 }
265 ],376 ],
266 "name": "supportsInterface",377 "name": "supportsInterface",
271 "type": "bool"382 "type": "bool"
272 }383 }
273 ],384 ],
274 "stateMutability": "pure",385 "stateMutability": "view",
275 "type": "function"386 "type": "function"
276 },387 },
277 {388 {
280 "outputs": [391 "outputs": [
281 {392 {
282 "internalType": "string",393 "internalType": "string",
283 "name": "res_symbol",394 "name": "",
284 "type": "string"395 "type": "string"
285 }396 }
286 ],397 ],
364 },475 },
365 {476 {
366 "inputs": [477 "inputs": [
367 {
368 "internalType": "address",
369 "name": "from",
370 "type": "address"
371 },
372 {478 {
373 "internalType": "address",479 "internalType": "address",
374 "name": "to",480 "name": "to",
380 "type": "uint256"486 "type": "uint256"
381 }487 }
382 ],488 ],
383 "name": "transferFrom",489 "name": "transfer",
384 "outputs": [],490 "outputs": [],
385 "stateMutability": "payable",491 "stateMutability": "nonpayable",
386 "type": "function"492 "type": "function"
387 },493 },
388 {494 {
389 "inputs": [495 "inputs": [
496 {
497 "internalType": "address",
498 "name": "from",
499 "type": "address"
500 },
390 {501 {
391 "internalType": "address",502 "internalType": "address",
392 "name": "to",503 "name": "to",
398 "type": "uint256"509 "type": "uint256"
399 }510 }
400 ],511 ],
401 "name": "transfer",512 "name": "transferFrom",
402 "outputs": [],513 "outputs": [],
403 "stateMutability": "payable",514 "stateMutability": "nonpayable",
404 "type": "function"515 "type": "function"
405 }516 }
406]517]