difftreelog
Merge pull request #160 from UniqueNetwork/feature/erc-mintable-burnable
in: master
Implement ERC721Burnable/ERC721Mintable
13 files changed
Cargo.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",
crates/evm-coder-macros/src/solidity_interface.rsdiffbeforeafterboth1#![allow(dead_code)]23use quote::quote;4use darling::FromMeta;5use inflector::cases;6use std::fmt::Write;7use syn::{8 FnArg, Generics, Ident, ImplItem, ImplItemMethod, ItemImpl, Meta, NestedMeta, PatType, Path,9 ReturnType, Type, spanned::Spanned,10};1112use crate::{13 fn_selector_str, parse_ident_from_pat, parse_ident_from_path, parse_ident_from_type,14 parse_result_ok, pascal_ident_to_call, pascal_ident_to_snake_call, snake_ident_to_pascal,15 snake_ident_to_screaming,16};1718struct Is {19 name: Ident,20 pascal_call_name: Ident,21 snake_call_name: Ident,22}23impl Is {24 fn try_from(path: &Path) -> syn::Result<Self> {25 let name = parse_ident_from_path(path, false)?.clone();26 Ok(Self {27 pascal_call_name: pascal_ident_to_call(&name),28 snake_call_name: pascal_ident_to_snake_call(&name),29 name,30 })31 }3233 fn expand_call_def(&self) -> proc_macro2::TokenStream {34 let name = &self.name;35 let pascal_call_name = &self.pascal_call_name;36 quote! {37 #name(#pascal_call_name)38 }39 }4041 fn expand_interface_id(&self) -> proc_macro2::TokenStream {42 let pascal_call_name = &self.pascal_call_name;43 quote! {44 interface_id ^= #pascal_call_name::interface_id();45 }46 }4748 fn expand_supports_interface(&self) -> proc_macro2::TokenStream {49 let pascal_call_name = &self.pascal_call_name;50 quote! {51 #pascal_call_name::supports_interface(interface_id)52 }53 }5455 fn expand_variant_call(&self) -> proc_macro2::TokenStream {56 let name = &self.name;57 let pascal_call_name = &self.pascal_call_name;58 quote! {59 InternalCall::#name(call) => return <Self as ::evm_coder::Callable<#pascal_call_name>>::call(self, Msg {60 call,61 caller: c.caller,62 value: c.value,63 })64 }65 }6667 fn expand_parse(&self) -> proc_macro2::TokenStream {68 let name = &self.name;69 let pascal_call_name = &self.pascal_call_name;70 quote! {71 if let Some(parsed_call) = #pascal_call_name::parse(method_id, reader)? {72 return Ok(Some(Self::#name(parsed_call)))73 }74 }75 }76}7778#[derive(Default)]79struct IsList(Vec<Is>);80impl FromMeta for IsList {81 fn from_list(items: &[NestedMeta]) -> darling::Result<Self> {82 let mut out = Vec::new();83 for item in items {84 match item {85 NestedMeta::Meta(Meta::Path(path)) => out.push(Is::try_from(path)?),86 _ => return Err(syn::Error::new(item.span(), "expected path").into()),87 }88 }89 Ok(Self(out))90 }91}9293#[derive(FromMeta)]94pub struct InterfaceInfo {95 name: Ident,96 #[darling(default)]97 is: IsList,98 #[darling(default)]99 inline_is: IsList,100 #[darling(default)]101 events: IsList,102}103104#[derive(FromMeta)]105struct MethodInfo {106 #[darling(default)]107 rename_selector: Option<String>,108}109110struct MethodArg {111 name: Ident,112 ty: Ident,113}114impl MethodArg {115 fn try_from(value: &PatType) -> syn::Result<Self> {116 Ok(Self {117 name: parse_ident_from_pat(&value.pat)?.clone(),118 ty: parse_ident_from_type(&value.ty, false)?.clone(),119 })120 }121 fn is_value(&self) -> bool {122 self.ty == "value"123 }124 fn is_caller(&self) -> bool {125 self.ty == "caller"126 }127 fn is_special(&self) -> bool {128 self.is_value() || self.is_caller()129 }130 fn selector_ty(&self) -> &Ident {131 assert!(!self.is_special());132 &self.ty133 }134135 fn expand_call_def(&self) -> proc_macro2::TokenStream {136 assert!(!self.is_special());137 let name = &self.name;138 let ty = &self.ty;139140 quote! {141 #name: #ty142 }143 }144145 fn expand_parse(&self) -> proc_macro2::TokenStream {146 assert!(!self.is_special());147 let name = &self.name;148 quote! {149 #name: reader.abi_read()?150 }151 }152153 fn expand_call_arg(&self) -> proc_macro2::TokenStream {154 if self.is_value() {155 quote! {156 c.value.clone()157 }158 } else if self.is_caller() {159 quote! {160 c.caller.clone()161 }162 } else {163 let name = &self.name;164 quote! {165 #name166 }167 }168 }169170 fn expand_solidity_argument(&self) -> proc_macro2::TokenStream {171 let name = &self.name.to_string();172 let ty = &self.ty;173 quote! {174 <NamedArgument<#ty>>::new(#name)175 }176 }177}178179#[derive(PartialEq)]180enum Mutability {181 Mutable,182 View,183 Pure,184}185186struct Method {187 name: Ident,188 camel_name: String,189 pascal_name: Ident,190 screaming_name: Ident,191 selector_str: String,192 selector: u32,193 args: Vec<MethodArg>,194 has_normal_args: bool,195 mutability: Mutability,196 result: Type,197}198impl Method {199 fn try_from(value: &ImplItemMethod) -> syn::Result<Self> {200 let mut info = MethodInfo {201 rename_selector: None,202 };203 for attr in &value.attrs {204 let ident = parse_ident_from_path(&attr.path, false)?;205 if ident == "solidity" {206 let args = attr.parse_meta().unwrap();207 info = MethodInfo::from_meta(&args).unwrap();208 } else if ident == "doc" {209 // TODO: Add docs to evm interfaces210 }211 }212 let ident = &value.sig.ident;213 let ident_str = ident.to_string();214 if !cases::snakecase::is_snake_case(&ident_str) {215 return Err(syn::Error::new(ident.span(), "method name should be snake_cased\nif alternative solidity name needs to be set - use #[solidity] attribute"));216 }217218 let mut mutability = Mutability::Pure;219220 if let Some(FnArg::Receiver(receiver)) = value221 .sig222 .inputs223 .iter()224 .find(|arg| matches!(arg, FnArg::Receiver(_)))225 {226 if receiver.reference.is_none() {227 return Err(syn::Error::new(228 receiver.span(),229 "receiver should be by ref",230 ));231 }232 if receiver.mutability.is_some() {233 mutability = Mutability::Mutable;234 } else {235 mutability = Mutability::View;236 }237 }238 let mut args = Vec::new();239 for typ in value240 .sig241 .inputs242 .iter()243 .filter(|arg| matches!(arg, FnArg::Typed(_)))244 {245 let typ = match typ {246 FnArg::Typed(typ) => typ,247 _ => unreachable!(),248 };249 args.push(MethodArg::try_from(typ)?);250 }251252 if mutability != Mutability::Mutable && args.iter().any(|arg| arg.is_value()) {253 return Err(syn::Error::new(254 args.iter().find(|arg| arg.is_value()).unwrap().ty.span(),255 "payable function should be mutable",256 ));257 }258259 let result = match &value.sig.output {260 ReturnType::Type(_, ty) => ty,261 _ => return Err(syn::Error::new(value.sig.output.span(), "interface method should return Result<value>\nif there is no value to return - specify void (which is alias to unit)")),262 };263 let result = parse_result_ok(result)?;264265 let camel_name = info266 .rename_selector267 .unwrap_or_else(|| cases::camelcase::to_camel_case(&ident.to_string()));268 let mut selector_str = camel_name.clone();269 selector_str.push('(');270 let mut has_normal_args = false;271 for (i, arg) in args.iter().filter(|arg| !arg.is_special()).enumerate() {272 if i != 0 {273 selector_str.push(',');274 }275 write!(selector_str, "{}", arg.selector_ty()).unwrap();276 has_normal_args = true;277 }278 selector_str.push(')');279 let selector = fn_selector_str(&selector_str);280281 Ok(Self {282 name: ident.clone(),283 camel_name,284 pascal_name: snake_ident_to_pascal(ident),285 screaming_name: snake_ident_to_screaming(ident),286 selector_str,287 selector,288 args,289 has_normal_args,290 mutability,291 result: result.clone(),292 })293 }294 fn expand_call_def(&self) -> proc_macro2::TokenStream {295 let defs = self296 .args297 .iter()298 .filter(|a| !a.is_special())299 .map(|a| a.expand_call_def());300 let pascal_name = &self.pascal_name;301302 if self.has_normal_args {303 quote! {304 #pascal_name {305 #(306 #defs,307 )*308 }309 }310 } else {311 quote! {#pascal_name}312 }313 }314315 fn expand_const(&self) -> proc_macro2::TokenStream {316 let screaming_name = &self.screaming_name;317 let selector = self.selector;318 let selector_str = &self.selector_str;319 quote! {320 #[doc = #selector_str]321 const #screaming_name: u32 = #selector;322 }323 }324325 fn expand_interface_id(&self) -> proc_macro2::TokenStream {326 let screaming_name = &self.screaming_name;327 quote! {328 interface_id ^= Self::#screaming_name;329 }330 }331332 fn expand_parse(&self) -> proc_macro2::TokenStream {333 let pascal_name = &self.pascal_name;334 let screaming_name = &self.screaming_name;335 if self.has_normal_args {336 let parsers = self337 .args338 .iter()339 .filter(|a| !a.is_special())340 .map(|a| a.expand_parse());341 quote! {342 Self::#screaming_name => return Ok(Some(Self::#pascal_name {343 #(344 #parsers,345 )*346 }))347 }348 } else {349 quote! { Self::#screaming_name => return Ok(Some(Self::#pascal_name)) }350 }351 }352353 fn expand_variant_call(&self) -> proc_macro2::TokenStream {354 let pascal_name = &self.pascal_name;355 let name = &self.name;356357 let matcher = if self.has_normal_args {358 let names = self359 .args360 .iter()361 .filter(|a| !a.is_special())362 .map(|a| &a.name);363364 quote! {{365 #(366 #names,367 )*368 }}369 } else {370 quote! {}371 };372373 let receiver = match self.mutability {374 Mutability::Mutable | Mutability::View => quote! {self.},375 Mutability::Pure => quote! {Self::},376 };377 let args = self.args.iter().map(|a| a.expand_call_arg());378379 quote! {380 InternalCall::#pascal_name #matcher => {381 let result = #receiver #name(382 #(383 #args,384 )*385 )?;386 (&result).abi_write(&mut writer);387 }388 }389 }390391 fn expand_solidity_function(&self) -> proc_macro2::TokenStream {392 let camel_name = &self.camel_name;393 let mutability = match self.mutability {394 Mutability::Mutable => quote! {SolidityMutability::Mutable},395 Mutability::View => quote! { SolidityMutability::View },396 Mutability::Pure => quote! {SolidityMutability::Pure},397 };398 let result = &self.result;399400 let args = self.args.iter().map(MethodArg::expand_solidity_argument);401402 quote! {403 SolidityFunction {404 name: #camel_name,405 mutability: #mutability,406 args: (407 #(408 #args,409 )*410 ),411 result: <UnnamedArgument<#result>>::default(),412 }413 }414 }415}416417pub struct SolidityInterface {418 generics: Generics,419 name: Box<syn::Type>,420 info: InterfaceInfo,421 methods: Vec<Method>,422}423impl SolidityInterface {424 pub fn try_from(info: InterfaceInfo, value: &ItemImpl) -> syn::Result<Self> {425 let mut methods = Vec::new();426427 for item in &value.items {428 if let ImplItem::Method(method) = item {429 methods.push(Method::try_from(method)?)430 }431 }432 Ok(Self {433 generics: value.generics.clone(),434 name: value.self_ty.clone(),435 info,436 methods,437 })438 }439 pub fn expand(self) -> proc_macro2::TokenStream {440 let name = self.name;441442 let solidity_name = self.info.name.to_string();443 let call_name = pascal_ident_to_call(&self.info.name);444 let generics = self.generics;445446 let call_sub = self447 .info448 .inline_is449 .0450 .iter()451 .chain(self.info.is.0.iter())452 .map(Is::expand_call_def);453 let call_parse = self454 .info455 .inline_is456 .0457 .iter()458 .chain(self.info.is.0.iter())459 .map(Is::expand_parse);460 let call_variants = self461 .info462 .inline_is463 .0464 .iter()465 .chain(self.info.is.0.iter())466 .map(Is::expand_variant_call);467468 let inline_interface_id = self.info.inline_is.0.iter().map(Is::expand_interface_id);469 let supports_interface = self.info.is.0.iter().map(Is::expand_supports_interface);470471 let calls = self.methods.iter().map(Method::expand_call_def);472 let consts = self.methods.iter().map(Method::expand_const);473 let interface_id = self.methods.iter().map(Method::expand_interface_id);474 let parsers = self.methods.iter().map(Method::expand_parse);475 let call_variants_this = self.methods.iter().map(Method::expand_variant_call);476 let solidity_functions = self.methods.iter().map(Method::expand_solidity_function);477478 // let methods = self.methods.iter().map(Method::solidity_def);479480 quote! {481 #[derive(Debug)]482 pub enum #call_name {483 #(484 #calls,485 )*486 #(487 #call_sub,488 )*489 }490 impl #call_name {491 #(492 #consts493 )*494 pub const fn interface_id() -> u32 {495 let mut interface_id = 0;496 #(#interface_id)*497 #(#inline_interface_id)*498 interface_id499 }500 pub fn supports_interface(interface_id: u32) -> bool {501 interface_id != 0xffffff && (502 interface_id == Self::interface_id()503 #(504 || #supports_interface505 )*506 )507 }508 pub fn generate_solidity_interface() -> string {509 use evm_coder::solidity::*;510 use core::fmt::Write;511 let interface = SolidityInterface {512 name: #solidity_name,513 functions: (#(514 #solidity_functions,515 )*),516 };517 let mut out = string::new();518 let _ = interface.format(&mut out);519 out520 }521 }522 impl ::evm_coder::Call for #call_name {523 fn parse(method_id: u32, reader: &mut ::evm_coder::abi::AbiReader) -> ::evm_coder::execution::Result<Option<Self>> {524 use ::evm_coder::abi::AbiRead;525 match method_id {526 #(527 #parsers,528 )*529 _ => {},530 }531 #(532 #call_parse533 )else*534 return Ok(None);535 }536 }537 impl #generics ::evm_coder::Callable<#call_name> for #name {538 #[allow(unreachable_code)] // In case of no inner calls539 fn call(&mut self, c: Msg<#call_name>) -> Result<::evm_coder::abi::AbiWriter> {540 use ::evm_coder::abi::AbiWrite;541 type InternalCall = #call_name;542 match c.call {543 #(544 #call_variants,545 )*546 _ => {},547 }548 let mut writer = ::evm_coder::abi::AbiWriter::default();549 match c.call {550 #(551 #call_variants_this,552 )*553 _ => unreachable!()554 }555 Ok(writer)556 }557 }558 }559 }560}crates/evm-coder-macros/src/to_log.rsdiffbeforeafterboth--- a/crates/evm-coder-macros/src/to_log.rs
+++ b/crates/evm-coder-macros/src/to_log.rs
@@ -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]
crates/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");
+ }
+}
crates/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, ");")
+ }
+}
pallets/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);
pallets/nft/src/eth/stubs/ERC20.bindiffbeforeafterbothbinary blob — no preview
pallets/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
pallets/nft/src/eth/stubs/ERC721.bindiffbeforeafterbothbinary blob — no preview
pallets/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
pallets/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(())
}
tests/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
+});
tests/src/eth/nonFungibleAbi.jsondiffbeforeafterboth--- 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