Skip to content
Closed

NEP-25 #4043

Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
87 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
09c4839
Update tests/Neo.UnitTests/SmartContract/Manifest/UT_ExtendedType.cs
shargon Oct 9, 2025
41782d5
Merge branch 'dev' into nep-25
ajara87 Oct 11, 2025
afa0632
Update tests/Neo.UnitTests/SmartContract/Manifest/UT_ExtendedType.cs
shargon Oct 11, 2025
d327be8
Update src/Neo/SmartContract/Manifest/ExtendedType.cs
shargon Oct 11, 2025
a8408f4
Merge branch 'dev' into nep-25
shargon Oct 11, 2025
169ea7b
@ajara87 suggestion
shargon Oct 11, 2025
60eac04
Merge branch 'dev' into nep-25
ajara87 Oct 11, 2025
c4bf05a
fix wrong review of @cschuchardt88
shargon Oct 12, 2025
ad1ffde
Add Ut without the key
shargon Oct 12, 2025
c49499d
Clean code
shargon Oct 13, 2025
4e1efa0
Remove void restriction
shargon Oct 13, 2025
054e5de
Clean enum
shargon Oct 13, 2025
aca22c0
Merge branch 'dev' into nep-25
ajara87 Oct 14, 2025
81b7b5a
Merge branch 'dev' into nep-25
NGDAdmin Oct 14, 2025
7be35bd
Merge branch 'dev' into nep-25
shargon Oct 16, 2025
a4a3d3d
Merge branch 'dev' into nep-25
shargon Oct 18, 2025
a23ee2d
- Styling
shargon Oct 18, 2025
c719313
Roman's review: Ensure no circular named type
shargon Oct 18, 2025
56ea443
Merge branch 'dev' into nep-25
ajara87 Oct 23, 2025
533d472
Merge branch 'dev' into nep-25
shargon Oct 23, 2025
fefdb95
Merge branch 'dev' into nep-25
ajara87 Oct 23, 2025
f9e1526
Merge branch 'dev' into nep-25
shargon Oct 24, 2025
88b861b
Circular reference change
shargon Oct 24, 2025
a799ca6
Merge branch 'nep-25' of https://github.com/neo-project/neo into nep-25
shargon Oct 24, 2025
c575449
Merge branch 'dev' into nep-25
ajara87 Oct 26, 2025
9b4cccc
UT for circular reference (#4251)
ajara87 Oct 26, 2025
2d3e1fe
Merge branch 'dev' into nep-25
ajara87 Oct 28, 2025
20aa730
Merge branch 'dev' into nep-25
ajara87 Oct 29, 2025
0c5d4c6
[NEP25] - Validate circular reference (#4258)
ajara87 Oct 30, 2025
ba009ea
Allow Fields with 0 length
shargon Oct 31, 2025
3fd4f14
Merge branch 'dev' into nep-25
ajara87 Nov 2, 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
32 changes: 29 additions & 3 deletions src/Neo/SmartContract/Manifest/ContractMethodDescriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,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 @@ -46,6 +51,16 @@ public override void FromStackItem(StackItem stackItem)
ReturnType = (ContractParameterType)(byte)@struct[2].GetInteger();
Offset = (int)@struct[3].GetInteger();
Safe = @struct[4].GetBoolean();

if (@struct.Count >= 6)
{
ExtendedReturnType = new ExtendedType();
ExtendedReturnType.FromStackItem((VM.Types.Array)@struct[5], 0);
}
else
{
ExtendedReturnType = null;
}
}

public override StackItem ToStackItem(IReferenceCounter referenceCounter)
Expand All @@ -54,6 +69,11 @@ public override StackItem ToStackItem(IReferenceCounter referenceCounter)
@struct.Add((byte)ReturnType);
@struct.Add(Offset);
@struct.Add(Safe);
if (ExtendedReturnType != null)
{
var structExtended = new Struct(referenceCounter);
@struct.Add(ExtendedReturnType.ToStackItem(referenceCounter, structExtended));
}
return @struct;
}

Expand All @@ -70,7 +90,8 @@ public override StackItem ToStackItem(IReferenceCounter referenceCounter)
Parameters = ((JArray)json["parameters"]).Select(u => ContractParameterDefinition.FromJson((JObject)u)).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();
_ = descriptor.Parameters.ToDictionary(p => p.Name);
Expand All @@ -89,6 +110,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 @@ -100,7 +125,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 @@ -113,7 +139,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
227 changes: 227 additions & 0 deletions src/Neo/SmartContract/Manifest/ExtendedType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
// 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;

