From c9c4f746d1c2f1f6294ae250b8de55a774a10901 Mon Sep 17 00:00:00 2001
From: Wi1l-B0t <201105916+Wi1l-B0t@users.noreply.github.com>
Date: Sat, 19 Jul 2025 15:33:41 +0800
Subject: [PATCH] Optimize: merge RpcMethod and RpcMethodWithParams
---
.../RpcServer/RpcMethodWithParamsAttribute.cs | 24 ---
src/Plugins/RpcServer/RpcServer.Blockchain.cs | 32 +--
src/Plugins/RpcServer/RpcServer.Node.cs | 10 +-
src/Plugins/RpcServer/RpcServer.cs | 186 +++++++++---------
.../UT_RpcServer.cs | 54 ++++-
5 files changed, 157 insertions(+), 149 deletions(-)
delete mode 100644 src/Plugins/RpcServer/RpcMethodWithParamsAttribute.cs
diff --git a/src/Plugins/RpcServer/RpcMethodWithParamsAttribute.cs b/src/Plugins/RpcServer/RpcMethodWithParamsAttribute.cs
deleted file mode 100644
index 6e99b43a8c..0000000000
--- a/src/Plugins/RpcServer/RpcMethodWithParamsAttribute.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright (C) 2015-2025 The Neo Project.
-//
-// RpcMethodWithParamsAttribute.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 System;
-
-namespace Neo.Plugins.RpcServer
-{
- ///
- /// Indicates that the method is an RPC method with parameters.
- ///
- [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
- public class RpcMethodWithParamsAttribute : Attribute
- {
- public string Name { get; set; }
- }
-}
diff --git a/src/Plugins/RpcServer/RpcServer.Blockchain.cs b/src/Plugins/RpcServer/RpcServer.Blockchain.cs
index a8b6fa2a42..fb7f9af5c2 100644
--- a/src/Plugins/RpcServer/RpcServer.Blockchain.cs
+++ b/src/Plugins/RpcServer/RpcServer.Blockchain.cs
@@ -37,7 +37,7 @@ partial class RpcServer
///
///
/// The hash of the best block as a .
- [RpcMethodWithParams]
+ [RpcMethod]
protected internal virtual JToken GetBestBlockHash()
{
return NativeContract.Ledger.CurrentHash(system.StoreView).ToString();
@@ -88,7 +88,7 @@ protected internal virtual JToken GetBestBlockHash()
/// Optional, the default value is false.
/// The block data as a . If the second item of _params is true, then
/// block data is json format, otherwise, the return type is Base64-encoded byte array.
- [RpcMethodWithParams]
+ [RpcMethod]
protected internal virtual JToken GetBlock(BlockHashOrIndex blockHashOrIndex, bool verbose = false)
{
RpcException.ThrowIfNull(blockHashOrIndex, nameof(blockHashOrIndex), RpcError.InvalidParams);
@@ -118,7 +118,7 @@ protected internal virtual JToken GetBlock(BlockHashOrIndex blockHashOrIndex, bo
/// {"jsonrpc": "2.0", "id": 1, "result": 100 /* The number of block headers in the blockchain */}
///
/// The count of block headers as a .
- [RpcMethodWithParams]
+ [RpcMethod]
internal virtual JToken GetBlockHeaderCount()
{
return (system.HeaderCache.Last?.Index ?? NativeContract.Ledger.CurrentIndex(system.StoreView)) + 1;
@@ -132,7 +132,7 @@ internal virtual JToken GetBlockHeaderCount()
/// {"jsonrpc": "2.0", "id": 1, "result": 100 /* The number of blocks in the blockchain */}
///
/// The count of blocks as a .
- [RpcMethodWithParams]
+ [RpcMethod]
protected internal virtual JToken GetBlockCount()
{
return NativeContract.Ledger.CurrentIndex(system.StoreView) + 1;
@@ -149,7 +149,7 @@ protected internal virtual JToken GetBlockCount()
///
/// Block index (block height)
/// The hash of the block at the specified height as a .
- [RpcMethodWithParams]
+ [RpcMethod]
protected internal virtual JToken GetBlockHash(uint height)
{
var snapshot = system.StoreView;
@@ -209,7 +209,7 @@ protected internal virtual JToken GetBlockHash(uint height)
/// The block header data as a .
/// In json format if the second item of _params is true, otherwise Base64-encoded byte array.
///
- [RpcMethodWithParams]
+ [RpcMethod]
protected internal virtual JToken GetBlockHeader(BlockHashOrIndex blockHashOrIndex, bool verbose = false)
{
RpcException.ThrowIfNull(blockHashOrIndex, nameof(blockHashOrIndex), RpcError.InvalidParams);
@@ -249,7 +249,7 @@ protected internal virtual JToken GetBlockHeader(BlockHashOrIndex blockHashOrInd
///
/// Contract name or script hash or the native contract id.
/// The contract state in json format as a .
- [RpcMethodWithParams]
+ [RpcMethod]
protected internal virtual JToken GetContractState(ContractNameOrHashOrId contractNameOrHashOrId)
{
RpcException.ThrowIfNull(contractNameOrHashOrId, nameof(contractNameOrHashOrId), RpcError.InvalidParams);
@@ -300,7 +300,7 @@ private static UInt160 ToScriptHash(string keyword)
///
/// Optional, the default value is false.
/// The memory pool transactions in json format as a .
- [RpcMethodWithParams]
+ [RpcMethod]
protected internal virtual JToken GetRawMemPool(bool shouldGetUnverified = false)
{
if (!shouldGetUnverified)
@@ -355,7 +355,7 @@ protected internal virtual JToken GetRawMemPool(bool shouldGetUnverified = false
/// The transaction hash.
/// Optional, the default value is false.
/// The transaction data as a . In json format if verbose is true, otherwise base64string.
- [RpcMethodWithParams]
+ [RpcMethod]
protected internal virtual JToken GetRawTransaction(UInt256 hash, bool verbose = false)
{
RpcException.ThrowIfNull(hash, nameof(hash), RpcError.InvalidParams);
@@ -406,7 +406,7 @@ private static int GetContractId(IReadOnlyStore snapshot, ContractNameOrHashOrId
/// The contract ID or script hash.
/// The Base64-encoded storage key.
/// The storage item as a .
- [RpcMethodWithParams]
+ [RpcMethod]
protected internal virtual JToken GetStorage(ContractNameOrHashOrId contractNameOrHashOrId, string base64Key)
{
RpcException.ThrowIfNull(contractNameOrHashOrId, nameof(contractNameOrHashOrId), RpcError.InvalidParams);
@@ -456,7 +456,7 @@ protected internal virtual JToken GetStorage(ContractNameOrHashOrId contractName
/// The Base64-encoded storage key prefix.
/// The start index.
/// The found storage items as a .
- [RpcMethodWithParams]
+ [RpcMethod]
protected internal virtual JToken FindStorage(ContractNameOrHashOrId contractNameOrHashOrId, string base64KeyPrefix, int start = 0)
{
RpcException.ThrowIfNull(contractNameOrHashOrId, nameof(contractNameOrHashOrId), RpcError.InvalidParams);
@@ -509,7 +509,7 @@ protected internal virtual JToken FindStorage(ContractNameOrHashOrId contractNam
///
/// The transaction hash.
/// The height of the transaction as a .
- [RpcMethodWithParams]
+ [RpcMethod]
protected internal virtual JToken GetTransactionHeight(UInt256 hash)
{
RpcException.ThrowIfNull(hash, nameof(hash), RpcError.InvalidParams);
@@ -534,7 +534,7 @@ protected internal virtual JToken GetTransactionHeight(UInt256 hash)
/// }
///
/// The next block validators as a .
- [RpcMethodWithParams]
+ [RpcMethod]
protected internal virtual JToken GetNextBlockValidators()
{
using var snapshot = system.GetSnapshotCache();
@@ -563,7 +563,7 @@ protected internal virtual JToken GetNextBlockValidators()
/// }
///
/// The candidates public key list as a JToken.
- [RpcMethodWithParams]
+ [RpcMethod]
protected internal virtual JToken GetCandidates()
{
using var snapshot = system.GetSnapshotCache();
@@ -624,7 +624,7 @@ protected internal virtual JToken GetCandidates()
/// {"jsonrpc": "2.0", "id": 1, "result": ["The public key"]}
///
/// The committee members publickeys as a .
- [RpcMethodWithParams]
+ [RpcMethod]
protected internal virtual JToken GetCommittee()
{
return new JArray(NativeContract.NEO.GetCommittee(system.StoreView).Select(p => (JToken)p.ToString()));
@@ -711,7 +711,7 @@ protected internal virtual JToken GetCommittee()
/// }
///
/// The native contract states as a .
- [RpcMethodWithParams]
+ [RpcMethod]
protected internal virtual JToken GetNativeContracts()
{
var storeView = system.StoreView;
diff --git a/src/Plugins/RpcServer/RpcServer.Node.cs b/src/Plugins/RpcServer/RpcServer.Node.cs
index 7d418dca0f..f87d586bb2 100644
--- a/src/Plugins/RpcServer/RpcServer.Node.cs
+++ b/src/Plugins/RpcServer/RpcServer.Node.cs
@@ -33,7 +33,7 @@ partial class RpcServer
/// {"jsonrpc": "2.0", "id": 1, "result": 10}
///
/// The number of connections as a JToken.
- [RpcMethodWithParams]
+ [RpcMethod]
protected internal virtual JToken GetConnectionCount()
{
return localNode.ConnectedCount;
@@ -56,7 +56,7 @@ protected internal virtual JToken GetConnectionCount()
/// }
///
/// A JObject containing information about unconnected, bad, and connected peers.
- [RpcMethodWithParams]
+ [RpcMethod]
protected internal virtual JToken GetPeers()
{
return new JObject()
@@ -146,7 +146,7 @@ private static JObject GetRelayResult(VerifyResult reason, UInt256 hash)
/// }
///
/// A JObject containing detailed version and configuration information.
- [RpcMethodWithParams]
+ [RpcMethod]
protected internal virtual JToken GetVersion()
{
JObject json = new();
@@ -205,7 +205,7 @@ private static string StripPrefix(string s, string prefix)
///
/// The base64-encoded transaction.
/// A JToken containing the result of the transaction relay.
- [RpcMethodWithParams]
+ [RpcMethod]
protected internal virtual JToken SendRawTransaction(string base64Tx)
{
var tx = Result.Ok_Or(
@@ -225,7 +225,7 @@ protected internal virtual JToken SendRawTransaction(string base64Tx)
///
/// The base64-encoded block.
/// A JToken containing the result of the block submission.
- [RpcMethodWithParams]
+ [RpcMethod]
protected internal virtual JToken SubmitBlock(string base64Block)
{
var block = Result.Ok_Or(
diff --git a/src/Plugins/RpcServer/RpcServer.cs b/src/Plugins/RpcServer/RpcServer.cs
index 1929aa9ff9..63968ee5d2 100644
--- a/src/Plugins/RpcServer/RpcServer.cs
+++ b/src/Plugins/RpcServer/RpcServer.cs
@@ -38,9 +38,10 @@ namespace Neo.Plugins.RpcServer
public partial class RpcServer : IDisposable
{
private const int MaxParamsDepth = 32;
+ private const string HttpMethodGet = "GET";
+ private const string HttpMethodPost = "POST";
- private readonly Dictionary> methods = new();
- private readonly Dictionary _methodsWithParams = new();
+ private readonly Dictionary _methods = new();
private IWebHost host;
private RpcServersSettings settings;
@@ -94,11 +95,10 @@ internal bool CheckAuth(HttpContext context)
}
int colonIndex = Array.IndexOf(auths, (byte)':');
- if (colonIndex == -1)
- return false;
+ if (colonIndex == -1) return false;
- byte[] user = auths[..colonIndex];
- byte[] pass = auths[(colonIndex + 1)..];
+ var user = auths[..colonIndex];
+ var pass = auths[(colonIndex + 1)..];
// Always execute both checks, but both must evaluate to true
return CryptographicOperations.FixedTimeEquals(user, _rpcUser) & CryptographicOperations.FixedTimeEquals(pass, _rpcPass);
@@ -106,17 +106,18 @@ internal bool CheckAuth(HttpContext context)
private static JObject CreateErrorResponse(JToken id, RpcError rpcError)
{
- JObject response = CreateResponse(id);
+ var response = CreateResponse(id);
response["error"] = rpcError.ToJson();
return response;
}
private static JObject CreateResponse(JToken id)
{
- JObject response = new();
- response["jsonrpc"] = "2.0";
- response["id"] = id;
- return response;
+ return new JObject
+ {
+ ["jsonrpc"] = "2.0",
+ ["id"] = id
+ };
}
///
@@ -168,23 +169,19 @@ public void StartRpcServer()
if (string.IsNullOrEmpty(settings.SslCert)) return;
listenOptions.UseHttps(settings.SslCert, settings.SslCertPassword, httpsConnectionAdapterOptions =>
{
- if (settings.TrustedAuthorities is null || settings.TrustedAuthorities.Length == 0)
- return;
+ if (settings.TrustedAuthorities is null || settings.TrustedAuthorities.Length == 0) return;
httpsConnectionAdapterOptions.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
httpsConnectionAdapterOptions.ClientCertificateValidation = (cert, chain, err) =>
{
- if (err != SslPolicyErrors.None)
- return false;
- X509Certificate2 authority = chain.ChainElements[^1].Certificate;
+ if (err != SslPolicyErrors.None) return false;
+ var authority = chain.ChainElements[^1].Certificate;
return settings.TrustedAuthorities.Contains(authority.Thumbprint);
};
});
}))
.Configure(app =>
{
- if (settings.EnableCors)
- app.UseCors("All");
-
+ if (settings.EnableCors) app.UseCors("All");
app.UseResponseCompression();
app.Run(ProcessAsync);
})
@@ -193,28 +190,32 @@ public void StartRpcServer()
if (settings.EnableCors)
{
if (settings.AllowOrigins.Length == 0)
+ {
services.AddCors(options =>
{
options.AddPolicy("All", policy =>
{
policy.AllowAnyOrigin()
- .WithHeaders("Content-Type")
- .WithMethods("GET", "POST");
+ .WithHeaders("Content-Type")
+ .WithMethods(HttpMethodGet, HttpMethodPost);
// The CORS specification states that setting origins to "*" (all origins)
// is invalid if the Access-Control-Allow-Credentials header is present.
});
});
+ }
else
+ {
services.AddCors(options =>
{
options.AddPolicy("All", policy =>
{
policy.WithOrigins(settings.AllowOrigins)
- .WithHeaders("Content-Type")
- .AllowCredentials()
- .WithMethods("GET", "POST");
+ .WithHeaders("Content-Type")
+ .AllowCredentials()
+ .WithMethods(HttpMethodGet, HttpMethodPost);
});
});
+ }
}
services.AddResponseCompression(options =>
@@ -241,9 +242,10 @@ internal void UpdateSettings(RpcServersSettings settings)
public async Task ProcessAsync(HttpContext context)
{
- if (context.Request.Method != "GET" && context.Request.Method != "POST") return;
+ if (context.Request.Method != HttpMethodGet && context.Request.Method != HttpMethodPost) return;
+
JToken request = null;
- if (context.Request.Method == "GET")
+ if (context.Request.Method == HttpMethodGet)
{
string jsonrpc = context.Request.Query["jsonrpc"];
string id = context.Request.Query["id"];
@@ -256,6 +258,7 @@ public async Task ProcessAsync(HttpContext context)
_params = Encoding.UTF8.GetString(Convert.FromBase64String(_params));
}
catch (FormatException) { }
+
request = new JObject();
if (!string.IsNullOrEmpty(jsonrpc))
request["jsonrpc"] = jsonrpc;
@@ -264,15 +267,16 @@ public async Task ProcessAsync(HttpContext context)
request["params"] = JToken.Parse(_params, MaxParamsDepth);
}
}
- else if (context.Request.Method == "POST")
+ else if (context.Request.Method == HttpMethodPost)
{
- using StreamReader reader = new(context.Request.Body);
+ using var reader = new StreamReader(context.Request.Body);
try
{
request = JToken.Parse(await reader.ReadToEndAsync(), MaxParamsDepth);
}
catch (FormatException) { }
}
+
JToken response;
if (request == null)
{
@@ -295,6 +299,7 @@ public async Task ProcessAsync(HttpContext context)
{
response = await ProcessRequestAsync(context, (JObject)request);
}
+
if (response == null || (response as JArray)?.Count == 0) return;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(response.ToString(), Encoding.UTF8);
@@ -316,55 +321,9 @@ internal async Task ProcessRequestAsync(HttpContext context, JObject re
var method = request["method"].AsString();
(CheckAuth(context) && !settings.DisabledMethods.Contains(method)).True_Or(RpcError.AccessDenied);
- if (methods.TryGetValue(method, out var func))
+ if (_methods.TryGetValue(method, out var func))
{
- response["result"] = func(jsonParameters) switch
- {
- JToken result => result,
- Task task => await task,
- _ => throw new NotSupportedException()
- };
- return response;
- }
-
- if (_methodsWithParams.TryGetValue(method, out var func2))
- {
- var paramInfos = func2.Method.GetParameters();
- var args = new object[paramInfos.Length];
-
- for (var i = 0; i < paramInfos.Length; i++)
- {
- var param = paramInfos[i];
- if (jsonParameters.Count > i && jsonParameters[i] != null)
- {
- try
- {
- args[i] = ParameterConverter.AsParameter(jsonParameters[i], param.ParameterType);
- }
- catch (Exception e) when (e is not RpcException)
- {
- throw new ArgumentException($"Invalid value for parameter '{param.Name}'", e);
- }
- }
- else
- {
- if (param.IsOptional)
- {
- args[i] = param.DefaultValue;
- }
- else if (param.ParameterType.IsValueType &&
- Nullable.GetUnderlyingType(param.ParameterType) == null)
- {
- throw new ArgumentException($"Required parameter '{param.Name}' is missing");
- }
- else
- {
- args[i] = null;
- }
- }
- }
-
- response["result"] = func2.DynamicInvoke(args) switch
+ response["result"] = ProcessParamsMethod(jsonParameters, func) switch
{
JToken result => result,
Task task => await task,
@@ -386,49 +345,82 @@ internal async Task ProcessRequestAsync(HttpContext context, JObject re
catch (Exception ex) when (ex is not RpcException)
{
// Unwrap the exception to get the original error code
- var unwrappedException = UnwrapException(ex);
+ var unwrapped = UnwrapException(ex);
#if DEBUG
return CreateErrorResponse(request["id"],
- RpcErrorFactory.NewCustomError(unwrappedException.HResult, unwrappedException.Message, unwrappedException.StackTrace));
+ RpcErrorFactory.NewCustomError(unwrapped.HResult, unwrapped.Message, unwrapped.StackTrace));
#else
- return CreateErrorResponse(request["id"], RpcErrorFactory.NewCustomError(unwrappedException.HResult, unwrappedException.Message));
+ return CreateErrorResponse(request["id"], RpcErrorFactory.NewCustomError(unwrapped.HResult, unwrapped.Message));
#endif
}
catch (RpcException ex)
{
#if DEBUG
- return CreateErrorResponse(request["id"],
- RpcErrorFactory.NewCustomError(ex.HResult, ex.Message, ex.StackTrace));
+ return CreateErrorResponse(request["id"], RpcErrorFactory.NewCustomError(ex.HResult, ex.Message, ex.StackTrace));
#else
return CreateErrorResponse(request["id"], ex.GetError());
#endif
}
}
- public void RegisterMethods(object handler)
+ private object ProcessParamsMethod(JArray arguments, Delegate func)
{
- foreach (var method in handler.GetType().GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
+ var parameterInfos = func.Method.GetParameters();
+ var args = new object[parameterInfos.Length];
+
+ // If the method has only one parameter of type JArray, invoke the method directly with the arguments
+ if (parameterInfos.Length == 1 && parameterInfos[0].ParameterType == typeof(JArray))
{
- var attribute = method.GetCustomAttribute();
- var attributeWithParams = method.GetCustomAttribute();
- if (attribute is null && attributeWithParams is null) continue;
- if (attribute is not null && attributeWithParams is not null) throw new InvalidOperationException("Method cannot have both RpcMethodAttribute and RpcMethodWithParamsAttribute");
+ return func.DynamicInvoke(arguments);
+ }
- if (attribute is not null)
+ for (var i = 0; i < parameterInfos.Length; i++)
+ {
+ var param = parameterInfos[i];
+ if (arguments.Count > i && arguments[i] != null)
{
- var name = string.IsNullOrEmpty(attribute.Name) ? method.Name.ToLowerInvariant() : attribute.Name;
- methods[name] = method.CreateDelegate>(handler);
+ try
+ {
+ args[i] = ParameterConverter.AsParameter(arguments[i], param.ParameterType);
+ }
+ catch (Exception e) when (e is not RpcException)
+ {
+ throw new ArgumentException($"Invalid value for parameter '{param.Name}'", e);
+ }
}
-
- if (attributeWithParams is not null)
+ else
{
- var name = string.IsNullOrEmpty(attributeWithParams.Name) ? method.Name.ToLowerInvariant() : attributeWithParams.Name;
+ if (param.IsOptional)
+ {
+ args[i] = param.DefaultValue;
+ }
+ else if (param.ParameterType.IsValueType && Nullable.GetUnderlyingType(param.ParameterType) == null)
+ {
+ throw new ArgumentException($"Required parameter '{param.Name}' is missing");
+ }
+ else
+ {
+ args[i] = null;
+ }
+ }
+ }
- var parameters = method.GetParameters().Select(p => p.ParameterType).ToArray();
- var delegateType = Expression.GetDelegateType(parameters.Concat([method.ReturnType]).ToArray());
+ return func.DynamicInvoke(args);
+ }
- _methodsWithParams[name] = Delegate.CreateDelegate(delegateType, handler, method);
- }
+ public void RegisterMethods(object handler)
+ {
+ var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
+ foreach (var method in handler.GetType().GetMethods(flags))
+ {
+ var rpcMethod = method.GetCustomAttribute();
+ if (rpcMethod is null) continue;
+
+ var name = string.IsNullOrEmpty(rpcMethod.Name) ? method.Name.ToLowerInvariant() : rpcMethod.Name;
+ var parameters = method.GetParameters().Select(p => p.ParameterType).ToArray();
+ var delegateType = Expression.GetDelegateType(parameters.Concat([method.ReturnType]).ToArray());
+
+ _methods[name] = Delegate.CreateDelegate(delegateType, handler, method);
}
}
}
diff --git a/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.cs b/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.cs
index 97479adf4b..44eadb4bee 100644
--- a/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.cs
+++ b/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.cs
@@ -187,7 +187,7 @@ private async Task SimulatePostRequest(string requestBody)
[TestMethod]
public async Task TestProcessRequest_MalformedJsonPostBody()
{
- var malformedJson = "{\"jsonrpc\": \"2.0\", \"method\": \"getblockcount\", \"params\": [], \"id\": 1"; // Missing closing brace
+ var malformedJson = """{"jsonrpc": "2.0", "method": "getblockcount", "params": [], "id": 1"""; // Missing closing brace
var response = await SimulatePostRequest(malformedJson);
Assert.IsNotNull(response["error"]);
@@ -207,12 +207,14 @@ public async Task TestProcessRequest_EmptyBatch()
[TestMethod]
public async Task TestProcessRequest_MixedBatch()
{
- var mixedBatchJson = "[" +
- "{\"jsonrpc\": \"2.0\", \"method\": \"getblockcount\", \"params\": [], \"id\": 1}," + // Valid
- "{\"jsonrpc\": \"2.0\", \"method\": \"nonexistentmethod\", \"params\": [], \"id\": 2}," + // Invalid method
- "{\"jsonrpc\": \"2.0\", \"method\": \"getblock\", \"params\": [\"invalid_index\"], \"id\": 3}," + // Invalid params
- "{\"jsonrpc\": \"2.0\", \"method\": \"getversion\", \"id\": 4}" + // Valid (no params needed)
- "]";
+ var mixedBatchJson = """
+ [
+ {"jsonrpc": "2.0", "method": "getblockcount", "params": [], "id": 1},
+ {"jsonrpc": "2.0", "method": "nonexistentmethod", "params": [], "id": 2},
+ {"jsonrpc": "2.0", "method": "getblock", "params": ["invalid_index"], "id": 3},
+ {"jsonrpc": "2.0", "method": "getversion", "id": 4}
+ ]
+ """;
var response = await SimulatePostRequest(mixedBatchJson);
Assert.IsInstanceOfType(response, typeof(JArray));
@@ -240,5 +242,43 @@ public async Task TestProcessRequest_MixedBatch()
Assert.IsNotNull(batchResults[3]["result"]);
Assert.AreEqual(4, batchResults[3]["id"].AsNumber());
}
+
+ private class MockRpcMethods
+ {
+ [RpcMethod]
+ internal JToken GetMockMethod() => "mock";
+ }
+
+ [TestMethod]
+ public async Task TestRegisterMethods()
+ {
+ _rpcServer.RegisterMethods(new MockRpcMethods());
+
+ // Request ProcessAsync with a valid request
+ var context = new DefaultHttpContext();
+ var body = """
+ {"jsonrpc": "2.0", "method": "getmockmethod", "params": [], "id": 1 }
+ """;
+ context.Request.Method = "POST";
+ context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body));
+ context.Request.ContentType = "application/json";
+
+ // Set up a writable response body
+ var responseBody = new MemoryStream();
+ context.Response.Body = responseBody;
+
+ await _rpcServer.ProcessAsync(context);
+ Assert.IsNotNull(context.Response.Body);
+
+ // Reset the stream position to read from the beginning
+ responseBody.Position = 0;
+ var output = new StreamReader(responseBody).ReadToEnd();
+
+ // Parse the JSON response and check the result
+ var responseJson = JToken.Parse(output);
+ Assert.IsNotNull(responseJson["result"]);
+ Assert.AreEqual("mock", responseJson["result"].AsString());
+ Assert.AreEqual(200, context.Response.StatusCode);
+ }
}
}