diff --git a/src/Plugins/RpcServer/Model/SignerWithWitness.cs b/src/Plugins/RpcServer/Model/SignerWithWitness.cs new file mode 100644 index 0000000000..9d756196ef --- /dev/null +++ b/src/Plugins/RpcServer/Model/SignerWithWitness.cs @@ -0,0 +1,106 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// SignerWithWitness.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. + +#nullable enable + +using Neo.Cryptography.ECC; +using Neo.Json; +using Neo.Network.P2P.Payloads; +using Neo.Wallets; +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace Neo.Plugins.RpcServer.Model; + +public class SignerWithWitness(Signer? signer, Witness? witness) +{ + public Signer? Signer { get; } = signer; + public Witness? Witness { get; } = witness; + + public static bool TryParse(JToken value, ProtocolSettings settings, [NotNullWhen(true)] out SignerWithWitness? signerWithWitness) + { + signerWithWitness = null; + + if (value == null) + return false; + + if (value is JObject jObject) + { + Signer? signer = null; + Witness? witness = null; + + if (jObject.ContainsProperty("account")) + { + signer = SignerFromJson(jObject, settings); + } + if (jObject.ContainsProperty("invocation") || jObject.ContainsProperty("verification")) + { + witness = WitnessFromJson(jObject); + } + + if (signer != null || witness != null) + { + signerWithWitness = new SignerWithWitness(signer, witness); + return true; + } + } + + return false; + } + + private static Signer SignerFromJson(JObject jObject, ProtocolSettings settings) + { + return new Signer + { + Account = AddressToScriptHash(jObject["account"].AsString(), settings.AddressVersion), + Scopes = (WitnessScope)Enum.Parse(typeof(WitnessScope), jObject["scopes"]?.AsString()), + AllowedContracts = ((JArray)jObject["allowedcontracts"])?.Select(p => UInt160.Parse(p.AsString())).ToArray() ?? Array.Empty(), + AllowedGroups = ((JArray)jObject["allowedgroups"])?.Select(p => ECPoint.Parse(p.AsString(), ECCurve.Secp256r1)).ToArray() ?? Array.Empty(), + Rules = ((JArray)jObject["rules"])?.Select(r => WitnessRule.FromJson((JObject)r)).ToArray() ?? Array.Empty(), + }; + } + + private static Witness WitnessFromJson(JObject jObject) + { + return new Witness + { + InvocationScript = Convert.FromBase64String(jObject["invocation"]?.AsString() ?? string.Empty), + VerificationScript = Convert.FromBase64String(jObject["verification"]?.AsString() ?? string.Empty) + }; + } + + public static SignerWithWitness[] ParseArray(JArray array, ProtocolSettings settings) + { + if (array == null) + throw new ArgumentNullException(nameof(array)); + + if (array.Count > Transaction.MaxTransactionAttributes) + throw new RpcException(RpcError.InvalidParams.WithData("Max allowed signers or witnesses exceeded.")); + + return array.Select(item => + { + if (TryParse(item, settings, out var signerWithWitness)) + return signerWithWitness; + throw new ArgumentException($"Invalid signer or witness format: {item}"); + }).ToArray(); + } + + private static UInt160 AddressToScriptHash(string address, byte version) + { + if (UInt160.TryParse(address, out var scriptHash)) + { + return scriptHash; + } + + return address.ToScriptHash(version); + } +} diff --git a/src/Plugins/RpcServer/ParameterConverter.cs b/src/Plugins/RpcServer/ParameterConverter.cs index 7d9b754f7c..c37f96eee7 100644 --- a/src/Plugins/RpcServer/ParameterConverter.cs +++ b/src/Plugins/RpcServer/ParameterConverter.cs @@ -10,10 +10,13 @@ // modifications are permitted. using Neo.Json; +using Neo.Network.P2P.Payloads; using Neo.Plugins.RpcServer.Model; +using Neo.SmartContract; using Neo.Wallets; using System; using System.Collections.Generic; +using System.Linq; using JToken = Neo.Json.JToken; namespace Neo.Plugins.RpcServer; @@ -39,7 +42,12 @@ static ParameterConverter() { typeof(bool), token => Result.Ok_Or(token.AsBoolean, CreateInvalidParamError(token)) }, { typeof(UInt256), ConvertUInt256 }, { typeof(ContractNameOrHashOrId), ConvertContractNameOrHashOrId }, - { typeof(BlockHashOrIndex), ConvertBlockHashOrIndex } + { typeof(BlockHashOrIndex), ConvertBlockHashOrIndex }, + { typeof(Signer), ConvertSigner }, + { typeof(ContractParameter), ConvertContractParameter }, + { typeof(Signer[]), ConvertSignerArray }, + { typeof(ContractParameter[]), ConvertContractParameterArray }, + { typeof(Guid), ConvertGuid } }; } @@ -107,6 +115,23 @@ internal static object ConvertUInt160(JToken token, byte addressVersion) RpcError.InvalidParams.WithData($"Invalid UInt160 Format: {token}")); } + internal static object ConvertSignerWithWitnessArray(JToken token, ProtocolSettings settings) + { + if (token is JArray jArray) + { + return SignerWithWitness.ParseArray(jArray, settings); + } + + if (token is JObject jObject) + { + if (SignerWithWitness.TryParse(jObject, settings, out var signerWithWitness)) + { + return new[] { signerWithWitness }; + } + } + throw new RpcException(RpcError.InvalidParams.WithData($"Invalid SignerWithWitness format: {token}")); + } + private static object ConvertUInt256(JToken token) { if (UInt256.TryParse(token.AsString(), out var hash)) @@ -134,6 +159,79 @@ private static object ConvertBlockHashOrIndex(JToken token) throw new RpcException(RpcError.InvalidParams.WithData($"Invalid block hash or index Format: {token}")); } + private static object ConvertSigner(JToken token) + { + if (token is JObject jObject) + { + try + { + return Signer.FromJson(jObject); + } + catch (FormatException) + { + throw new RpcException(CreateInvalidParamError(token)); + } + } + throw new RpcException(CreateInvalidParamError(token)); + } + + private static object ConvertContractParameter(JToken token) + { + if (token is JObject jObject) + { + try + { + return ContractParameter.FromJson(jObject); + } + catch (FormatException) + { + throw new RpcException(CreateInvalidParamError(token)); + } + } + throw new RpcException(CreateInvalidParamError(token)); + } + + private static object ConvertSignerArray(JToken token) + { + if (token is JArray jArray) + { + try + { + return jArray.Select(t => Signer.FromJson(t as JObject)).ToArray(); + } + catch (FormatException) + { + throw new RpcException(CreateInvalidParamError(token)); + } + } + throw new RpcException(CreateInvalidParamError(token)); + } + + private static object ConvertContractParameterArray(JToken token) + { + if (token is JArray jArray) + { + try + { + return jArray.Select(t => ContractParameter.FromJson(t as JObject)).ToArray(); + } + catch (FormatException) + { + throw new RpcException(CreateInvalidParamError(token)); + } + } + throw new RpcException(CreateInvalidParamError(token)); + } + + private static object ConvertGuid(JToken token) + { + if (Guid.TryParse(token.AsString(), out var guid)) + { + return guid; + } + throw new RpcException(CreateInvalidParamError(token)); + } + private static RpcError CreateInvalidParamError(JToken token) { return RpcError.InvalidParams.WithData($"Invalid {typeof(T)} value: {token}"); diff --git a/src/Plugins/RpcServer/RpcServer.SmartContract.cs b/src/Plugins/RpcServer/RpcServer.SmartContract.cs index 11e1218e57..ff6d68f03d 100644 --- a/src/Plugins/RpcServer/RpcServer.SmartContract.cs +++ b/src/Plugins/RpcServer/RpcServer.SmartContract.cs @@ -15,6 +15,7 @@ using Neo.Json; using Neo.Network.P2P.Payloads; using Neo.Persistence; +using Neo.Plugins.RpcServer.Model; using Neo.SmartContract; using Neo.SmartContract.Iterators; using Neo.SmartContract.Native; @@ -172,122 +173,126 @@ private static JObject ToJson(StackItem item, Session session) return json; } - private static Signer[] SignersFromJson(JArray _params, ProtocolSettings settings) + /// + /// Invokes a smart contract with its scripthash based on the specified operation and parameters and returns the result. + /// + /// + /// This method is used to test your VM script as if they ran on the blockchain at that point in time. + /// This RPC call does not affect the blockchain in any way. + /// + /// Smart contract scripthash. Use big endian for Hash160, little endian for ByteArray. + /// The operation name (string) + /// Optional. The parameters to be passed into the smart contract operation + /// Optional. List of signers and/or witnesses. + /// Optional. Flag to enable diagnostic information. + /// A JToken containing the result of the invocation. + [RpcMethodWithParams] + protected internal virtual JToken InvokeFunction(string scriptHash, string operation, ContractParameter[] args = null, SignerWithWitness[] signerWithWitnesses = null, bool useDiagnostic = false) { - if (_params.Count > Transaction.MaxTransactionAttributes) - { - throw new RpcException(RpcError.InvalidParams.WithData("Max allowed witness exceeded.")); - } - - var ret = _params.Select(u => new Signer - { - Account = AddressToScriptHash(u["account"].AsString(), settings.AddressVersion), - Scopes = (WitnessScope)Enum.Parse(typeof(WitnessScope), u["scopes"]?.AsString()), - AllowedContracts = ((JArray)u["allowedcontracts"])?.Select(p => UInt160.Parse(p.AsString())).ToArray() ?? Array.Empty(), - AllowedGroups = ((JArray)u["allowedgroups"])?.Select(p => ECPoint.Parse(p.AsString(), ECCurve.Secp256r1)).ToArray() ?? Array.Empty(), - Rules = ((JArray)u["rules"])?.Select(r => WitnessRule.FromJson((JObject)r)).ToArray() ?? Array.Empty(), - }).ToArray(); - - // Validate format - - _ = ret.ToByteArray().AsSerializableArray(); - - return ret; - } - - private static Witness[] WitnessesFromJson(JArray _params) - { - if (_params.Count > Transaction.MaxTransactionAttributes) - { - throw new RpcException(RpcError.InvalidParams.WithData("Max allowed witness exceeded.")); - } - - return _params.Select(u => new - { - Invocation = u["invocation"]?.AsString(), - Verification = u["verification"]?.AsString() - }).Where(x => x.Invocation != null || x.Verification != null).Select(x => new Witness() - { - InvocationScript = Convert.FromBase64String(x.Invocation ?? string.Empty), - VerificationScript = Convert.FromBase64String(x.Verification ?? string.Empty) - }).ToArray(); - } - - [RpcMethod] - protected internal virtual JToken InvokeFunction(JArray _params) - { - UInt160 script_hash = Result.Ok_Or(() => UInt160.Parse(_params[0].AsString()), RpcError.InvalidParams.WithData($"Invalid script hash {nameof(script_hash)}")); - string operation = Result.Ok_Or(() => _params[1].AsString(), RpcError.InvalidParams); - ContractParameter[] args = _params.Count >= 3 ? ((JArray)_params[2]).Select(p => ContractParameter.FromJson((JObject)p)).ToArray() : System.Array.Empty(); - Signer[] signers = _params.Count >= 4 ? SignersFromJson((JArray)_params[3], system.Settings) : null; - Witness[] witnesses = _params.Count >= 4 ? WitnessesFromJson((JArray)_params[3]) : null; - bool useDiagnostic = _params.Count >= 5 && _params[4].GetBoolean(); - + UInt160 contractHash = Result.Ok_Or(() => UInt160.Parse(scriptHash), RpcError.InvalidParams); byte[] script; using (ScriptBuilder sb = new()) { - script = sb.EmitDynamicCall(script_hash, operation, args).ToArray(); + script = sb.EmitDynamicCall(contractHash, operation, args ?? Array.Empty()).ToArray(); } + var signers = signerWithWitnesses?.Where(u => u.Signer != null).Select(u => u.Signer).ToArray() ?? []; + var witnesses = signerWithWitnesses?.Where(u => u.Witness != null).Select(u => u.Witness).ToArray() ?? []; return GetInvokeResult(script, signers, witnesses, useDiagnostic); } - [RpcMethod] - protected internal virtual JToken InvokeScript(JArray _params) + /// + /// Returns the result after passing a script through the VM. + /// + /// + /// This method is to test your VM script as if they ran on the blockchain at that point in time. + /// This RPC call does not affect the blockchain in any way. + /// You must install the plugin RpcServer before you can invoke the method. + /// + /// A script runnable in the VM, encoded as Base64. e.g. "AQIDBAUGBwgJCgsMDQ4PEA==" + /// Optional. The list of contract signature accounts and/or witnesses for the transaction. + /// Optional. Flag to enable diagnostic information. + /// A JToken containing the result of the invocation. + [RpcMethodWithParams] + protected internal virtual JToken InvokeScript(string scriptBase64, SignerWithWitness[] signerWithWitnesses = null, bool useDiagnostic = false) { - byte[] script = Result.Ok_Or(() => Convert.FromBase64String(_params[0].AsString()), RpcError.InvalidParams); - Signer[] signers = _params.Count >= 2 ? SignersFromJson((JArray)_params[1], system.Settings) : null; - Witness[] witnesses = _params.Count >= 2 ? WitnessesFromJson((JArray)_params[1]) : null; - bool useDiagnostic = _params.Count >= 3 && _params[2].GetBoolean(); + var script = Result.Ok_Or(() => Convert.FromBase64String(scriptBase64), RpcError.InvalidParams); + var signers = signerWithWitnesses?.Where(u => u.Signer != null).Select(u => u.Signer).ToArray() ?? []; + var witnesses = signerWithWitnesses?.Where(u => u.Witness != null).Select(u => u.Witness).ToArray() ?? []; + return GetInvokeResult(script, signers, witnesses, useDiagnostic); } - [RpcMethod] - protected internal virtual JToken TraverseIterator(JArray _params) + /// + /// Gets the Iterator value from session and Iterator id returned by invokefunction or invokescript. + /// + /// + /// + /// This method queries Iterator type data and does not affect the blockchain data. + /// You must install the plugin RpcServer before you can invoke the method. + /// Before you can use the method, make sure that the SessionEnabled value in config.json of the plugin RpcServer is true, + /// and you have obtained Iterator id and session by invoking invokefunction or invokescript. + /// + /// + /// The validity of the session and iterator id is set by SessionExpirationTime in the config.json file of the RpcServer plug-in, in seconds. + /// + /// + /// Cache id. It is session returned by invokefunction or invokescript. e.g. "c5b628b6-10d9-4cc5-b850-3cfc0b659fcf" + /// Iterator data id. It is the id of stack returned by invokefunction or invokescript. e.g. "593b02c6-138d-4945-846d-1e5974091daa" + /// The number of values returned. It cannot exceed the value of the MaxIteratorResultItems field in config.json of the RpcServer plug-in. + /// A JToken containing the iterator values. + [RpcMethodWithParams] + protected internal virtual JToken TraverseIterator(Guid session, Guid iteratorId, int count) { settings.SessionEnabled.True_Or(RpcError.SessionsDisabled); - Guid sid = Result.Ok_Or(() => Guid.Parse(_params[0].GetString()), RpcError.InvalidParams.WithData($"Invalid session id {nameof(sid)}")); - Guid iid = Result.Ok_Or(() => Guid.Parse(_params[1].GetString()), RpcError.InvalidParams.WithData($"Invliad iterator id {nameof(iid)}")); - int count = _params[2].GetInt32(); - Result.True_Or(() => count <= settings.MaxIteratorResultItems, RpcError.InvalidParams.WithData($"Invalid iterator items count {nameof(count)}")); - Session session; + Result.True_Or(() => count <= settings.MaxIteratorResultItems, RpcError.InvalidParams.WithData($"Invalid iterator items count: {count}")); + + Session currentSession; lock (sessions) { - session = Result.Ok_Or(() => sessions[sid], RpcError.UnknownSession); - session.ResetExpiration(); + currentSession = Result.Ok_Or(() => sessions[session], RpcError.UnknownSession); + currentSession.ResetExpiration(); } - IIterator iterator = Result.Ok_Or(() => session.Iterators[iid], RpcError.UnknownIterator); + IIterator iterator = Result.Ok_Or(() => currentSession.Iterators[iteratorId], RpcError.UnknownIterator); JArray json = new(); while (count-- > 0 && iterator.Next()) json.Add(iterator.Value(null).ToJson()); return json; } - [RpcMethod] - protected internal virtual JToken TerminateSession(JArray _params) + /// + /// Terminates a session with the specified GUID. + /// + /// The GUID of the session to terminate. e.g. "00000000-0000-0000-0000-000000000000" + /// A JToken indicating whether the session was successfully terminated. + [RpcMethodWithParams] + protected internal virtual JToken TerminateSession(Guid guid) { settings.SessionEnabled.True_Or(RpcError.SessionsDisabled); - Guid sid = Result.Ok_Or(() => Guid.Parse(_params[0].GetString()), RpcError.InvalidParams.WithData("Invalid session id")); Session session = null; bool result; lock (sessions) { - result = Result.Ok_Or(() => sessions.Remove(sid, out session), RpcError.UnknownSession); + result = Result.Ok_Or(() => sessions.Remove(guid, out session), RpcError.UnknownSession); } if (result) session.Dispose(); return result; } - [RpcMethod] - protected internal virtual JToken GetUnclaimedGas(JArray _params) + /// + /// Gets the unclaimed GAS for the specified address. + /// + /// The account to check for unclaimed GAS. e.g. "NQ5D43HX4QBXZ3XZ4QBXZ3XZ4QBXZ3XZ" + /// A JToken containing the unclaimed GAS amount and the address. + [RpcMethodWithParams] + protected internal virtual JToken GetUnclaimedGas(string account) { - string address = Result.Ok_Or(() => _params[0].AsString(), RpcError.InvalidParams.WithData($"Invalid address {nameof(address)}")); JObject json = new(); - UInt160 script_hash = Result.Ok_Or(() => AddressToScriptHash(address, system.Settings.AddressVersion), RpcError.InvalidParams); + var scriptHash = Result.Ok_Or(() => AddressToScriptHash(account, system.Settings.AddressVersion), RpcError.InvalidParams) ?? throw new ArgumentNullException("Result.Ok_Or(() => AddressToScriptHash(account, system.Settings.AddressVersion), RpcError.InvalidParams)"); var snapshot = system.StoreView; - json["unclaimed"] = NativeContract.NEO.UnclaimedGas(snapshot, script_hash, NativeContract.Ledger.CurrentIndex(snapshot) + 1).ToString(); - json["address"] = script_hash.ToAddress(system.Settings.AddressVersion); + json["unclaimed"] = NativeContract.NEO.UnclaimedGas(snapshot, scriptHash, NativeContract.Ledger.CurrentIndex(snapshot) + 1).ToString(); + json["address"] = scriptHash.ToAddress(system.Settings.AddressVersion); return json; } diff --git a/src/Plugins/RpcServer/RpcServer.Wallet.cs b/src/Plugins/RpcServer/RpcServer.Wallet.cs index 25a7333aac..77d04f1160 100644 --- a/src/Plugins/RpcServer/RpcServer.Wallet.cs +++ b/src/Plugins/RpcServer/RpcServer.Wallet.cs @@ -14,6 +14,7 @@ using Neo.Json; using Neo.Network.P2P.Payloads; using Neo.Persistence; +using Neo.Plugins.RpcServer.Model; using Neo.SmartContract; using Neo.SmartContract.Native; using Neo.VM; @@ -477,8 +478,8 @@ protected internal virtual JToken InvokeContractVerify(JArray _params) { UInt160 script_hash = Result.Ok_Or(() => UInt160.Parse(_params[0].AsString()), RpcError.InvalidParams.WithData($"Invalid script hash: {_params[0]}")); ContractParameter[] args = _params.Count >= 2 ? ((JArray)_params[1]).Select(p => ContractParameter.FromJson((JObject)p)).ToArray() : Array.Empty(); - Signer[] signers = _params.Count >= 3 ? SignersFromJson((JArray)_params[2], system.Settings) : null; - Witness[] witnesses = _params.Count >= 3 ? WitnessesFromJson((JArray)_params[2]) : null; + Signer[] signers = _params.Count >= 3 ? SignerWithWitness.ParseArray((JArray)_params[2], system.Settings).Where(u => u.Signer != null).Select(u => u.Signer).ToArray() : null; + Witness[] witnesses = _params.Count >= 3 ? SignerWithWitness.ParseArray((JArray)_params[2], system.Settings).Where(u => u.Witness != null).Select(u => u.Witness).ToArray() : null; return GetVerificationResult(script_hash, args, signers, witnesses); } diff --git a/src/Plugins/RpcServer/RpcServer.cs b/src/Plugins/RpcServer/RpcServer.cs index 0dc3d467bf..9a97774fe2 100644 --- a/src/Plugins/RpcServer/RpcServer.cs +++ b/src/Plugins/RpcServer/RpcServer.cs @@ -19,6 +19,7 @@ using Neo.Extensions; using Neo.Json; using Neo.Network.P2P; +using Neo.Plugins.RpcServer.Model; using System; using System.Collections.Generic; using System.IO; @@ -274,7 +275,7 @@ public async Task ProcessAsync(HttpContext context) await context.Response.WriteAsync(response.ToString(), Encoding.UTF8); } - private async Task ProcessRequestAsync(HttpContext context, JObject request) + protected async Task ProcessRequestAsync(HttpContext context, JObject request) { if (!request.ContainsProperty("id")) return null; var @params = request["params"] ?? new JArray(); @@ -317,6 +318,10 @@ private async Task ProcessRequestAsync(HttpContext context, JObject req { args[i] = ParameterConverter.ConvertUInt160(jsonParameters[i], system.Settings.AddressVersion); } + else if (param.ParameterType == typeof(SignerWithWitness[])) + { + args[i] = ParameterConverter.ConvertSignerWithWitnessArray(jsonParameters[i], system.Settings); + } else { args[i] = ParameterConverter.ConvertParameter(jsonParameters[i], param.ParameterType); diff --git a/tests/Neo.Plugins.RpcServer.Tests/RpcServerMock.cs b/tests/Neo.Plugins.RpcServer.Tests/RpcServerMock.cs new file mode 100644 index 0000000000..19d194f0d8 --- /dev/null +++ b/tests/Neo.Plugins.RpcServer.Tests/RpcServerMock.cs @@ -0,0 +1,33 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// RpcServerMock.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 Microsoft.AspNetCore.Http; +using Neo.Json; +using System.Threading.Tasks; + +namespace Neo.Plugins.RpcServer.Tests; + +public class RpcServerMock(NeoSystem system, RpcServerSettings settings) : RpcServer(system, settings) +{ + public async Task ProcessRequestMock(HttpContext context, string method, JArray parameters) + { + var request = new JObject + { + ["jsonrpc"] = "2.0", + ["id"] = 1, + ["method"] = method, + ["params"] = parameters + }; + + var res = await ProcessRequestAsync(context, request); + return (JObject)res["result"]; + } +} diff --git a/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.SmartContract.cs b/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.SmartContract.cs index 9a8b87da9a..187a803b75 100644 --- a/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.SmartContract.cs +++ b/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.SmartContract.cs @@ -9,11 +9,7 @@ // Redistribution and use in source and binary forms with or without // modifications are permitted. -using FluentAssertions; -using Microsoft.AspNetCore.Http; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.DataCollection; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Neo.Cryptography.ECC; using Neo.IO; using Neo.Json; using Neo.Network.P2P.Payloads; @@ -22,11 +18,8 @@ using Neo.SmartContract; using Neo.SmartContract.Native; using Neo.UnitTests; -using Neo.UnitTests.Extensions; using Neo.Wallets; using System; -using System.IO; -using System.Linq; using System.Text; using System.Threading; @@ -64,7 +57,7 @@ public void TestInvokeFunction() { _rpcServer.wallet = _wallet; - JObject resp = (JObject)_rpcServer.InvokeFunction(new JArray(NeoToken.NEO.Hash.ToString(), "totalSupply", new JArray([]), validatorSigner, true)); + var resp = _rpcServer.ProcessRequestMock(_mockHttpContext.Object, "invokefunction", new JArray(NeoToken.NEO.Hash.ToString(), "totalSupply", new JArray([]), validatorSigner, true)).GetAwaiter().GetResult(); Assert.AreEqual(resp.Count, 8); Assert.AreEqual(resp["script"], NeoTotalSupplyScript); Assert.IsTrue(resp.ContainsProperty("gasconsumed")); @@ -78,7 +71,7 @@ public void TestInvokeFunction() Assert.AreEqual(resp["stack"][0]["value"], "100000000"); Assert.IsTrue(resp.ContainsProperty("tx")); - resp = (JObject)_rpcServer.InvokeFunction(new JArray(NeoToken.NEO.Hash.ToString(), "symbol")); + resp = _rpcServer.ProcessRequestMock(_mockHttpContext.Object, "invokefunction", new JArray(NeoToken.NEO.Hash.ToString(), "symbol")).GetAwaiter().GetResult(); Assert.AreEqual(resp.Count, 6); Assert.IsTrue(resp.ContainsProperty("script")); Assert.IsTrue(resp.ContainsProperty("gasconsumed")); @@ -89,12 +82,12 @@ public void TestInvokeFunction() Assert.AreEqual(resp["stack"][0]["value"], Convert.ToBase64String(Encoding.UTF8.GetBytes("NEO"))); // This call triggers not only NEO but also unclaimed GAS - resp = (JObject)_rpcServer.InvokeFunction(new JArray(NeoToken.NEO.Hash.ToString(), "transfer", new JArray([ + resp = _rpcServer.ProcessRequestMock(_mockHttpContext.Object, "invokefunction", new JArray(NeoToken.NEO.Hash.ToString(), "transfer", new JArray([ new JObject() { ["type"] = nameof(ContractParameterType.Hash160), ["value"] = MultisigScriptHash.ToString() }, new JObject() { ["type"] = nameof(ContractParameterType.Hash160), ["value"] = ValidatorScriptHash.ToString() }, new JObject() { ["type"] = nameof(ContractParameterType.Integer), ["value"] = "1" }, new JObject() { ["type"] = nameof(ContractParameterType.Any) }, - ]), multisigSigner, true)); + ]), multisigSigner, true)).GetAwaiter().GetResult(); Assert.AreEqual(resp.Count, 7); Assert.AreEqual(resp["script"], NeoTransferScript); Assert.IsTrue(resp.ContainsProperty("gasconsumed")); @@ -104,7 +97,7 @@ public void TestInvokeFunction() Assert.AreEqual(resp["state"], nameof(VM.VMState.HALT)); Assert.AreEqual(resp["exception"], $"The smart contract or address {MultisigScriptHash} ({MultisigAddress}) is not found. " + $"If this is your wallet address and you want to sign a transaction with it, make sure you have opened this wallet."); - JArray notifications = (JArray)resp["notifications"]; + var notifications = (JArray)resp["notifications"]; Assert.AreEqual(notifications.Count, 2); Assert.AreEqual(notifications[0]["eventname"].AsString(), "Transfer"); Assert.AreEqual(notifications[0]["contract"].AsString(), NeoToken.NEO.Hash.ToString()); @@ -119,7 +112,7 @@ public void TestInvokeFunction() [TestMethod] public void TestInvokeScript() { - JObject resp = (JObject)_rpcServer.InvokeScript(new JArray(NeoTotalSupplyScript, validatorSigner, true)); + var resp = _rpcServer.ProcessRequestMock(_mockHttpContext.Object, "invokescript", new JArray(NeoTotalSupplyScript, validatorSigner, true)).GetAwaiter().GetResult(); Assert.AreEqual(resp.Count, 7); Assert.IsTrue(resp.ContainsProperty("gasconsumed")); Assert.IsTrue(resp.ContainsProperty("diagnostics")); @@ -130,31 +123,31 @@ public void TestInvokeScript() Assert.AreEqual(resp["stack"][0]["type"], nameof(Neo.VM.Types.Integer)); Assert.AreEqual(resp["stack"][0]["value"], "100000000"); - resp = (JObject)_rpcServer.InvokeScript(new JArray(NeoTransferScript)); - Assert.AreEqual(resp.Count, 6); + resp = _rpcServer.ProcessRequestMock(_mockHttpContext.Object, "invokescript", new JArray(NeoTransferScript, validatorSigner, true)).GetAwaiter().GetResult(); + Assert.AreEqual(resp.Count, 7); Assert.AreEqual(resp["stack"][0]["type"], nameof(Neo.VM.Types.Boolean)); Assert.AreEqual(resp["stack"][0]["value"], false); } [TestMethod] - public void TestTraverseIterator() + public async void TestTraverseIterator() { // GetAllCandidates that should return 0 candidates - JObject resp = (JObject)_rpcServer.InvokeFunction(new JArray(NeoToken.NEO.Hash.ToString(), "getAllCandidates", new JArray([]), validatorSigner, true)); - string sessionId = resp["session"].AsString(); - string iteratorId = resp["stack"][0]["id"].AsString(); - JArray respArray = (JArray)_rpcServer.TraverseIterator([sessionId, iteratorId, 100]); + var resp = _rpcServer.ProcessRequestMock(_mockHttpContext.Object, "invokefunction", new JArray(NeoToken.NEO.Hash.ToString(), "getAllCandidates", new JArray([]), validatorSigner, true)).GetAwaiter().GetResult(); + var sessionId = resp["session"].AsString(); + var iteratorId = resp["stack"][0]["id"].AsString(); + var respArray = (JArray)(JToken)_rpcServer.ProcessRequestMock(_mockHttpContext.Object, "traverseiterator", new JArray([sessionId, iteratorId, 100])).GetAwaiter().GetResult(); Assert.AreEqual(respArray.Count, 0); - _rpcServer.TerminateSession([sessionId]); - Assert.ThrowsException(() => (JArray)_rpcServer.TraverseIterator([sessionId, iteratorId, 100]), "Unknown session"); + _rpcServer.ProcessRequestMock(_mockHttpContext.Object, "terminatesession", new JArray([sessionId])).GetAwaiter().GetResult(); + Assert.ThrowsException(() => _rpcServer.ProcessRequestMock(_mockHttpContext.Object, "traverseiterator", new JArray([sessionId, iteratorId, 100])).GetAwaiter().GetResult(), "Unknown session"); // register candidate in snapshot - resp = (JObject)_rpcServer.InvokeFunction(new JArray(NeoToken.NEO.Hash.ToString(), "registerCandidate", + resp = (JObject)_rpcServer.ProcessRequestMock(_mockHttpContext.Object, "invokefunction", new JArray(NeoToken.NEO.Hash.ToString(), "registerCandidate", new JArray([new JObject() { ["type"] = nameof(ContractParameterType.PublicKey), ["value"] = TestProtocolSettings.SoleNode.StandbyCommittee[0].ToString(), - }]), validatorSigner, true)); + }]), validatorSigner, true)).GetAwaiter().GetResult(); Assert.AreEqual(resp["state"], nameof(VM.VMState.HALT)); SnapshotCache snapshot = _neoSystem.GetSnapshotCache(); Transaction? tx = new Transaction @@ -170,10 +163,11 @@ public void TestTraverseIterator() engine.SnapshotCache.Commit(); // GetAllCandidates that should return 1 candidate - resp = (JObject)_rpcServer.InvokeFunction(new JArray(NeoToken.NEO.Hash.ToString(), "getAllCandidates", new JArray([]), validatorSigner, true)); + resp = _rpcServer.ProcessRequestMock(_mockHttpContext.Object, "invokeFunction", new JArray(NeoToken.NEO.Hash.ToString(), "getAllCandidates", new JArray([]), validatorSigner, true)).GetAwaiter().GetResult(); sessionId = resp["session"].AsString(); iteratorId = resp["stack"][0]["id"].AsString(); - respArray = (JArray)_rpcServer.TraverseIterator([sessionId, iteratorId, 100]); + respArray = (JArray)(JToken)_rpcServer.ProcessRequestMock(_mockHttpContext.Object, "traverseIterator", new JArray([sessionId, iteratorId, 100])).GetAwaiter().GetResult(); + Assert.AreEqual(respArray.Count, 1); Assert.AreEqual(respArray[0]["type"], nameof(Neo.VM.Types.Struct)); JArray value = (JArray)respArray[0]["value"]; @@ -184,50 +178,50 @@ public void TestTraverseIterator() Assert.AreEqual(value[1]["value"], "0"); // No result when traversed again - respArray = (JArray)_rpcServer.TraverseIterator([sessionId, iteratorId, 100]); + respArray = (JArray)(JToken)_rpcServer.ProcessRequestMock(_mockHttpContext.Object, "traverseIterator", new JArray([sessionId, iteratorId, 100])).GetAwaiter().GetResult(); Assert.AreEqual(respArray.Count, 0); // GetAllCandidates again - resp = (JObject)_rpcServer.InvokeFunction(new JArray(NeoToken.NEO.Hash.ToString(), "getAllCandidates", new JArray([]), validatorSigner, true)); + resp = _rpcServer.ProcessRequestMock(_mockHttpContext.Object, "invokeFunction", new JArray(NeoToken.NEO.Hash.ToString(), "getAllCandidates", new JArray([]), validatorSigner, true)).GetAwaiter().GetResult(); sessionId = resp["session"].AsString(); iteratorId = resp["stack"][0]["id"].AsString(); // Insufficient result count limit - respArray = (JArray)_rpcServer.TraverseIterator([sessionId, iteratorId, 0]); + respArray = (JArray)(JToken)_rpcServer.ProcessRequestMock(_mockHttpContext.Object, "traverseIterator", new JArray([sessionId, iteratorId, 0])).GetAwaiter().GetResult(); Assert.AreEqual(respArray.Count, 0); - respArray = (JArray)_rpcServer.TraverseIterator([sessionId, iteratorId, 1]); + respArray = (JArray)(JToken)_rpcServer.ProcessRequestMock(_mockHttpContext.Object, "traverseIterator", new JArray([sessionId, iteratorId, 1])).GetAwaiter().GetResult(); Assert.AreEqual(respArray.Count, 1); - respArray = (JArray)_rpcServer.TraverseIterator([sessionId, iteratorId, 1]); + respArray = (JArray)(JToken)_rpcServer.ProcessRequestMock(_mockHttpContext.Object, "traverseIterator", new JArray([sessionId, iteratorId, 1])).GetAwaiter().GetResult(); Assert.AreEqual(respArray.Count, 0); // Mocking session timeout Thread.Sleep((int)_rpcServerSettings.SessionExpirationTime.TotalMilliseconds + 1); // build another session that did not expire - resp = (JObject)_rpcServer.InvokeFunction(new JArray(NeoToken.NEO.Hash.ToString(), "getAllCandidates", new JArray([]), validatorSigner, true)); + resp = _rpcServer.ProcessRequestMock(_mockHttpContext.Object, "InvokeFunction", new JArray(NeoToken.NEO.Hash.ToString(), "getAllCandidates", new JArray([]), validatorSigner, true)).GetAwaiter().GetResult(); string notExpiredSessionId = resp["session"].AsString(); string notExpiredIteratorId = resp["stack"][0]["id"].AsString(); _rpcServer.OnTimer(new object()); - Assert.ThrowsException(() => (JArray)_rpcServer.TraverseIterator([sessionId, iteratorId, 100]), "Unknown session"); + Assert.ThrowsException(() => _rpcServer.ProcessRequestMock(_mockHttpContext.Object, "traverseIterator", new JArray([sessionId, iteratorId, 100])).GetAwaiter().GetResult(), "Unknown session"); // If you want to run the following line without exception, // DO NOT BREAK IN THE DEBUGGER, because the session expires quickly - respArray = (JArray)_rpcServer.TraverseIterator([notExpiredSessionId, notExpiredIteratorId, 1]); + respArray = (JArray)(JToken)_rpcServer.ProcessRequestMock(_mockHttpContext.Object, "traverseIterator", new JArray([notExpiredSessionId, notExpiredIteratorId, 1])).GetAwaiter().GetResult(); Assert.AreEqual(respArray.Count, 1); // Mocking disposal - resp = (JObject)_rpcServer.InvokeFunction(new JArray(NeoToken.NEO.Hash.ToString(), "getAllCandidates", new JArray([]), validatorSigner, true)); + resp = _rpcServer.ProcessRequestMock(_mockHttpContext.Object, "invokeFunction", new JArray(NeoToken.NEO.Hash.ToString(), "getAllCandidates", new JArray([]), validatorSigner, true)).GetAwaiter().GetResult(); sessionId = resp["session"].AsString(); iteratorId = resp["stack"][0]["id"].AsString(); _rpcServer.Dispose_SmartContract(); - Assert.ThrowsException(() => (JArray)_rpcServer.TraverseIterator([sessionId, iteratorId, 100]), "Unknown session"); + Assert.ThrowsException(() => _rpcServer.ProcessRequestMock(_mockHttpContext.Object, "traverseIterator", new JArray([sessionId, iteratorId, 100])).GetAwaiter().GetResult(), "Unknown session"); } [TestMethod] public void TestGetUnclaimedGas() { - JObject resp = (JObject)_rpcServer.GetUnclaimedGas([MultisigAddress]); + var resp = _rpcServer.ProcessRequestMock(_mockHttpContext.Object, "getunclaimedgas", new JArray([MultisigAddress])).GetAwaiter().GetResult(); Assert.AreEqual(resp["unclaimed"], "50000000"); Assert.AreEqual(resp["address"], MultisigAddress); - resp = (JObject)_rpcServer.GetUnclaimedGas([ValidatorAddress]); + resp = (JObject)_rpcServer.ProcessRequestMock(_mockHttpContext.Object, "getunclaimedgas", new JArray([ValidatorAddress])).GetAwaiter().GetResult(); Assert.AreEqual(resp["unclaimed"], "0"); Assert.AreEqual(resp["address"], ValidatorAddress); } diff --git a/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.Wallet.cs b/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.Wallet.cs index 0897a12804..9829ce173f 100644 --- a/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.Wallet.cs +++ b/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.Wallet.cs @@ -9,18 +9,19 @@ // Redistribution and use in source and binary forms with or without // modifications are permitted. -using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; using Neo.IO; using Neo.Json; using Neo.Network.P2P.Payloads; using Neo.Network.P2P.Payloads.Conditions; using Neo.Persistence; +using Neo.Plugins.RpcServer.Model; using Neo.SmartContract; using Neo.SmartContract.Native; using Neo.UnitTests; using Neo.UnitTests.Extensions; using System; +using System.Buffers.Text; using System.IO; using System.Linq; @@ -28,6 +29,15 @@ namespace Neo.Plugins.RpcServer.Tests; partial class UT_RpcServer { + private static readonly JArray ValidatorSigner = [new JObject() + { + ["account"] = ValidatorScriptHash.ToString(), + ["scopes"] = nameof(WitnessScope.CalledByEntry), + ["allowedcontracts"] = new JArray([NeoToken.NEO.Hash.ToString(), GasToken.GAS.Hash.ToString()]), + ["allowedgroups"] = new JArray([TestProtocolSettings.SoleNode.StandbyCommittee[0].ToString()]), + ["rules"] = new JArray([new JObject() { ["action"] = nameof(WitnessRuleAction.Allow), ["condition"] = new JObject { ["type"] = nameof(WitnessConditionType.CalledByEntry) } }]), + }]; + [TestMethod] public void TestOpenWallet() { @@ -393,7 +403,7 @@ public void TestInvokeContractVerify() exception = Assert.ThrowsException(() => _rpcServer.InvokeContractVerify(invalidParamsArray), "Should throw RpcException for invalid script hash"); Assert.AreEqual(exception.HResult, RpcError.InvalidParams.Code); - // deploy a contract with `Verify` method; + // deploy a contract with `Verify` method; string _contractSourceCode = """ using Neo;using Neo.SmartContract.Framework;using Neo.SmartContract.Framework.Services; namespace ContractWithVerify{public class ContractWithVerify:SmartContract { @@ -407,13 +417,20 @@ public static void _deploy(object data, bool update) { """; string base64NefFile = "TkVGM05lby5Db21waWxlci5DU2hhcnAgMy43LjQrNjAzNGExODIxY2E3MDk0NjBlYzMxMzZjNzBjMmRjYzNiZWEuLi4AAAAAAGNXAAJ5JgQiGEEtUQgwE84MASDbMEGb9mfOQeY/GIRADAEg2zBBm/ZnzkGSXegxStgkCUrKABQoAzpB\u002BCfsjEBXAAERiEoQeNBBm/ZnzkGSXegxStgkCUrKABQoAzpB\u002BCfsjEDo2WhC"; string manifest = """{"name":"ContractWithVerify","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"_deploy","parameters":[{"name":"data","type":"Any"},{"name":"update","type":"Boolean"}],"returntype":"Void","offset":0,"safe":false},{"name":"verify","parameters":[],"returntype":"Boolean","offset":31,"safe":false},{"name":"verify","parameters":[{"name":"prefix","type":"Integer"}],"returntype":"Boolean","offset":63,"safe":false}],"events":[]},"permissions":[],"trusts":[],"extra":{"nef":{"optimization":"All"}}}"""; - JObject deployResp = (JObject)_rpcServer.InvokeFunction(new JArray([ContractManagement.ContractManagement.Hash.ToString(), + JObject deployResp = (JObject)_rpcServer.InvokeFunction(ContractManagement.ContractManagement.Hash.ToString(), "deploy", - new JArray([ - new JObject() { ["type"] = nameof(ContractParameterType.ByteArray), ["value"] = base64NefFile }, - new JObject() { ["type"] = nameof(ContractParameterType.String), ["value"] = manifest }, - ]), - validatorSigner])); + [ + new ContractParameter { Type = ContractParameterType.ByteArray, Value = Convert.FromBase64String(base64NefFile) }, + new ContractParameter { Type = ContractParameterType.String, Value = manifest }, + ], + [new SignerWithWitness(new Signer + { + Account = ValidatorScriptHash.ToString(), + Scopes = WitnessScope.CalledByEntry, + AllowedContracts = [NeoToken.NEO.Hash, GasToken.GAS.Hash], + AllowedGroups = [TestProtocolSettings.SoleNode.StandbyCommittee[0]], + Rules = [new WitnessRule { Action = WitnessRuleAction.Allow, Condition = new CalledByEntryCondition() }], + }, null)]); Assert.AreEqual(deployResp["state"], nameof(VM.VMState.HALT)); UInt160 deployedScriptHash = new UInt160(Convert.FromBase64String(deployResp["notifications"][0]["state"]["value"][0]["value"].AsString())); SnapshotCache snapshot = _neoSystem.GetSnapshotCache(); @@ -434,19 +451,19 @@ public static void _deploy(object data, bool update) { Assert.AreEqual(resp["state"], nameof(VM.VMState.HALT)); Assert.AreEqual(resp["stack"][0]["value"].AsBoolean(), false); // invoke verify with signer; should return true - resp = (JObject)_rpcServer.InvokeContractVerify([deployedScriptHash.ToString(), new JArray([]), validatorSigner]); + resp = (JObject)_rpcServer.InvokeContractVerify([deployedScriptHash.ToString(), new JArray([]), ValidatorSigner]); Assert.AreEqual(resp["state"], nameof(VM.VMState.HALT)); Assert.AreEqual(resp["stack"][0]["value"].AsBoolean(), true); // invoke verify with wrong input value; should FAULT - resp = (JObject)_rpcServer.InvokeContractVerify([deployedScriptHash.ToString(), new JArray([new JObject() { ["type"] = nameof(ContractParameterType.Integer), ["value"] = "0" }]), validatorSigner]); + resp = (JObject)_rpcServer.InvokeContractVerify([deployedScriptHash.ToString(), new JArray([new JObject() { ["type"] = nameof(ContractParameterType.Integer), ["value"] = "0" }]), ValidatorSigner]); Assert.AreEqual(resp["state"], nameof(VM.VMState.FAULT)); Assert.AreEqual(resp["exception"], "Object reference not set to an instance of an object."); // invoke verify with 1 param and signer; should return true - resp = (JObject)_rpcServer.InvokeContractVerify([deployedScriptHash.ToString(), new JArray([new JObject() { ["type"] = nameof(ContractParameterType.Integer), ["value"] = "32" }]), validatorSigner]); + resp = (JObject)_rpcServer.InvokeContractVerify([deployedScriptHash.ToString(), new JArray([new JObject() { ["type"] = nameof(ContractParameterType.Integer), ["value"] = "32" }]), ValidatorSigner]); Assert.AreEqual(resp["state"], nameof(VM.VMState.HALT)); Assert.AreEqual(resp["stack"][0]["value"].AsBoolean(), true); // invoke verify with 2 param (which does not exist); should throw Exception - Assert.ThrowsException(() => _rpcServer.InvokeContractVerify([deployedScriptHash.ToString(), new JArray([new JObject() { ["type"] = nameof(ContractParameterType.Integer), ["value"] = "32" }, new JObject() { ["type"] = nameof(ContractParameterType.Integer), ["value"] = "32" }]), validatorSigner]), + Assert.ThrowsException(() => _rpcServer.InvokeContractVerify([deployedScriptHash.ToString(), new JArray([new JObject() { ["type"] = nameof(ContractParameterType.Integer), ["value"] = "32" }, new JObject() { ["type"] = nameof(ContractParameterType.Integer), ["value"] = "32" }]), ValidatorSigner]), $"Invalid contract verification function - The smart contract {deployedScriptHash} haven't got verify method with 2 input parameters."); } diff --git a/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.cs b/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.cs index b8425ee0a9..8c0dc949dc 100644 --- a/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.cs +++ b/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; using Neo.IO; using Neo.Ledger; using Neo.Persistence; @@ -32,7 +33,9 @@ public partial class UT_RpcServer { private NeoSystem _neoSystem; private RpcServerSettings _rpcServerSettings; - private RpcServer _rpcServer; + private RpcServerMock _rpcServer; + private Mock _mockHttpContext; + private TestMemoryStoreProvider _memoryStoreProvider; private MemoryStore _memoryStore; private readonly NEP6Wallet _wallet = TestUtils.GenerateTestWallet("123"); @@ -54,7 +57,10 @@ public void TestSetup() MaxGasInvoke = 1500_0000_0000, Network = TestProtocolSettings.SoleNode.Network, }; - _rpcServer = new RpcServer(_neoSystem, _rpcServerSettings); + _rpcServer = new RpcServerMock(_neoSystem, _rpcServerSettings); + _mockHttpContext = new Mock(); + _mockHttpContext.Setup(c => c.Request.Headers).Returns(new HeaderDictionary()); + _mockHttpContext.Setup(c => c.Response.Headers).Returns(new HeaderDictionary()); _walletAccount = _wallet.Import("KxuRSsHgJMb3AMSN6B9P3JHNGMFtxmuimqgR9MmXPcv3CLLfusTd"); var key = new KeyBuilder(NativeContract.GAS.Id, 20).Add(_walletAccount.ScriptHash); var snapshot = _neoSystem.GetSnapshotCache();