diff --git a/exonum-light-client/pom.xml b/exonum-light-client/pom.xml
index 33aeb1469a..6f99881a78 100644
--- a/exonum-light-client/pom.xml
+++ b/exonum-light-client/pom.xml
@@ -76,7 +76,7 @@
5.4.0
2.24.0
2.0.0.0
-
+ 2.4.0
warning
@@ -121,6 +121,12 @@
test
+
+ org.junit.jupiter
+ junit-jupiter-params
+ test
+
+
org.mockito
mockito-junit-jupiter
@@ -142,6 +148,13 @@
test
+
+ com.jayway.jsonpath
+ json-path-assert
+ ${jsonpath.version}
+ test
+
+
diff --git a/exonum-light-client/src/main/java/com/exonum/client/ExonumClient.java b/exonum-light-client/src/main/java/com/exonum/client/ExonumClient.java
index d3d554ecc2..e1fbc33f39 100644
--- a/exonum-light-client/src/main/java/com/exonum/client/ExonumClient.java
+++ b/exonum-light-client/src/main/java/com/exonum/client/ExonumClient.java
@@ -21,6 +21,7 @@
import com.exonum.binding.common.hash.HashCode;
import com.exonum.binding.common.message.TransactionMessage;
+import com.exonum.client.response.HealthCheckInfo;
import java.net.MalformedURLException;
import java.net.URL;
import okhttp3.OkHttpClient;
@@ -28,6 +29,9 @@
/**
* Main interface for Exonum Light client.
* Provides a convenient way for interaction with Exonum framework APIs.
+ * All the methods of the interface work in a blocking way
+ * i.e. invoke underlying request immediately, and block until the response can be processed
+ * or an error occurs.
* Implementations of that interface are required to be thread-safe.
**/
public interface ExonumClient {
@@ -40,6 +44,28 @@ public interface ExonumClient {
*/
HashCode submitTransaction(TransactionMessage tx);
+ /**
+ * Returns a number of unconfirmed transactions which are currently located in
+ * the unconfirmed transactions pool and are waiting for acceptance to a block.
+ * @throws RuntimeException if the client is unable to complete a request
+ * (e.g., in case of connectivity problems)
+ */
+ int getUnconfirmedTransactionsCount();
+
+ /**
+ * Returns the node health check information.
+ * @throws RuntimeException if the client is unable to complete a request
+ * (e.g., in case of connectivity problems)
+ */
+ HealthCheckInfo healthCheck();
+
+ /**
+ * Returns string containing information about Exonum, Rust and OS version.
+ * @throws RuntimeException if the client is unable to complete a request
+ * (e.g., in case of connectivity problems)
+ */
+ String getUserAgentInfo();
+
/**
* Returns Exonum client builder.
*/
diff --git a/exonum-light-client/src/main/java/com/exonum/client/ExonumHttpClient.java b/exonum-light-client/src/main/java/com/exonum/client/ExonumHttpClient.java
index dfc080bae4..9d96995c43 100644
--- a/exonum-light-client/src/main/java/com/exonum/client/ExonumHttpClient.java
+++ b/exonum-light-client/src/main/java/com/exonum/client/ExonumHttpClient.java
@@ -17,16 +17,20 @@
package com.exonum.client;
-import static com.exonum.binding.common.serialization.json.JsonSerializer.json;
+import static com.exonum.client.ExonumUrls.HEALTH_CHECK;
+import static com.exonum.client.ExonumUrls.MEMORY_POOL;
import static com.exonum.client.ExonumUrls.SUBMIT_TRANSACTION;
+import static com.exonum.client.ExonumUrls.USER_AGENT;
import com.exonum.binding.common.hash.HashCode;
import com.exonum.binding.common.message.TransactionMessage;
+import com.exonum.client.response.HealthCheckInfo;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.io.BaseEncoding;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
+import java.util.function.Function;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
@@ -53,20 +57,45 @@ class ExonumHttpClient implements ExonumClient {
@Override
public HashCode submitTransaction(TransactionMessage transactionMessage) {
String msg = toHex(transactionMessage.toBytes());
+ Request request = postRequest(toFullUrl(SUBMIT_TRANSACTION),
+ ExplorerApiHelper.createSubmitTxBody(msg));
- Request request = createRequest(toFullUrl(SUBMIT_TRANSACTION), new SubmitTxRequest(msg));
- SubmitTxResponse result = blockingExecuteWithResponse(request, SubmitTxResponse.class);
+ return blockingExecuteAndParse(request, ExplorerApiHelper::parseSubmitTxResponse);
+ }
+
+ @Override
+ public int getUnconfirmedTransactionsCount() {
+ Request request = getRequest(toFullUrl(MEMORY_POOL));
+
+ return blockingExecuteAndParse(request, SystemApiHelper::parseMemoryPoolJson);
+ }
+
+ @Override
+ public HealthCheckInfo healthCheck() {
+ Request request = getRequest(toFullUrl(HEALTH_CHECK));
+
+ return blockingExecuteAndParse(request, SystemApiHelper::parseHealthCheckJson);
+ }
- return result.getHash();
+ @Override
+ public String getUserAgentInfo() {
+ Request request = getRequest(toFullUrl(USER_AGENT));
+
+ return blockingExecutePlainText(request);
}
private static String toHex(byte[] array) {
return HEX_ENCODER.encode(array);
}
- private static Request createRequest(URL url, Object requestBody) {
- String jsonBody = json().toJson(requestBody);
+ private static Request getRequest(URL url) {
+ return new Request.Builder()
+ .url(url)
+ .get()
+ .build();
+ }
+ private static Request postRequest(URL url, String jsonBody) {
return new Request.Builder()
.url(url)
.post(RequestBody.create(MEDIA_TYPE_JSON, jsonBody))
@@ -81,17 +110,20 @@ private URL toFullUrl(String relativeUrl) {
}
}
- private T blockingExecuteWithResponse(Request request, Class responseObject) {
+ private String blockingExecutePlainText(Request request) {
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new RuntimeException("Execution wasn't success: " + response.toString());
}
- String responseJson = response.body().string();
-
- return json().fromJson(responseJson, responseObject);
+ return response.body().string();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
+ private T blockingExecuteAndParse(Request request, Function parser) {
+ String response = blockingExecutePlainText(request);
+ return parser.apply(response);
+ }
+
}
diff --git a/exonum-light-client/src/main/java/com/exonum/client/ExonumUrls.java b/exonum-light-client/src/main/java/com/exonum/client/ExonumUrls.java
index 069994c17d..68caf101e9 100644
--- a/exonum-light-client/src/main/java/com/exonum/client/ExonumUrls.java
+++ b/exonum-light-client/src/main/java/com/exonum/client/ExonumUrls.java
@@ -21,5 +21,11 @@
* Contains Exonum API URLs.
*/
final class ExonumUrls {
- static final String SUBMIT_TRANSACTION = "/api/explorer/v1/transactions";
+ private static final String EXPLORER_PATHS_PREFIX = "/api/explorer/v1";
+ private static final String SYS_PATHS_PREFIX = "/api/system/v1";
+ static final String SUBMIT_TRANSACTION = EXPLORER_PATHS_PREFIX + "/transactions";
+ static final String MEMORY_POOL = SYS_PATHS_PREFIX + "/mempool";
+ static final String HEALTH_CHECK = SYS_PATHS_PREFIX + "/healthcheck";
+ static final String USER_AGENT = SYS_PATHS_PREFIX + "/user_agent";
+
}
diff --git a/exonum-light-client/src/main/java/com/exonum/client/ExplorerApiHelper.java b/exonum-light-client/src/main/java/com/exonum/client/ExplorerApiHelper.java
new file mode 100644
index 0000000000..128dba7ccd
--- /dev/null
+++ b/exonum-light-client/src/main/java/com/exonum/client/ExplorerApiHelper.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2019 The Exonum Team
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.exonum.client;
+
+import static com.exonum.binding.common.serialization.json.JsonSerializer.json;
+
+import com.exonum.binding.common.hash.HashCode;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.gson.annotations.SerializedName;
+import lombok.Value;
+
+/**
+ * Utility class for Exonum Explorer API.
+ */
+final class ExplorerApiHelper {
+
+ static String createSubmitTxBody(String message) {
+ SubmitTxRequest request = new SubmitTxRequest(message);
+ return json().toJson(request);
+ }
+
+ static HashCode parseSubmitTxResponse(String json) {
+ SubmitTxResponse response = json().fromJson(json, SubmitTxResponse.class);
+ return response.getHash();
+ }
+
+ /**
+ * Json object wrapper for submit transaction request.
+ */
+ @Value
+ @VisibleForTesting
+ static class SubmitTxRequest {
+ @SerializedName("tx_body")
+ String body;
+ }
+
+ /**
+ * Json object wrapper for submit transaction response.
+ */
+ @Value
+ private static class SubmitTxResponse {
+ @SerializedName("tx_hash")
+ HashCode hash;
+ }
+
+ private ExplorerApiHelper() {
+ throw new UnsupportedOperationException("Not instantiable");
+ }
+}
diff --git a/exonum-light-client/src/main/java/com/exonum/client/SystemApiHelper.java b/exonum-light-client/src/main/java/com/exonum/client/SystemApiHelper.java
new file mode 100644
index 0000000000..15af670b69
--- /dev/null
+++ b/exonum-light-client/src/main/java/com/exonum/client/SystemApiHelper.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2019 The Exonum Team
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.exonum.client;
+
+import static com.exonum.binding.common.serialization.json.JsonSerializer.json;
+
+import com.exonum.client.response.ConsensusStatus;
+import com.exonum.client.response.HealthCheckInfo;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.annotations.SerializedName;
+import lombok.Value;
+
+/**
+ * Utility class for Exonum System API.
+ */
+final class SystemApiHelper {
+
+ static HealthCheckInfo parseHealthCheckJson(String json) {
+ // TODO: ECR-2925 (core dependency) change after the response format update
+ HealthCheckResponse response = json().fromJson(json, HealthCheckResponse.class);
+ String consensusStatus = response.getConsensusStatus().toUpperCase();
+ JsonElement connectivity = response.getConnectivity();
+ if (connectivity.isJsonObject()) {
+ JsonObject connectivityObject = connectivity.getAsJsonObject();
+ int connectionsNumber = connectivityObject
+ .get("Connected").getAsJsonObject()
+ .get("amount").getAsInt();
+ return new HealthCheckInfo(ConsensusStatus.valueOf(consensusStatus), connectionsNumber);
+ } else {
+ return new HealthCheckInfo(ConsensusStatus.valueOf(consensusStatus), 0);
+ }
+ }
+
+ static int parseMemoryPoolJson(String json) {
+ MemoryPoolResponse response = json().fromJson(json, MemoryPoolResponse.class);
+ return response.getSize();
+ }
+
+ /**
+ * Json object wrapper for memory pool response.
+ */
+ @Value
+ private static class MemoryPoolResponse {
+ int size;
+ }
+
+ /**
+ * Json object wrapper for health check response.
+ */
+ @Value
+ private class HealthCheckResponse {
+ @SerializedName("consensus_status")
+ String consensusStatus;
+ JsonElement connectivity;
+ }
+
+ private SystemApiHelper() {
+ throw new UnsupportedOperationException("Not instantiable");
+ }
+}
diff --git a/exonum-light-client/src/main/java/com/exonum/client/SubmitTxRequest.java b/exonum-light-client/src/main/java/com/exonum/client/response/ConsensusStatus.java
similarity index 61%
rename from exonum-light-client/src/main/java/com/exonum/client/SubmitTxRequest.java
rename to exonum-light-client/src/main/java/com/exonum/client/response/ConsensusStatus.java
index 9feadb2ed0..ba7fd3ac37 100644
--- a/exonum-light-client/src/main/java/com/exonum/client/SubmitTxRequest.java
+++ b/exonum-light-client/src/main/java/com/exonum/client/response/ConsensusStatus.java
@@ -15,16 +15,23 @@
*
*/
-package com.exonum.client;
-
-import com.google.gson.annotations.SerializedName;
-import lombok.Value;
+package com.exonum.client.response;
/**
- * Json object wrapper for submit transaction request.
+ * Consensus status of a particular node.
*/
-@Value
-class SubmitTxRequest {
- @SerializedName("tx_body")
- String body;
+public enum ConsensusStatus {
+ /**
+ * Shows that consensus is active
+ * i.e. it is enabled and the node has enough connected peers.
+ */
+ ACTIVE,
+ /**
+ * Shows that consensus is enabled on the node.
+ */
+ ENABLED,
+ /**
+ * Shows that consensus is disabled on the node.
+ */
+ DISABLED
}
diff --git a/exonum-light-client/src/main/java/com/exonum/client/SubmitTxResponse.java b/exonum-light-client/src/main/java/com/exonum/client/response/HealthCheckInfo.java
similarity index 59%
rename from exonum-light-client/src/main/java/com/exonum/client/SubmitTxResponse.java
rename to exonum-light-client/src/main/java/com/exonum/client/response/HealthCheckInfo.java
index f04341a70c..2b8f7284a0 100644
--- a/exonum-light-client/src/main/java/com/exonum/client/SubmitTxResponse.java
+++ b/exonum-light-client/src/main/java/com/exonum/client/response/HealthCheckInfo.java
@@ -15,17 +15,22 @@
*
*/
-package com.exonum.client;
+package com.exonum.client.response;
-import com.exonum.binding.common.hash.HashCode;
-import com.google.gson.annotations.SerializedName;
import lombok.Value;
-/**
- * Json object wrapper for submit transaction response.
- */
@Value
-class SubmitTxResponse {
- @SerializedName("tx_hash")
- HashCode hash;
+public class HealthCheckInfo {
+ /**
+ * Shows information about whether it is possible to achieve the consensus between
+ * validators in the current state.
+ */
+ ConsensusStatus consensusStatus;
+
+ /**
+ * The number of peers that the node is connected to;
+ * {@code = 0} if the node is not connected to the network,
+ * or it's the single node network.
+ */
+ int connectionsNumber;
}
diff --git a/exonum-light-client/src/test/java/com/exonum/client/ExonumHttpClientIntegrationTest.java b/exonum-light-client/src/test/java/com/exonum/client/ExonumHttpClientIntegrationTest.java
index b5477795dd..ee1c41d74a 100644
--- a/exonum-light-client/src/test/java/com/exonum/client/ExonumHttpClientIntegrationTest.java
+++ b/exonum-light-client/src/test/java/com/exonum/client/ExonumHttpClientIntegrationTest.java
@@ -20,13 +20,19 @@
import static com.exonum.binding.common.crypto.CryptoFunctions.ed25519;
import static com.exonum.binding.common.serialization.json.JsonSerializer.json;
import static com.exonum.client.ExonumHttpClient.HEX_ENCODER;
+import static com.exonum.client.ExonumUrls.HEALTH_CHECK;
+import static com.exonum.client.ExonumUrls.MEMORY_POOL;
import static com.exonum.client.ExonumUrls.SUBMIT_TRANSACTION;
+import static com.exonum.client.ExonumUrls.USER_AGENT;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import com.exonum.binding.common.crypto.KeyPair;
import com.exonum.binding.common.hash.HashCode;
import com.exonum.binding.common.message.TransactionMessage;
+import com.exonum.client.ExplorerApiHelper.SubmitTxRequest;
+import com.exonum.client.response.ConsensusStatus;
+import com.exonum.client.response.HealthCheckInfo;
import java.io.IOException;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
@@ -89,4 +95,59 @@ void submitTransactionTest() throws InterruptedException {
assertThat(actualTxMessage, is(txMessage));
}
+ @Test
+ void getUnconfirmedTransactions() throws InterruptedException {
+ // Mock response
+ int mockCount = 10;
+ String mockResponse = "{\"size\": " + mockCount + " }";
+ server.enqueue(new MockResponse().setBody(mockResponse));
+
+ // Call
+ int actualCount = exonumClient.getUnconfirmedTransactionsCount();
+
+ // Assert response
+ assertThat(actualCount, is(mockCount));
+
+ // Assert request params
+ RecordedRequest recordedRequest = server.takeRequest();
+ assertThat(recordedRequest.getMethod(), is("GET"));
+ assertThat(recordedRequest.getPath(), is(MEMORY_POOL));
+ }
+
+ @Test
+ void healthCheck() throws InterruptedException {
+ // Mock response
+ HealthCheckInfo expected = new HealthCheckInfo(ConsensusStatus.ENABLED, 0);
+ String mockResponse = "{\"consensus_status\": \"Enabled\", \"connectivity\": \"NotConnected\"}";
+ server.enqueue(new MockResponse().setBody(mockResponse));
+
+ // Call
+ HealthCheckInfo actual = exonumClient.healthCheck();
+
+ // Assert response
+ assertThat(actual, is(expected));
+
+ // Assert request params
+ RecordedRequest recordedRequest = server.takeRequest();
+ assertThat(recordedRequest.getMethod(), is("GET"));
+ assertThat(recordedRequest.getPath(), is(HEALTH_CHECK));
+ }
+
+ @Test
+ void getUserAgentInfo() throws InterruptedException {
+ // Mock response
+ String mockResponse = "exonum 0.6.0/rustc 1.26.0 (2789b067d 2018-03-06)\n\n/Mac OS10.13.3";
+ server.enqueue(new MockResponse().setBody(mockResponse));
+
+ // Call
+ String actualResponse = exonumClient.getUserAgentInfo();
+
+ // Assert response
+ assertThat(actualResponse, is(mockResponse));
+
+ // Assert request params
+ RecordedRequest recordedRequest = server.takeRequest();
+ assertThat(recordedRequest.getMethod(), is("GET"));
+ assertThat(recordedRequest.getPath(), is(USER_AGENT));
+ }
}
diff --git a/exonum-light-client/src/test/java/com/exonum/client/ExplorerApiHelperTest.java b/exonum-light-client/src/test/java/com/exonum/client/ExplorerApiHelperTest.java
new file mode 100644
index 0000000000..2cdd95d6c5
--- /dev/null
+++ b/exonum-light-client/src/test/java/com/exonum/client/ExplorerApiHelperTest.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2019 The Exonum Team
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.exonum.client;
+
+import static com.jayway.jsonpath.matchers.JsonPathMatchers.isJson;
+import static com.jayway.jsonpath.matchers.JsonPathMatchers.withJsonPath;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+
+import com.exonum.binding.common.hash.HashCode;
+import org.junit.jupiter.api.Test;
+
+class ExplorerApiHelperTest {
+
+ @Test
+ void createSubmitTxBody() {
+ String msg = "some data";
+ String json = ExplorerApiHelper.createSubmitTxBody(msg);
+
+ assertThat(json, isJson(withJsonPath("$.tx_body", equalTo(msg))));
+ }
+
+ @Test
+ void parseSubmitTxResponse() {
+ String expected = "f128c720e04b8243";
+ String json = "{\"tx_hash\":\"" + expected + "\"}";
+
+ HashCode actual = ExplorerApiHelper.parseSubmitTxResponse(json);
+ assertThat(actual, equalTo(HashCode.fromString(expected)));
+ }
+}
diff --git a/exonum-light-client/src/test/java/com/exonum/client/SystemApiHelperTest.java b/exonum-light-client/src/test/java/com/exonum/client/SystemApiHelperTest.java
new file mode 100644
index 0000000000..c08ae55d48
--- /dev/null
+++ b/exonum-light-client/src/test/java/com/exonum/client/SystemApiHelperTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2019 The Exonum Team
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.exonum.client;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+import com.exonum.client.response.ConsensusStatus;
+import com.exonum.client.response.HealthCheckInfo;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+class SystemApiHelperTest {
+
+ @ParameterizedTest
+ @MethodSource("healthCheckSource")
+ void parseHealthCheckJson(String json, HealthCheckInfo expected) {
+ HealthCheckInfo actual = SystemApiHelper.parseHealthCheckJson(json);
+ assertThat(actual, is(expected));
+ }
+
+ @ParameterizedTest
+ @MethodSource("memoryPoolSource")
+ void parseMemoryPoolJson(String json, int expected) {
+ int actual = SystemApiHelper.parseMemoryPoolJson(json);
+ assertThat(actual, is(expected));
+ }
+
+ private static List memoryPoolSource() {
+ return ImmutableList.of(
+ Arguments.of("{\"size\": 0}", 0),
+ Arguments.of("{\"size\": 2}", 2),
+ Arguments.of("{\"size\": " + Integer.MAX_VALUE + "}", Integer.MAX_VALUE)
+ );
+ }
+
+ private static List healthCheckSource() {
+ return ImmutableList.of(
+ Arguments.of("{\"consensus_status\": \"Enabled\", \"connectivity\": \"NotConnected\"}",
+ new HealthCheckInfo(ConsensusStatus.ENABLED, 0)),
+ Arguments.of("{\"consensus_status\": \"Disabled\", \"connectivity\": \"NotConnected\"}",
+ new HealthCheckInfo(ConsensusStatus.DISABLED, 0)),
+ Arguments.of("{\"consensus_status\": \"Active\","
+ + "\"connectivity\": {\"Connected\": {\"amount\": 1 } }"
+ + "}",
+ new HealthCheckInfo(ConsensusStatus.ACTIVE, 1))
+ );
+ }
+
+}