Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
8983731
Add invokeabi command for simplified contract invocation
Jim8y Jun 29, 2025
65334cb
Merge branch 'dev' into feature/invokeabi-command
Jun 29, 2025
5756d86
Add unit tests for invokeabi command
Jim8y Jun 29, 2025
cd1610c
Apply dotnet format
Jim8y Jun 29, 2025
416c407
Make tests production ready with proper mocking and integration tests
Jim8y Jun 29, 2025
7929f41
Apply reviewer feedback and fix test issues
Jim8y Jun 29, 2025
e8f8f13
Apply dotnet format - remove trailing whitespace and improve JArray i…
Jim8y Jun 29, 2025
f218608
Apply refactoring suggestion: extract type inference logic to separat…
Jim8y Jun 29, 2025
d4a2a1e
Remove unrelated benchmark files
Jim8y Jun 29, 2025
0819565
Fix GitHub Actions CI compilation error
Jim8y Jun 29, 2025
0587ba7
Update src/Neo.CLI/CLI/MainService.Contracts.cs
Jun 30, 2025
739d1fc
Update tests/Neo.CLI.Tests/UT_MainService_Contracts.cs
Jun 30, 2025
a0c620c
Update tests/Neo.CLI.Tests/UT_MainService_Contracts.cs
Jun 30, 2025
d41688b
Improve invokeabi command implementation
Jim8y Jun 30, 2025
61e8dd0
Format code with dotnet format
Jim8y Jun 30, 2025
bcce3f7
Merge branch 'dev' into feature/invokeabi-command
cschuchardt88 Jun 30, 2025
373793f
Merge branch 'dev' into feature/invokeabi-command
Jun 30, 2025
68b6d52
Fix array parsing of ContractParameter objects
Jim8y Jun 30, 2025
f195c25
Fix array parameter parsing to preserve ContractParameter format
Jim8y Jun 30, 2025
4874f4c
Add Map parameter support for ContractParameter format
Jim8y Jun 30, 2025
76ec56d
Merge branch 'dev' into feature/invokeabi-command
Wi1l-B0t Jun 30, 2025
ba63755
Merge branch 'dev' into feature/invokeabi-command
NGDAdmin Jul 1, 2025
66a317b
Merge branch 'dev' into feature/invokeabi-command
Jul 1, 2025
75d7797
Merge branch 'dev' into feature/invokeabi-command
Wi1l-B0t Jul 3, 2025
911f927
Merge branch 'dev' into feature/invokeabi-command
Wi1l-B0t Jul 7, 2025
7ffba62
Merge branch 'dev' into feature/invokeabi-command
Jim8y Jul 23, 2025
864bbb1
Address PR review comments for invokeabi command
Jim8y Jul 24, 2025
538a363
fmt
Jim8y Jul 24, 2025
f667d30
Merge branch 'dev' into feature/invokeabi-command
shargon Jul 29, 2025
750beb9
Merge branch 'dev' into feature/invokeabi-command
Jim8y Jul 31, 2025
b698d22
Merge branch 'dev' into feature/invokeabi-command
Wi1l-B0t Jul 31, 2025
456d14f
Merge branch 'dev' into feature/invokeabi-command
NGDAdmin Aug 1, 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
279 changes: 279 additions & 0 deletions src/Neo.CLI/CLI/MainService.Contracts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
// modifications are permitted.

using Neo.ConsoleService;
using Neo.Cryptography.ECC;
using Neo.Json;
using Neo.Network.P2P.Payloads;
using Neo.SmartContract;
using Neo.SmartContract.Native;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;

Expand Down Expand Up @@ -186,5 +188,282 @@ private void OnInvokeCommand(UInt160 scriptHash, string operation, JArray? contr
}
SignAndSendTx(NeoSystem.StoreView, tx);
}

