--- 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", --- 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 { + 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! { - >::new(#name) + >::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, 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 { --- 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! { + >::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, 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] --- 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(&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::from_utf8(self.bytes()?).map_err(|_| Error::Error(ExitError::InvalidRange)) + } pub fn uint32(&mut self) -> Result { Ok(u32::from_be_bytes(self.read_padleft()?)) @@ -103,9 +118,13 @@ fn subresult(&mut self) -> Result> { 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, 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"); + } +} --- 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 SolidityFunctions for SolidityFunction { - 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 { pub name: &'static str, + pub is: &'static [&'static str], pub functions: F, } impl SolidityInterface { - 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 { + pub name: &'static str, + pub args: A, +} + +impl SolidityFunctions for SolidityEvent { + 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, ");") + } +} --- 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 CollectionHandle { @@ -42,9 +45,15 @@ #[solidity_interface(name = "ERC721Metadata", inline_is(InlineNameSymbol))] impl CollectionHandle { + #[solidity(rename_selector = "tokenURI")] fn token_uri(&self, token_id: uint256) -> Result { - // 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( + &>::get(self.id, token_id) + .ok_or("token not found")? + .const_data, + ) + .into()) } } @@ -178,6 +187,93 @@ } } +#[solidity_interface(name = "ERC721Burnable")] +impl CollectionHandle { + fn burn(&mut self, caller: caller, token_id: uint256) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + let token_id = token_id.try_into().map_err(|_| "amount overflow")?; + + >::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 CollectionHandle { + fn minting_finished(&self) -> Result { + Ok(false) + } + + fn mint(&mut self, caller: caller, to: address, token_id: uint256) -> Result { + 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 ::get(self.id) + .checked_add(1) + .ok_or("item id overflow")? + != token_id + { + return Err("item id should be next".into()); + } + + >::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 { + 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 ::get(self.id) + .checked_add(1) + .ok_or("item id overflow")? + != token_id + { + return Err("item id should be next".into()); + } + + >::create_item_internal( + &caller, + &self, + &to, + CreateItemData::NFT(CreateNftData { + const_data: Vec::::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 { + Err("not implementable".into()) + } +} + #[solidity_interface(name = "ERC721UniqueExtensions")] impl CollectionHandle { #[solidity(rename_selector = "transfer")] @@ -196,6 +292,13 @@ .map_err(|_| "transfer error")?; Ok(()) } + + fn next_token_id(&self) -> Result { + 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 CollectionHandle {} @@ -297,3 +402,32 @@ #[solidity_interface(name = "UniqueFungible", is(ERC165, ERC20))] impl CollectionHandle {} + +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); --- 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 --- 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 --- 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::::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::::AddressIsZero + ); + // Limits check Self::is_correct_transfer(target_collection, recipient)?; @@ -1829,6 +1841,11 @@ >::remove(collection_id, item_id); >::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(()) } --- 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 +}); --- a/tests/src/eth/nonFungibleAbi.json +++ b/tests/src/eth/nonFungibleAbi.json @@ -50,6 +50,12 @@ "type": "event" }, { + "anonymous": true, + "inputs": [], + "name": "MintingFinished", + "type": "event" + }, + { "anonymous": false, "inputs": [ { @@ -89,7 +95,7 @@ ], "name": "approve", "outputs": [], - "stateMutability": "payable", + "stateMutability": "nonpayable", "type": "function" }, { @@ -119,6 +125,32 @@ "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": [ { @@ -146,11 +178,77 @@ "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" }, @@ -160,7 +258,7 @@ "outputs": [ { "internalType": "string", - "name": "res_name", + "name": "", "type": "string" } ], @@ -168,6 +266,19 @@ "type": "function" }, { + "inputs": [], + "name": "nextTokenId", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { "inputs": [ { "internalType": "uint256", @@ -206,7 +317,7 @@ ], "name": "safeTransferFrom", "outputs": [], - "stateMutability": "payable", + "stateMutability": "nonpayable", "type": "function" }, { @@ -232,9 +343,9 @@ "type": "bytes" } ], - "name": "safeTransferFrom", + "name": "safeTransferFromWithData", "outputs": [], - "stateMutability": "payable", + "stateMutability": "nonpayable", "type": "function" }, { @@ -258,9 +369,9 @@ { "inputs": [ { - "internalType": "bytes4", - "name": "interfaceID", - "type": "bytes4" + "internalType": "uint32", + "name": "interfaceId", + "type": "uint32" } ], "name": "supportsInterface", @@ -271,7 +382,7 @@ "type": "bool" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { @@ -280,7 +391,7 @@ "outputs": [ { "internalType": "string", - "name": "res_symbol", + "name": "", "type": "string" } ], @@ -366,11 +477,6 @@ "inputs": [ { "internalType": "address", - "name": "from", - "type": "address" - }, - { - "internalType": "address", "name": "to", "type": "address" }, @@ -380,15 +486,20 @@ "type": "uint256" } ], - "name": "transferFrom", + "name": "transfer", "outputs": [], - "stateMutability": "payable", + "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", "name": "to", "type": "address" }, @@ -398,9 +509,9 @@ "type": "uint256" } ], - "name": "transfer", + "name": "transferFrom", "outputs": [], - "stateMutability": "payable", + "stateMutability": "nonpayable", "type": "function" } ] \ No newline at end of file