You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tuweni.apache.org by to...@apache.org on 2021/07/29 14:23:23 UTC

[incubator-tuweni] branch main updated: Add JSON-RPC client to talk to endpoint

This is an automated email from the ASF dual-hosted git repository.

toulmean pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-tuweni.git


The following commit(s) were added to refs/heads/main by this push:
     new c0aea6f  Add JSON-RPC client to talk to endpoint
     new 4ce263a  Merge pull request #323 from atoulme/add_client
c0aea6f is described below

commit c0aea6f5860737f4f41d3ed486c0d8ae47f2ff03
Author: Antoine Toulme <an...@lunar-ocean.com>
AuthorDate: Wed Jul 28 23:45:45 2021 -0700

    Add JSON-RPC client to talk to endpoint
---
 .../kotlin/org/apache/tuweni/eth/JSONRPCRequest.kt |  3 +-
 .../org/apache/tuweni/eth/JSONRPCResponse.kt       | 14 ++--
 .../org/apache/tuweni/jsonrpc/app/JSONRPCApp.kt    | 17 ++--
 .../org/apache/tuweni/jsonrpc/app/JSONRPCConfig.kt |  5 ++
 .../org/apache/tuweni/jsonrpc/JSONRPCClient.kt     | 92 +++++++++++-----------
 .../tuweni/jsonrpc/methods/MethodsHandler.kt       |  3 +-
 .../tuweni/jsonrpc/methods/MethodsHandlerTest.kt   |  9 ++-
 7 files changed, 78 insertions(+), 65 deletions(-)

diff --git a/eth/src/main/kotlin/org/apache/tuweni/eth/JSONRPCRequest.kt b/eth/src/main/kotlin/org/apache/tuweni/eth/JSONRPCRequest.kt
index 7b73959..5305818 100644
--- a/eth/src/main/kotlin/org/apache/tuweni/eth/JSONRPCRequest.kt
+++ b/eth/src/main/kotlin/org/apache/tuweni/eth/JSONRPCRequest.kt
@@ -23,5 +23,6 @@ import com.fasterxml.jackson.annotation.JsonProperty
 data class JSONRPCRequest(
   @JsonProperty("id") val id: Int,
   @JsonProperty("method") val method: String,
-  @JsonProperty("params") val params: Array<String>
+  @JsonProperty("params") val params: Array<String>,
+  @JsonProperty("jsonrpc") val jsonrpc: String = "2.0"
 )
diff --git a/eth/src/main/kotlin/org/apache/tuweni/eth/JSONRPCResponse.kt b/eth/src/main/kotlin/org/apache/tuweni/eth/JSONRPCResponse.kt
index e97f108..d5d0362 100644
--- a/eth/src/main/kotlin/org/apache/tuweni/eth/JSONRPCResponse.kt
+++ b/eth/src/main/kotlin/org/apache/tuweni/eth/JSONRPCResponse.kt
@@ -20,10 +20,12 @@ import com.fasterxml.jackson.annotation.JsonInclude
 import com.fasterxml.jackson.annotation.JsonProperty
 
 @JsonInclude(JsonInclude.Include.NON_NULL)
-data class JSONRPCResponse(@JsonProperty("id") val id: Int, @JsonProperty("result") val result: Any? = null, @JsonProperty("error") val error: Any? = null)
+data class JSONRPCResponse(@JsonProperty("id") val id: Int, @JsonProperty("result") val result: Any? = null, @JsonProperty("error") val error: JSONRPCError? = null)
 
-val parseError = JSONRPCResponse(id = 0, error = mapOf(Pair("code", -32700), Pair("message", "Parse error")))
-val invalidRequest = JSONRPCResponse(id = 0, error = mapOf(Pair("code", -32600), Pair("message", "Invalid Request")))
-val methodNotFound = JSONRPCResponse(id = 0, error = mapOf(Pair("code", -32601), Pair("message", "Method not found")))
-val invalidParams = JSONRPCResponse(id = 0, error = mapOf(Pair("code", -32602), Pair("message", "Invalid params")))
-val internalError = JSONRPCResponse(id = 0, error = mapOf(Pair("code", -32603), Pair("message", "Internal error")))
+data class JSONRPCError(@JsonProperty("code") val code: Int, @JsonProperty("message") val message: String)
+
+val parseError = JSONRPCResponse(id = 0, error = JSONRPCError(-32700, "Parse error"))
+val invalidRequest = JSONRPCResponse(id = 0, error = JSONRPCError(-32600, "Invalid Request"))
+val methodNotFound = JSONRPCResponse(id = 0, error = JSONRPCError(-32601, "Method not found"))
+val invalidParams = JSONRPCResponse(id = 0, error = JSONRPCError(-32602, "Invalid params"))
+val internalError = JSONRPCResponse(id = 0, error = JSONRPCError(-32603, "Internal error"))
diff --git a/jsonrpc-app/src/main/kotlin/org/apache/tuweni/jsonrpc/app/JSONRPCApp.kt b/jsonrpc-app/src/main/kotlin/org/apache/tuweni/jsonrpc/app/JSONRPCApp.kt
index b2ba1c7..fc4748c 100644
--- a/jsonrpc-app/src/main/kotlin/org/apache/tuweni/jsonrpc/app/JSONRPCApp.kt
+++ b/jsonrpc-app/src/main/kotlin/org/apache/tuweni/jsonrpc/app/JSONRPCApp.kt
@@ -21,8 +21,7 @@ import io.vertx.core.VertxOptions
 import io.vertx.tracing.opentelemetry.OpenTelemetryOptions
 import kotlinx.coroutines.asCoroutineDispatcher
 import kotlinx.coroutines.runBlocking