/// <summary>
/// Process "invokeabi" command - invokes a contract method with parameters parsed according to the contract's ABI
/// </summary>
/// <param name="scriptHash">Script hash</param>
/// <param name="operation">Operation</param>
/// <param name="args">Arguments as an array of values that will be parsed according to the ABI</param>
/// <param name="sender">Transaction's sender</param>
/// <param name="signerAccounts">Signer's accounts</param>
/// <param name="maxGas">Max fee for running the script, in the unit of GAS</param>
[ConsoleCommand("invokeabi", Category = "Contract Commands")]
private void OnInvokeAbiCommand(UInt160 scriptHash, string operation,
JArray? args = null, UInt160? sender = null, UInt160[]? signerAccounts = null, decimal maxGas = 20)
{
// Get the contract from storage
var contract = NativeContract.ContractManagement.GetContract(NeoSystem.StoreView, scriptHash);
if (contract == null)
{
ConsoleHelper.Error("Contract does not exist.");
return;
}

// Check if contract has valid ABI
if (contract.Manifest?.Abi == null)
{
ConsoleHelper.Error("Contract ABI is not available.");
return;
}

// Find the method in the ABI with matching parameter count
var paramCount = args?.Count ?? 0;
var method = contract.Manifest.Abi.GetMethod(operation, paramCount);
if (method == null)
{
// Try to find any method with that name for a better error message
var anyMethod = contract.Manifest.Abi.GetMethod(operation, -1);
if (anyMethod != null)
{
ConsoleHelper.Error($"Method '{operation}' exists but expects {anyMethod.Parameters.Length} parameters, not {paramCount}.");
}
else
{
ConsoleHelper.Error($"Method '{operation}' does not exist in this contract.");
}
return;
}

// Validate parameter count - moved outside parsing loop for better performance
var expectedParamCount = method.Parameters.Length;
var actualParamCount = args?.Count ?? 0;

if (actualParamCount != expectedParamCount)
{
ConsoleHelper.Error($"Method '{operation}' expects exactly {expectedParamCount} parameters but {actualParamCount} were provided.");
return;
}

// Parse parameters according to the ABI
JArray? contractParameters = null;
if (args != null && args.Count > 0)
{
contractParameters = new JArray();
for (int i = 0; i < args.Count; i++)
{
var paramDef = method.Parameters[i];
var paramValue = args[i];

try
{
var contractParam = ParseParameterFromAbi(paramDef.Type, paramValue);
contractParameters.Add(contractParam.ToJson());
Copy link
Member

@superboyiii superboyiii Jun 30, 2025

Choose a reason for hiding this comment

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

This could return wrong contractParameters if it includes an array type param inside, for example:
the contractParameters of

invokeabi 0x49cf4e5378ffcd4dec034fd98a174c5491e395e2 designateAsRole [4,[{"type":"PublicKey","value":"0244d12f3e6b8eba7d0bc0cf0c176d9df545141f4d3447f8463c1b16afb90b1ea8"}]] NgrQRsSfE6v66GpDCFDdzj1d2KeFVoPfBP NgrQRsSfE6v66GpDCFDdzj1d2KeFVoPfBP

will be:

{[{"type":"Integer","value":"4"},{"type":"Array","value":[{"type":"Map","value":[{"key":{"type":"String","value":"type"},"value":{"type":"String","value":"PublicKey"}},{"key":{"type":"String","value":"value"},"value":{"type":"String","value":"0244d12f3e6b8eba7d0bc0cf0c176d9df545141f4d3447f8463c1b16afb90b1ea8"}}]}]}]}

but it should be:

[{"type":"Integer","value":"4"},{"type":"Array","value":[{"type":"PublicKey","value":"0244d12f3e6b8eba7d0bc0cf0c176d9df545141f4d3447f8463c1b16afb90b1ea8"}]}]

This can make invokeabi fail:
1751267880345

Copy link
Member

Choose a reason for hiding this comment

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

This should be fixed before merge

Copy link
Member

Choose a reason for hiding this comment

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

This was addressed? Otherwise please create an issue for that

}
catch (Exception ex)
{
ConsoleHelper.Error($"Failed to parse parameter '{paramDef.Name ?? $"at index {i}"}' (index {i}): {ex.Message}");
return;
}
}
}

// Call the original invoke command with the parsed parameters
OnInvokeCommand(scriptHash, operation, contractParameters, sender, signerAccounts, maxGas);
}

/// <summary>
/// Parse a parameter value according to its ABI type
/// </summary>
private ContractParameter ParseParameterFromAbi(ContractParameterType type, JToken? value)
Copy link
Member

@cschuchardt88 cschuchardt88 Jun 30, 2025

Choose a reason for hiding this comment

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

Copy this code instead, Simpler and cleaner and faster. All the logic is there. For example when a JToken is null

