Better handle exceptions for JSON-RPC
diff --git a/dependency-versions.gradle b/dependency-versions.gradle
index 1b45efd..b003a43 100644
--- a/dependency-versions.gradle
+++ b/dependency-versions.gradle
@@ -39,6 +39,7 @@
 
     dependency('io.lettuce:lettuce-core:5.1.3.RELEASE')
     dependency('io.vertx:vertx-core:3.9.4')
+    dependency('io.vertx:vertx-web-client:3.9.4')
     dependency('io.vertx:vertx-lang-kotlin:3.9.4')
     dependency('io.vertx:vertx-lang-kotlin-coroutines:3.9.4')
     dependency('io.vertx:vertx-web:3.9.4')
@@ -97,6 +98,9 @@
     dependency('org.rocksdb:rocksdbjni:5.17.2')
     dependency('org.slf4j:slf4j-api:1.7.30')
 
+    dependency('org.webjars:bootstrap:4.1.3')
+    dependency('org.webjars:webjars-locator:0.40')
+
     dependency('org.xerial.snappy:snappy-java:1.1.7.2')
   }
 }
diff --git a/jsonrpc/build.gradle b/jsonrpc/build.gradle
index 4f155dc..b7d7168 100644
--- a/jsonrpc/build.gradle
+++ b/jsonrpc/build.gradle
@@ -18,6 +18,7 @@
   implementation "com.google.guava:guava"
   implementation "org.jetbrains.kotlin:kotlin-stdlib"
   implementation 'io.vertx:vertx-core'
+  implementation 'io.vertx:vertx-web-client'
   implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core'
   implementation 'io.vertx:vertx-lang-kotlin-coroutines'
   implementation 'io.vertx:vertx-lang-kotlin'
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 cf351bf..5d146a1 100644
--- a/jsonrpc/src/main/kotlin/org/apache/tuweni/jsonrpc/JSONRPCClient.kt
+++ b/jsonrpc/src/main/kotlin/org/apache/tuweni/jsonrpc/JSONRPCClient.kt
@@ -19,30 +19,32 @@
 import com.fasterxml.jackson.databind.ObjectMapper
 import io.vertx.core.Vertx
 import io.vertx.core.buffer.Buffer
-import io.vertx.core.http.HttpMethod
-import io.vertx.core.json.JsonObject
-import io.vertx.kotlin.core.http.endAwait
+import io.vertx.ext.web.client.WebClient
 import kotlinx.coroutines.CompletableDeferred
 import org.apache.tuweni.eth.Address
 import org.apache.tuweni.eth.Transaction
 import org.apache.tuweni.units.bigints.UInt256
-import org.slf4j.LoggerFactory
 import java.io.Closeable
 
-val logger = LoggerFactory.getLogger(JSONRPCClient::class.java)
 val mapper = ObjectMapper()
+
 /**
  * JSON-RPC client to send requests to an Ethereum client.
  */
