diff --git a/src/Plugins/RpcServer/Result.cs b/src/Plugins/RpcServer/Result.cs index 9c7ace227c..c76e15153b 100644 --- a/src/Plugins/RpcServer/Result.cs +++ b/src/Plugins/RpcServer/Result.cs @@ -23,7 +23,7 @@ public static class Result /// The return type /// The execution result /// The Rpc exception - public static T Ok_Or(this Func function, RpcError err, bool withData = false) + public static T Ok_Or(Func function, RpcError err, bool withData = false) { try { diff --git a/src/Plugins/RpcServer/RpcServer.SmartContract.cs b/src/Plugins/RpcServer/RpcServer.SmartContract.cs index 473ae0cb5d..70edc4cedb 100644 --- a/src/Plugins/RpcServer/RpcServer.SmartContract.cs +++ b/src/Plugins/RpcServer/RpcServer.SmartContract.cs @@ -213,7 +213,7 @@ private static Witness[] WitnessesFromJson(JArray _params) } [RpcMethod] - protected virtual JToken InvokeFunction(JArray _params) + 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); @@ -231,7 +231,7 @@ protected virtual JToken InvokeFunction(JArray _params) } [RpcMethod] - protected virtual JToken InvokeScript(JArray _params) + protected internal virtual JToken InvokeScript(JArray _params) { byte[] script = Result.Ok_Or(() => Convert.FromBase64String(_params[0].AsString()), RpcError.InvalidParams); Signer[] signers = _params.Count >= 2 ? SignersFromJson((JArray)_params[1], system.Settings) : null; @@ -241,7 +241,7 @@ protected virtual JToken InvokeScript(JArray _params) } [RpcMethod] - protected virtual JToken TraverseIterator(JArray _params) + protected internal virtual JToken TraverseIterator(JArray _params) { settings.SessionEnabled.True_Or(RpcError.SessionsDisabled); Guid sid = Result.Ok_Or(() => Guid.Parse(_params[0].GetString()), RpcError.InvalidParams.WithData($"Invalid session id {nameof(sid)}")); @@ -262,7 +262,7 @@ protected virtual JToken TraverseIterator(JArray _params) } [RpcMethod] - protected virtual JToken TerminateSession(JArray _params) + protected internal virtual JToken TerminateSession(JArray _params) { settings.SessionEnabled.True_Or(RpcError.SessionsDisabled); Guid sid = Result.Ok_Or(() => Guid.Parse(_params[0].GetString()), RpcError.InvalidParams.WithData("Invalid session id")); @@ -278,7 +278,7 @@ protected virtual JToken TerminateSession(JArray _params) } [RpcMethod] - protected virtual JToken GetUnclaimedGas(JArray _params) + protected internal virtual JToken GetUnclaimedGas(JArray _params) { string address = Result.Ok_Or(() => _params[0].AsString(), RpcError.InvalidParams.WithData($"Invalid address {nameof(address)}")); JObject json = new(); diff --git a/src/Plugins/RpcServer/RpcServer.Wallet.cs b/src/Plugins/RpcServer/RpcServer.Wallet.cs index 155c56d91b..50f3c7a1e0 100644 --- a/src/Plugins/RpcServer/RpcServer.Wallet.cs +++ b/src/Plugins/RpcServer/RpcServer.Wallet.cs @@ -48,22 +48,36 @@ public override void Delete() { } public override void Save() { } } - protected Wallet wallet; + protected internal Wallet wallet; + /// + /// Checks if a wallet is open and throws an error if not. + /// private void CheckWallet() { wallet.NotNull_Or(RpcError.NoOpenedWallet); } + /// + /// Closes the currently opened wallet. + /// + /// An empty array. + /// Returns true if the wallet was successfully closed. [RpcMethod] - protected virtual JToken CloseWallet(JArray _params) + protected internal virtual JToken CloseWallet(JArray _params) { wallet = null; return true; } + /// + /// Exports the private key of a specified address. + /// + /// An array containing the address as a string. + /// The exported private key as a string. + /// Thrown when no wallet is open or the address is invalid. [RpcMethod] - protected virtual JToken DumpPrivKey(JArray _params) + protected internal virtual JToken DumpPrivKey(JArray _params) { CheckWallet(); UInt160 scriptHash = AddressToScriptHash(_params[0].AsString(), system.Settings.AddressVersion); @@ -71,8 +85,14 @@ protected virtual JToken DumpPrivKey(JArray _params) return account.GetKey().Export(); } + /// + /// Creates a new address in the wallet. + /// + /// An empty array. + /// The newly created address as a string. + /// Thrown when no wallet is open. [RpcMethod] - protected virtual JToken GetNewAddress(JArray _params) + protected internal virtual JToken GetNewAddress(JArray _params) { CheckWallet(); WalletAccount account = wallet.CreateAccount(); @@ -81,8 +101,14 @@ protected virtual JToken GetNewAddress(JArray _params) return account.Address; } + /// + /// Gets the balance of a specified asset in the wallet. + /// + /// An array containing the asset ID as a string. + /// A JSON object containing the balance of the specified asset. + /// Thrown when no wallet is open or the asset ID is invalid. [RpcMethod] - protected virtual JToken GetWalletBalance(JArray _params) + protected internal virtual JToken GetWalletBalance(JArray _params) { CheckWallet(); UInt160 asset_id = Result.Ok_Or(() => UInt160.Parse(_params[0].AsString()), RpcError.InvalidParams.WithData($"Invalid asset id: {_params[0]}")); @@ -91,8 +117,14 @@ protected virtual JToken GetWalletBalance(JArray _params) return json; } + /// + /// Gets the amount of unclaimed GAS in the wallet. + /// + /// An empty array. + /// The amount of unclaimed GAS as a string. + /// Thrown when no wallet is open. [RpcMethod] - protected virtual JToken GetWalletUnclaimedGas(JArray _params) + protected internal virtual JToken GetWalletUnclaimedGas(JArray _params) { CheckWallet(); // Datoshi is the smallest unit of GAS, 1 GAS = 10^8 Datoshi @@ -106,8 +138,14 @@ protected virtual JToken GetWalletUnclaimedGas(JArray _params) return datoshi.ToString(); } + /// + /// Imports a private key into the wallet. + /// + /// An array containing the private key as a string. + /// A JSON object containing information about the imported account. + /// Thrown when no wallet is open or the private key is invalid. [RpcMethod] - protected virtual JToken ImportPrivKey(JArray _params) + protected internal virtual JToken ImportPrivKey(JArray _params) { CheckWallet(); string privkey = _params[0].AsString(); @@ -123,10 +161,20 @@ protected virtual JToken ImportPrivKey(JArray _params) }; } + /// + /// Calculates the network fee for a given transaction. + /// + /// An array containing the Base64-encoded serialized transaction. + /// A JSON object containing the calculated network fee. + /// Thrown when the input parameters are invalid or the transaction is malformed. [RpcMethod] - protected virtual JToken CalculateNetworkFee(JArray _params) + protected internal virtual JToken CalculateNetworkFee(JArray _params) { - var tx = Convert.FromBase64String(_params[0].AsString()); + if (_params.Count == 0) + { + throw new RpcException(RpcError.InvalidParams.WithData("Params array is empty, need a raw transaction.")); + } + var tx = Result.Ok_Or(() => Convert.FromBase64String(_params[0].AsString()), RpcError.InvalidParams.WithData($"Invalid tx: {_params[0]}")); ; JObject account = new(); var networkfee = Wallets.Helper.CalculateNetworkFee( @@ -136,8 +184,14 @@ protected virtual JToken CalculateNetworkFee(JArray _params) return account; } + /// + /// Lists all addresses in the wallet. + /// + /// An empty array. + /// An array of JSON objects, each containing information about an address in the wallet. + /// Thrown when no wallet is open. [RpcMethod] - protected virtual JToken ListAddress(JArray _params) + protected internal virtual JToken ListAddress(JArray _params) { CheckWallet(); return wallet.GetAccounts().Select(p => @@ -151,16 +205,39 @@ protected virtual JToken ListAddress(JArray _params) }).ToArray(); } + /// + /// Opens a wallet file. + /// + /// An array containing the wallet path and password. + /// Returns true if the wallet was successfully opened. + /// Thrown when the wallet file is not found, the wallet is not supported, or the password is invalid. [RpcMethod] - protected virtual JToken OpenWallet(JArray _params) + protected internal virtual JToken OpenWallet(JArray _params) { string path = _params[0].AsString(); string password = _params[1].AsString(); File.Exists(path).True_Or(RpcError.WalletNotFound); - wallet = Wallet.Open(path, password, system.Settings).NotNull_Or(RpcError.WalletNotSupported); + try + { + wallet = Wallet.Open(path, password, system.Settings).NotNull_Or(RpcError.WalletNotSupported); + } + catch (NullReferenceException) + { + throw new RpcException(RpcError.WalletNotSupported); + } + catch (InvalidOperationException) + { + throw new RpcException(RpcError.WalletNotSupported.WithData("Invalid password.")); + } + return true; } + /// + /// Processes the result of an invocation with wallet for signing. + /// + /// The result object to process. + /// Optional signers for the transaction. private void ProcessInvokeWithWallet(JObject result, Signer[] signers = null) { if (wallet == null || signers == null || signers.Length == 0) return; @@ -189,8 +266,14 @@ private void ProcessInvokeWithWallet(JObject result, Signer[] signers = null) } } + /// + /// Transfers an asset from a specific address to another address. + /// + /// An array containing asset ID, from address, to address, amount, and optional signers. + /// The transaction details if successful, or the contract parameters if signatures are incomplete. + /// Thrown when no wallet is open, parameters are invalid, or there are insufficient funds. [RpcMethod] - protected virtual JToken SendFrom(JArray _params) + protected internal virtual JToken SendFrom(JArray _params) { CheckWallet(); UInt160 assetId = Result.Ok_Or(() => UInt160.Parse(_params[0].AsString()), RpcError.InvalidParams.WithData($"Invalid asset id: {_params[0]}")); @@ -202,7 +285,7 @@ protected virtual JToken SendFrom(JArray _params) (amount.Sign > 0).True_Or(RpcErrorFactory.InvalidParams("Amount can't be negative.")); Signer[] signers = _params.Count >= 5 ? ((JArray)_params[4]).Select(p => new Signer() { Account = AddressToScriptHash(p.AsString(), system.Settings.AddressVersion), Scopes = WitnessScope.CalledByEntry }).ToArray() : null; - Transaction tx = wallet.MakeTransaction(snapshot, new[] + Transaction tx = Result.Ok_Or(() => wallet.MakeTransaction(snapshot, new[] { new TransferOutput { @@ -210,7 +293,7 @@ protected virtual JToken SendFrom(JArray _params) Value = amount, ScriptHash = to } - }, from, signers).NotNull_Or(RpcError.InsufficientFunds); + }, from, signers), RpcError.InvalidRequest.WithData("Can not process this request.")).NotNull_Or(RpcError.InsufficientFunds); ContractParametersContext transContext = new(snapshot, tx, settings.Network); wallet.Sign(transContext); @@ -227,8 +310,37 @@ protected virtual JToken SendFrom(JArray _params) return SignAndRelay(snapshot, tx); } + /// + /// Transfers assets to multiple addresses. + /// + /// + /// An array containing the following elements: + /// [0] (optional): The address to send from as a string. If omitted, the assets will be sent from any address in the wallet. + /// [1]: An array of transfer objects, each containing: + /// - "asset": The asset ID (UInt160) as a string. + /// - "value": The amount to transfer as a string. + /// - "address": The recipient address as a string. + /// [2] (optional): An array of signers, each containing: + /// - The address of the signer as a string. + /// + /// + /// If the transaction is successfully created and all signatures are present: + /// Returns a JSON object representing the transaction. + /// If not all signatures are present: + /// Returns a JSON object representing the contract parameters that need to be signed. + /// + /// + /// Thrown when: + /// - No wallet is open. + /// - The 'to' parameter is invalid or empty. + /// - Any of the asset IDs are invalid. + /// - Any of the amounts are negative or invalid. + /// - Any of the addresses are invalid. + /// - There are insufficient funds for the transfer. + /// - The network fee exceeds the maximum allowed fee. + /// [RpcMethod] - protected virtual JToken SendMany(JArray _params) + protected internal virtual JToken SendMany(JArray _params) { CheckWallet(); int to_start = 0; @@ -273,8 +385,14 @@ protected virtual JToken SendMany(JArray _params) return SignAndRelay(snapshot, tx); } + /// + /// Transfers an asset to a specific address. + /// + /// An array containing asset ID, to address, and amount. + /// The transaction details if successful, or the contract parameters if signatures are incomplete. + /// Thrown when no wallet is open, parameters are invalid, or there are insufficient funds. [RpcMethod] - protected virtual JToken SendToAddress(JArray _params) + protected internal virtual JToken SendToAddress(JArray _params) { CheckWallet(); UInt160 assetId = Result.Ok_Or(() => UInt160.Parse(_params[0].AsString()), RpcError.InvalidParams.WithData($"Invalid asset hash: {_params[0]}")); @@ -308,8 +426,14 @@ protected virtual JToken SendToAddress(JArray _params) return SignAndRelay(snapshot, tx); } + /// + /// Cancels an unconfirmed transaction. + /// + /// An array containing the transaction ID to cancel, signers, and optional extra fee. + /// The details of the cancellation transaction. + /// Thrown when no wallet is open, the transaction is already confirmed, or there are insufficient funds for the cancellation fee. [RpcMethod] - protected virtual JToken CancelTransaction(JArray _params) + protected internal virtual JToken CancelTransaction(JArray _params) { CheckWallet(); var txid = Result.Ok_Or(() => UInt256.Parse(_params[0].AsString()), RpcError.InvalidParams.WithData($"Invalid txid: {_params[0]}")); @@ -342,8 +466,14 @@ protected virtual JToken CancelTransaction(JArray _params) return SignAndRelay(system.StoreView, tx); } + /// + /// Invokes the verify method of a contract. + /// + /// An array containing the script hash, optional arguments, and optional signers and witnesses. + /// A JSON object containing the result of the verification. + /// Thrown when the script hash is invalid, the contract is not found, or the verification fails. [RpcMethod] - protected virtual JToken InvokeContractVerify(JArray _params) + 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(); @@ -352,6 +482,14 @@ protected virtual JToken InvokeContractVerify(JArray _params) return GetVerificationResult(script_hash, args, signers, witnesses); } + /// + /// Gets the result of the contract verification. + /// + /// The script hash of the contract. + /// The contract parameters. + /// Optional signers for the verification. + /// Optional witnesses for the verification. + /// A JSON object containing the verification result. private JObject GetVerificationResult(UInt160 scriptHash, ContractParameter[] args, Signer[] signers = null, Witness[] witnesses = null) { using var snapshot = system.GetSnapshotCache(); @@ -396,6 +534,12 @@ private JObject GetVerificationResult(UInt160 scriptHash, ContractParameter[] ar return json; } + /// + /// Signs and relays a transaction. + /// + /// The data snapshot. + /// The transaction to sign and relay. + /// A JSON object containing the transaction details. private JObject SignAndRelay(DataCache snapshot, Transaction tx) { ContractParametersContext context = new(snapshot, tx, settings.Network); @@ -412,6 +556,12 @@ private JObject SignAndRelay(DataCache snapshot, Transaction tx) } } + /// + /// Converts an address to a script hash. + /// + /// The address to convert. + /// The address version. + /// The script hash corresponding to the address. internal static UInt160 AddressToScriptHash(string address, byte version) { if (UInt160.TryParse(address, out var scriptHash)) diff --git a/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.Wallet.cs b/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.Wallet.cs new file mode 100644 index 0000000000..2db71ae570 --- /dev/null +++ b/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.Wallet.cs @@ -0,0 +1,403 @@ +// 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.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; + +partial class UT_RpcServer +{ + [TestMethod] + public void TestOpenWallet() + { + const string Path = "wallet.json"; + const string Password = "123456"; + File.WriteAllText(Path, "{\"name\":null,\"version\":\"1.0\",\"scrypt\":{\"n\":16384,\"r\":8,\"p\":8},\"accounts\":[{\"address\":\"NVizn8DiExdmnpTQfjiVY3dox8uXg3Vrxv\",\"label\":null,\"isDefault\":false,\"lock\":false,\"key\":\"6PYPMrsCJ3D4AXJCFWYT2WMSBGF7dLoaNipW14t4UFAkZw3Z9vQRQV1bEU\",\"contract\":{\"script\":\"DCEDaR\\u002BFVb8lOdiMZ/wCHLiI\\u002Bzuf17YuGFReFyHQhB80yMpBVuezJw==\",\"parameters\":[{\"name\":\"signature\",\"type\":\"Signature\"}],\"deployed\":false},\"extra\":null}],\"extra\":null}"); + var paramsArray = new JArray(Path, Password); + var res = _rpcServer.OpenWallet(paramsArray); + Assert.IsTrue(res.AsBoolean()); + Assert.IsNotNull(_rpcServer.wallet); + Assert.AreEqual(_rpcServer.wallet.GetAccounts().FirstOrDefault()!.Address, "NVizn8DiExdmnpTQfjiVY3dox8uXg3Vrxv"); + _rpcServer.CloseWallet([]); + File.Delete(Path); + Assert.IsNull(_rpcServer.wallet); + } + + [TestMethod] + public void TestOpenInvalidWallet() + { + const string Path = "wallet.json"; + const string Password = "password"; + File.Delete(Path); + var paramsArray = new JArray(Path, Password); + var exception = Assert.ThrowsException(() => _rpcServer.OpenWallet(paramsArray), "Should throw RpcException for unsupported wallet"); + Assert.AreEqual(RpcError.WalletNotFound.Code, exception.HResult); + + File.WriteAllText(Path, "{}"); + exception = Assert.ThrowsException(() => _rpcServer.OpenWallet(paramsArray), "Should throw RpcException for unsupported wallet"); + File.Delete(Path); + Assert.AreEqual(RpcError.WalletNotSupported.Code, exception.HResult); + var result = _rpcServer.CloseWallet(new JArray()); + Assert.IsTrue(result.AsBoolean()); + Assert.IsNull(_rpcServer.wallet); + + File.WriteAllText(Path, "{\"name\":null,\"version\":\"1.0\",\"scrypt\":{\"n\":16384,\"r\":8,\"p\":8},\"accounts\":[{\"address\":\"NVizn8DiExdmnpTQfjiVY3dox8uXg3Vrxv\",\"label\":null,\"isDefault\":false,\"lock\":false,\"key\":\"6PYPMrsCJ3D4AXJCFWYT2WMSBGF7dLoaNipW14t4UFAkZw3Z9vQRQV1bEU\",\"contract\":{\"script\":\"DCEDaR\\u002BFVb8lOdiMZ/wCHLiI\\u002Bzuf17YuGFReFyHQhB80yMpBVuezJw==\",\"parameters\":[{\"name\":\"signature\",\"type\":\"Signature\"}],\"deployed\":false},\"extra\":null}],\"extra\":null}"); + exception = Assert.ThrowsException(() => _rpcServer.OpenWallet(paramsArray), "Should throw RpcException for unsupported wallet"); + Assert.AreEqual(RpcError.WalletNotSupported.Code, exception.HResult); + Assert.AreEqual(exception.Message, "Wallet not supported - Invalid password."); + File.Delete(Path); + } + + [TestMethod] + public void TestDumpPrivKey() + { + TestUtilOpenWallet(); + var account = _rpcServer.wallet.GetAccounts().FirstOrDefault(); + Assert.IsNotNull(account); + var privKey = account.GetKey().Export(); + var address = account.Address; + var result = _rpcServer.DumpPrivKey(new JArray(address)); + Assert.AreEqual(privKey, result.AsString()); + TestUtilCloseWallet(); + } + + [TestMethod] + public void TestGetNewAddress() + { + TestUtilOpenWallet(); + var result = _rpcServer.GetNewAddress([]); + Assert.IsInstanceOfType(result, typeof(JString)); + Assert.IsTrue(_rpcServer.wallet.GetAccounts().Any(a => a.Address == result.AsString())); + TestUtilCloseWallet(); + } + + [TestMethod] + public void TestGetWalletBalance() + { + TestUtilOpenWallet(); + var assetId = NativeContract.NEO.Hash; + var paramsArray = new JArray(assetId.ToString()); + var result = _rpcServer.GetWalletBalance(paramsArray); + Assert.IsInstanceOfType(result, typeof(JObject)); + var json = (JObject)result; + Assert.IsTrue(json.ContainsProperty("balance")); + TestUtilCloseWallet(); + } + + [TestMethod] + public void TestGetWalletBalanceInvalidAsset() + { + TestUtilOpenWallet(); + var assetId = UInt160.Zero; + var paramsArray = new JArray(assetId.ToString()); + var result = _rpcServer.GetWalletBalance(paramsArray); + Assert.IsInstanceOfType(result, typeof(JObject)); + var json = (JObject)result; + Assert.IsTrue(json.ContainsProperty("balance")); + TestUtilCloseWallet(); + } + + [TestMethod] + public void TestGetWalletUnclaimedGas() + { + TestUtilOpenWallet(); + var result = _rpcServer.GetWalletUnclaimedGas([]); + Assert.IsInstanceOfType(result, typeof(JString)); + TestUtilCloseWallet(); + } + + [TestMethod] + public void TestImportPrivKey() + { + TestUtilOpenWallet(); + var privKey = _walletAccount.GetKey().Export(); + var paramsArray = new JArray(privKey); + var result = _rpcServer.ImportPrivKey(paramsArray); + Assert.IsInstanceOfType(result, typeof(JObject)); + var json = (JObject)result; + Assert.IsTrue(json.ContainsProperty("address")); + Assert.IsTrue(json.ContainsProperty("haskey")); + Assert.IsTrue(json.ContainsProperty("label")); + Assert.IsTrue(json.ContainsProperty("watchonly")); + TestUtilCloseWallet(); + } + + [TestMethod] + public void TestImportPrivKeyNoWallet() + { + var privKey = _walletAccount.GetKey().Export(); + var paramsArray = new JArray(privKey); + var exception = Assert.ThrowsException(() => _rpcServer.ImportPrivKey(paramsArray)); + Assert.AreEqual(exception.HResult, RpcError.NoOpenedWallet.Code); + } + + [TestMethod] + public void TestCalculateNetworkFee() + { + var snapshot = _neoSystem.GetSnapshot(); + var tx = TestUtils.CreateValidTx(snapshot, _wallet, _walletAccount); + var txBase64 = Convert.ToBase64String(tx.ToArray()); + var paramsArray = new JArray(txBase64); + var result = _rpcServer.CalculateNetworkFee(paramsArray); + Assert.IsInstanceOfType(result, typeof(JObject)); + var json = (JObject)result; + Assert.IsTrue(json.ContainsProperty("networkfee")); + } + + [TestMethod] + public void TestCalculateNetworkFeeNoParam() + { + var exception = Assert.ThrowsException(() => _rpcServer.CalculateNetworkFee([])); + Assert.AreEqual(exception.HResult, RpcError.InvalidParams.Code); + } + + [TestMethod] + public void TestListAddressNoWallet() + { + var exception = Assert.ThrowsException(() => _rpcServer.ListAddress([])); + Assert.AreEqual(exception.HResult, RpcError.NoOpenedWallet.Code); + } + + [TestMethod] + public void TestListAddress() + { + TestUtilOpenWallet(); + var result = _rpcServer.ListAddress([]); + Assert.IsInstanceOfType(result, typeof(JArray)); + var json = (JArray)result; + Assert.IsTrue(json.Count > 0); + TestUtilCloseWallet(); + } + + [TestMethod] + public void TestSendFromNoWallet() + { + var assetId = NativeContract.GAS.Hash; + var from = _walletAccount.Address; + var to = _walletAccount.Address; + var amount = "1"; + var paramsArray = new JArray(assetId.ToString(), from, to, amount); + var exception = Assert.ThrowsException(() => _rpcServer.SendFrom(paramsArray), "Should throw RpcException for insufficient funds"); + Assert.AreEqual(exception.HResult, RpcError.NoOpenedWallet.Code); + } + + [TestMethod] + public void TestSendFrom() + { + TestUtilOpenWallet(); + var assetId = NativeContract.GAS.Hash; + var from = _walletAccount.Address; + var to = _walletAccount.Address; + var amount = "1"; + var paramsArray = new JArray(assetId.ToString(), from, to, amount); + var exception = Assert.ThrowsException(() => _rpcServer.SendFrom(paramsArray)); + Assert.AreEqual(exception.HResult, RpcError.InvalidRequest.Code); + TestUtilCloseWallet(); + } + + [TestMethod] + public void TestSendMany() + { + var from = _walletAccount.Address; + var to = new JArray { new JObject { ["asset"] = NativeContract.GAS.Hash.ToString(), ["value"] = "1", ["address"] = _walletAccount.Address } }; + 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); + } + + [TestMethod] + public void TestSendToAddress() + { + var assetId = NativeContract.GAS.Hash; + var to = _walletAccount.Address; + var amount = "1"; + 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); + } + + [TestMethod] + public void TestCloseWallet_WhenWalletNotOpen() + { + _rpcServer.wallet = null; + var result = _rpcServer.CloseWallet(new JArray()); + Assert.IsTrue(result.AsBoolean()); + } + + [TestMethod] + public void TestDumpPrivKey_WhenWalletNotOpen() + { + _rpcServer.wallet = null; + var exception = Assert.ThrowsException(() => _rpcServer.DumpPrivKey(new JArray(_walletAccount.Address)), "Should throw RpcException for no opened wallet"); + Assert.AreEqual(exception.HResult, RpcError.NoOpenedWallet.Code); + } + + [TestMethod] + public void TestGetNewAddress_WhenWalletNotOpen() + { + _rpcServer.wallet = null; + var exception = Assert.ThrowsException(() => _rpcServer.GetNewAddress(new JArray()), "Should throw RpcException for no opened wallet"); + Assert.AreEqual(exception.HResult, RpcError.NoOpenedWallet.Code); + } + + [TestMethod] + public void TestGetWalletBalance_WhenWalletNotOpen() + { + _rpcServer.wallet = null; + var exception = Assert.ThrowsException(() => _rpcServer.GetWalletBalance(new JArray(NativeContract.NEO.Hash.ToString())), "Should throw RpcException for no opened wallet"); + Assert.AreEqual(exception.HResult, RpcError.NoOpenedWallet.Code); + } + + [TestMethod] + public void TestGetWalletUnclaimedGas_WhenWalletNotOpen() + { + _rpcServer.wallet = null; + var exception = Assert.ThrowsException(() => _rpcServer.GetWalletUnclaimedGas(new JArray()), "Should throw RpcException for no opened wallet"); + Assert.AreEqual(exception.HResult, RpcError.NoOpenedWallet.Code); + } + + [TestMethod] + public void TestImportPrivKey_WhenWalletNotOpen() + { + _rpcServer.wallet = null; + var privKey = _walletAccount.GetKey().Export(); + var exception = Assert.ThrowsException(() => _rpcServer.ImportPrivKey(new JArray(privKey)), "Should throw RpcException for no opened wallet"); + Assert.AreEqual(exception.HResult, RpcError.NoOpenedWallet.Code); + } + + [TestMethod] + public void TestCalculateNetworkFee_InvalidTransactionFormat() + { + var invalidTxBase64 = "invalid_base64"; + var paramsArray = new JArray(invalidTxBase64); + var exception = Assert.ThrowsException(() => _rpcServer.CalculateNetworkFee(paramsArray), "Should throw RpcException for invalid transaction format"); + Assert.AreEqual(exception.HResult, RpcError.InvalidParams.Code); + } + + [TestMethod] + public void TestListAddress_WhenWalletNotOpen() + { + // Ensure the wallet is not open + _rpcServer.wallet = null; + + // Attempt to call ListAddress and expect an RpcException + var exception = Assert.ThrowsException(() => _rpcServer.ListAddress(new JArray())); + + // Verify the exception has the expected error code + Assert.AreEqual(RpcError.NoOpenedWallet.Code, exception.HResult); + } + + [TestMethod] + public void TestCancelTransaction() + { + TestUtilOpenWallet(); + var snapshot = _neoSystem.GetSnapshot(); + var tx = TestUtils.CreateValidTx(snapshot, _wallet, _walletAccount); + snapshot.Commit(); + var paramsArray = new JArray(tx.Hash.ToString(), new JArray(_walletAccount.Address)); + var exception = Assert.ThrowsException(() => _rpcServer.CancelTransaction(paramsArray), "Should throw RpcException for non-existing transaction"); + + Assert.AreEqual(RpcError.InsufficientFunds.Code, exception.HResult); + + // Test with invalid transaction id + var invalidParamsArray = new JArray("invalid_txid", new JArray(_walletAccount.Address)); + exception = Assert.ThrowsException(() => _rpcServer.CancelTransaction(invalidParamsArray), "Should throw RpcException for invalid txid"); + Assert.AreEqual(exception.HResult, RpcError.InvalidParams.Code); + + // Test with no signer + invalidParamsArray = new JArray(tx.Hash.ToString()); + exception = Assert.ThrowsException(() => _rpcServer.CancelTransaction(invalidParamsArray), "Should throw RpcException for invalid txid"); + Assert.AreEqual(exception.HResult, RpcError.BadRequest.Code); + + // Test with null wallet + _rpcServer.wallet = null; + exception = Assert.ThrowsException(() => _rpcServer.CancelTransaction(paramsArray), "Should throw RpcException for no opened wallet"); + Assert.AreEqual(exception.HResult, RpcError.NoOpenedWallet.Code); + TestUtilCloseWallet(); + } + + [TestMethod] + public void TestInvokeContractVerify() + { + var scriptHash = UInt160.Parse("0x70cde1619e405cdef363ab66a1e8dce430d798d5"); + var paramsArray = new JArray(scriptHash.ToString()); + var exception = Assert.ThrowsException(() => _rpcServer.InvokeContractVerify(paramsArray), "Should throw RpcException for unknown contract"); + Assert.AreEqual(exception.HResult, RpcError.UnknownContract.Code); + // Test with invalid script hash + 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); + } + + + private void TestUtilOpenWallet() + { + try + { + const string Path = "wallet.json"; + const string Password = "123456"; + File.WriteAllText(Path, "{\"name\":null,\"version\":\"1.0\",\"scrypt\":{\"n\":16384,\"r\":8,\"p\":8},\"accounts\":[{\"address\":\"NVizn8DiExdmnpTQfjiVY3dox8uXg3Vrxv\",\"label\":null,\"isDefault\":false,\"lock\":false,\"key\":\"6PYPMrsCJ3D4AXJCFWYT2WMSBGF7dLoaNipW14t4UFAkZw3Z9vQRQV1bEU\",\"contract\":{\"script\":\"DCEDaR\\u002BFVb8lOdiMZ/wCHLiI\\u002Bzuf17YuGFReFyHQhB80yMpBVuezJw==\",\"parameters\":[{\"name\":\"signature\",\"type\":\"Signature\"}],\"deployed\":false},\"extra\":null}],\"extra\":null}"); + var paramsArray = new JArray(Path, Password); + _rpcServer.OpenWallet(paramsArray); + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + + private void TestUtilCloseWallet() + { + try + { + const string Path = "wallet.json"; + _rpcServer.CloseWallet([]); + File.Delete(Path); + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + + private UInt160 TestUtilAddTestContract() + { + var state = TestUtils.GetContract(); + + var storageKey = new StorageKey + { + Id = state.Id, + Key = new byte[] { 0x01 } + }; + + var storageItem = new StorageItem + { + Value = new byte[] { 0x01, 0x02, 0x03, 0x04 } + }; + + var snapshot = _neoSystem.GetSnapshotCache(); + snapshot.AddContract(state.Hash, state); + snapshot.Add(storageKey, storageItem); + snapshot.Commit(); + return state.Hash; + } +} diff --git a/tests/Neo.UnitTests/TestUtils.Contract.cs b/tests/Neo.UnitTests/TestUtils.Contract.cs new file mode 100644 index 0000000000..da3139561c --- /dev/null +++ b/tests/Neo.UnitTests/TestUtils.Contract.cs @@ -0,0 +1,105 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// TestUtils.Contract.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.SmartContract; +using Neo.SmartContract.Manifest; +using System; +using System.Linq; + +namespace Neo.UnitTests; + +partial class TestUtils +{ + public static ContractManifest CreateDefaultManifest() + { + return new ContractManifest + { + Name = "testManifest", + Groups = [], + SupportedStandards = [], + Abi = new ContractAbi + { + Events = [], + Methods = + [ + new ContractMethodDescriptor + { + Name = "testMethod", + Parameters = [], + ReturnType = ContractParameterType.Void, + Offset = 0, + Safe = true + } + ] + }, + Permissions = [ContractPermission.DefaultPermission], + Trusts = WildcardContainer.Create(), + Extra = null + }; + } + + public static ContractManifest CreateManifest(string method, ContractParameterType returnType, params ContractParameterType[] parameterTypes) + { + var manifest = CreateDefaultManifest(); + manifest.Abi.Methods = + [ + new ContractMethodDescriptor() + { + Name = method, + Parameters = parameterTypes.Select((p, i) => new ContractParameterDefinition + { + Name = $"p{i}", + Type = p + }).ToArray(), + ReturnType = returnType + } + ]; + return manifest; + } + + public static ContractState GetContract(string method = "test", int parametersCount = 0) + { + NefFile nef = new() + { + Compiler = "", + Source = "", + Tokens = [], + Script = new byte[] { 0x01, 0x01, 0x01, 0x01 } + }; + nef.CheckSum = NefFile.ComputeChecksum(nef); + return new ContractState + { + Id = 0x43000000, + Nef = nef, + Hash = nef.Script.Span.ToScriptHash(), + Manifest = CreateManifest(method, ContractParameterType.Any, Enumerable.Repeat(ContractParameterType.Any, parametersCount).ToArray()) + }; + } + + internal static ContractState GetContract(byte[] script, ContractManifest manifest = null) + { + NefFile nef = new() + { + Compiler = "", + Source = "", + Tokens = [], + Script = script + }; + nef.CheckSum = NefFile.ComputeChecksum(nef); + return new ContractState + { + Id = 1, + Hash = script.ToScriptHash(), + Nef = nef, + Manifest = manifest ?? CreateDefaultManifest() + }; + } +} diff --git a/tests/Neo.UnitTests/TestUtils.Transaction.cs b/tests/Neo.UnitTests/TestUtils.Transaction.cs index f96ac8ec74..f6d6ebd4b5 100644 --- a/tests/Neo.UnitTests/TestUtils.Transaction.cs +++ b/tests/Neo.UnitTests/TestUtils.Transaction.cs @@ -11,6 +11,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Neo.Cryptography; +using Neo.Cryptography.ECC; using Neo.IO; using Neo.Network.P2P.Payloads; using Neo.Persistence; @@ -22,11 +23,112 @@ using System; using System.IO; using System.Linq; +using System.Numerics; namespace Neo.UnitTests; public partial class TestUtils { + public static Transaction CreateValidTx(DataCache snapshot, NEP6Wallet wallet, WalletAccount account) + { + return CreateValidTx(snapshot, wallet, account.ScriptHash, (uint)new Random().Next()); + } + + public static Transaction CreateValidTx(DataCache snapshot, NEP6Wallet wallet, UInt160 account, uint nonce) + { + var tx = wallet.MakeTransaction(snapshot, [ + new TransferOutput + { + AssetId = NativeContract.GAS.Hash, + ScriptHash = account, + Value = new BigDecimal(BigInteger.One, 8) + } + ], + account); + + tx.Nonce = nonce; + tx.Signers = [new Signer { Account = account, Scopes = WitnessScope.CalledByEntry }]; + var data = new ContractParametersContext(snapshot, tx, TestProtocolSettings.Default.Network); + Assert.IsNull(data.GetSignatures(tx.Sender)); + Assert.IsTrue(wallet.Sign(data)); + Assert.IsTrue(data.Completed); + Assert.AreEqual(1, data.GetSignatures(tx.Sender).Count); + + tx.Witnesses = data.GetWitnesses(); + return tx; + } + + public static Transaction CreateValidTx(DataCache snapshot, NEP6Wallet wallet, UInt160 account, uint nonce, UInt256[] conflicts) + { + var tx = wallet.MakeTransaction(snapshot, [ + new TransferOutput + { + AssetId = NativeContract.GAS.Hash, + ScriptHash = account, + Value = new BigDecimal(BigInteger.One, 8) + } + ], + account); + tx.Attributes = conflicts.Select(conflict => new Conflicts { Hash = conflict }).ToArray(); + tx.Nonce = nonce; + tx.Signers = [new Signer { Account = account, Scopes = WitnessScope.CalledByEntry }]; + var data = new ContractParametersContext(snapshot, tx, TestProtocolSettings.Default.Network); + Assert.IsNull(data.GetSignatures(tx.Sender)); + Assert.IsTrue(wallet.Sign(data)); + Assert.IsTrue(data.Completed); + Assert.AreEqual(1, data.GetSignatures(tx.Sender).Count); + tx.Witnesses = data.GetWitnesses(); + return tx; + } + + public static Transaction CreateRandomHashTransaction() + { + var randomBytes = new byte[16]; + TestRandom.NextBytes(randomBytes); + return new Transaction + { + Script = randomBytes, + Attributes = [], + Signers = [new Signer { Account = UInt160.Zero }], + Witnesses = + [ + new Witness + { + InvocationScript = Array.Empty(), + VerificationScript = Array.Empty() + } + ] + }; + } + + public static Transaction GetTransaction(UInt160 sender) + { + return new Transaction + { + Script = new[] { (byte)OpCode.PUSH2 }, + Attributes = [], + Signers = + [ + new Signer + { + Account = sender, + Scopes = WitnessScope.CalledByEntry, + AllowedContracts = [], + AllowedGroups = [], + Rules = [], + } + ], + Witnesses = + [ + new Witness + { + InvocationScript = Array.Empty(), + VerificationScript = Array.Empty() + } + ] + }; + } + public static Transaction CreateInvalidTransaction(DataCache snapshot, NEP6Wallet wallet, WalletAccount account, InvalidTransactionType type, UInt256 conflict = null) { var rand = new Random(); diff --git a/tests/Neo.UnitTests/TestUtils.cs b/tests/Neo.UnitTests/TestUtils.cs index c1694347a7..a9ad4d8ff7 100644 --- a/tests/Neo.UnitTests/TestUtils.cs +++ b/tests/Neo.UnitTests/TestUtils.cs @@ -48,53 +48,6 @@ public static UInt160 RandomUInt160() return new UInt160(data); } - public static ContractManifest CreateDefaultManifest() - { - return new ContractManifest() - { - Name = "testManifest", - Groups = new ContractGroup[0], - SupportedStandards = Array.Empty(), - Abi = new ContractAbi() - { - Events = new ContractEventDescriptor[0], - Methods = new[] - { - new ContractMethodDescriptor - { - Name = "testMethod", - Parameters = new ContractParameterDefinition[0], - ReturnType = ContractParameterType.Void, - Offset = 0, - Safe = true - } - } - }, - Permissions = new[] { ContractPermission.DefaultPermission }, - Trusts = WildcardContainer.Create(), - Extra = null - }; - } - - public static ContractManifest CreateManifest(string method, ContractParameterType returnType, params ContractParameterType[] parameterTypes) - { - ContractManifest manifest = CreateDefaultManifest(); - manifest.Abi.Methods = new ContractMethodDescriptor[] - { - new ContractMethodDescriptor() - { - Name = method, - Parameters = parameterTypes.Select((p, i) => new ContractParameterDefinition - { - Name = $"p{i}", - Type = p - }).ToArray(), - ReturnType = returnType - } - }; - return manifest; - } - public static StorageKey CreateStorageKey(this NativeContract contract, byte prefix, ISerializable key = null) { var k = new KeyBuilder(contract.Id, prefix); @@ -130,118 +83,6 @@ public static NEP6Wallet GenerateTestWallet(string password) return new NEP6Wallet(null, password, TestProtocolSettings.Default, wallet); } - public static Transaction CreateValidTx(DataCache snapshot, NEP6Wallet wallet, WalletAccount account) - { - return CreateValidTx(snapshot, wallet, account.ScriptHash, (uint)new Random().Next()); - } - - public static Transaction CreateValidTx(DataCache snapshot, NEP6Wallet wallet, UInt160 account, uint nonce) - { - var tx = wallet.MakeTransaction(snapshot, [ - new TransferOutput - { - AssetId = NativeContract.GAS.Hash, - ScriptHash = account, - Value = new BigDecimal(BigInteger.One, 8) - } - ], - account); - - tx.Nonce = nonce; - - var data = new ContractParametersContext(snapshot, tx, TestProtocolSettings.Default.Network); - Assert.IsNull(data.GetSignatures(tx.Sender)); - Assert.IsTrue(wallet.Sign(data)); - Assert.IsTrue(data.Completed); - Assert.AreEqual(1, data.GetSignatures(tx.Sender).Count()); - - tx.Witnesses = data.GetWitnesses(); - return tx; - } - - public static Transaction CreateValidTx(DataCache snapshot, NEP6Wallet wallet, UInt160 account, uint nonce, UInt256[] conflicts) - { - var tx = wallet.MakeTransaction(snapshot, [ - new TransferOutput - { - AssetId = NativeContract.GAS.Hash, - ScriptHash = account, - Value = new BigDecimal(BigInteger.One, 8) - } - ], - account); - tx.Attributes = conflicts.Select(conflict => new Conflicts { Hash = conflict }).ToArray(); - tx.Nonce = nonce; - - var data = new ContractParametersContext(snapshot, tx, TestProtocolSettings.Default.Network); - Assert.IsNull(data.GetSignatures(tx.Sender)); - Assert.IsTrue(wallet.Sign(data)); - Assert.IsTrue(data.Completed); - Assert.AreEqual(1, data.GetSignatures(tx.Sender).Count); - tx.Witnesses = data.GetWitnesses(); - return tx; - } - - public static Transaction GetTransaction(UInt160 sender) - { - return new Transaction - { - Script = new byte[] { (byte)OpCode.PUSH2 }, - Attributes = Array.Empty(), - Signers = new[]{ new Signer() - { - Account = sender, - Scopes = WitnessScope.CalledByEntry, - AllowedContracts = Array.Empty(), - AllowedGroups = Array.Empty(), - Rules = Array.Empty(), - } }, - Witnesses = new Witness[]{ new Witness - { - InvocationScript = Array.Empty(), - VerificationScript = Array.Empty() - } } - }; - } - - public static ContractState GetContract(string method = "test", int parametersCount = 0) - { - NefFile nef = new() - { - Compiler = "", - Source = "", - Tokens = Array.Empty(), - Script = new byte[] { 0x01, 0x01, 0x01, 0x01 } - }; - nef.CheckSum = NefFile.ComputeChecksum(nef); - return new ContractState - { - Id = 0x43000000, - Nef = nef, - Hash = nef.Script.Span.ToScriptHash(), - Manifest = CreateManifest(method, ContractParameterType.Any, Enumerable.Repeat(ContractParameterType.Any, parametersCount).ToArray()) - }; - } - - internal static ContractState GetContract(byte[] script, ContractManifest manifest = null) - { - NefFile nef = new() - { - Compiler = "", - Source = "", - Tokens = Array.Empty(), - Script = script - }; - nef.CheckSum = NefFile.ComputeChecksum(nef); - return new ContractState - { - Id = 1, - Hash = script.ToScriptHash(), - Nef = nef, - Manifest = manifest ?? CreateDefaultManifest() - }; - } - internal static StorageItem GetStorageItem(byte[] value) { return new StorageItem @@ -268,26 +109,6 @@ public static void StorageItemAdd(DataCache snapshot, int id, byte[] keyValue, b }, new StorageItem(value)); } - public static Transaction CreateRandomHashTransaction() - { - var randomBytes = new byte[16]; - TestRandom.NextBytes(randomBytes); - return new Transaction - { - Script = randomBytes, - Attributes = Array.Empty(), - Signers = new Signer[] { new Signer() { Account = UInt160.Zero } }, - Witnesses = new[] - { - new Witness - { - InvocationScript = new byte[0], - VerificationScript = new byte[0] - } - } - }; - } - public static void FillMemoryPool(DataCache snapshot, NeoSystem system, NEP6Wallet wallet, WalletAccount account) { for (int i = 0; i < system.Settings.MemoryPoolMaxTransactions; i++)