Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 59 additions & 3 deletions src/Neo/SmartContract/Manifest/ContractAbi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ void IInteroperable.FromStackItem(StackItem stackItem)
NamedTypes = ((Map)data[2]).ToDictionary(p => p.Key.GetString()!, p => p.Value.ToInteroperable<ExtendedType>());
else
NamedTypes = null;

ValidateExtendedTypes();
}

public StackItem ToStackItem(IReferenceCounter referenceCounter)
Expand All @@ -75,16 +77,70 @@ public StackItem ToStackItem(IReferenceCounter referenceCounter)
/// <returns>The converted ABI.</returns>
public static ContractAbi FromJson(JObject json)
{
Dictionary<string, ExtendedType>? namedTypes = null;
var knownNamedTypes = new HashSet<string>(StringComparer.Ordinal);
if (json!["namedtypes"] is JObject namedTypesJson)
{
foreach (var key in namedTypesJson.Properties.Keys)
{
knownNamedTypes.Add(key);
}

namedTypes = new Dictionary<string, ExtendedType>(namedTypesJson.Properties.Count, StringComparer.Ordinal);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
namedTypes = new Dictionary<string, ExtendedType>(namedTypesJson.Properties.Count, StringComparer.Ordinal);
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() ?? [],
NamedTypes = ((JObject)json!["namedtypes"]!)?.Properties.ToDictionary(u => u.Key, u => ExtendedType.FromJson((JObject)u.Value!))
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");

abi.ValidateExtendedTypes();
return abi;
}

internal void ValidateExtendedTypes()
{
ISet<string> knownNamedTypes = NamedTypes != null
? new HashSet<string>(NamedTypes.Keys, StringComparer.Ordinal)
: new HashSet<string>(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);
}
}
}

