Skip to content
Open

NEP-25 #4283

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
166 changes: 149 additions & 17 deletions src/Neo/SmartContract/Manifest/ContractAbi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
using System.Linq;
using Array = Neo.VM.Types.Array;

#nullable enable

namespace Neo.SmartContract.Manifest
{
/// <summary>
Expand All @@ -25,32 +27,69 @@ namespace Neo.SmartContract.Manifest
/// <remarks>For more details, see NEP-14.</remarks>
public class ContractAbi : IInteroperable
{
private IReadOnlyDictionary<(string, int), ContractMethodDescriptor> methodDictionary;
private IReadOnlyDictionary<(string, int), ContractMethodDescriptor>? _methodDictionary;
private const int STATE_UNCHECK = 0;
private const int STATE_CHECKING = 1;
private const int STATE_CHECK = 2;

/// <summary>
/// Gets the methods in the ABI.
/// </summary>
public ContractMethodDescriptor[] Methods { get; set; }
public ContractMethodDescriptor[] Methods { get; set; } = [];

/// <summary>
/// Gets the events in the ABI.
/// </summary>
public ContractEventDescriptor[] Events { get; set; }
public ContractEventDescriptor[] Events { get; set; } = [];

/// <summary>
/// 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.
/// </summary>
public Dictionary<string, ExtendedType>? NamedTypes { get; set; }

public bool HasNEP25
{
get
{
return NamedTypes != null || Events.Any(x => x.HasNEP25) || Methods.Any(x => x.HasNEP25);
}
}

void IInteroperable.FromStackItem(StackItem stackItem)
{
Struct @struct = (Struct)stackItem;
Methods = ((Array)@struct[0]).Select(p => p.ToInteroperable<ContractMethodDescriptor>()).ToArray();
Events = ((Array)@struct[1]).Select(p => p.ToInteroperable<ContractEventDescriptor>()).ToArray();
var data = (Struct)stackItem;
Methods = [.. ((Array)data[0]).Select(p => p.ToInteroperable<ContractMethodDescriptor>())];
Events = [.. ((Array)data[1]).Select(p => p.ToInteroperable<ContractEventDescriptor>())];

if (data.Count >= 3 && !data[2].IsNull)
NamedTypes = ((Map)data[2]).ToDictionary(p => p.Key.GetString()!, p => p.Value.ToInteroperable<ExtendedType>());
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)
{
var map = new Map(referenceCounter);

foreach (var nt in NamedTypes)
{
map[nt.Key] = nt.Value.ToStackItem(referenceCounter);
}

ret.Add(map);
}

return ret;
}

/// <summary>
Expand All @@ -60,15 +99,97 @@ 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(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");

abi.ValidateExtendedTypes();
return abi;
}

private static bool HasCircularReference(string name, IReadOnlyDictionary<string, ExtendedType> namedTypes, Dictionary<string, int> states)
{
if (!states.TryGetValue(name, out var state))
state = STATE_UNCHECK;

if (state == STATE_CHECKING) return true;
if (state == STATE_CHECK) return false;

states[name] = STATE_CHECKING;

var next = namedTypes[name].NamedType;
if (next is not null && namedTypes.ContainsKey(next))
{
if (HasCircularReference(next, namedTypes, states))
return true;
}

states[name] = STATE_CHECK;
return false;
}

internal void ValidateExtendedTypes()
{
ISet<string> knownNamedTypes = NamedTypes != null
? new HashSet<string>(NamedTypes.Keys, StringComparer.Ordinal)
: new HashSet<string>(StringComparer.Ordinal);

if (NamedTypes != null)
{
var states = new Dictionary<string, int>(NamedTypes.Count, StringComparer.Ordinal);
foreach (var (name, type) in NamedTypes)
{
ExtendedType.EnsureValidNamedTypeIdentifier(name);
if (HasCircularReference(name, NamedTypes, states))
{
throw new FormatException($"Circular reference in namedtypes starting at '{name}'");
}

type.ValidateForNamedTypeDefinition(name, 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 All @@ -81,15 +202,17 @@ public static ContractAbi FromJson(JObject json)
/// The method that matches the specified name and number of parameters.
/// If <paramref name="pcount"/> is set to -1, the first method with the specified name will be returned.
/// </returns>
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
{
Expand All @@ -103,11 +226,20 @@ public ContractMethodDescriptor GetMethod(string name, int pcount)
/// <returns>The ABI represented by a JSON object.</returns>
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
14 changes: 12 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 All @@ -34,6 +35,14 @@ public class ContractEventDescriptor : IInteroperable, IEquatable<ContractEventD
/// </summary>
public ContractParameterDefinition[] Parameters { get; set; }

public virtual bool HasNEP25
{
get
{
return Parameters.Any(x => x.ExtendedType != null);
}
}

public virtual void FromStackItem(StackItem stackItem)
{
Struct @struct = (Struct)stackItem;
Expand All @@ -54,13 +63,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
10 changes: 7 additions & 3 deletions src/Neo/SmartContract/Manifest/ContractManifest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,20 +176,24 @@ public JObject ToJson()
/// <summary>
/// Determines whether the manifest is valid.
/// </summary>
/// <param name="limits">The <see cref="ExecutionEngineLimits"/> used for test serialization.</param>
/// <param name="engine">The <see cref="ApplicationEngine"/> used for get the Limits.</param>
/// <param name="hash">The hash of the contract.</param>
/// <returns><see langword="true"/> if the manifest is valid; otherwise, <see langword="false"/>.</returns>
public bool IsValid(ExecutionEngineLimits limits, UInt160 hash)
public bool IsValid(ApplicationEngine engine, UInt160 hash)
{
// Ensure that is serializable
try
{
_ = BinarySerializer.Serialize(ToStackItem(null), limits);
_ = BinarySerializer.Serialize(ToStackItem(null), engine.Limits);
}
catch
{
return false;
}

if (Abi.HasNEP25 && !engine.IsHardforkEnabled(Hardfork.HF_Faun))
return false;

// Check groups
return Groups.All(u => u.IsValid(hash));
}
Expand Down
Loading