--- 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/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 @@ -395,3 +395,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