namespace Neo.SmartContract.Manifest
{
#nullable enable

public class ExtendedType : IInteroperable, IEquatable<ExtendedType>
{
/// <summary>
/// The type of the parameter. It can be any value of <see cref="ContractParameterType"/> except <see cref="ContractParameterType.Void"/>.
/// </summary>
public ContractParameterType Type { get; set; }

/// <summary>
/// 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 diffent 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.
/// </summary>
public string? NamedType { get; set; }

/// <summary>
/// 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".
/// </summary>
public int? Length { get; set; }

/// <summary>
/// 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.
/// </summary>
public bool? ForbidNull { get; set; }

/// <summary>
/// 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.
/// </summary>
public Nep25Interface? Interface { get; set; }

/// <summary>
/// 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.
/// </summary>
public Nep25Key? Key { get; set; }

void IInteroperable.FromStackItem(StackItem stackItem)
{
FromStackItem((VM.Types.Array)stackItem, 0);
}

internal void FromStackItem(VM.Types.Array @struct, int startIndex)
{
Type = (ContractParameterType)(byte)@struct[startIndex++].GetInteger();
if (!Enum.IsDefined(typeof(ContractParameterType), Type)) throw new FormatException();
NamedType = @struct[startIndex++].GetString();

if (@struct[startIndex++] is Integer length)
{
Length = checked((int)length.GetInteger());
if (Length < 0) throw new FormatException("Length must be non-negative.");
if (Length > ExecutionEngineLimits.Default.MaxItemSize) throw new FormatException($"Length must less than {ExecutionEngineLimits.Default.MaxItemSize}.");
}
else
{
Length = null;
}

if (@struct[startIndex++] is VM.Types.Boolean forbidnull)
{
ForbidNull = forbidnull.GetBoolean();
}
else
{
ForbidNull = null;
}

if (@struct[startIndex++] is ByteString interf)
{
if (!Enum.TryParse<Nep25Interface>(interf.GetString(), false, out var inferValue))
throw new FormatException();

Interface = inferValue;
}
else
{
Interface = null;
}

if (@struct[startIndex++] is ByteString key)
{
if (!Enum.TryParse<Nep25Key>(key.GetString(), false, out var keyValue))
throw new FormatException();

Key = keyValue;
}
else
{
Key = null;
}
}
Copy link
Member

Choose a reason for hiding this comment

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

I think we need to add a verification method that will be used by both FromStackItem and FromJson. This verification method should check that the combination of non-Null fields is valid.

For example, according to NEP-25 NamedType can't be combined with other fields except Type. There are other similar cases, we need to check all of them during deserialization.

Copy link
Member Author

Choose a reason for hiding this comment

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

We should enumerate these checks in NEP-25

Copy link
Member Author

Choose a reason for hiding this comment

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

@AnnaShaleva this property it's also optional in the proposal

Copy link
Contributor

Choose a reason for hiding this comment

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

The question is more about semantic checks, almost all fields are optional, but not all combinations of fields are valid. This is described in NEP-25 and both deserialization methods should check for it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Still here and #4187 (comment) is relevant, we can check a lot of things in ExtendedType without external data and this can be leveraged, then an additional ABI-level check on knownNamedTypes can be performed and that's it. Allows to fail faster in error case.


internal StackItem ToStackItem(IReferenceCounter referenceCounter, Struct @struct)
{
@struct.Add((byte)Type);
@struct.Add(NamedType ?? StackItem.Null);
@struct.Add(Length ?? StackItem.Null);
@struct.Add(ForbidNull ?? StackItem.Null);
@struct.Add(Interface?.ToString() ?? StackItem.Null);
@struct.Add(Key?.ToString() ?? StackItem.Null);
return @struct;
}

StackItem IInteroperable.ToStackItem(IReferenceCounter referenceCounter)
{
var @struct = new Struct(referenceCounter);
ToStackItem(referenceCounter, @struct);
return @struct;
}

/// <summary>
/// Converts the type from a JSON object.
/// </summary>
/// <param name="json">The method represented by a JSON object.</param>
/// <returns>The extended type.</returns>
public static ExtendedType FromJson(JObject json)
{
ExtendedType type = new()
{
Type = Enum.Parse<ContractParameterType>(json["type"]?.GetString() ?? throw new FormatException()),
NamedType = json["namedtype"]?.GetString(),
};
if (!Enum.IsDefined(typeof(ContractParameterType), type.Type)) throw new FormatException();
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<Nep25Interface>(json["interface"]!.GetString(), true, out var interfaceValue))
throw new FormatException("Invalid interface value.");
type.Interface = interfaceValue;
}
if (json["key"] != null)
{
if (!Enum.TryParse<Nep25Key>(json["key"]!.GetString(), true, out var keyValue))
throw new FormatException("Invalid key value.");
type.Key = keyValue;
}
return type;
}

/// <summary>
/// Converts the parameter to a JSON object.
/// </summary>
/// <returns>The parameter represented by a JSON object.</returns>
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();
}
return json;
}

public override bool Equals(object? obj) => Equals(obj as ExtendedType);

public override int GetHashCode() => HashCode.Combine(Type, NamedType, Length, ForbidNull, Interface, Key);

public bool Equals(ExtendedType? other)
{
if (other is null) return false;
if (ReferenceEquals(this, other)) return true;

return Type == other.Type
&& NamedType == other.NamedType
&& Length == other.Length
&& ForbidNull == other.ForbidNull
&& Interface == other.Interface
&& Key == other.Key;
}
}
#nullable disable
}

26 changes: 26 additions & 0 deletions src/Neo/SmartContract/Manifest/Nep25Interface.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// </summary>
public enum Nep25Interface
{
/// <summary>
/// Iterator object
/// </summary>
IIterator
}
}
30 changes: 30 additions & 0 deletions src/Neo/SmartContract/Manifest/Nep25Key.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (C) 2015-2025 The Neo Project.
//
// Nep25Key.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
{
/// <summary>
/// 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.
/// </summary>
public enum Nep25Key
{
Signature,
Boolean,
Integer,
Hash160,
Hash256,
ByteArray,
PublicKey,
String
}
}
Loading