private static StackItem ToStackItem(ContractParameter parameter, List<(StackItem, ContractParameter)> context)
{
if (parameter is null) throw new ArgumentNullException(nameof(parameter));
if (parameter.Value is null) return StackItem.Null;
StackItem stackItem = null;
switch (parameter.Type)
{
case ContractParameterType.Array:
if (context is null)
context = [];
else
(stackItem, _) = context.FirstOrDefault(p => ReferenceEquals(p.Item2, parameter));
if (stackItem is null)
{
stackItem = new Array(((IList<ContractParameter>)parameter.Value).Select(p => ToStackItem(p, context)));
context.Add((stackItem, parameter));
}
break;
case ContractParameterType.Map:
if (context is null)
context = [];
else
(stackItem, _) = context.FirstOrDefault(p => ReferenceEquals(p.Item2, parameter));
if (stackItem is null)
{
Map map = new();
foreach (var pair in (IList<KeyValuePair<ContractParameter, ContractParameter>>)parameter.Value)
map[(PrimitiveType)ToStackItem(pair.Key, context)] = ToStackItem(pair.Value, context);
stackItem = map;
context.Add((stackItem, parameter));
}
break;
case ContractParameterType.Boolean:
stackItem = (bool)parameter.Value;
break;
case ContractParameterType.ByteArray:
case ContractParameterType.Signature:
stackItem = (byte[])parameter.Value;
break;
case ContractParameterType.Integer:
stackItem = (BigInteger)parameter.Value;
break;
case ContractParameterType.Hash160:
stackItem = ((UInt160)parameter.Value).ToArray();
break;
case ContractParameterType.Hash256:
stackItem = ((UInt256)parameter.Value).ToArray();
break;
case ContractParameterType.PublicKey:
stackItem = ((ECPoint)parameter.Value).EncodePoint(true);
break;
case ContractParameterType.String:
stackItem = (string)parameter.Value;
break;
default:
throw new ArgumentException($"ContractParameterType({parameter.Type}) is not supported to StackItem.");
}
return stackItem;
}

Copy link
Member

Choose a reason for hiding this comment

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

agreed

{
if (value == null || value == JToken.Null)
return new ContractParameter { Type = type, Value = null };

return type switch
{
ContractParameterType.Boolean => new ContractParameter { Type = type, Value = value.AsBoolean() },
ContractParameterType.Integer => ParseIntegerParameter(value),
ContractParameterType.ByteArray => ParseByteArrayParameter(value),
ContractParameterType.String => new ContractParameter { Type = type, Value = value.AsString() },
ContractParameterType.Hash160 => ParseHash160Parameter(value),
ContractParameterType.Hash256 => ParseHash256Parameter(value),
ContractParameterType.PublicKey => ParsePublicKeyParameter(value),
ContractParameterType.Signature => ParseSignatureParameter(value),
ContractParameterType.Array => ParseArrayParameter(value),
ContractParameterType.Map => ParseMapParameter(value),
ContractParameterType.Any => InferParameterFromToken(value),
ContractParameterType.InteropInterface => throw new NotSupportedException("InteropInterface type cannot be parsed from JSON"),
_ => throw new ArgumentException($"Unsupported parameter type: {type}")
};
}

/// <summary>
/// Parse integer parameter with error handling
/// </summary>
private ContractParameter ParseIntegerParameter(JToken value)
{
try
{
return new ContractParameter { Type = ContractParameterType.Integer, Value = BigInteger.Parse(value.AsString()) };
}
catch (FormatException)
{
throw new ArgumentException($"Invalid integer format. Expected a numeric string, got: '{value.AsString()}'");
}
}

/// <summary>
/// Parse byte array parameter with error handling
/// </summary>
private ContractParameter ParseByteArrayParameter(JToken value)
{
try
{
return new ContractParameter { Type = ContractParameterType.ByteArray, Value = Convert.FromBase64String(value.AsString()) };
}
catch (FormatException)
{
throw new ArgumentException($"Invalid ByteArray format. Expected a Base64 encoded string, got: '{value.AsString()}'");
}
}

/// <summary>
/// Parse Hash160 parameter with error handling
/// </summary>
private ContractParameter ParseHash160Parameter(JToken value)
{
try
{
return new ContractParameter { Type = ContractParameterType.Hash160, Value = UInt160.Parse(value.AsString()) };
}
catch (FormatException)
{
throw new ArgumentException($"Invalid Hash160 format. Expected format: '0x' followed by 40 hex characters (e.g., '0x1234...abcd'), got: '{value.AsString()}'");
}
}