-class JSONRPCClient(vertx: Vertx, val serverPort: Int, val serverHost: String) : Closeable {
+class JSONRPCClient(
+  vertx: Vertx,
+  val serverPort: Int,
+  val serverHost: String
+) : Closeable {
 
-  val client = vertx.createHttpClient()
+  val client = WebClient.create(vertx)
 
   /**
    * 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 is the request is rejected
+   * @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(
@@ -53,23 +55,22 @@
     )
     val deferred = CompletableDeferred<String>()
 
-    @Suppress("DEPRECATION")
-    client.request(HttpMethod.POST, serverPort, serverHost, "/") { response ->
-      response.bodyHandler {
-        val jsonResponse = it.toJson() as JsonObject
-        if (jsonResponse.containsKey("error")) {
-          val err = jsonResponse.getJsonObject("error")
-          val errorMessage = "Code ${err.getInteger("code")}: ${err.getString("message")}"
-          deferred.completeExceptionally(ClientRequestException(errorMessage))
+    client.post(serverPort, serverHost, "/")
+      .putHeader("Content-Type", "application/json")
+      .sendBuffer(Buffer.buffer(mapper.writeValueAsBytes(body))) { response ->
+        if (response.failed()) {
+          deferred.completeExceptionally(response.cause())
         } else {
-          deferred.complete(jsonResponse.getString("result"))
+          val jsonResponse = response.result().bodyAsJsonObject()
+          if (jsonResponse.containsKey("error")) {
+            val err = jsonResponse.getJsonObject("error")
+            val errorMessage = "Code ${err.getInteger("code")}: ${err.getString("message")}"
+            deferred.completeExceptionally(ClientRequestException(errorMessage))
+          } else {
+            deferred.complete(jsonResponse.getString("result"))
+          }
         }
-      }.exceptionHandler {
-        deferred.completeExceptionally(it)
       }
-    }.putHeader("Content-Type", "application/json")
-      .exceptionHandler { deferred.completeExceptionally(it) }
-      .endAwait(Buffer.buffer(mapper.writeValueAsBytes(body)))
 
     return deferred.await()
   }
@@ -78,7 +79,8 @@
    * Gets the account balance.
    * @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 is the request is rejected
+   * @throws ClientRequestException if the request is rejected
+   * @throws ConnectException if it cannot dial the remote client
    */
   suspend fun getBalance_latest(address: Address): UInt256 {
     val body = mapOf(
@@ -89,17 +91,16 @@
     )
     val deferred = CompletableDeferred<UInt256>()
 
-    @Suppress("DEPRECATION")
-    client.request(HttpMethod.POST, serverPort, serverHost, "/") { response ->
-      response.bodyHandler {
-        val jsonResponse = it.toJson() as JsonObject
-        deferred.complete(UInt256.fromHexString(jsonResponse.getString("result")))
-      }.exceptionHandler {
-        deferred.completeExceptionally(it)
+    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")))
+        }
       }
-    }.putHeader("Content-Type", "application/json")
-      .exceptionHandler { deferred.completeExceptionally(it) }
-      .endAwait(Buffer.buffer(mapper.writeValueAsBytes(body)))
 
     return deferred.await()
   }
@@ -108,7 +109,8 @@
    * Gets the number of transactions sent from an address.
    * @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 is the request is rejected
+   * @throws ClientRequestException if the request is rejected
+   * @throws ConnectException if it cannot dial the remote client
    */
   suspend fun getTransactionCount_latest(address: Address): UInt256 {
     val body = mapOf(
@@ -119,17 +121,16 @@
     )
     val deferred = CompletableDeferred<UInt256>()
 
-    @Suppress("DEPRECATION")
-    client.request(HttpMethod.POST, serverPort, serverHost, "/") { response ->
-      response.bodyHandler {
-        val jsonResponse = it.toJson() as JsonObject
-        deferred.complete(UInt256.fromHexString(jsonResponse.getString("result")))
-      }.exceptionHandler {
-        deferred.completeExceptionally(it)
+    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")))
+        }
       }
-    }.putHeader("Content-Type", "application/json")
-      .exceptionHandler { deferred.completeExceptionally(it) }
-      .endAwait(Buffer.buffer(mapper.writeValueAsBytes(body)))
 
     return deferred.await()
   }
diff --git a/jsonrpc/src/test/kotlin/org/apache/tuweni/jsonrpc/JSONRPCClientTest.kt b/jsonrpc/src/test/kotlin/org/apache/tuweni/jsonrpc/JSONRPCClientTest.kt
index 30ac56d..335ddb5 100644
--- a/jsonrpc/src/test/kotlin/org/apache/tuweni/jsonrpc/JSONRPCClientTest.kt
+++ b/jsonrpc/src/test/kotlin/org/apache/tuweni/jsonrpc/JSONRPCClientTest.kt
@@ -36,7 +36,9 @@
 import org.junit.jupiter.api.Assertions.assertEquals
 import org.junit.jupiter.api.BeforeAll
 import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
 import org.junit.jupiter.api.extension.ExtendWith
+import java.net.ConnectException
 import java.nio.charset.StandardCharsets
 import java.util.concurrent.atomic.AtomicReference
 
@@ -97,4 +99,13 @@
       )
     }
   }
+
+  @Test
+  fun testGetBalanceToMissingClient(@VertxInstance vertx: Vertx) {
+    JSONRPCClient(vertx, 1234, "localhost").use {
+      assertThrows<ConnectException> {
+        runBlocking { it.getBalance_latest(Address.fromHexString("0x0102030405060708090a0b0c0d0e0f0102030405")) }
+      }
+    }
+  }
 }