Skip to content
Open

NEP-25 #4043

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
b5b0a26
NEP-25
shargon Jul 2, 2025
9fa095d
Clean
shargon Jul 2, 2025
ddba39e
Merge branch 'dev' into nep-25
Jul 19, 2025
46ea96f
Update src/Neo/SmartContract/Manifest/ExtendedType.cs
shargon Jul 29, 2025
18a250c
Update src/Neo/SmartContract/Manifest/ExtendedType.cs
shargon Jul 29, 2025
368cfdc
Use also param
shargon Jul 29, 2025
418a8ab
Merge branch 'dev' into nep-25
shargon Jul 29, 2025
d0f8d21
Rename var
shargon Jul 30, 2025
d392f50
Use the same type
shargon Jul 30, 2025
55ec5e8
Revert "Use the same type"
shargon Jul 30, 2025
7ab0c64
Add value
shargon Jul 30, 2025
b36370a
Fields
shargon Jul 30, 2025
2e0152f
Merge branch 'dev' into nep-25
shargon Jul 30, 2025
5e47abc
Merge branch 'dev' into nep-25
shargon Jul 31, 2025
8929079
Merge branch 'dev' into nep-25
shargon Aug 3, 2025
1f8d488
Merge branch 'dev' into nep-25
Jim8y Aug 6, 2025
d21d906
docs: add NEP-25 implementation documentation
Jim8y Aug 6, 2025
7c073f2
test: fix ExtendedType tests for 8-field StackItem structure
Jim8y Aug 6, 2025
a1248b2
Merge branch 'dev' into nep-25
cschuchardt88 Aug 6, 2025
b0cd9b1
Merge branch 'dev' into nep-25
shargon Aug 7, 2025
d3c8564
Move to ContractParameterDefinition
shargon Aug 7, 2025
0e85e4a
Use ContractParameterType for Key
shargon Aug 7, 2025
65f27ec
Merge branch 'dev' into nep-25
shargon Aug 12, 2025
cfd07b4
Fix UT
shargon Aug 12, 2025
f6aa6aa
Merge branch 'dev' into nep-25
shargon Aug 28, 2025
890dcd4
remove md
shargon Aug 29, 2025
5ffeb94
Anna's review
shargon Aug 29, 2025
de230cc
Merge branch 'dev' into nep-25
shargon Sep 8, 2025
a7967e5
namedtypes
shargon Sep 8, 2025
c3759a8
Merge branch 'nep-25' of https://github.com/neo-project/neo into nep-25
shargon Sep 8, 2025
8c6aa4b
allow null
shargon Sep 8, 2025
3b20380
clean
shargon Sep 8, 2025
d093162
clean
shargon Sep 8, 2025
2fa6247
Merge branch 'dev' into nep-25
shargon Sep 11, 2025
761ca46
fix build
shargon Sep 11, 2025
2ac2da1
Merge branch 'dev' into nep-25
ajara87 Sep 13, 2025
d2e9871
Merge branch 'dev' into nep-25
shargon Sep 16, 2025
14a9176
Merge branch 'dev' into nep-25
ajara87 Sep 16, 2025
7cd74e6
Merge branch 'dev' into nep-25
Jim8y Sep 22, 2025
35229d4
Merge branch 'dev' into nep-25
shargon Oct 3, 2025
8718ed4
Fix Serialize with null item
shargon Oct 3, 2025
3b56be4
Ensure the right type or null
shargon Oct 3, 2025
e2ac006
use map for serialize
shargon Oct 3, 2025
ac21144
Fix map
shargon Oct 3, 2025
242dbb5
Merge branch 'dev' into nep-25
shargon Oct 4, 2025
7c3a377
Merge branch 'dev' into nep-25
shargon Oct 6, 2025
3fb806a
Enforce NEP-25 extended type validation (#4187)
Jim8y Oct 6, 2025
20965c4
Apply suggestions from code review
shargon Oct 6, 2025
b9cfdb6
Update src/Neo/SmartContract/Manifest/ContractAbi.cs
shargon Oct 6, 2025
c0fd6f2
Update src/Neo/SmartContract/Manifest/ContractEventDescriptor.cs
shargon Oct 6, 2025
b6a80d1
Update src/Neo/SmartContract/Manifest/ContractMethodDescriptor.cs
shargon Oct 6, 2025
c45eecf
Update src/Neo/SmartContract/Manifest/ContractParameterDefinition.cs
shargon Oct 6, 2025
51adba8
Update src/Neo/SmartContract/Manifest/ContractMethodDescriptor.cs
shargon Oct 6, 2025
3b0ab01
Update src/Neo/SmartContract/Manifest/ContractMethodDescriptor.cs
shargon Oct 6, 2025
4ca1f65
Update src/Neo/SmartContract/Manifest/ContractParameterDefinition.cs
shargon Oct 6, 2025
0118b4c
Apply suggestions from code review
shargon Oct 7, 2025
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
123 changes: 105 additions & 18 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,51 @@ 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;

/// <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; }

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)
{
ret.Add(new Map(NamedTypes.ToDictionary(p => (PrimitiveType)p.Key, p => (StackItem)p.Value.ToStackItem(referenceCounter)), referenceCounter));
}

return ret;
}

/// <summary>
Expand All @@ -60,15 +81,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(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<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 All @@ -81,15 +157,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 +181,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
8 changes: 5 additions & 3 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,15 +55,16 @@ 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");
if (string.IsNullOrEmpty(descriptor.Name)) throw new FormatException("Name in ContractEventDescriptor are empty");
_ = descriptor.Parameters.ToDictionary(p => p.Name);
return descriptor;
}
Expand Down
64 changes: 46 additions & 18 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 All @@ -28,6 +29,11 @@ public class ContractMethodDescriptor : ContractEventDescriptor, IEquatable<Cont
/// </summary>
public ContractParameterType ReturnType { get; set; }

/// <summary>
/// NEP-25 extended return type
/// </summary>
public ExtendedType ExtendedReturnType { get; set; }

/// <summary>
/// The position of the method in the contract script.
/// </summary>
Expand All @@ -42,46 +48,63 @@ public class ContractMethodDescriptor : ContractEventDescriptor, IEquatable<Cont
public override void FromStackItem(StackItem stackItem)
{
base.FromStackItem(stackItem);
Struct @struct = (Struct)stackItem;
ReturnType = (ContractParameterType)(byte)@struct[2].GetInteger();
Offset = (int)@struct[3].GetInteger();
Safe = @struct[4].GetBoolean();
var item = (Struct)stackItem;
ReturnType = (ContractParameterType)(byte)item[2].GetInteger();
Offset = (int)item[3].GetInteger();
Safe = item[4].GetBoolean();

if (item.Count >= 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));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like a bit inconsistent encoding, we add NULL stackitem for missing NamedTypes, but do not add it for missing ExtendedReturnType. Should work both ways, but I'd prefer some unified approach to new fields. Same thing with other types.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think it's solved now

return item;
}

/// <summary>
/// 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()
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;
}

Expand All @@ -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;
}

Expand All @@ -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)
Expand All @@ -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)]
Expand Down
Loading
Loading