diff --git a/src/Plugins/RpcServer/RpcErrorFactory.cs b/src/Plugins/RpcServer/RpcErrorFactory.cs index 3d2ac7c9a5..4ab6f04cbf 100644 --- a/src/Plugins/RpcServer/RpcErrorFactory.cs +++ b/src/Plugins/RpcServer/RpcErrorFactory.cs @@ -33,7 +33,7 @@ public static RpcError NewCustomError(int code, string message, string data = nu public static RpcError BadRequest(string data) => RpcError.BadRequest.WithData(data); public static RpcError InsufficientFundsWallet(string data) => RpcError.InsufficientFundsWallet.WithData(data); public static RpcError VerificationFailed(string data) => RpcError.VerificationFailed.WithData(data); - public static RpcError InvalidContractVerification(UInt160 contractHash) => RpcError.InvalidContractVerification.WithData($"The smart contract {contractHash} haven't got verify method."); + public static RpcError InvalidContractVerification(UInt160 contractHash, int pcount) => RpcError.InvalidContractVerification.WithData($"The smart contract {contractHash} haven't got verify method with {pcount} input parameters."); public static RpcError InvalidContractVerification(string data) => RpcError.InvalidContractVerification.WithData(data); public static RpcError InvalidSignature(string data) => RpcError.InvalidSignature.WithData(data); public static RpcError OracleNotDesignatedNode(ECPoint oraclePub) => RpcError.OracleNotDesignatedNode.WithData($"{oraclePub} isn't an oracle node."); diff --git a/src/Plugins/RpcServer/RpcServer.SmartContract.cs b/src/Plugins/RpcServer/RpcServer.SmartContract.cs index 70edc4cedb..a3de939f4b 100644 --- a/src/Plugins/RpcServer/RpcServer.SmartContract.cs +++ b/src/Plugins/RpcServer/RpcServer.SmartContract.cs @@ -39,7 +39,7 @@ private void Initialize_SmartContract() timer = new(OnTimer, null, settings.SessionExpirationTime, settings.SessionExpirationTime); } - private void Dispose_SmartContract() + internal void Dispose_SmartContract() { timer?.Dispose(); Session[] toBeDestroyed; @@ -52,7 +52,7 @@ private void Dispose_SmartContract() session.Dispose(); } - private void OnTimer(object state) + internal void OnTimer(object state) { List<(Guid Id, Session Session)> toBeDestroyed = new(); lock (sessions) diff --git a/src/Plugins/RpcServer/RpcServer.Utilities.cs b/src/Plugins/RpcServer/RpcServer.Utilities.cs index f9b874c824..e08dfd28ee 100644 --- a/src/Plugins/RpcServer/RpcServer.Utilities.cs +++ b/src/Plugins/RpcServer/RpcServer.Utilities.cs @@ -18,7 +18,7 @@ namespace Neo.Plugins.RpcServer partial class RpcServer { [RpcMethod] - protected virtual JToken ListPlugins(JArray _params) + protected internal virtual JToken ListPlugins(JArray _params) { return new JArray(Plugin.Plugins .OrderBy(u => u.Name) @@ -34,7 +34,7 @@ protected virtual JToken ListPlugins(JArray _params) } [RpcMethod] - protected virtual JToken ValidateAddress(JArray _params) + protected internal virtual JToken ValidateAddress(JArray _params) { string address = Result.Ok_Or(() => _params[0].AsString(), RpcError.InvalidParams.WithData($"Invlid address format: {_params[0]}")); JObject json = new(); diff --git a/src/Plugins/RpcServer/RpcServer.Wallet.cs b/src/Plugins/RpcServer/RpcServer.Wallet.cs index 50f3c7a1e0..25a7333aac 100644 --- a/src/Plugins/RpcServer/RpcServer.Wallet.cs +++ b/src/Plugins/RpcServer/RpcServer.Wallet.cs @@ -494,7 +494,7 @@ private JObject GetVerificationResult(UInt160 scriptHash, ContractParameter[] ar { using var snapshot = system.GetSnapshotCache(); var contract = NativeContract.ContractManagement.GetContract(snapshot, scriptHash).NotNull_Or(RpcError.UnknownContract); - var md = contract.Manifest.Abi.GetMethod(ContractBasicMethod.Verify, ContractBasicMethod.VerifyPCount).NotNull_Or(RpcErrorFactory.InvalidContractVerification(contract.Hash)); + var md = contract.Manifest.Abi.GetMethod(ContractBasicMethod.Verify, args.Count()).NotNull_Or(RpcErrorFactory.InvalidContractVerification(contract.Hash, args.Count())); (md.ReturnType == ContractParameterType.Boolean).True_Or(RpcErrorFactory.InvalidContractVerification("The verify method doesn't return boolean value.")); Transaction tx = new() { diff --git a/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.SmartContract.cs b/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.SmartContract.cs new file mode 100644 index 0000000000..f994137258 --- /dev/null +++ b/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.SmartContract.cs @@ -0,0 +1,233 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// UT_RpcServer.Wallet.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 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; +using Neo.Network.P2P.Payloads.Conditions; +using Neo.Persistence; +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; + +namespace Neo.Plugins.RpcServer.Tests; + +public partial class UT_RpcServer +{ + static readonly string NeoTotalSupplyScript = "wh8MC3RvdGFsU3VwcGx5DBT1Y\u002BpAvCg9TQ4FxI6jBbPyoHNA70FifVtS"; + static readonly string NeoTransferScript = "CxEMFPlu76Cuc\u002BbgteStE4ozsOWTNUdrDBQtYNweHko3YcnMFOes3ceblcI/lRTAHwwIdHJhbnNmZXIMFPVj6kC8KD1NDgXEjqMFs/Kgc0DvQWJ9W1I="; + static readonly UInt160 ValidatorScriptHash = Contract + .CreateSignatureRedeemScript(TestProtocolSettings.SoleNode.StandbyCommittee[0]) + .ToScriptHash(); + static readonly string ValidatorAddress = ValidatorScriptHash.ToAddress(ProtocolSettings.Default.AddressVersion); + static readonly UInt160 MultisigScriptHash = Contract + .CreateMultiSigRedeemScript(1, TestProtocolSettings.SoleNode.StandbyCommittee) + .ToScriptHash(); + static readonly string MultisigAddress = MultisigScriptHash.ToAddress(ProtocolSettings.Default.AddressVersion); + + 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) } }]), + }]; + static readonly JArray multisigSigner = [new JObject() + { + ["account"] = MultisigScriptHash.ToString(), + ["scopes"] = nameof(WitnessScope.CalledByEntry), + }]; + + [TestMethod] + public void TestInvokeFunction() + { + _rpcServer.wallet = _wallet; + + JObject resp = (JObject)_rpcServer.InvokeFunction(new JArray(NeoToken.NEO.Hash.ToString(), "totalSupply", new JArray([]), validatorSigner, true)); + Assert.AreEqual(resp.Count, 8); + Assert.AreEqual(resp["script"], NeoTotalSupplyScript); + Assert.IsTrue(resp.ContainsProperty("gasconsumed")); + Assert.IsTrue(resp.ContainsProperty("diagnostics")); + Assert.AreEqual(resp["diagnostics"]["invokedcontracts"]["call"][0]["hash"], NeoToken.NEO.Hash.ToString()); + Assert.IsTrue(((JArray)resp["diagnostics"]["storagechanges"]).Count == 0); + Assert.AreEqual(resp["state"], nameof(VM.VMState.HALT)); + Assert.AreEqual(resp["exception"], null); + Assert.AreEqual(((JArray)resp["notifications"]).Count, 0); + Assert.AreEqual(resp["stack"][0]["type"], nameof(Neo.VM.Types.Integer)); + Assert.AreEqual(resp["stack"][0]["value"], "100000000"); + Assert.IsTrue(resp.ContainsProperty("tx")); + + resp = (JObject)_rpcServer.InvokeFunction(new JArray(NeoToken.NEO.Hash.ToString(), "symbol")); + Assert.AreEqual(resp.Count, 6); + Assert.IsTrue(resp.ContainsProperty("script")); + Assert.IsTrue(resp.ContainsProperty("gasconsumed")); + Assert.AreEqual(resp["state"], nameof(VM.VMState.HALT)); + Assert.AreEqual(resp["exception"], null); + Assert.AreEqual(((JArray)resp["notifications"]).Count, 0); + Assert.AreEqual(resp["stack"][0]["type"], nameof(Neo.VM.Types.ByteString)); + 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([ + 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)); + Assert.AreEqual(resp.Count, 7); + Assert.AreEqual(resp["script"], NeoTransferScript); + Assert.IsTrue(resp.ContainsProperty("gasconsumed")); + Assert.IsTrue(resp.ContainsProperty("diagnostics")); + Assert.AreEqual(resp["diagnostics"]["invokedcontracts"]["call"][0]["hash"], NeoToken.NEO.Hash.ToString()); + Assert.IsTrue(((JArray)resp["diagnostics"]["storagechanges"]).Count == 4); + Assert.AreEqual(resp["state"], nameof(VM.VMState.HALT)); + Assert.AreEqual(resp["exception"], $"The smart contract or address {MultisigScriptHash.ToString()} is not found"); + JArray 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()); + Assert.AreEqual(notifications[0]["state"]["value"][2]["value"], "1"); + Assert.AreEqual(notifications[1]["eventname"].AsString(), "Transfer"); + Assert.AreEqual(notifications[1]["contract"].AsString(), GasToken.GAS.Hash.ToString()); + Assert.AreEqual(notifications[1]["state"]["value"][2]["value"], "50000000"); + + _rpcServer.wallet = null; + } + + [TestMethod] + public void TestInvokeScript() + { + JObject resp = (JObject)_rpcServer.InvokeScript(new JArray(NeoTotalSupplyScript, validatorSigner, true)); + Assert.AreEqual(resp.Count, 7); + Assert.IsTrue(resp.ContainsProperty("gasconsumed")); + Assert.IsTrue(resp.ContainsProperty("diagnostics")); + Assert.AreEqual(resp["diagnostics"]["invokedcontracts"]["call"][0]["hash"], NeoToken.NEO.Hash.ToString()); + Assert.AreEqual(resp["state"], nameof(VM.VMState.HALT)); + Assert.AreEqual(resp["exception"], null); + Assert.AreEqual(((JArray)resp["notifications"]).Count, 0); + 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); + Assert.AreEqual(resp["stack"][0]["type"], nameof(Neo.VM.Types.Boolean)); + Assert.AreEqual(resp["stack"][0]["value"], false); + } + + [TestMethod] + public 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]); + Assert.AreEqual(respArray.Count, 0); + _rpcServer.TerminateSession([sessionId]); + Assert.ThrowsException(() => (JArray)_rpcServer.TraverseIterator([sessionId, iteratorId, 100]), "Unknown session"); + + // register candidate in snapshot + resp = (JObject)_rpcServer.InvokeFunction(new JArray(NeoToken.NEO.Hash.ToString(), "registerCandidate", + new JArray([new JObject() + { + ["type"] = nameof(ContractParameterType.PublicKey), + ["value"] = TestProtocolSettings.SoleNode.StandbyCommittee[0].ToString(), + }]), validatorSigner, true)); + Assert.AreEqual(resp["state"], nameof(VM.VMState.HALT)); + SnapshotCache snapshot = _neoSystem.GetSnapshotCache(); + Transaction? tx = new Transaction + { + Nonce = 233, + ValidUntilBlock = NativeContract.Ledger.CurrentIndex(snapshot) + _neoSystem.Settings.MaxValidUntilBlockIncrement, + Signers = [new Signer() { Account = ValidatorScriptHash, Scopes = WitnessScope.CalledByEntry }], + Attributes = Array.Empty(), + Script = Convert.FromBase64String(resp["script"].AsString()), + Witnesses = null, + }; + ApplicationEngine engine = ApplicationEngine.Run(tx.Script, snapshot, container: tx, settings: _neoSystem.Settings, gas: 1200_0000_0000); + engine.SnapshotCache.Commit(); + + // GetAllCandidates that should return 1 candidate + resp = (JObject)_rpcServer.InvokeFunction(new JArray(NeoToken.NEO.Hash.ToString(), "getAllCandidates", new JArray([]), validatorSigner, true)); + sessionId = resp["session"].AsString(); + iteratorId = resp["stack"][0]["id"].AsString(); + respArray = (JArray)_rpcServer.TraverseIterator([sessionId, iteratorId, 100]); + Assert.AreEqual(respArray.Count, 1); + Assert.AreEqual(respArray[0]["type"], nameof(Neo.VM.Types.Struct)); + JArray value = (JArray)respArray[0]["value"]; + Assert.AreEqual(value.Count, 2); + Assert.AreEqual(value[0]["type"], nameof(Neo.VM.Types.ByteString)); + Assert.AreEqual(value[0]["value"], Convert.ToBase64String(TestProtocolSettings.SoleNode.StandbyCommittee[0].ToArray())); + Assert.AreEqual(value[1]["type"], nameof(Neo.VM.Types.Integer)); + Assert.AreEqual(value[1]["value"], "0"); + + // No result when traversed again + respArray = (JArray)_rpcServer.TraverseIterator([sessionId, iteratorId, 100]); + Assert.AreEqual(respArray.Count, 0); + + // GetAllCandidates again + resp = (JObject)_rpcServer.InvokeFunction(new JArray(NeoToken.NEO.Hash.ToString(), "getAllCandidates", new JArray([]), validatorSigner, true)); + sessionId = resp["session"].AsString(); + iteratorId = resp["stack"][0]["id"].AsString(); + + // Insufficient result count limit + respArray = (JArray)_rpcServer.TraverseIterator([sessionId, iteratorId, 0]); + Assert.AreEqual(respArray.Count, 0); + respArray = (JArray)_rpcServer.TraverseIterator([sessionId, iteratorId, 1]); + Assert.AreEqual(respArray.Count, 1); + respArray = (JArray)_rpcServer.TraverseIterator([sessionId, iteratorId, 1]); + 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)); + 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"); + // 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]); + Assert.AreEqual(respArray.Count, 1); + + // Mocking disposal + resp = (JObject)_rpcServer.InvokeFunction(new JArray(NeoToken.NEO.Hash.ToString(), "getAllCandidates", new JArray([]), validatorSigner, true)); + sessionId = resp["session"].AsString(); + iteratorId = resp["stack"][0]["id"].AsString(); + _rpcServer.Dispose_SmartContract(); + Assert.ThrowsException(() => (JArray)_rpcServer.TraverseIterator([sessionId, iteratorId, 100]), "Unknown session"); + } + + [TestMethod] + public void TestGetUnclaimedGas() + { + JObject resp = (JObject)_rpcServer.GetUnclaimedGas([MultisigAddress]); + Assert.AreEqual(resp["unclaimed"], "50000000"); + Assert.AreEqual(resp["address"], MultisigAddress); + resp = (JObject)_rpcServer.GetUnclaimedGas([ValidatorAddress]); + Assert.AreEqual(resp["unclaimed"], "0"); + Assert.AreEqual(resp["address"], ValidatorAddress); + } +} diff --git a/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.Utilities.cs b/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.Utilities.cs new file mode 100644 index 0000000000..a0f2384ce5 --- /dev/null +++ b/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.Utilities.cs @@ -0,0 +1,54 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// UT_RpcServer.Wallet.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 FluentAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.IO; +using Neo.Json; +using Neo.Network.P2P.Payloads; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.UnitTests; +using Neo.UnitTests.Extensions; +using System; +using System.IO; +using System.Linq; + +namespace Neo.Plugins.RpcServer.Tests; + +public partial class UT_RpcServer +{ + [TestMethod] + public void TestListPlugins() + { + JArray resp = (JArray)_rpcServer.ListPlugins([]); + Assert.AreEqual(resp.Count, 0); + Plugins.Plugin.Plugins.Add(new RpcServerPlugin()); + resp = (JArray)_rpcServer.ListPlugins([]); + Assert.AreEqual(resp.Count, 2); + foreach (JObject p in resp) + Assert.AreEqual(p["name"], nameof(RpcServer)); + } + + [TestMethod] + public void TestValidateAddress() + { + string validAddr = "NM7Aky765FG8NhhwtxjXRx7jEL1cnw7PBP"; + JObject resp = (JObject)_rpcServer.ValidateAddress([validAddr]); + Assert.AreEqual(resp["address"], validAddr); + Assert.AreEqual(resp["isvalid"], true); + string invalidAddr = "ANeo2toNeo3MigrationAddressxwPB2Hz"; + resp = (JObject)_rpcServer.ValidateAddress([invalidAddr]); + Assert.AreEqual(resp["address"], invalidAddr); + Assert.AreEqual(resp["isvalid"], false); + } +} diff --git a/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.Wallet.cs b/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.Wallet.cs index 2db71ae570..0897a12804 100644 --- a/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.Wallet.cs +++ b/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.Wallet.cs @@ -14,6 +14,8 @@ using Neo.IO; using Neo.Json; using Neo.Network.P2P.Payloads; +using Neo.Network.P2P.Payloads.Conditions; +using Neo.Persistence; using Neo.SmartContract; using Neo.SmartContract.Native; using Neo.UnitTests; @@ -212,6 +214,16 @@ public void TestSendFrom() var exception = Assert.ThrowsException(() => _rpcServer.SendFrom(paramsArray)); Assert.AreEqual(exception.HResult, RpcError.InvalidRequest.Code); TestUtilCloseWallet(); + + _rpcServer.wallet = _wallet; + JObject resp = (JObject)_rpcServer.SendFrom(paramsArray); + Assert.AreEqual(resp.Count, 12); + Assert.AreEqual(resp["sender"], ValidatorAddress); + JArray signers = (JArray)resp["signers"]; + Assert.AreEqual(signers.Count, 1); + Assert.AreEqual(signers[0]["account"], ValidatorScriptHash.ToString()); + Assert.AreEqual(signers[0]["scopes"], nameof(WitnessScope.CalledByEntry)); + _rpcServer.wallet = null; } [TestMethod] @@ -222,6 +234,16 @@ public void TestSendMany() var paramsArray = new JArray(from, to); var exception = Assert.ThrowsException(() => _rpcServer.SendMany(paramsArray), "Should throw RpcException for insufficient funds"); Assert.AreEqual(exception.HResult, RpcError.NoOpenedWallet.Code); + + _rpcServer.wallet = _wallet; + JObject resp = (JObject)_rpcServer.SendMany(paramsArray); + Assert.AreEqual(resp.Count, 12); + Assert.AreEqual(resp["sender"], ValidatorAddress); + JArray signers = (JArray)resp["signers"]; + Assert.AreEqual(signers.Count, 1); + Assert.AreEqual(signers[0]["account"], ValidatorScriptHash.ToString()); + Assert.AreEqual(signers[0]["scopes"], nameof(WitnessScope.CalledByEntry)); + _rpcServer.wallet = null; } [TestMethod] @@ -233,6 +255,16 @@ public void TestSendToAddress() var paramsArray = new JArray(assetId.ToString(), to, amount); var exception = Assert.ThrowsException(() => _rpcServer.SendToAddress(paramsArray), "Should throw RpcException for insufficient funds"); Assert.AreEqual(exception.HResult, RpcError.NoOpenedWallet.Code); + + _rpcServer.wallet = _wallet; + JObject resp = (JObject)_rpcServer.SendToAddress(paramsArray); + Assert.AreEqual(resp.Count, 12); + Assert.AreEqual(resp["sender"], ValidatorAddress); + JArray signers = (JArray)resp["signers"]; + Assert.AreEqual(signers.Count, 1); + Assert.AreEqual(signers[0]["account"], ValidatorScriptHash.ToString()); + Assert.AreEqual(signers[0]["scopes"], nameof(WitnessScope.CalledByEntry)); + _rpcServer.wallet = null; } [TestMethod] @@ -333,6 +365,20 @@ public void TestCancelTransaction() exception = Assert.ThrowsException(() => _rpcServer.CancelTransaction(paramsArray), "Should throw RpcException for no opened wallet"); Assert.AreEqual(exception.HResult, RpcError.NoOpenedWallet.Code); TestUtilCloseWallet(); + + // Test valid cancel + _rpcServer.wallet = _wallet; + JObject resp = (JObject)_rpcServer.SendFrom(new JArray(NativeContract.GAS.Hash.ToString(), _walletAccount.Address, _walletAccount.Address, "1")); + string txHash = resp["hash"].AsString(); + resp = (JObject)_rpcServer.CancelTransaction(new JArray(txHash, new JArray(ValidatorAddress), "1")); + Assert.AreEqual(resp.Count, 12); + Assert.AreEqual(resp["sender"], ValidatorAddress); + JArray signers = (JArray)resp["signers"]; + Assert.AreEqual(signers.Count, 1); + Assert.AreEqual(signers[0]["account"], ValidatorScriptHash.ToString()); + Assert.AreEqual(signers[0]["scopes"], nameof(WitnessScope.None)); + Assert.AreEqual(resp["attributes"][0]["type"], nameof(TransactionAttributeType.Conflicts)); + _rpcServer.wallet = null; } [TestMethod] @@ -346,6 +392,62 @@ public void TestInvokeContractVerify() var invalidParamsArray = new JArray("invalid_script_hash"); 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; + string _contractSourceCode = """ +using Neo;using Neo.SmartContract.Framework;using Neo.SmartContract.Framework.Services; +namespace ContractWithVerify{public class ContractWithVerify:SmartContract { + const byte PREFIX_OWNER = 0x20; + public static void _deploy(object data, bool update) { + if (update) return; + Storage.Put(Storage.CurrentContext, new byte[] { PREFIX_OWNER }, + ((Transaction)Runtime.ScriptContainer).Sender);} + public static bool Verify() => Runtime.CheckWitness((UInt160)Storage.Get(Storage.CurrentContext, new byte[] { PREFIX_OWNER })); + public static bool Verify(byte prefix) => Runtime.CheckWitness((UInt160)Storage.Get(Storage.CurrentContext, new byte[] { prefix }));}} +"""; + 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(), + "deploy", + new JArray([ + new JObject() { ["type"] = nameof(ContractParameterType.ByteArray), ["value"] = base64NefFile }, + new JObject() { ["type"] = nameof(ContractParameterType.String), ["value"] = manifest }, + ]), + validatorSigner])); + 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(); + Transaction? tx = new Transaction + { + Nonce = 233, + ValidUntilBlock = NativeContract.Ledger.CurrentIndex(snapshot) + _neoSystem.Settings.MaxValidUntilBlockIncrement, + Signers = [new Signer() { Account = ValidatorScriptHash, Scopes = WitnessScope.CalledByEntry }], + Attributes = Array.Empty(), + Script = Convert.FromBase64String(deployResp["script"].AsString()), + Witnesses = null, + }; + ApplicationEngine engine = ApplicationEngine.Run(tx.Script, snapshot, container: tx, settings: _neoSystem.Settings, gas: 1200_0000_0000); + engine.SnapshotCache.Commit(); + + // invoke verify without signer; should return false + JObject resp = (JObject)_rpcServer.InvokeContractVerify([deployedScriptHash.ToString()]); + 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]); + 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]); + 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]); + 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]), + $"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 2561171c81..b63380f803 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 Neo.IO; using Neo.Ledger; using Neo.Persistence; using Neo.SmartContract; @@ -19,6 +20,9 @@ using Neo.Wallets; using Neo.Wallets.NEP6; using System; +using System.Linq; +using System.Net; +using System.Numerics; using System.Text; namespace Neo.Plugins.RpcServer.Tests @@ -27,19 +31,30 @@ namespace Neo.Plugins.RpcServer.Tests public partial class UT_RpcServer { private NeoSystem _neoSystem; + private RpcServerSettings _rpcServerSettings; private RpcServer _rpcServer; private TestMemoryStoreProvider _memoryStoreProvider; private MemoryStore _memoryStore; private readonly NEP6Wallet _wallet = TestUtils.GenerateTestWallet("123"); private WalletAccount _walletAccount; + const byte NativePrefixAccount = 20; + const byte NativePrefixTotalSupply = 11; + [TestInitialize] public void TestSetup() { _memoryStore = new MemoryStore(); _memoryStoreProvider = new TestMemoryStoreProvider(_memoryStore); _neoSystem = new NeoSystem(TestProtocolSettings.SoleNode, _memoryStoreProvider); - _rpcServer = new RpcServer(_neoSystem, RpcServerSettings.Default); + _rpcServerSettings = RpcServerSettings.Default with + { + SessionEnabled = true, + SessionExpirationTime = TimeSpan.FromSeconds(0.3), + MaxGasInvoke = 1500_0000_0000, + Network = TestProtocolSettings.SoleNode.Network, + }; + _rpcServer = new RpcServer(_neoSystem, _rpcServerSettings); _walletAccount = _wallet.Import("KxuRSsHgJMb3AMSN6B9P3JHNGMFtxmuimqgR9MmXPcv3CLLfusTd"); var key = new KeyBuilder(NativeContract.GAS.Id, 20).Add(_walletAccount.ScriptHash); var snapshot = _neoSystem.GetSnapshotCache();