/// <summary>
/// Parse Hash256 parameter with error handling
/// </summary>
private ContractParameter ParseHash256Parameter(JToken value)
{
try
{
return new ContractParameter { Type = ContractParameterType.Hash256, Value = UInt256.Parse(value.AsString()) };
}
catch (FormatException)
{
throw new ArgumentException($"Invalid Hash256 format. Expected format: '0x' followed by 64 hex characters, got: '{value.AsString()}'");
}
}

/// <summary>
/// Parse PublicKey parameter with error handling
/// </summary>
private ContractParameter ParsePublicKeyParameter(JToken value)
{
try
{
return new ContractParameter { Type = ContractParameterType.PublicKey, Value = ECPoint.Parse(value.AsString(), ECCurve.Secp256r1) };
}
catch (FormatException)
{
throw new ArgumentException($"Invalid PublicKey format. Expected a hex string starting with '02' or '03' (33 bytes) or '04' (65 bytes), got: '{value.AsString()}'");
}
}

/// <summary>
/// Parse Signature parameter with error handling
/// </summary>
private ContractParameter ParseSignatureParameter(JToken value)
{
try
{
return new ContractParameter { Type = ContractParameterType.Signature, Value = Convert.FromBase64String(value.AsString()) };
}
catch (FormatException)
{
throw new ArgumentException($"Invalid Signature format. Expected a Base64 encoded string, got: '{value.AsString()}'");
}
}

/// <summary>
/// Parse Array parameter with type inference
/// </summary>
private ContractParameter ParseArrayParameter(JToken value)
{
if (value is not JArray array)
throw new ArgumentException($"Expected array value for Array parameter type, got: {value.GetType().Name}");

var items = new ContractParameter[array.Count];
for (int j = 0; j < array.Count; j++)
{
var element = array[j];
// Check if this is already a ContractParameter format
if (element is JObject obj && obj.ContainsProperty("type") && obj.ContainsProperty("value"))
{
items[j] = ContractParameter.FromJson(obj);
}
else
{
// Otherwise, infer the type
items[j] = element != null ? InferParameterFromToken(element) : new ContractParameter { Type = ContractParameterType.Any, Value = null };
}
}
return new ContractParameter { Type = ContractParameterType.Array, Value = items };
}

/// <summary>
/// Parse Map parameter with type inference
/// </summary>
private ContractParameter ParseMapParameter(JToken value)
{
if (value is not JObject map)
throw new ArgumentException("Expected object value for Map parameter type");

// Check if this is a ContractParameter format map
if (map.ContainsProperty("type") && map["type"]?.AsString() == "Map" && map.ContainsProperty("value"))
{
return ContractParameter.FromJson(map);
}

// Otherwise, parse as a regular map with inferred types
var dict = new List<KeyValuePair<ContractParameter, ContractParameter>>();
foreach (var kvp in map.Properties)
{
// Keys are always strings in JSON
var key = new ContractParameter { Type = ContractParameterType.String, Value = kvp.Key };

// For values, check if they are ContractParameter format
var val = kvp.Value;
if (val is JObject valObj && valObj.ContainsProperty("type") && valObj.ContainsProperty("value"))
{
dict.Add(new KeyValuePair<ContractParameter, ContractParameter>(key, ContractParameter.FromJson(valObj)));
}
else
{
var valueParam = val != null ? InferParameterFromToken(val) : new ContractParameter { Type = ContractParameterType.Any, Value = null };
dict.Add(new KeyValuePair<ContractParameter, ContractParameter>(key, valueParam));
}
}
return new ContractParameter { Type = ContractParameterType.Map, Value = dict };
}

/// <summary>
/// Infers the parameter type from a JToken and parses it accordingly
/// </summary>
private ContractParameter InferParameterFromToken(JToken value)
{
return value switch
{
JBoolean => ParseParameterFromAbi(ContractParameterType.Boolean, value),
JNumber => ParseParameterFromAbi(ContractParameterType.Integer, value),
JString => ParseParameterFromAbi(ContractParameterType.String, value),
JArray => ParseParameterFromAbi(ContractParameterType.Array, value),
JObject => ParseParameterFromAbi(ContractParameterType.Map, value),
_ => throw new ArgumentException($"Cannot infer type for value: {value}")
};
}
}
}
19 changes: 19 additions & 0 deletions tests/Neo.CLI.Tests/Neo.CLI.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="MSTest" Version="$(MSTestVersion)" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Neo.CLI\Neo.CLI.csproj" />
<ProjectReference Include="..\Neo.UnitTests\Neo.UnitTests.csproj" />
</ItemGroup>

</Project>
Loading
Loading