/// <summary>
/// Gets the method with the specified name.
/// </summary>
Expand Down
6 changes: 4 additions & 2 deletions src/Neo/SmartContract/Manifest/ContractEventDescriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -54,13 +55,14 @@ public virtual StackItem ToStackItem(IReferenceCounter referenceCounter)
/// Converts the event from a JSON object.
/// </summary>
/// <param name="json">The event represented by a JSON object.</param>
/// <param name="knownNamedTypes">Set of named type identifiers declared in the manifest, if any.</param>
/// <returns>The converted event.</returns>
public static ContractEventDescriptor FromJson(JObject json)
public static ContractEventDescriptor FromJson(JObject json, ISet<string> 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");
_ = descriptor.Parameters.ToDictionary(p => p.Name);
Expand Down
9 changes: 6 additions & 3 deletions src/Neo/SmartContract/Manifest/ContractMethodDescriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Neo.VM;
using Neo.VM.Types;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;

Expand Down Expand Up @@ -56,6 +57,7 @@ public override void FromStackItem(StackItem stackItem)
{
ExtendedReturnType = new ExtendedType();
ExtendedReturnType.FromStackItem((VM.Types.Array)item[5]);
ExtendedReturnType.ValidateForParameterOrReturn(ReturnType, null);
}
else
{
Expand All @@ -80,13 +82,14 @@ public override StackItem ToStackItem(IReferenceCounter referenceCounter)
/// Converts the method from a JSON object.
/// </summary>
/// <param name="json">The method represented by a JSON object.</param>
/// <param name="knownNamedTypes">Set of named type identifiers declared in the manifest, if any.</param>
/// <returns>The converted method.</returns>
public new static ContractMethodDescriptor FromJson(JObject json)
public new static ContractMethodDescriptor FromJson(JObject json, ISet<string> 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<ContractParameterType>(json["returntype"].GetString()),
Offset = json["offset"].GetInt32(),
Safe = json["safe"].GetBoolean(),
Expand All @@ -97,11 +100,11 @@ public override StackItem ToStackItem(IReferenceCounter referenceCounter)
throw new FormatException("Name in ContractMethodDescriptor is 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");
if (descriptor.Offset < 0)
throw new FormatException($"Offset({descriptor.Offset}) in ContractMethodDescriptor is not valid");
descriptor.ExtendedReturnType?.ValidateForParameterOrReturn(descriptor.ReturnType, knownNamedTypes);
return descriptor;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -47,6 +48,7 @@ public void FromStackItem(StackItem stackItem)
{
ExtendedType = new ExtendedType();
ExtendedType.FromStackItem((VM.Types.Array)item[2]);
ExtendedType.ValidateForParameterOrReturn(Type, null);
}
else
{
Expand All @@ -70,8 +72,9 @@ public StackItem ToStackItem(IReferenceCounter referenceCounter)
/// Converts the parameter from a JSON object.
/// </summary>
/// <param name="json">The parameter represented by a JSON object.</param>
/// <param name="knownNamedTypes">Set of named type identifiers declared in the manifest, if any.</param>
/// <returns>The converted parameter.</returns>
public static ContractParameterDefinition FromJson(JObject json)
public static ContractParameterDefinition FromJson(JObject json, ISet<string> knownNamedTypes = null)
{
ContractParameterDefinition parameter = new()
{
Expand All @@ -83,6 +86,7 @@ public static ContractParameterDefinition FromJson(JObject json)
throw new FormatException("Name in ContractParameterDefinition is empty");
if (!Enum.IsDefined(typeof(ContractParameterType), parameter.Type) || parameter.Type == ContractParameterType.Void)
throw new FormatException($"Type({parameter.Type}) in ContractParameterDefinition is not valid");
parameter.ExtendedType?.ValidateForParameterOrReturn(parameter.Type, knownNamedTypes);
return parameter;
}

Expand Down
165 changes: 164 additions & 1 deletion src/Neo/SmartContract/Manifest/ExtendedType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,62 @@
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<ExtendedType>
{
private static readonly Regex NamedTypePattern = new("^[A-Za-z][A-Za-z0-9.]{0,63}$", RegexOptions.Compiled);

private static readonly HashSet<ContractParameterType> LengthAllowedTypes = new()
{
ContractParameterType.Integer,
ContractParameterType.ByteArray,
ContractParameterType.String,
ContractParameterType.Array
};

private static readonly HashSet<ContractParameterType> ForbidNullAllowedTypes = new()
{
ContractParameterType.Hash160,
ContractParameterType.Hash256,
ContractParameterType.ByteArray,
ContractParameterType.String,
ContractParameterType.Array,
ContractParameterType.Map,
ContractParameterType.InteropInterface
};

private static readonly HashSet<ContractParameterType> 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}");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont like this approach.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But why? And what's the alternative?

Copy link
Member

@cschuchardt88 cschuchardt88 Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are many alternatives. But the simplest is to just use throw new FormatException($"Invalid NEP-25 extended type: {message}") instead of a function. But I would prefer to use an exception class with the name Nep25TypeException. The reason for this is when you throw Nep25Error(message) you dont know the exception at hand and can make it hard to track down when debugging. Makes you have to look at Call Stack...etc. It just harder to maintain.


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.");
}

/// <summary>
/// The type of the parameter. It can be any value of <see cref="ContractParameterType"/> except <see cref="ContractParameterType.Void"/>.
/// </summary>
Expand Down Expand Up @@ -214,6 +263,7 @@ public static ExtendedType FromJson(JObject json)
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.");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Somewhat duplicating ValdateCore

if (json["length"] != null)
{
type.Length = json["length"]!.GetInt32();
Expand Down Expand Up @@ -329,7 +379,120 @@ public bool Equals(ExtendedType? other)

return true;
}

internal void ValidateForParameterOrReturn(ContractParameterType expectedType, ISet<string>? knownNamedTypes)
{
ValidateCore(expectedType, allowFields: false, knownNamedTypes, allowNamedTypeReference: true);
}

internal void ValidateForNamedTypeDefinition(ISet<string>? knownNamedTypes)
{
ValidateCore(expectedType: null, allowFields: true, knownNamedTypes, allowNamedTypeReference: true);
}

private void ValidateCore(ContractParameterType? expectedType, bool allowFields, ISet<string>? knownNamedTypes, bool allowNamedTypeReference)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

allowNamedTypeReference is always true

{
if (expectedType.HasValue && Type != expectedType.Value)
throw Nep25Error($"Type mismatch. Expected '{expectedType.Value}', got '{Type}'.");

if (!Enum.IsDefined(typeof(ContractParameterType), Type) || Type == ContractParameterType.Void)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!Enum.IsDefined(typeof(ContractParameterType), Type) || Type == ContractParameterType.Void)
if (!Enum.IsDefined<ContractParameterType>(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.");

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many of the checks are context-independent (can be performed on a single ExtendedType value), so can be done at ExtendedType.FromJSON/FromStackItem level. But that's just a suggestion, functionally ABI-level checks can be sufficient.

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
}

Loading
Loading