diff --git a/src/Neo/SmartContract/Manifest/ContractAbi.cs b/src/Neo/SmartContract/Manifest/ContractAbi.cs index 15ae2fabbc..6be6063464 100644 --- a/src/Neo/SmartContract/Manifest/ContractAbi.cs +++ b/src/Neo/SmartContract/Manifest/ContractAbi.cs @@ -17,6 +17,8 @@ using System.Linq; using Array = Neo.VM.Types.Array; +#nullable enable + namespace Neo.SmartContract.Manifest { /// @@ -25,32 +27,51 @@ namespace Neo.SmartContract.Manifest /// For more details, see NEP-14. public class ContractAbi : IInteroperable { - private IReadOnlyDictionary<(string, int), ContractMethodDescriptor> methodDictionary; + private IReadOnlyDictionary<(string, int), ContractMethodDescriptor>? _methodDictionary; /// /// Gets the methods in the ABI. /// - public ContractMethodDescriptor[] Methods { get; set; } + public ContractMethodDescriptor[] Methods { get; set; } = []; /// /// Gets the events in the ABI. /// - public ContractEventDescriptor[] Events { get; set; } + public ContractEventDescriptor[] Events { get; set; } = []; + + /// + /// An object with each member having a name (a string consisting of one or more identifiers joined by dots) and a value of ExtendedType object. + /// + public Dictionary? NamedTypes { get; set; } void IInteroperable.FromStackItem(StackItem stackItem) { - Struct @struct = (Struct)stackItem; - Methods = ((Array)@struct[0]).Select(p => p.ToInteroperable()).ToArray(); - Events = ((Array)@struct[1]).Select(p => p.ToInteroperable()).ToArray(); + var data = (Struct)stackItem; + Methods = [.. ((Array)data[0]).Select(p => p.ToInteroperable())]; + Events = [.. ((Array)data[1]).Select(p => p.ToInteroperable())]; + + if (data.Count >= 3 && !data[2].IsNull) + NamedTypes = ((Map)data[2]).ToDictionary(p => p.Key.GetString()!, p => p.Value.ToInteroperable()); + else + NamedTypes = null; + + ValidateExtendedTypes(); } public StackItem ToStackItem(IReferenceCounter referenceCounter) { - return new Struct(referenceCounter) + var ret = new Struct(referenceCounter) { new Array(referenceCounter, Methods.Select(p => p.ToStackItem(referenceCounter))), - new Array(referenceCounter, Events.Select(p => p.ToStackItem(referenceCounter))), + new Array(referenceCounter, Events.Select(p => p.ToStackItem(referenceCounter))) }; + + if (NamedTypes != null) + { + ret.Add(new Map(NamedTypes.ToDictionary(p => (PrimitiveType)p.Key, p => (StackItem)p.Value.ToStackItem(referenceCounter)), referenceCounter)); + } + + return ret; } /// @@ -60,15 +81,70 @@ public StackItem ToStackItem(IReferenceCounter referenceCounter) /// The converted ABI. public static ContractAbi FromJson(JObject json) { + Dictionary? namedTypes = null; + var knownNamedTypes = new HashSet(StringComparer.Ordinal); + if (json!["namedtypes"] is JObject namedTypesJson) + { + foreach (var key in namedTypesJson.Properties.Keys) + { + knownNamedTypes.Add(key); + } + + namedTypes = new(namedTypesJson.Properties.Count, StringComparer.Ordinal); + foreach (var (name, token) in namedTypesJson.Properties) + { + if (token is not JObject valueObject) + throw new FormatException("Named type definition must be a JSON object."); + namedTypes[name] = ExtendedType.FromJson(valueObject); + } + } + ContractAbi abi = new() { - Methods = ((JArray)json!["methods"])?.Select(u => ContractMethodDescriptor.FromJson((JObject)u)).ToArray() ?? [], - Events = ((JArray)json!["events"])?.Select(u => ContractEventDescriptor.FromJson((JObject)u)).ToArray() ?? [] + Methods = ((JArray)json!["methods"]!)?.Select(u => ContractMethodDescriptor.FromJson((JObject)u!, knownNamedTypes)).ToArray() ?? [], + Events = ((JArray)json!["events"]!)?.Select(u => ContractEventDescriptor.FromJson((JObject)u!, knownNamedTypes)).ToArray() ?? [], + NamedTypes = namedTypes }; - if (abi.Methods.Length == 0) throw new FormatException("Methods in ContractAbi is empty"); + if (abi.Methods.Length == 0) throw new FormatException("Methods in ContractAbi are empty"); + + abi.ValidateExtendedTypes(); return abi; } + internal void ValidateExtendedTypes() + { + ISet knownNamedTypes = NamedTypes != null + ? new HashSet(NamedTypes.Keys, StringComparer.Ordinal) + : new HashSet(StringComparer.Ordinal); + + if (NamedTypes != null) + { + foreach (var (name, type) in NamedTypes) + { + ExtendedType.EnsureValidNamedTypeIdentifier(name); + type.ValidateForNamedTypeDefinition(knownNamedTypes); + } + } + + foreach (var method in Methods) + { + foreach (var parameter in method.Parameters) + { + parameter.ExtendedType?.ValidateForParameterOrReturn(parameter.Type, knownNamedTypes); + } + + method.ExtendedReturnType?.ValidateForParameterOrReturn(method.ReturnType, knownNamedTypes); + } + + foreach (var ev in Events) + { + foreach (var parameter in ev.Parameters) + { + parameter.ExtendedType?.ValidateForParameterOrReturn(parameter.Type, knownNamedTypes); + } + } + } + /// /// Gets the method with the specified name. /// @@ -81,15 +157,17 @@ public static ContractAbi FromJson(JObject json) /// The method that matches the specified name and number of parameters. /// If is set to -1, the first method with the specified name will be returned. /// - public ContractMethodDescriptor GetMethod(string name, int pcount) + public ContractMethodDescriptor? GetMethod(string name, int pcount) { if (pcount < -1 || pcount > ushort.MaxValue) throw new ArgumentOutOfRangeException(nameof(pcount), $"`pcount` must be between [-1, {ushort.MaxValue}]"); if (pcount >= 0) { - methodDictionary ??= Methods.ToDictionary(p => (p.Name, p.Parameters.Length)); - methodDictionary.TryGetValue((name, pcount), out var method); - return method; + _methodDictionary ??= Methods.ToDictionary(p => (p.Name, p.Parameters.Length)); + if (_methodDictionary.TryGetValue((name, pcount), out var method)) + return method; + + return null; } else { @@ -103,11 +181,20 @@ public ContractMethodDescriptor GetMethod(string name, int pcount) /// The ABI represented by a JSON object. public JObject ToJson() { - return new JObject() + var ret = new JObject() { - ["methods"] = new JArray(Methods.Select(u => u.ToJson()).ToArray()), - ["events"] = new JArray(Events.Select(u => u.ToJson()).ToArray()) + ["methods"] = new JArray([.. Methods.Select(u => u.ToJson())]), + ["events"] = new JArray([.. Events.Select(u => u.ToJson())]) }; + + if (NamedTypes != null) + { + ret["namedtypes"] = new JObject(NamedTypes.ToDictionary(u => u.Key, u => (JToken?)u.Value.ToJson())); + } + + return ret; } } } + +#nullable disable diff --git a/src/Neo/SmartContract/Manifest/ContractEventDescriptor.cs b/src/Neo/SmartContract/Manifest/ContractEventDescriptor.cs index 3f1c47e00d..faa691dbb9 100644 --- a/src/Neo/SmartContract/Manifest/ContractEventDescriptor.cs +++ b/src/Neo/SmartContract/Manifest/ContractEventDescriptor.cs @@ -13,6 +13,7 @@ using Neo.VM; using Neo.VM.Types; using System; +using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; using Array = Neo.VM.Types.Array; @@ -54,15 +55,16 @@ public virtual StackItem ToStackItem(IReferenceCounter referenceCounter) /// Converts the event from a JSON object. /// /// The event represented by a JSON object. + /// Set of named type identifiers declared in the manifest, if any. /// The converted event. - public static ContractEventDescriptor FromJson(JObject json) + public static ContractEventDescriptor FromJson(JObject json, ISet knownNamedTypes = null) { ContractEventDescriptor descriptor = new() { Name = json["name"].GetString(), - Parameters = ((JArray)json["parameters"]).Select(u => ContractParameterDefinition.FromJson((JObject)u)).ToArray(), + Parameters = ((JArray)json["parameters"]).Select(u => ContractParameterDefinition.FromJson((JObject)u, knownNamedTypes)).ToArray(), }; - if (string.IsNullOrEmpty(descriptor.Name)) throw new FormatException("Name in ContractEventDescriptor is empty"); + if (string.IsNullOrEmpty(descriptor.Name)) throw new FormatException("Name in ContractEventDescriptor are empty"); _ = descriptor.Parameters.ToDictionary(p => p.Name); return descriptor; } diff --git a/src/Neo/SmartContract/Manifest/ContractMethodDescriptor.cs b/src/Neo/SmartContract/Manifest/ContractMethodDescriptor.cs index aa02d07c9c..3e522aa9b6 100644 --- a/src/Neo/SmartContract/Manifest/ContractMethodDescriptor.cs +++ b/src/Neo/SmartContract/Manifest/ContractMethodDescriptor.cs @@ -13,6 +13,7 @@ using Neo.VM; using Neo.VM.Types; using System; +using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; @@ -28,6 +29,11 @@ public class ContractMethodDescriptor : ContractEventDescriptor, IEquatable public ContractParameterType ReturnType { get; set; } + /// + /// NEP-25 extended return type + /// + public ExtendedType ExtendedReturnType { get; set; } + /// /// The position of the method in the contract script. /// @@ -42,46 +48,63 @@ public class ContractMethodDescriptor : ContractEventDescriptor, IEquatable= 6) + { + ExtendedReturnType = new ExtendedType(); + ExtendedReturnType.FromStackItem(item[5]); + ExtendedReturnType.ValidateForParameterOrReturn(ReturnType, null); + } + else + { + ExtendedReturnType = null; + } } public override StackItem ToStackItem(IReferenceCounter referenceCounter) { - Struct @struct = (Struct)base.ToStackItem(referenceCounter); - @struct.Add((byte)ReturnType); - @struct.Add(Offset); - @struct.Add(Safe); - return @struct; + var item = (Struct)base.ToStackItem(referenceCounter); + item.Add((byte)ReturnType); + item.Add(Offset); + item.Add(Safe); + if (ExtendedReturnType != null) + { + item.Add(ExtendedReturnType.ToStackItem(referenceCounter)); + } + return item; } /// /// Converts the method from a JSON object. /// /// The method represented by a JSON object. + /// Set of named type identifiers declared in the manifest, if any. /// The converted method. - public new static ContractMethodDescriptor FromJson(JObject json) + public new static ContractMethodDescriptor FromJson(JObject json, ISet knownNamedTypes = null) { ContractMethodDescriptor descriptor = new() { Name = json["name"].GetString(), - Parameters = ((JArray)json["parameters"]).Select(u => ContractParameterDefinition.FromJson((JObject)u)).ToArray(), + Parameters = ((JArray)json["parameters"]).Select(u => ContractParameterDefinition.FromJson((JObject)u, knownNamedTypes)).ToArray(), ReturnType = Enum.Parse(json["returntype"].GetString()), Offset = json["offset"].GetInt32(), - Safe = json["safe"].GetBoolean() + Safe = json["safe"].GetBoolean(), + ExtendedReturnType = json["extendedreturntype"] != null ? ExtendedType.FromJson((JObject)json["extendedreturntype"]) : null }; if (string.IsNullOrEmpty(descriptor.Name)) - throw new FormatException("Name in ContractMethodDescriptor is empty"); + throw new FormatException("Name in ContractMethodDescriptor are empty"); _ = descriptor.Parameters.ToDictionary(p => p.Name); - if (!Enum.IsDefined(typeof(ContractParameterType), descriptor.ReturnType)) - throw new FormatException($"ReturnType({descriptor.ReturnType}) in ContractMethodDescriptor is not valid"); + throw new FormatException($"ReturnType({descriptor.ReturnType}) in ContractMethodDescriptor are not valid"); if (descriptor.Offset < 0) - throw new FormatException($"Offset({descriptor.Offset}) in ContractMethodDescriptor is not valid"); + throw new FormatException($"Offset({descriptor.Offset}) in ContractMethodDescriptor are not valid"); + descriptor.ExtendedReturnType?.ValidateForParameterOrReturn(descriptor.ReturnType, knownNamedTypes); return descriptor; } @@ -95,6 +118,10 @@ public override JObject ToJson() json["returntype"] = ReturnType.ToString(); json["offset"] = Offset; json["safe"] = Safe; + if (ExtendedReturnType != null) + { + json["extendedreturntype"] = ExtendedReturnType.ToJson(); + } return json; } @@ -106,7 +133,8 @@ public bool Equals(ContractMethodDescriptor other) base.Equals(other) && // Already check null ReturnType == other.ReturnType && Offset == other.Offset - && Safe == other.Safe; + && Safe == other.Safe + && Equals(ExtendedReturnType, other.ExtendedReturnType); } public override bool Equals(object other) @@ -119,7 +147,7 @@ public override bool Equals(object other) public override int GetHashCode() { - return HashCode.Combine(ReturnType, Offset, Safe, base.GetHashCode()); + return HashCode.Combine(ReturnType, Offset, Safe, ExtendedReturnType?.GetHashCode() ?? -1, base.GetHashCode()); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/Neo/SmartContract/Manifest/ContractParameterDefinition.cs b/src/Neo/SmartContract/Manifest/ContractParameterDefinition.cs index 32d02b0423..b25f2aa455 100644 --- a/src/Neo/SmartContract/Manifest/ContractParameterDefinition.cs +++ b/src/Neo/SmartContract/Manifest/ContractParameterDefinition.cs @@ -13,6 +13,7 @@ using Neo.VM; using Neo.VM.Types; using System; +using System.Collections.Generic; using System.Runtime.CompilerServices; namespace Neo.SmartContract.Manifest @@ -32,34 +33,60 @@ public class ContractParameterDefinition : IInteroperable, IEquatable public ContractParameterType Type { get; set; } - void IInteroperable.FromStackItem(StackItem stackItem) + /// + /// NEP-25 extended type + /// + public ExtendedType ExtendedType { get; set; } + + public void FromStackItem(StackItem stackItem) { - Struct @struct = (Struct)stackItem; - Name = @struct[0].GetString(); - Type = (ContractParameterType)(byte)@struct[1].GetInteger(); + var item = (Struct)stackItem; + Name = item[0].GetString(); + Type = (ContractParameterType)(byte)item[1].GetInteger(); + + if (item.Count >= 3) + { + ExtendedType = new ExtendedType(); + ExtendedType.FromStackItem(item[2]); + ExtendedType.ValidateForParameterOrReturn(Type, null); + } + else + { + ExtendedType = null; + } } public StackItem ToStackItem(IReferenceCounter referenceCounter) { - return new Struct(referenceCounter) { Name, (byte)Type }; + var item = new Struct(referenceCounter) { Name, (byte)Type }; + + if (ExtendedType != null) + { + item.Add(ExtendedType.ToStackItem(referenceCounter)); + } + + return item; } /// /// Converts the parameter from a JSON object. /// /// The parameter represented by a JSON object. + /// Set of named type identifiers declared in the manifest, if any. /// The converted parameter. - public static ContractParameterDefinition FromJson(JObject json) + public static ContractParameterDefinition FromJson(JObject json, ISet knownNamedTypes = null) { ContractParameterDefinition parameter = new() { Name = json["name"].GetString(), - Type = Enum.Parse(json["type"].GetString()) + Type = Enum.Parse(json["type"].GetString()), + ExtendedType = json["extendedtype"] != null ? ExtendedType.FromJson((JObject)json["extendedtype"]) : null }; if (string.IsNullOrEmpty(parameter.Name)) - throw new FormatException("Name in ContractParameterDefinition is empty"); + throw new FormatException("Name in ContractParameterDefinition are empty"); if (!Enum.IsDefined(typeof(ContractParameterType), parameter.Type) || parameter.Type == ContractParameterType.Void) - throw new FormatException($"Type({parameter.Type}) in ContractParameterDefinition is not valid"); + throw new FormatException($"Type({parameter.Type}) in ContractParameterDefinition are not valid"); + parameter.ExtendedType?.ValidateForParameterOrReturn(parameter.Type, knownNamedTypes); return parameter; } @@ -69,11 +96,18 @@ public static ContractParameterDefinition FromJson(JObject json) /// The parameter represented by a JSON object. public JObject ToJson() { - return new JObject() + var json = new JObject() { ["name"] = Name, ["type"] = Type.ToString() }; + + if (ExtendedType != null) + { + json["extendedtype"] = ExtendedType.ToJson(); + } + + return json; } public bool Equals(ContractParameterDefinition other) @@ -81,7 +115,8 @@ public bool Equals(ContractParameterDefinition other) if (other == null) return false; if (ReferenceEquals(this, other)) return true; - return Name == other.Name && Type == other.Type; + return Name == other.Name && Type == other.Type + && Equals(ExtendedType, other.ExtendedType); } public override bool Equals(object other) @@ -94,7 +129,7 @@ public override bool Equals(object other) public override int GetHashCode() { - return HashCode.Combine(Name, Type); + return HashCode.Combine(Name, Type, ExtendedType?.GetHashCode() ?? -1); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/Neo/SmartContract/Manifest/ExtendedType.cs b/src/Neo/SmartContract/Manifest/ExtendedType.cs new file mode 100644 index 0000000000..f9241f69d0 --- /dev/null +++ b/src/Neo/SmartContract/Manifest/ExtendedType.cs @@ -0,0 +1,516 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ExtendedType.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Json; +using Neo.VM; +using Neo.VM.Types; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Neo.SmartContract.Manifest +{ +#nullable enable + + public class ExtendedType : IInteroperable, IEquatable + { + private static readonly Regex NamedTypePattern = new("^[A-Za-z][A-Za-z0-9.]{0,63}$", RegexOptions.Compiled); + + private static readonly HashSet LengthAllowedTypes = new() + { + ContractParameterType.Integer, + ContractParameterType.ByteArray, + ContractParameterType.String, + ContractParameterType.Array + }; + + private static readonly HashSet ForbidNullAllowedTypes = new() + { + ContractParameterType.Hash160, + ContractParameterType.Hash256, + ContractParameterType.ByteArray, + ContractParameterType.String, + ContractParameterType.Array, + ContractParameterType.Map, + ContractParameterType.InteropInterface + }; + + private static readonly HashSet MapKeyAllowedTypes = new() + { + ContractParameterType.Signature, + ContractParameterType.Boolean, + ContractParameterType.Integer, + ContractParameterType.Hash160, + ContractParameterType.Hash256, + ContractParameterType.ByteArray, + ContractParameterType.PublicKey, + ContractParameterType.String + }; + + private static FormatException Nep25Error(string message) => new($"Invalid NEP-25 extended type: {message}"); + + internal static bool IsValidNamedTypeIdentifier(string name) + { + return !string.IsNullOrEmpty(name) && NamedTypePattern.IsMatch(name); + } + + internal static void EnsureValidNamedTypeIdentifier(string name) + { + if (!IsValidNamedTypeIdentifier(name)) + throw Nep25Error($"Named type '{name}' must start with a letter, contain only alphanumeric characters or dots, and be at most 64 characters long."); + } + + /// + /// The type of the parameter. It can be any value of except . + /// + public ContractParameterType Type { get; set; } + + /// + /// NamedType is used to refer to one of the types defined in the namedtypes object of Contract, + /// so namedtypes MUST contain a field named name. + /// This field is only used for structures (ordered set of named values of different types), + /// if used other fields MUST NOT be set, except for the type which MUST be an Array. + /// Value string MUST start with a letter and can contain alphanumeric characters and dots. + /// It MUST NOT be longer than 64 characters. + /// + public string? NamedType { get; set; } + + /// + /// length is an optional field that can be used for Integer, ByteArray, String or Array types and MUST NOT be used for other types. + /// When used it specifies the maximum possible byte length of an integer/byte array/string or number of array elements. + /// Any of these lengths MUST NOT exceed NeoVM limitations that are relevant for the current version of it. + /// Length 0 means "unlimited". + /// + public int? Length { get; set; } + + /// + /// forbidnull is an optional field that can be used for Hash160, Hash256, + /// ByteArray, String, Array, Map or InteropInterface types and MUST NOT be used for other types. + /// It allows to specify that the method accepts or event emits only non-null values for this field. + /// The default (if not specified) is "false", meaning that null can be used. + /// + public bool? ForbidNull { get; set; } + + /// + /// interface is only used in conjunction with the InteropInterface type and MUST NOT be used for other types, + /// when used it specifies which interop interface is used. + /// The only valid defined value for it is "IIterator" which means an iterator object. + /// When used it MUST be accompanied with the value object that specifies the type of each individual element returned from the iterator. + /// + public Nep25Interface? Interface { get; set; } + + /// + /// key is only used along with the Map type (MUST NOT be used for other types) and can have Signature, Boolean, Integer, + /// Hash160, Hash256, ByteArray, PublicKey or String value, that is all the basic types that can be used as a map key. + /// + public ContractParameterType? Key { get; set; } + + /// + /// value is used for Array, InteropInterface and Map types (type field) and MUST NOT be used with other base types. + /// When used for Array it contains the type of an individual element of an array (ordered set of values of one type). + /// If used for InteropInterface it contains the type of an individual iterator's value. If used for Map it contains map value type. + /// If this field is used, fields MUST NOT be present. + /// + public ExtendedType? Value { get; set; } + + /// + /// fields is used for Array type and when used it means that the type is a structure (ordered set of named values of diffent types), + /// which has its fields defined directly here (unlike namedtype which is just a reference to namedtypes). + /// It's an array with each member being a Parameter. fields MUST NOT be used in method parameter or return value definitions + /// (these MUST use namedtype referring to a valid type specified in the namedtypes object). + /// + public ContractParameterDefinition[]? Fields { get; set; } + + public void FromStackItem(StackItem stackItem) + { + if (stackItem is not Map map) throw new FormatException("Map type was expected"); + + if (!map.TryGetValue("type", out var type)) throw new FormatException("Incorrect Type"); + + Type = (ContractParameterType)(byte)type.GetInteger(); + if (!Enum.IsDefined(typeof(ContractParameterType), Type)) throw new FormatException("Incorrect Type"); + if (Type == ContractParameterType.Void) throw new FormatException("Void Type is not allowed in NEP-25"); + + if (map.TryGetValue("namedtype", out var val)) + { + NamedType = val.GetString(); + } + + if (map.TryGetValue("length", out val)) + { + if (val is Integer length) + Length = checked((int)length.GetInteger()); + else + { + Length = null; + if (val is not null) throw new FormatException("Length must be Integer or null"); + } + } + + if (map.TryGetValue("forbidnull", out val)) + { + if (val is VM.Types.Boolean forbidnull) + ForbidNull = forbidnull.GetBoolean(); + else + { + ForbidNull = null; + if (val is not null) throw new FormatException("ForbidNull must be Boolean or null"); + } + } + + if (map.TryGetValue("interface", out val)) + { + if (val is ByteString interf) + { + if (!Enum.TryParse(interf.GetString(), false, out var inferValue)) + throw new FormatException("Incorrect NEP-25 interface"); + + Interface = inferValue; + } + else + { + Interface = null; + if (val is not null) throw new FormatException("Interface must be ByteString or null"); + } + } + + if (map.TryGetValue("key", out val)) + { + if (val is ByteString key) + { + if (!Enum.TryParse(key.GetString(), false, out var keyValue)) + throw new FormatException("Incorrect Parameter Type"); + + Key = keyValue; + } + else + { + Key = null; + if (val is not null) throw new FormatException("Key must be ByteString or null"); + } + } + + if (map.TryGetValue("value", out val)) + { + if (val is Struct value) + { + Value = new ExtendedType(); + Value.FromStackItem(value); + } + else + { + Value = null; + if (val is not null) throw new FormatException("Value must be Struct or null"); + } + } + + if (map.TryGetValue("fields", out val)) + { + if (val is VM.Types.Array fields) + { + Fields = new ContractParameterDefinition[fields.Count]; + for (var i = 0; i < fields.Count; i++) + { + var field = new ContractParameterDefinition(); + field.FromStackItem((VM.Types.Array)fields[i]); + Fields[i] = field; + } + } + else + { + Fields = null; + if (val is not null) throw new FormatException("Fields must be Array or null"); + } + } + } + + internal StackItem ToStackItem(IReferenceCounter referenceCounter) + { + var map = new Map(referenceCounter); + return ToStackItem(referenceCounter, map); + } + + internal Map ToStackItem(IReferenceCounter referenceCounter, Map map) + { + map["type"] = (byte)Type; + + if (NamedType != null) map["namedtype"] = NamedType; + if (Length != null) map["length"] = Length; + if (ForbidNull != null) map["forbidnull"] = ForbidNull; + if (Interface != null) map["interface"] = Interface.ToString()!; + if (Key != null) map["key"] = Key.ToString()!; + if (Value != null) map["value"] = Value.ToStackItem(referenceCounter); + if (Fields != null) + { + var fields = new VM.Types.Array(referenceCounter); + foreach (var field in Fields) + { + fields.Add(field.ToStackItem(referenceCounter)); + } + map["fields"] = fields; + } + return map; + } + + StackItem IInteroperable.ToStackItem(IReferenceCounter referenceCounter) + { + var item = new Map(referenceCounter); + ToStackItem(referenceCounter, item); + return item; + } + + /// + /// Converts the type from a JSON object. + /// + /// The method represented by a JSON object. + /// The extended type. + public static ExtendedType FromJson(JObject json) + { + ExtendedType type = new() + { + Type = Enum.Parse(json["type"]?.GetString() ?? throw new FormatException()), + NamedType = json["namedtype"]?.GetString(), + }; + if (!Enum.IsDefined(typeof(ContractParameterType), type.Type)) throw new FormatException(); + if (type.Type == ContractParameterType.Void) throw Nep25Error("Void type is not allowed."); + if (json["length"] != null) + { + type.Length = json["length"]!.GetInt32(); + if (type.Length < 0) throw new FormatException("Length must be non-negative."); + if (type.Length > ExecutionEngineLimits.Default.MaxItemSize) throw new FormatException($"Length must less than {ExecutionEngineLimits.Default.MaxItemSize}."); + } + if (json["forbidnull"] != null) + { + type.ForbidNull = json["forbidnull"]!.GetBoolean(); + } + if (json["interface"] != null) + { + if (!Enum.TryParse(json["interface"]!.GetString(), true, out var interfaceValue)) + throw new FormatException("Invalid interface value."); + type.Interface = interfaceValue; + } + if (json["key"] != null) + { + if (!Enum.TryParse(json["key"]!.GetString(), true, out var keyValue)) + throw new FormatException("Invalid key value."); + type.Key = keyValue; + } + if (json["value"] is JObject jValue) + { + type.Value = FromJson(jValue); + } + if (json["fields"] is JArray jFields) + { + type.Fields = new ContractParameterDefinition[jFields.Count]; + + for (var i = 0; i < jFields.Count; i++) + { + if (jFields[i] is not JObject jField) + throw new FormatException("Invalid Field entry"); + + type.Fields[i] = ContractParameterDefinition.FromJson(jField); + } + } + return type; + } + + /// + /// Converts the parameter to a JSON object. + /// + /// The parameter represented by a JSON object. + public virtual JObject ToJson() + { + var json = new JObject(); + json["type"] = Type.ToString(); + json["namedtype"] = NamedType; + if (Length.HasValue) + { + json["length"] = Length.Value; + } + if (ForbidNull.HasValue) + { + json["forbidnull"] = ForbidNull.Value; + } + if (Interface.HasValue) + { + json["interface"] = Interface.Value.ToString(); + } + if (Key.HasValue) + { + json["key"] = Key.Value.ToString(); + } + if (Value != null) + { + json["value"] = Value.ToJson(); + } + if (Fields != null) + { + var jFields = new JArray(); + + foreach (var field in Fields) + { + jFields.Add(field.ToJson()); + } + + json["fields"] = jFields; + } + return json; + } + + public override bool Equals(object? obj) => Equals(obj as ExtendedType); + public override int GetHashCode() => HashCode.Combine(Type, NamedType, Length, ForbidNull, Interface, Key, Value, Fields?.Length ?? -1); + + public bool Equals(ExtendedType? other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + + if (Type != other.Type + || NamedType != other.NamedType + || Length != other.Length + || ForbidNull != other.ForbidNull + || Interface != other.Interface + || Key != other.Key + || !Equals(Value, other.Value)) + return false; + + if (Fields == null != (other.Fields == null)) return false; + + if (Fields != null) + { + if (Fields.Length != other.Fields!.Length) return false; + + for (var i = 0; i < Fields.Length; i++) + { + if (!Equals(Fields[i], other.Fields[i])) return false; + } + } + + return true; + } + + internal void ValidateForParameterOrReturn(ContractParameterType expectedType, ISet? knownNamedTypes) + { + ValidateCore(expectedType, allowFields: false, knownNamedTypes, allowNamedTypeReference: true); + } + + internal void ValidateForNamedTypeDefinition(ISet? knownNamedTypes) + { + ValidateCore(expectedType: null, allowFields: true, knownNamedTypes, allowNamedTypeReference: true); + } + + private void ValidateCore(ContractParameterType? expectedType, bool allowFields, ISet? knownNamedTypes, bool allowNamedTypeReference) + { + if (expectedType.HasValue && Type != expectedType.Value) + throw Nep25Error($"Type mismatch. Expected '{expectedType.Value}', got '{Type}'."); + + if (!Enum.IsDefined(Type) || Type == ContractParameterType.Void) + throw Nep25Error($"Unsupported type '{Type}'."); + + if (Length.HasValue && !LengthAllowedTypes.Contains(Type)) + throw Nep25Error($"length cannot be specified for type '{Type}'."); + + if (ForbidNull.HasValue && !ForbidNullAllowedTypes.Contains(Type)) + throw Nep25Error($"forbidnull cannot be specified for type '{Type}'."); + + if (Interface.HasValue && Type != ContractParameterType.InteropInterface) + throw Nep25Error($"interface can only be used with InteropInterface type."); + + if (Type == ContractParameterType.InteropInterface && !Interface.HasValue) + throw Nep25Error("interface is required for InteropInterface type."); + + if (Key.HasValue && Type != ContractParameterType.Map) + throw Nep25Error($"key cannot be used with type '{Type}'."); + + if (Key.HasValue && !MapKeyAllowedTypes.Contains(Key.Value)) + throw Nep25Error($"key '{Key.Value}' is not allowed for map definitions."); + + if (Type == ContractParameterType.Map && !Key.HasValue) + throw Nep25Error("key is required for Map type."); + + if (NamedType != null) + { + if (!allowNamedTypeReference) + throw Nep25Error("namedtype is not allowed in this context."); + + if (Type != ContractParameterType.Array) + throw Nep25Error("namedtype can only be used with Array type."); + + EnsureValidNamedTypeIdentifier(NamedType); + + if (Length.HasValue || ForbidNull.HasValue || Interface.HasValue || Key.HasValue || Value is not null || (Fields is not null && Fields.Length > 0)) + throw Nep25Error("namedtype cannot be combined with other modifiers."); + + if (knownNamedTypes != null && !knownNamedTypes.Contains(NamedType)) + throw Nep25Error($"namedtype '{NamedType}' is not defined in the manifest."); + } + + if (Value is not null) + { + if (Type != ContractParameterType.Array && Type != ContractParameterType.InteropInterface && Type != ContractParameterType.Map) + throw Nep25Error("value can only be specified for Array, Map or InteropInterface types."); + + if (Fields is not null && Fields.Length > 0) + throw Nep25Error("value and fields cannot be used together."); + + if (Type == ContractParameterType.InteropInterface && !Interface.HasValue) + throw Nep25Error("interface must be provided when value is specified for InteropInterface type."); + + if (Type == ContractParameterType.Map && !Key.HasValue) + throw Nep25Error("key must be provided when value is specified for Map type."); + + Value.ValidateCore(expectedType: null, allowFields, knownNamedTypes, allowNamedTypeReference); + } + else + { + if (Type == ContractParameterType.Map) + throw Nep25Error("value is required for Map type."); + + if (Type == ContractParameterType.InteropInterface) + throw Nep25Error("value is required for InteropInterface type."); + + if (Type == ContractParameterType.Array && NamedType is null && (Fields is null || Fields.Length == 0)) + throw Nep25Error("value, namedtype or fields must be provided for Array type to describe element type."); + } + + if (Fields is not null && Fields.Length > 0) + { + if (!allowFields) + throw Nep25Error("fields cannot be used in method parameters or return values."); + + if (Type != ContractParameterType.Array) + throw Nep25Error("fields can only be used with Array type."); + + if (Value is not null) + throw Nep25Error("fields and value cannot be used together."); + + if (NamedType != null) + throw Nep25Error("fields cannot be combined with namedtype."); + + foreach (var field in Fields) + { + field.ExtendedType?.ValidateCore(field.Type, allowFields: true, knownNamedTypes, allowNamedTypeReference); + } + } + + if (!allowFields) + { + if (Fields is not null && Fields.Length > 0) + throw Nep25Error("fields cannot be used in method parameters or return values."); + + if (Value?.Fields is { Length: > 0 }) + throw Nep25Error("fields cannot be used in method parameters or return values."); + } + } + } +#nullable disable +} diff --git a/src/Neo/SmartContract/Manifest/Nep25Interface.cs b/src/Neo/SmartContract/Manifest/Nep25Interface.cs new file mode 100644 index 0000000000..783b5a92f9 --- /dev/null +++ b/src/Neo/SmartContract/Manifest/Nep25Interface.cs @@ -0,0 +1,26 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Nep25Interface.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +namespace Neo.SmartContract.Manifest +{ + /// + /// interface is only used in conjuction with the InteropInterface type and MUST NOT be used for other types, when used it specifies which interop interface is used. + /// The only valid defined value for it is "IIterator" which means an iterator object. + /// When used it MUST be accompanied with the value object that specifies the type of each individual element returned from the iterator. + /// + public enum Nep25Interface + { + /// + /// Iterator object + /// + IIterator + } +} diff --git a/src/Plugins/RestServer/Helpers/ContractHelper.cs b/src/Plugins/RestServer/Helpers/ContractHelper.cs index cbb8ef63d4..3e76306948 100644 --- a/src/Plugins/RestServer/Helpers/ContractHelper.cs +++ b/src/Plugins/RestServer/Helpers/ContractHelper.cs @@ -57,16 +57,16 @@ public static bool IsNep17Supported(ContractState contractState) var balanceOfMethod = manifest.Abi.GetMethod("balanceOf", 1); var transferMethod = manifest.Abi.GetMethod("transfer", 4); - var symbolValid = symbolMethod.Safe == true && + var symbolValid = symbolMethod != null && symbolMethod.Safe == true && symbolMethod.ReturnType == ContractParameterType.String; - var decimalsValid = decimalsMethod.Safe == true && + var decimalsValid = decimalsMethod != null && decimalsMethod.Safe == true && decimalsMethod.ReturnType == ContractParameterType.Integer; - var totalSupplyValid = totalSupplyMethod.Safe == true && + var totalSupplyValid = totalSupplyMethod != null && totalSupplyMethod.Safe == true && totalSupplyMethod.ReturnType == ContractParameterType.Integer; - var balanceOfValid = balanceOfMethod.Safe == true && + var balanceOfValid = balanceOfMethod != null && balanceOfMethod.Safe == true && balanceOfMethod.ReturnType == ContractParameterType.Integer && balanceOfMethod.Parameters[0].Type == ContractParameterType.Hash160; - var transferValid = transferMethod.Safe == false && + var transferValid = transferMethod != null && transferMethod.Safe == false && transferMethod.ReturnType == ContractParameterType.Boolean && transferMethod.Parameters[0].Type == ContractParameterType.Hash160 && transferMethod.Parameters[1].Type == ContractParameterType.Hash160 && @@ -111,29 +111,29 @@ public static bool IsNep11Supported(ContractState contractState) var transferMethod1 = manifest.Abi.GetMethod("transfer", 3); var transferMethod2 = manifest.Abi.GetMethod("transfer", 5); - var symbolValid = symbolMethod.Safe == true && + var symbolValid = symbolMethod != null && symbolMethod.Safe == true && symbolMethod.ReturnType == ContractParameterType.String; - var decimalsValid = decimalsMethod.Safe == true && + var decimalsValid = decimalsMethod != null && decimalsMethod.Safe == true && decimalsMethod.ReturnType == ContractParameterType.Integer; - var totalSupplyValid = totalSupplyMethod.Safe == true && + var totalSupplyValid = totalSupplyMethod != null && totalSupplyMethod.Safe == true && totalSupplyMethod.ReturnType == ContractParameterType.Integer; - var balanceOfValid1 = balanceOfMethod1.Safe == true && + var balanceOfValid1 = balanceOfMethod1 != null && balanceOfMethod1.Safe == true && balanceOfMethod1.ReturnType == ContractParameterType.Integer && balanceOfMethod1.Parameters[0].Type == ContractParameterType.Hash160; var balanceOfValid2 = balanceOfMethod2?.Safe == true && balanceOfMethod2?.ReturnType == ContractParameterType.Integer && balanceOfMethod2?.Parameters[0].Type == ContractParameterType.Hash160 && balanceOfMethod2?.Parameters[0].Type == ContractParameterType.ByteArray; - var tokensOfValid = tokensOfMethod.Safe == true && + var tokensOfValid = tokensOfMethod != null && tokensOfMethod.Safe == true && tokensOfMethod.ReturnType == ContractParameterType.InteropInterface && tokensOfMethod.Parameters[0].Type == ContractParameterType.Hash160; - var ownerOfValid1 = ownerOfMethod.Safe == true && + var ownerOfValid1 = ownerOfMethod != null && ownerOfMethod.Safe == true && ownerOfMethod.ReturnType == ContractParameterType.Hash160 && ownerOfMethod.Parameters[0].Type == ContractParameterType.ByteArray; - var ownerOfValid2 = ownerOfMethod.Safe == true && + var ownerOfValid2 = ownerOfMethod != null && ownerOfMethod.Safe == true && ownerOfMethod.ReturnType == ContractParameterType.InteropInterface && ownerOfMethod.Parameters[0].Type == ContractParameterType.ByteArray; - var transferValid1 = transferMethod1.Safe == false && + var transferValid1 = transferMethod1 != null && transferMethod1.Safe == false && transferMethod1.ReturnType == ContractParameterType.Boolean && transferMethod1.Parameters[0].Type == ContractParameterType.Hash160 && transferMethod1.Parameters[1].Type == ContractParameterType.ByteArray && diff --git a/tests/Neo.UnitTests/SmartContract/Manifest/UT_ContractManifest.cs b/tests/Neo.UnitTests/SmartContract/Manifest/UT_ContractManifest.cs index 4482d891be..f94c3bbb86 100644 --- a/tests/Neo.UnitTests/SmartContract/Manifest/UT_ContractManifest.cs +++ b/tests/Neo.UnitTests/SmartContract/Manifest/UT_ContractManifest.cs @@ -215,6 +215,85 @@ public void ParseFromJson_Trust() Assert.AreEqual(manifest.ToJson().ToString(), check.ToJson().ToString()); } + [TestMethod] + public void ParseFromJson_ExtendedTypeMismatch_ShouldThrow() + { + var json = """ + { + "name":"testManifest", + "groups":[], + "features":{}, + "supportedstandards":[], + "abi":{ + "methods":[ + { + "name":"testMethod", + "parameters":[ + { + "name":"arg", + "type":"Integer", + "extendedtype":{ + "type":"String" + } + } + ], + "returntype":"Void", + "offset":0, + "safe":true + } + ], + "events":[] + }, + "permissions":[], + "trusts":[], + "extra":null + } + """; + + json = Regex.Replace(json, @"\s+", ""); + Assert.ThrowsExactly(() => ContractManifest.Parse(json)); + } + + [TestMethod] + public void ParseFromJson_UnknownNamedType_ShouldThrow() + { + var json = """ + { + "name":"testManifest", + "groups":[], + "features":{}, + "supportedstandards":[], + "abi":{ + "methods":[ + { + "name":"testMethod", + "parameters":[ + { + "name":"arg", + "type":"Array", + "extendedtype":{ + "type":"Array", + "namedtype":"Custom.Struct" + } + } + ], + "returntype":"Void", + "offset":0, + "safe":true + } + ], + "events":[] + }, + "permissions":[], + "trusts":[], + "extra":null + } + """; + + json = Regex.Replace(json, @"\s+", ""); + Assert.ThrowsExactly(() => ContractManifest.Parse(json)); + } + [TestMethod] public void ToInteroperable_Trust() { diff --git a/tests/Neo.UnitTests/SmartContract/Manifest/UT_ExtendedType.cs b/tests/Neo.UnitTests/SmartContract/Manifest/UT_ExtendedType.cs new file mode 100644 index 0000000000..e592ac5b38 --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/Manifest/UT_ExtendedType.cs @@ -0,0 +1,272 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_ExtendedType.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.Json; +using Neo.SmartContract; +using Neo.SmartContract.Manifest; +using Neo.VM; +using Neo.VM.Types; +using System; +using System.Linq; + +namespace Neo.UnitTests.SmartContract.Manifest +{ + [TestClass] + public class UT_ExtendedType + { + [TestMethod] + public void Equals_SameValues_ShouldBeTrue() + { + var a = new ExtendedType + { + Type = ContractParameterType.Array, + NamedType = "MyType", + Length = 10, + ForbidNull = true, + Interface = Nep25Interface.IIterator, + Key = ContractParameterType.String + }; + + var b = new ExtendedType + { + Type = ContractParameterType.Array, + NamedType = "MyType", + Length = 10, + ForbidNull = true, + Interface = Nep25Interface.IIterator, + Key = ContractParameterType.String + }; + + Assert.IsTrue(a.Equals(b)); + Assert.AreEqual(a.GetHashCode(), b.GetHashCode()); + } + + [TestMethod] + public void Equals_DifferentValues_ShouldBeFalse() + { + var a = new ExtendedType + { + Type = ContractParameterType.Integer, + NamedType = "TypeA", + Length = 5, + ForbidNull = false, + Interface = Nep25Interface.IIterator, + Key = ContractParameterType.String + }; + + var b = new ExtendedType + { + Type = ContractParameterType.String, + NamedType = "TypeB", + Length = 8, + ForbidNull = true, + Interface = null, + Key = ContractParameterType.PublicKey + }; + + Assert.IsFalse(a.Equals(b)); + Assert.AreNotEqual(a.GetHashCode(), b.GetHashCode()); + } + + [TestMethod] + public void Equals_NullOrOtherType_ShouldBeFalse() + { + var a = new ExtendedType + { + Type = ContractParameterType.Boolean + }; + + Assert.IsFalse(a.Equals(null)); + Assert.IsFalse(a.Equals("not an ExtendedType")); + } + + [TestMethod] + public void FromStackItem_ShouldHandleNullFields() + { + var refCounter = new ReferenceCounter(); + var map = new Map(refCounter) + { + [(PrimitiveType)"type"] = (byte)ContractParameterType.String, + [(PrimitiveType)"fields"] = StackItem.Null, + [(PrimitiveType)"forbidnull"] = StackItem.Null, + [(PrimitiveType)"interface"] = StackItem.Null, + [(PrimitiveType)"key"] = StackItem.Null, + [(PrimitiveType)"length"] = StackItem.Null, + [(PrimitiveType)"namedtype"] = StackItem.Null, + [(PrimitiveType)"value"] = StackItem.Null, + }; + + var extended = new ExtendedType(); + extended.FromStackItem(map); + + Assert.AreEqual(ContractParameterType.String, extended.Type); + Assert.IsNull(extended.NamedType); + Assert.IsNull(extended.Length); + Assert.IsNull(extended.ForbidNull); + Assert.IsNull(extended.Interface); + Assert.IsNull(extended.Key); + } + + [TestMethod] + public void ToStackItem_ShouldProduceNullFields() + { + var extended = new ExtendedType + { + Type = ContractParameterType.String, + NamedType = null, + Length = null, + ForbidNull = null, + Interface = null, + Key = null, + Value = null, + Fields = null + }; + + var refCounter = new ReferenceCounter(); + var result = ((IInteroperable)extended).ToStackItem(refCounter) as Map; + + Assert.IsNotNull(result); + Assert.HasCount(1, result); + + var type = result.Single(it => it.Key.GetString() == "type"); + Assert.AreEqual((byte)ContractParameterType.String, type.Value.GetInteger()); + } + + [TestMethod] + public void FromJson_ValidInput_ShouldParseCorrectly() + { + var json = new JObject(); + json["type"] = "Integer"; + json["namedtype"] = "MyType"; + json["length"] = 123; + json["forbidnull"] = true; + json["interface"] = "IIterator"; + json["key"] = "String"; + + var result = ExtendedType.FromJson(json); + + Assert.AreEqual(ContractParameterType.Integer, result.Type); + Assert.AreEqual("MyType", result.NamedType); + Assert.AreEqual(123, result.Length); + Assert.IsTrue(result.ForbidNull); + Assert.AreEqual(Nep25Interface.IIterator, result.Interface); + Assert.AreEqual(ContractParameterType.String, result.Key); + } + + [TestMethod] + public void ToJson_ShouldSerializeCorrectly() + { + var ext = new ExtendedType + { + Type = ContractParameterType.String, + NamedType = "Test.Name", + Length = 50, + ForbidNull = true, + Interface = Nep25Interface.IIterator, + Key = ContractParameterType.String + }; + + var json = ext.ToJson(); + + Assert.AreEqual("String", json["type"]?.AsString()); + Assert.AreEqual("Test.Name", json["namedtype"]?.AsString()); + Assert.AreEqual(50, json["length"]?.AsNumber()); + Assert.IsTrue(json["forbidnull"]?.AsBoolean()); + Assert.AreEqual("IIterator", json["interface"]?.AsString()); + Assert.AreEqual("String", json["key"]?.AsString()); + } + + [TestMethod] + public void FromJson_ToJson_Roundtrip() + { + var json = new JObject(); + json["type"] = "Boolean"; + json["namedtype"] = "Type.A"; + json["length"] = 32; + json["forbidnull"] = false; + json["interface"] = "IIterator"; + json["key"] = "PublicKey"; + + var ext = ExtendedType.FromJson(json); + var output = ext.ToJson(); + + Assert.AreEqual("Boolean", output["type"]?.AsString()); + Assert.AreEqual("Type.A", output["namedtype"]?.AsString()); + Assert.AreEqual(32, output["length"]?.AsNumber()); + Assert.IsFalse(output["forbidnull"]?.AsBoolean()); + Assert.AreEqual("IIterator", output["interface"]?.AsString()); + Assert.AreEqual("PublicKey", output["key"]?.AsString()); + } + + [TestMethod] + public void FromJson_InvalidType_ShouldThrow() + { + var json = new JObject(); + json["type"] = "InvalidType"; + Assert.ThrowsExactly(() => ExtendedType.FromJson(json)); + } + + [TestMethod] + public void FromJson_NegativeLength_ShouldThrow() + { + var json = new JObject(); + json["type"] = "ByteArray"; + json["length"] = -1; + Assert.ThrowsExactly(() => ExtendedType.FromJson(json)); + } + + [TestMethod] + public void FromJson_InvalidInterface_ShouldThrow() + { + var json = new JObject(); + json["type"] = "InteropInterface"; + json["interface"] = "BadInterface"; + Assert.ThrowsExactly(() => ExtendedType.FromJson(json)); + } + + [TestMethod] + public void FromJson_InvalidKey_ShouldThrow() + { + var json = new JObject(); + json["type"] = "Map"; + json["key"] = "BadKey"; + Assert.ThrowsExactly(() => ExtendedType.FromJson(json)); + } + + [TestMethod] + public void ToStackItem_And_FromStackItem_ShouldRoundtrip() + { + var original = new ExtendedType + { + Type = ContractParameterType.Map, + NamedType = "MapType", + Length = 20, + ForbidNull = true, + Interface = Nep25Interface.IIterator, + Key = ContractParameterType.Hash160 + }; + + var refCounter = new ReferenceCounter(); + var structItem = original.ToStackItem(refCounter); + + var copy = new ExtendedType(); + copy.FromStackItem(structItem); + + Assert.AreEqual(original.Type, copy.Type); + Assert.AreEqual(original.NamedType, copy.NamedType); + Assert.AreEqual(original.Length, copy.Length); + Assert.AreEqual(original.ForbidNull, copy.ForbidNull); + Assert.AreEqual(original.Interface, copy.Interface); + Assert.AreEqual(original.Key, copy.Key); + } + } +}