-import org.apache.tuweni.eth.JSONRPCRequest
-import org.apache.tuweni.eth.JSONRPCResponse
+import org.apache.tuweni.jsonrpc.JSONRPCClient
 import org.apache.tuweni.jsonrpc.JSONRPCServer
 import org.apache.tuweni.jsonrpc.methods.MeteredHandler
 import org.apache.tuweni.jsonrpc.methods.MethodAllowListHandler
@@ -74,14 +73,20 @@ class JSONRPCApplication(
 ) {
 
   fun run() {
+    val client = JSONRPCClient(vertx, config.endpointPort(), config.endpointHost())
     // TODO allow more options such as allowlist of certificates, enforce client authentication.
     val trustOptions = VertxTrustOptions.recordClientFingerprints(config.clientFingerprintsFile())
 
-    val allowListHandler = MethodAllowListHandler(config.allowedMethods(), this::handleRequest)
+    val allowListHandler = MethodAllowListHandler(config.allowedMethods()) { req ->
+      runBlocking {
+        client.sendRequest(req).await()
+      }
+    }
 
     val meter = metricsService.meterSdkProvider.get("jsonrpc")
     val successCounter = meter.longCounterBuilder("success").build()
     val failureCounter = meter.longCounterBuilder("failure").build()
+
     val handler = MeteredHandler(successCounter, failureCounter, allowListHandler::handleRequest)
     val server = JSONRPCServer(
       vertx, config.port(), config.networkInterface(),
@@ -105,6 +110,7 @@ class JSONRPCApplication(
       Thread {
         runBlocking {
           server.stop().await()
+          client.close()
         }
       }
     )
@@ -113,9 +119,4 @@ class JSONRPCApplication(
       logger.info("JSON-RPC server started")
     }
   }
-
-  private fun handleRequest(request: JSONRPCRequest): JSONRPCResponse {
-    logger.info("Received request {}", request)
-    return JSONRPCResponse(1) // TODO implement
-  }
 }
diff --git a/jsonrpc-app/src/main/kotlin/org/apache/tuweni/jsonrpc/app/JSONRPCConfig.kt b/jsonrpc-app/src/main/kotlin/org/apache/tuweni/jsonrpc/app/JSONRPCConfig.kt
index 130efc8..4ba4122 100644
--- a/jsonrpc-app/src/main/kotlin/org/apache/tuweni/jsonrpc/app/JSONRPCConfig.kt
+++ b/jsonrpc-app/src/main/kotlin/org/apache/tuweni/jsonrpc/app/JSONRPCConfig.kt
@@ -47,6 +47,8 @@ class JSONRPCConfig(val filePath: Path) {
       .addListOfString("allowedMethods", Collections.emptyList(), "Allowed JSON-RPC methods", null)
       .addListOfString("allowedRanges", Collections.singletonList("0.0.0.0/0"), "Allowed IP ranges", null)
       .addListOfString("rejectedRanges", Collections.emptyList(), "Rejected IP ranges", null)
+      .addInteger("endpointPort", 8545, "JSON-RPC endpoint port", PropertyValidator.isValidPort())
+      .addString("endpointHost", "localhost", "JSON-RPC endpoint host", null)
       .toSchema()
   }
 
@@ -69,4 +71,7 @@ class JSONRPCConfig(val filePath: Path) {
   fun allowedMethods() = config.getListOfString("allowedMethods")
   fun allowedRanges() = config.getListOfString("allowedRanges")
   fun rejectedRanges() = config.getListOfString("rejectedRanges")
+
+  fun endpointPort() = config.getInteger("endpointPort")
+  fun endpointHost() = config.getString("endpointHost")
 }
diff --git a/jsonrpc/src/main/kotlin/org/apache/tuweni/jsonrpc/JSONRPCClient.kt b/jsonrpc/src/main/kotlin/org/apache/tuweni/jsonrpc/JSONRPCClient.kt
index 0440271..25ece27 100644
--- a/jsonrpc/src/main/kotlin/org/apache/tuweni/jsonrpc/JSONRPCClient.kt
+++ b/jsonrpc/src/main/kotlin/org/apache/tuweni/jsonrpc/JSONRPCClient.kt
@@ -23,10 +23,14 @@ import io.vertx.core.tracing.TracingPolicy
 import io.vertx.ext.web.client.WebClient
 import io.vertx.ext.web.client.WebClientOptions
 import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Deferred
 import org.apache.tuweni.eth.Address
+import org.apache.tuweni.eth.JSONRPCRequest
+import org.apache.tuweni.eth.JSONRPCResponse
 import org.apache.tuweni.eth.Transaction
 import org.apache.tuweni.units.bigints.UInt256
 import java.io.Closeable
+import java.util.concurrent.atomic.AtomicInteger
 
 val mapper = ObjectMapper()
 
@@ -40,42 +44,43 @@ class JSONRPCClient(
   val userAgent: String = "Apache Tuweni JSON-RPC Client",
 ) : Closeable {
 
+  val requestCounter = AtomicInteger(1)
   val client = WebClient.create(vertx, WebClientOptions().setUserAgent(userAgent).setTryUseCompression(true).setTracingPolicy(TracingPolicy.ALWAYS) as WebClientOptions)
 
-  /**
-   * Sends a signed transaction to the Ethereum network.
-   * @param tx the transaction object to send
-   * @return the hash of the transaction, or an empty string if the hash is not available yet.
-   * @throws ClientRequestException if the request is rejected
-   * @throws ConnectException if it cannot dial the remote client
-   */
-  suspend fun sendRawTransaction(tx: Transaction): String {
-    val body = mapOf(
-      Pair("jsonrpc", "2.0"),
-      Pair("method", "eth_sendRawTransaction"),
-      Pair("id", 1),
-      Pair("params", listOf(tx.toBytes().toHexString()))
-    )
-    val deferred = CompletableDeferred<String>()
+  suspend fun sendRequest(request: JSONRPCRequest): Deferred<JSONRPCResponse> {
+    val deferred = CompletableDeferred<JSONRPCResponse>()
 
     client.post(serverPort, serverHost, "/")
       .putHeader("Content-Type", "application/json")
-      .sendBuffer(Buffer.buffer(mapper.writeValueAsBytes(body))) { response ->
+      .sendBuffer(Buffer.buffer(mapper.writeValueAsBytes(request))) { response ->
         if (response.failed()) {
           deferred.completeExceptionally(response.cause())
         } else {
-          val jsonResponse = response.result().bodyAsJsonObject()
-          val err = jsonResponse.getJsonObject("error")
-          if (err != null) {
-            val errorMessage = "Code ${err.getInteger("code")}: ${err.getString("message")}"
-            deferred.completeExceptionally(ClientRequestException(errorMessage))
-          } else {
-            deferred.complete(jsonResponse.getString("result"))
-          }
+          val jsonResponse = response.result().bodyAsJson(JSONRPCResponse::class.java)
+          deferred.complete(jsonResponse)
         }
       }
 
-    return deferred.await()
+    return deferred
+  }
+
+  /**
+   * Sends a signed transaction to the Ethereum network.
+   * @param tx the transaction object to send
+   * @return the hash of the transaction, or an empty string if the hash is not available yet.
+   * @throws ClientRequestException if the request is rejected
+   * @throws ConnectException if it cannot dial the remote client
+   */
+  suspend fun sendRawTransaction(tx: Transaction): String {
+    val body = JSONRPCRequest(nextId(), "eth_sendRawTransaction", arrayOf(tx.toBytes().toHexString()))
+    val jsonResponse = sendRequest(body).await()
+    val err = jsonResponse.error
+    if (err != null) {
+      val errorMessage = "Code ${err.code}: ${err.message}"
+      throw ClientRequestException(errorMessage)
+    } else {
+      return jsonResponse.result.toString()
+    }
   }
 
   /**
@@ -116,29 +121,26 @@ class JSONRPCClient(
    * @throws ConnectException if it cannot dial the remote client
    */
   suspend fun getTransactionCount_latest(address: Address): UInt256 {
-    val body = mapOf(
-      Pair("jsonrpc", "2.0"),
-      Pair("method", "eth_getTransactionCount"),
-      Pair("id", 1),
-      Pair("params", listOf(address.toHexString(), "latest"))
-    )
-    val deferred = CompletableDeferred<UInt256>()
-
-    client.post(serverPort, serverHost, "/")
-      .putHeader("Content-Type", "application/json")
-      .sendBuffer(Buffer.buffer(mapper.writeValueAsBytes(body))) { response ->
-        if (response.failed()) {
-          deferred.completeExceptionally(response.cause())
-        } else {
-          val jsonResponse = response.result().bodyAsJsonObject()
-          deferred.complete(UInt256.fromHexString(jsonResponse.getString("result")))
-        }
-      }
-
-    return deferred.await()
+    val body = JSONRPCRequest(nextId(), "eth_getTransactionCount", arrayOf(address.toHexString(), "latest"))
+    val jsonResponse = sendRequest(body).await()
+    val err = jsonResponse.error
+    if (err != null) {
+      val errorMessage = "Code ${err.code}: ${err.message}"
+      throw ClientRequestException(errorMessage)
+    } else {
+      return UInt256.fromHexString(jsonResponse.result.toString())
+    }
   }
 
   override fun close() {
     client.close()
   }
+
+  private fun nextId(): Int {
+    val next = requestCounter.incrementAndGet()
+    if (next == Int.MAX_VALUE) {
+      requestCounter.set(1)
+    }
+    return next
+  }
 }
diff --git a/jsonrpc/src/main/kotlin/org/apache/tuweni/jsonrpc/methods/MethodsHandler.kt b/jsonrpc/src/main/kotlin/org/apache/tuweni/jsonrpc/methods/MethodsHandler.kt
index ea50ed5..9dc92a0 100644
--- a/jsonrpc/src/main/kotlin/org/apache/tuweni/jsonrpc/methods/MethodsHandler.kt
+++ b/jsonrpc/src/main/kotlin/org/apache/tuweni/jsonrpc/methods/MethodsHandler.kt
@@ -18,6 +18,7 @@ package org.apache.tuweni.jsonrpc.methods
 
 import io.opentelemetry.api.metrics.LongCounter
 import io.opentelemetry.api.metrics.common.Labels
+import org.apache.tuweni.eth.JSONRPCError
 import org.apache.tuweni.eth.JSONRPCRequest
 import org.apache.tuweni.eth.JSONRPCResponse
 import org.apache.tuweni.eth.methodNotFound
@@ -59,7 +60,7 @@ class MethodAllowListHandler(private val allowedMethods: List<String>, private v
       }
     }
     if (!found) {
-      return JSONRPCResponse(request.id, null, mapOf(Pair("code", -32604), Pair("message", "Method not enabled")))
+      return JSONRPCResponse(request.id, null, JSONRPCError(-32604, "Method not enabled"))
     }
     return delegateHandler(request)
   }
diff --git a/jsonrpc/src/test/kotlin/org/apache/tuweni/jsonrpc/methods/MethodsHandlerTest.kt b/jsonrpc/src/test/kotlin/org/apache/tuweni/jsonrpc/methods/MethodsHandlerTest.kt
index fe0a2a2..c9778f8 100644
--- a/jsonrpc/src/test/kotlin/org/apache/tuweni/jsonrpc/methods/MethodsHandlerTest.kt
+++ b/jsonrpc/src/test/kotlin/org/apache/tuweni/jsonrpc/methods/MethodsHandlerTest.kt
@@ -20,6 +20,7 @@ import io.opentelemetry.sdk.metrics.SdkMeterProvider
 import io.opentelemetry.sdk.metrics.export.IntervalMetricReader
 import io.opentelemetry.sdk.metrics.export.MetricProducer
 import io.opentelemetry.sdk.metrics.testing.InMemoryMetricExporter
+import org.apache.tuweni.eth.JSONRPCError
 import org.apache.tuweni.eth.JSONRPCRequest
 import org.apache.tuweni.eth.JSONRPCResponse
 import org.apache.tuweni.eth.methodNotFound
@@ -89,7 +90,7 @@ class MethodsHandlerTest {
     val successCounter = meter.longCounterBuilder("success").build()
     val failCounter = meter.longCounterBuilder("fail").build()
     val meteredHandler = MeteredHandler(successCounter, failCounter) {
-      JSONRPCResponse(1, error = "foo")
+      JSONRPCResponse(1, error = JSONRPCError(123, "foo"))
     }
     meteredHandler.handleRequest(JSONRPCRequest(1, "foo", emptyArray()))
     Thread.sleep(1200)
@@ -117,8 +118,8 @@ class MethodAllowListHandlerTest {
     val filter = MethodAllowListHandler(listOf("eth_")) { JSONRPCResponse(1, "foo") }
     val resp = filter.handleRequest(JSONRPCRequest(1, "foo_client", emptyArray()))
     assertNotNull(resp.error)
-    val respContents = resp.error as Map<*, *>
-    assertEquals(-32604, respContents["code"])
-    assertEquals("Method not enabled", respContents["message"])
+    val respContents = resp.error as JSONRPCError
+    assertEquals(-32604, respContents.code)
+    assertEquals("Method not enabled", respContents.message)
   }
 }

---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tuweni.apache.org
For additional commands, e-mail: commits-help@tuweni.apache.org