diff --git a/pom.xml b/pom.xml
index 7a781439..e9cbf1d4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -7,6 +7,13 @@
jar
UTF-8
+ 1.60
+ r5836
+ 9.4.11.v20180605
+ 2.27
+ 2.11.0
+ 1.7.12
+ 2.0.6
3.19.0
@@ -156,105 +163,144 @@
+
org.hsqldb
hsqldb
- r5836
+ ${hsqldb.version}
+
+
+ org.ciyam
+ at
+ 1.0
+
+
+
+ org.bitcoinj
+ bitcoinj-core
+ 0.14.7
+
+
com.googlecode.json-simple
json-simple
1.1.1
-
- com.google.guava
- guava
- 25.0-jre
-
-
- org.apache.logging.log4j
- log4j-core
- 2.11.0
-
-
- org.apache.logging.log4j
- log4j-api
- 2.11.0
-
-
- commons-net
- commons-net
- 3.3
-
-
- org.glassfish.jersey.core
- jersey-server
- 2.27
-
-
- javax.servlet
- javax.servlet-api
- 4.0.1
-
-
- org.eclipse.jetty
- jetty-server
- 9.4.11.v20180605
- config
-
-
- org.glassfish.jersey.containers
- jersey-container-servlet-core
- 2.27
-
-
- org.eclipse.jetty
- jetty-servlet
- 9.4.11.v20180605
- jar
-
-
-
- org.eclipse.jetty
- jetty-servlets
- 9.4.11.v20180605
-
-
- org.glassfish.jersey.inject
- jersey-hk2
- 2.27
-
-
- io.swagger.core.v3
- swagger-jaxrs2
- 2.0.4
-
-
- io.swagger.core.v3
- swagger-jaxrs2-servlet-initializer
- 2.0.4
-
org.apache.commons
commons-text
1.4
+
+ commons-net
+ commons-net
+ 3.3
+
+
+ com.google.guava
+ guava
+ 25.0-jre
+
+
+
+ org.apache.logging.log4j
+ log4j-core
+ ${log4j.version}
+
+
+ org.apache.logging.log4j
+ log4j-api
+ ${log4j.version}
+
+
+
+ org.slf4j
+ slf4j-api
+ ${slf4j.version}
+
+
+ org.slf4j
+ slf4j-simple
+ ${slf4j.version}
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 4.0.1
+
+
+ javax.mail
+ mail
+ 1.5.0-b01
+
+
+
+ org.eclipse.jetty
+ jetty-server
+ ${jetty.version}
+ config
+
+
+ org.eclipse.jetty
+ jetty-servlet
+ ${jetty.version}
+ jar
+
+
+ org.eclipse.jetty
+ jetty-servlets
+ ${jetty.version}
+
+
+ org.eclipse.jetty
+ jetty-rewrite
+ ${jetty.version}
+
+
+
+ org.glassfish.jersey.core
+ jersey-server
+ ${jersey.version}
+
+
+ org.glassfish.jersey.containers
+ jersey-container-servlet-core
+ ${jersey.version}
+
+
+ org.glassfish.jersey.inject
+ jersey-hk2
+ ${jersey.version}
+
org.glassfish.jersey.media
jersey-media-moxy
- 2.27
+ ${jersey.version}
- org.ciyam
- at
- 1.0
+ org.glassfish.jersey.media
+ jersey-media-multipart
+ ${jersey.version}
+
+
+
+ io.swagger.core.v3
+ swagger-jaxrs2
+ ${swagger-api.version}
- org.hsqldb
- sqltool
- 2.4.1
- test
+ io.swagger.core.v3
+ swagger-jaxrs2-servlet-initializer
+ ${swagger-api.version}
+
+ org.webjars
+ swagger-ui
+ ${swagger-ui.version}
+
+
org.junit.jupiter
junit-jupiter-engine
@@ -265,48 +311,11 @@
hamcrest-library
1.3
-
- org.glassfish.jersey.media
- jersey-media-multipart
- 2.27
-
-
- javax.mail
- mail
- 1.5.0-b01
-
-
- org.webjars
- swagger-ui
- ${swagger-ui.version}
-
-
- org.eclipse.jetty
- jetty-rewrite
- 9.4.11.v20180605
-
-
- org.bitcoinj
- bitcoinj-core
- 0.14.7
-
-
+
org.bouncycastle
bcprov-jdk15on
- 1.60
-
-
-
- org.slf4j
- slf4j-api
- 1.7.12
-
-
-
- org.slf4j
- slf4j-simple
- 1.7.12
+ ${bouncycastle.version}
\ No newline at end of file
diff --git a/src/api/AddressesResource.java b/src/api/AddressesResource.java
index dad51a40..9d335e68 100644
--- a/src/api/AddressesResource.java
+++ b/src/api/AddressesResource.java
@@ -81,8 +81,6 @@ public class AddressesResource {
public String getLastReference(
@Parameter(description = "a base58-encoded address", required = true) @PathParam("address") String address
) {
- Security.checkApiCallAllowed("GET addresses/lastreference", request);
-
if (!Crypto.isValidAddress(address))
throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS);
@@ -130,8 +128,6 @@ public class AddressesResource {
}
)
public String getLastReferenceUnconfirmed(@PathParam("address") String address) {
- Security.checkApiCallAllowed("GET addresses/lastreference", request);
-
if (!Crypto.isValidAddress(address))
throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS);
@@ -177,8 +173,6 @@ public class AddressesResource {
}
)
public boolean validate(@PathParam("address") String address) {
- Security.checkApiCallAllowed("GET addresses/validate", request);
-
return Crypto.isValidAddress(address);
}
@@ -208,8 +202,6 @@ public class AddressesResource {
}
)
public BigDecimal getGeneratingBalanceOfAddress(@PathParam("address") String address) {
- Security.checkApiCallAllowed("GET addresses/generatingbalance", request);
-
if (!Crypto.isValidAddress(address))
throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS);
@@ -250,8 +242,6 @@ public class AddressesResource {
}
)
public BigDecimal getGeneratingBalance(@PathParam("address") String address) {
- Security.checkApiCallAllowed("GET addresses/balance", request);
-
if (!Crypto.isValidAddress(address))
throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS);
@@ -291,8 +281,6 @@ public class AddressesResource {
}
)
public BigDecimal getAssetBalance(@PathParam("assetid") long assetid, @PathParam("address") String address) {
- Security.checkApiCallAllowed("GET addresses/assetbalance", request);
-
if (!Crypto.isValidAddress(address))
throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS);
@@ -332,8 +320,6 @@ public class AddressesResource {
}
)
public List getAssets(@PathParam("address") String address) {
- Security.checkApiCallAllowed("GET addresses/assets", request);
-
if (!Crypto.isValidAddress(address))
throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS);
@@ -372,8 +358,6 @@ public class AddressesResource {
}
)
public String getGeneratingBalance(@PathParam("address") String address, @PathParam("confirmations") int confirmations) {
- Security.checkApiCallAllowed("GET addresses/balance", request);
-
throw new UnsupportedOperationException();
}
@@ -403,8 +387,6 @@ public class AddressesResource {
}
)
public String getPublicKey(@PathParam("address") String address) {
- Security.checkApiCallAllowed("GET addresses/publickey", request);
-
throw new UnsupportedOperationException();
}
diff --git a/src/api/AdminResource.java b/src/api/AdminResource.java
index 0f8a006d..ffb238f4 100644
--- a/src/api/AdminResource.java
+++ b/src/api/AdminResource.java
@@ -1,7 +1,8 @@
package api;
-import globalization.Translator;
import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.media.Content;
@@ -30,16 +31,14 @@ public class AdminResource {
@Context
HttpServletRequest request;
- private static final long startTime = System.currentTimeMillis();
-
- private ApiErrorFactory apiErrorFactory;
-
- public AdminResource() {
- this(new ApiErrorFactory(Translator.getInstance()));
- }
-
- public AdminResource(ApiErrorFactory apiErrorFactory) {
- this.apiErrorFactory = apiErrorFactory;
+ @GET
+ @Path("/dud")
+ @Parameter(name = "blockSignature", description = "Block signature", schema = @Schema(type = "string", format = "byte", minLength = 84, maxLength=88))
+ @Parameter(in = ParameterIn.QUERY, name = "limit", description = "Maximum number of entries to return", schema = @Schema(type = "integer", defaultValue = "10"))
+ @Parameter(in = ParameterIn.QUERY, name = "offset", description = "Starting entry in results", schema = @Schema(type = "integer"))
+ @Parameter(in = ParameterIn.QUERY, name = "includeTransactions", description = "Include associated transactions in results", schema = @Schema(type = "boolean"))
+ public String globalParameters() {
+ return "";
}
@GET
@@ -65,9 +64,7 @@ public class AdminResource {
}
)
public String uptime() {
- Security.checkApiCallAllowed("GET admin/uptime", request);
-
- return Long.toString(System.currentTimeMillis() - startTime);
+ return Long.toString(System.currentTimeMillis() - Controller.startTime);
}
@GET
@@ -93,16 +90,16 @@ public class AdminResource {
}
)
public String shutdown() {
- Security.checkApiCallAllowed("GET admin/stop", request);
+ Security.checkApiCallAllowed(request);
new Thread(new Runnable() {
@Override
public void run() {
Controller.shutdown();
}
- }); // disabled for now: .start();
+ }).start();
- return "false";
+ return "true";
}
}
diff --git a/src/api/AnnotationPostProcessor.java b/src/api/AnnotationPostProcessor.java
index f34413fe..f4b2f22a 100644
--- a/src/api/AnnotationPostProcessor.java
+++ b/src/api/AnnotationPostProcessor.java
@@ -6,23 +6,31 @@ import globalization.Translator;
import io.swagger.v3.core.converter.ModelConverters;
import io.swagger.v3.jaxrs2.Reader;
import io.swagger.v3.jaxrs2.ReaderListener;
-import io.swagger.v3.oas.models.media.Content;
+import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.examples.Example;
import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.media.Schema;
+import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.responses.ApiResponse;
+
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
public class AnnotationPostProcessor implements ReaderListener {
+ private static final Logger LOGGER = LogManager.getLogger(AnnotationPostProcessor.class);
+
private class ContextInformation {
public String path;
public Map keys;
@@ -41,10 +49,22 @@ public class AnnotationPostProcessor implements ReaderListener {
}
@Override
- public void beforeScan(Reader reader, OpenAPI openAPI) {}
+ public void beforeScan(Reader reader, OpenAPI openAPI) {
+ LOGGER.info("beforeScan");
+ }
@Override
public void afterScan(Reader reader, OpenAPI openAPI) {
+ LOGGER.info("afterScan");
+
+ // Populate Components section with reusable parameters, like "limit" and "offset"
+ // We take the reusable parameters from AdminResource.globalParameters path "/admin/dud"
+ Components components = openAPI.getComponents();
+ PathItem globalParametersPathItem = openAPI.getPaths().get("/admin/dud");
+ if (globalParametersPathItem != null)
+ for (Parameter parameter : globalParametersPathItem.getGet().getParameters())
+ components.addParameters(parameter.getName(), parameter);
+
// use context path and keys from "x-translation" extension annotations
// to translate supported annotations and finally remove "x-translation" extensions
Info resourceInfo = openAPI.getInfo();
diff --git a/src/api/ApiService.java b/src/api/ApiService.java
index f68453e0..2389eff6 100644
--- a/src/api/ApiService.java
+++ b/src/api/ApiService.java
@@ -31,10 +31,12 @@ public class ApiService {
this.resources.add(AdminResource.class);
this.resources.add(BlocksResource.class);
this.resources.add(TransactionsResource.class);
- this.resources.add(BlockExplorerResource.class);
- this.resources.add(OpenApiResource.class); // swagger
- this.resources.add(ApiDefinition.class); // for API definition
- this.resources.add(AnnotationPostProcessor.class); // for API resource annotations
+ this.resources.add(UtilsResource.class);
+
+ this.resources.add(BlockExplorerResource.class); // block-explorer.html
+ this.resources.add(OpenApiResource.class); // Swagger/OpenAPI
+ this.resources.add(ApiDefinition.class); // API info
+ this.resources.add(AnnotationPostProcessor.class); // For API resource annotations
ResourceConfig config = new ResourceConfig(this.resources);
// Create RPC server
diff --git a/src/api/BlockExplorerResource.java b/src/api/BlockExplorerResource.java
index e8720c11..6944ced0 100644
--- a/src/api/BlockExplorerResource.java
+++ b/src/api/BlockExplorerResource.java
@@ -7,12 +7,11 @@ import java.nio.file.Files;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
-import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
-import javax.ws.rs.core.MediaType;
+
+import io.swagger.v3.oas.annotations.Operation;
@Path("/")
-@Produces({ MediaType.TEXT_HTML })
public class BlockExplorerResource {
@Context
@@ -23,6 +22,7 @@ public class BlockExplorerResource {
@GET
@Path("/block-explorer.html")
+ @Operation(hidden = true)
public String getBlockExplorer() {
try {
byte[] htmlBytes = Files.readAllBytes(FileSystems.getDefault().getPath("block-explorer.html"));
diff --git a/src/api/BlocksResource.java b/src/api/BlocksResource.java
index dfbfed37..271ad9a7 100644
--- a/src/api/BlocksResource.java
+++ b/src/api/BlocksResource.java
@@ -3,6 +3,7 @@ package api;
import data.block.BlockData;
import globalization.Translator;
import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.media.Content;
@@ -11,19 +12,23 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.math.BigDecimal;
+import java.util.Base64;
+
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
+
+import api.models.BlockWithTransactions;
import qora.block.Block;
import repository.DataException;
import repository.Repository;
import repository.RepositoryManager;
-import utils.Base58;
@Path("blocks")
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
@@ -50,7 +55,7 @@ public class BlocksResource {
@GET
@Path("/{signature}")
@Operation(
- summary = "Fetch block using base58 signature",
+ summary = "Fetch block using base64 signature",
description = "returns the block that matches the given signature",
extensions = {
@Extension(name = "translation", properties = {
@@ -64,7 +69,7 @@ public class BlocksResource {
responses = {
@ApiResponse(
description = "the block",
- content = @Content(schema = @Schema(implementation = BlockData.class)),
+ content = @Content(schema = @Schema(implementation = BlockWithTransactions.class)),
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description")
@@ -73,34 +78,23 @@ public class BlocksResource {
)
}
)
- public BlockData getBlock(@PathParam("signature") String signature) {
- Security.checkApiCallAllowed("GET blocks", request);
-
- // decode signature
+ public BlockWithTransactions getBlock(@PathParam("signature") String signature, @Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) {
+ // Decode signature
byte[] signatureBytes;
- try
- {
- signatureBytes = Base58.decode(signature);
- }
- catch(Exception e)
- {
- throw this.apiErrorFactory.createError(ApiError.INVALID_SIGNATURE, e);
+ try {
+ signatureBytes = Base64.getDecoder().decode(signature);
+ } catch (NumberFormatException e) {
+ throw this.apiErrorFactory.createError(ApiError.INVALID_SIGNATURE, e);
}
- try (final Repository repository = RepositoryManager.getRepository()) {
- BlockData blockData = repository.getBlockRepository().fromSignature(signatureBytes);
-
- // check if block exists
- if(blockData == null)
- throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS);
-
- return blockData;
-
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ BlockData blockData = repository.getBlockRepository().fromSignature(signatureBytes);
+ return new BlockWithTransactions(repository, blockData, includeTransactions);
} catch (ApiException e) {
throw e;
- } catch (Exception e) {
- throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e);
- }
+ } catch (DataException e) {
+ throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
+ }
}
@GET
@@ -115,7 +109,7 @@ public class BlocksResource {
responses = {
@ApiResponse(
description = "the block",
- content = @Content(schema = @Schema(implementation = BlockData.class)),
+ content = @Content(schema = @Schema(implementation = BlockWithTransactions.class)),
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description")
@@ -124,18 +118,15 @@ public class BlocksResource {
)
}
)
- public BlockData getFirstBlock() {
- Security.checkApiCallAllowed("GET blocks/first", request);
-
- try (final Repository repository = RepositoryManager.getRepository()) {
- BlockData blockData = repository.getBlockRepository().fromHeight(1);
- return blockData;
-
+ public BlockWithTransactions getFirstBlock(@Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ BlockData blockData = repository.getBlockRepository().fromHeight(1);
+ return new BlockWithTransactions(repository, blockData, includeTransactions);
} catch (ApiException e) {
throw e;
- } catch (Exception e) {
- throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e);
- }
+ } catch (DataException e) {
+ throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
+ }
}
@GET
@@ -150,7 +141,7 @@ public class BlocksResource {
responses = {
@ApiResponse(
description = "the block",
- content = @Content(schema = @Schema(implementation = BlockData.class)),
+ content = @Content(schema = @Schema(implementation = BlockWithTransactions.class)),
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description")
@@ -159,24 +150,21 @@ public class BlocksResource {
)
}
)
- public BlockData getLastBlock() {
- Security.checkApiCallAllowed("GET blocks/last", request);
-
- try (final Repository repository = RepositoryManager.getRepository()) {
- BlockData blockData = repository.getBlockRepository().getLastBlock();
- return blockData;
-
+ public BlockWithTransactions getLastBlock(@Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ BlockData blockData = repository.getBlockRepository().getLastBlock();
+ return new BlockWithTransactions(repository, blockData, includeTransactions);
} catch (ApiException e) {
throw e;
- } catch (Exception e) {
- throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e);
- }
+ } catch (DataException e) {
+ throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
+ }
}
@GET
@Path("/child/{signature}")
@Operation(
- summary = "Fetch child block using base58 signature of parent block",
+ summary = "Fetch child block using base64 signature of parent block",
description = "returns the child block of the block that matches the given signature",
extensions = {
@Extension(name = "translation", properties = {
@@ -190,7 +178,7 @@ public class BlocksResource {
responses = {
@ApiResponse(
description = "the block",
- content = @Content(schema = @Schema(implementation = BlockData.class)),
+ content = @Content(schema = @Schema(implementation = BlockWithTransactions.class)),
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description")
@@ -199,13 +187,11 @@ public class BlocksResource {
)
}
)
- public BlockData getChild(@PathParam("signature") String signature) {
- Security.checkApiCallAllowed("GET blocks/child", request);
-
- // decode signature
+ public BlockWithTransactions getChild(@PathParam("signature") String signature, @Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) {
+ // Decode signature
byte[] signatureBytes;
try {
- signatureBytes = Base58.decode(signature);
+ signatureBytes = Base64.getDecoder().decode(signature);
} catch (NumberFormatException e) {
throw this.apiErrorFactory.createError(ApiError.INVALID_SIGNATURE, e);
}
@@ -213,17 +199,17 @@ public class BlocksResource {
try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().fromSignature(signatureBytes);
- // check if block exists
+ // Check block exists
if(blockData == null)
throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS);
BlockData childBlockData = repository.getBlockRepository().fromReference(signatureBytes);
- // check if child exists
+ // Check child exists
if(childBlockData == null)
throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS);
- return childBlockData;
+ return new BlockWithTransactions(repository, childBlockData, includeTransactions);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
@@ -252,18 +238,15 @@ public class BlocksResource {
}
)
public BigDecimal getGeneratingBalance() {
- Security.checkApiCallAllowed("GET blocks/generatingbalance", request);
-
- try (final Repository repository = RepositoryManager.getRepository()) {
- BlockData blockData = repository.getBlockRepository().getLastBlock();
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ BlockData blockData = repository.getBlockRepository().getLastBlock();
Block block = new Block(repository, blockData);
return block.calcNextBlockGeneratingBalance();
-
} catch (ApiException e) {
throw e;
- } catch (Exception e) {
- throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e);
- }
+ } catch (DataException e) {
+ throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
+ }
}
@GET
@@ -292,34 +275,28 @@ public class BlocksResource {
}
)
public BigDecimal getGeneratingBalance(@PathParam("signature") String signature) {
- Security.checkApiCallAllowed("GET blocks/generatingbalance", request);
-
- // decode signature
+ // Decode signature
byte[] signatureBytes;
- try
- {
- signatureBytes = Base58.decode(signature);
- }
- catch(Exception e)
- {
- throw this.apiErrorFactory.createError(ApiError.INVALID_SIGNATURE, e);
+ try {
+ signatureBytes = Base64.getDecoder().decode(signature);
+ } catch (NumberFormatException e) {
+ throw this.apiErrorFactory.createError(ApiError.INVALID_SIGNATURE, e);
}
- try (final Repository repository = RepositoryManager.getRepository()) {
- BlockData blockData = repository.getBlockRepository().fromSignature(signatureBytes);
-
- // check if block exists
- if(blockData == null)
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ BlockData blockData = repository.getBlockRepository().fromSignature(signatureBytes);
+
+ // Check block exists
+ if (blockData == null)
throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS);
Block block = new Block(repository, blockData);
return block.calcNextBlockGeneratingBalance();
-
} catch (ApiException e) {
throw e;
- } catch (Exception e) {
- throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e);
- }
+ } catch (DataException e) {
+ throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
+ }
}
@GET
@@ -343,17 +320,14 @@ public class BlocksResource {
}
)
public long getTimePerBlock() {
- Security.checkApiCallAllowed("GET blocks/time", request);
-
- try (final Repository repository = RepositoryManager.getRepository()) {
- BlockData blockData = repository.getBlockRepository().getLastBlock();
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ BlockData blockData = repository.getBlockRepository().getLastBlock();
return Block.calcForgingDelay(blockData.getGeneratingBalance());
-
} catch (ApiException e) {
throw e;
- } catch (Exception e) {
- throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e);
- }
+ } catch (DataException e) {
+ throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
+ }
}
@GET
@@ -377,8 +351,6 @@ public class BlocksResource {
}
)
public long getTimePerBlock(@PathParam("generating") BigDecimal generatingbalance) {
- Security.checkApiCallAllowed("GET blocks/time", request);
-
return Block.calcForgingDelay(generatingbalance);
}
@@ -403,16 +375,13 @@ public class BlocksResource {
}
)
public int getHeight() {
- Security.checkApiCallAllowed("GET blocks/height", request);
-
- try (final Repository repository = RepositoryManager.getRepository()) {
- return repository.getBlockRepository().getBlockchainHeight();
-
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ return repository.getBlockRepository().getBlockchainHeight();
} catch (ApiException e) {
throw e;
- } catch (Exception e) {
- throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e);
- }
+ } catch (DataException e) {
+ throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
+ }
}
@GET
@@ -441,33 +410,27 @@ public class BlocksResource {
}
)
public int getHeight(@PathParam("signature") String signature) {
- Security.checkApiCallAllowed("GET blocks/height", request);
-
- // decode signature
+ // Decode signature
byte[] signatureBytes;
- try
- {
- signatureBytes = Base58.decode(signature);
- }
- catch(Exception e)
- {
- throw this.apiErrorFactory.createError(ApiError.INVALID_SIGNATURE, e);
+ try {
+ signatureBytes = Base64.getDecoder().decode(signature);
+ } catch (NumberFormatException e) {
+ throw this.apiErrorFactory.createError(ApiError.INVALID_SIGNATURE, e);
}
- try (final Repository repository = RepositoryManager.getRepository()) {
- BlockData blockData = repository.getBlockRepository().fromSignature(signatureBytes);
-
- // check if block exists
- if(blockData == null)
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ BlockData blockData = repository.getBlockRepository().fromSignature(signatureBytes);
+
+ // Check block exists
+ if (blockData == null)
throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS);
return blockData.getHeight();
-
} catch (ApiException e) {
throw e;
- } catch (Exception e) {
- throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e);
- }
+ } catch (DataException e) {
+ throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
+ }
}
@GET
@@ -486,7 +449,7 @@ public class BlocksResource {
responses = {
@ApiResponse(
description = "the block",
- content = @Content(schema = @Schema(implementation = BlockData.class)),
+ content = @Content(schema = @Schema(implementation = BlockWithTransactions.class)),
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description")
@@ -495,22 +458,15 @@ public class BlocksResource {
)
}
)
- public BlockData getbyHeight(@PathParam("height") int height) {
- Security.checkApiCallAllowed("GET blocks/byheight", request);
-
- try (final Repository repository = RepositoryManager.getRepository()) {
- BlockData blockData = repository.getBlockRepository().fromHeight(height);
-
- // check if block exists
- if(blockData == null)
- throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS);
-
- return blockData;
-
+ public BlockWithTransactions getbyHeight(@PathParam("height") int height, @Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ BlockData blockData = repository.getBlockRepository().fromHeight(height);
+ return new BlockWithTransactions(repository, blockData, includeTransactions);
} catch (ApiException e) {
throw e;
- } catch (Exception e) {
- throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e);
- }
+ } catch (DataException e) {
+ throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
+ }
}
+
}
diff --git a/src/api/Security.java b/src/api/Security.java
index a7599d48..2d981b2c 100644
--- a/src/api/Security.java
+++ b/src/api/Security.java
@@ -1,12 +1,22 @@
package api;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
import javax.servlet.http.HttpServletRequest;
public class Security {
- public static void checkApiCallAllowed(final String messageToDisplay, HttpServletRequest request) {
- // TODO
+ // TODO: replace with proper authentication
+ public static void checkApiCallAllowed(HttpServletRequest request) {
+ InetAddress remoteAddr;
+ try {
+ remoteAddr = InetAddress.getByName(request.getRemoteAddr());
+ } catch (UnknownHostException e) {
+ throw ApiErrorFactory.getInstance().createError(ApiError.UNAUTHORIZED);
+ }
- // throw this.apiErrorFactory.createError(ApiError.UNAUTHORIZED);
+ if (!remoteAddr.isLoopbackAddress())
+ throw ApiErrorFactory.getInstance().createError(ApiError.UNAUTHORIZED);
}
}
diff --git a/src/api/TransactionsResource.java b/src/api/TransactionsResource.java
index d396d3c3..a7ba240b 100644
--- a/src/api/TransactionsResource.java
+++ b/src/api/TransactionsResource.java
@@ -2,6 +2,8 @@ package api;
import globalization.Translator;
import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.media.ArraySchema;
@@ -19,9 +21,12 @@ import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
+import data.transaction.GenesisTransactionData;
+import data.transaction.PaymentTransactionData;
import data.transaction.TransactionData;
import repository.DataException;
import repository.Repository;
@@ -56,6 +61,9 @@ public class TransactionsResource {
@Operation(
summary = "Fetch transactions involving address",
description = "Returns list of transactions",
+ parameters = {
+ @Parameter(in = ParameterIn.PATH, name = "address", description = "Account's address", schema = @Schema(type = "string"))
+ },
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="GET block:signature"),
@@ -77,9 +85,7 @@ public class TransactionsResource {
)
}
)
- public List getAddressTransactions(@PathParam("address") String address) {
- Security.checkApiCallAllowed("GET transactions/address", request);
-
+ public List getAddressTransactions(@PathParam("address") String address, @Parameter(ref = "limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) {
if (!Crypto.isValidAddress(address))
throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS);
@@ -89,6 +95,9 @@ public class TransactionsResource {
List signatures = txRepo.getAllSignaturesInvolvingAddress(address);
// Pagination would take effect here (or as part of the repository access)
+ int fromIndex = Integer.min(offset, signatures.size());
+ int toIndex = limit == 0 ? signatures.size() : Integer.min(fromIndex + limit, signatures.size());
+ signatures = signatures.subList(fromIndex, toIndex);
// Expand signatures to transactions
List transactions = new ArrayList(signatures.size());
@@ -101,13 +110,12 @@ public class TransactionsResource {
} catch (DataException e) {
throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
}
-
}
@GET
@Path("/block/{signature}")
@Operation(
- summary = "Fetch transactions via block signature",
+ summary = "Fetch transactions using block signature",
description = "Returns list of transactions",
extensions = {
@Extension(name = "translation", properties = {
@@ -121,7 +129,9 @@ public class TransactionsResource {
responses = {
@ApiResponse(
description = "list of transactions",
- content = @Content(array = @ArraySchema(schema = @Schema(implementation = TransactionData.class))),
+ content = @Content(array = @ArraySchema(schema = @Schema(
+ oneOf = { GenesisTransactionData.class, PaymentTransactionData.class }
+ ))),
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description")
@@ -130,9 +140,7 @@ public class TransactionsResource {
)
}
)
- public List getBlockTransactions(@PathParam("signature") String signature) {
- Security.checkApiCallAllowed("GET transactions/block", request);
-
+ public List getBlockTransactions(@PathParam("signature") String signature, @Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) {
// decode signature
byte[] signatureBytes;
try {
@@ -148,13 +156,17 @@ public class TransactionsResource {
if(transactions == null)
throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS);
+ // Pagination would take effect here (or as part of the repository access)
+ int fromIndex = Integer.min(offset, transactions.size());
+ int toIndex = limit == 0 ? transactions.size() : Integer.min(fromIndex + limit, transactions.size());
+ transactions = transactions.subList(fromIndex, toIndex);
+
return transactions;
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
}
-
}
}
diff --git a/src/api/UtilsResource.java b/src/api/UtilsResource.java
new file mode 100644
index 00000000..ba77a707
--- /dev/null
+++ b/src/api/UtilsResource.java
@@ -0,0 +1,66 @@
+package api;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import utils.Base58;
+
+import java.util.Base64;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+
+@Path("/utils")
+@Produces({MediaType.TEXT_PLAIN})
+@Tag(name = "utils")
+public class UtilsResource {
+
+ @Context
+ HttpServletRequest request;
+
+ @GET
+ @Path("/base58from64/{base64}")
+ @Operation(
+ summary = "Convert base64 data to base58",
+ responses = {
+ @ApiResponse(
+ description = "base58 data",
+ content = @Content(schema = @Schema(implementation = String.class))
+ )
+ }
+ )
+ public String base58from64(@PathParam("base64") String base64) {
+ try {
+ return Base58.encode(Base64.getDecoder().decode(base64));
+ } catch (IllegalArgumentException e) {
+ throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_DATA);
+ }
+ }
+
+ @GET
+ @Path("/base64from58/{base58}")
+ @Operation(
+ summary = "Convert base58 data to base64",
+ responses = {
+ @ApiResponse(
+ description = "base64 data",
+ content = @Content(schema = @Schema(implementation = String.class))
+ )
+ }
+ )
+ public String base64from58(@PathParam("base58") String base58) {
+ try {
+ return Base64.getEncoder().encodeToString(Base58.decode(base58));
+ } catch (NumberFormatException e) {
+ throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_DATA);
+ }
+ }
+
+}
diff --git a/src/api/models/BlockWithTransactions.java b/src/api/models/BlockWithTransactions.java
new file mode 100644
index 00000000..ae323aa9
--- /dev/null
+++ b/src/api/models/BlockWithTransactions.java
@@ -0,0 +1,43 @@
+package api.models;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.xml.bind.annotation.XmlElement;
+
+import api.ApiError;
+import api.ApiErrorFactory;
+import data.block.BlockData;
+import data.transaction.TransactionData;
+import io.swagger.v3.oas.annotations.media.Schema;
+import qora.block.Block;
+import repository.DataException;
+import repository.Repository;
+
+@Schema(description = "Block with (optional) transactions")
+public class BlockWithTransactions {
+
+ @Schema(implementation = BlockData.class, name = "block", title = "block data")
+ @XmlElement(name = "block")
+ public BlockData blockData;
+
+ public List transactions;
+
+ // For JAX-RS
+ @SuppressWarnings("unused")
+ private BlockWithTransactions() {
+ }
+
+ public BlockWithTransactions(Repository repository, BlockData blockData, boolean includeTransactions) throws DataException {
+ if (blockData == null)
+ throw ApiErrorFactory.getInstance().createError(ApiError.BLOCK_NO_EXISTS);
+
+ this.blockData = blockData;
+
+ if (includeTransactions) {
+ Block block = new Block(repository, blockData);
+ this.transactions = block.getTransactions().stream().map(transaction -> transaction.getTransactionData()).collect(Collectors.toList());
+ }
+ }
+
+}
diff --git a/src/controller/Controller.java b/src/controller/Controller.java
index 37c01250..460e7368 100644
--- a/src/controller/Controller.java
+++ b/src/controller/Controller.java
@@ -14,19 +14,31 @@ public class Controller {
private static final Logger LOGGER = LogManager.getLogger(Controller.class);
private static final String connectionUrl = "jdbc:hsqldb:file:db/test;create=true";
+
+ public static final long startTime = System.currentTimeMillis();
private static final Object shutdownLock = new Object();
private static boolean isStopping = false;
- public static void main(String args[]) throws DataException {
+ public static void main(String args[]) {
LOGGER.info("Starting up...");
LOGGER.info("Starting repository");
- RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(connectionUrl);
- RepositoryManager.setRepositoryFactory(repositoryFactory);
+ try {
+ RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(connectionUrl);
+ RepositoryManager.setRepositoryFactory(repositoryFactory);
+ } catch (DataException e) {
+ LOGGER.error("Unable to start repository", e);
+ System.exit(1);
+ }
LOGGER.info("Starting API");
- ApiService apiService = ApiService.getInstance();
- apiService.start();
+ try {
+ ApiService apiService = ApiService.getInstance();
+ apiService.start();
+ } catch (Exception e) {
+ LOGGER.error("Unable to start API", e);
+ System.exit(1);
+ }
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
diff --git a/src/data/block/BlockData.java b/src/data/block/BlockData.java
index 1af4803e..4b794426 100644
--- a/src/data/block/BlockData.java
+++ b/src/data/block/BlockData.java
@@ -29,8 +29,7 @@ public class BlockData implements Serializable {
private BigDecimal atFees;
// necessary for JAX-RS serialization
- @SuppressWarnings("unused")
- private BlockData() {
+ protected BlockData() {
}
public BlockData(int version, byte[] reference, int transactionCount, BigDecimal totalFees, byte[] transactionsSignature, Integer height, long timestamp,
diff --git a/src/data/transaction/GenesisTransactionData.java b/src/data/transaction/GenesisTransactionData.java
index 06393825..f30561b6 100644
--- a/src/data/transaction/GenesisTransactionData.java
+++ b/src/data/transaction/GenesisTransactionData.java
@@ -2,9 +2,16 @@ package data.transaction;
import java.math.BigDecimal;
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+
+import io.swagger.v3.oas.annotations.media.Schema;
import qora.account.GenesisAccount;
import qora.transaction.Transaction.TransactionType;
+// All properties to be converted to JSON via JAX-RS
+@XmlAccessorType(XmlAccessType.FIELD)
+@Schema( allOf = { TransactionData.class } )
public class GenesisTransactionData extends TransactionData {
// Properties
@@ -13,6 +20,10 @@ public class GenesisTransactionData extends TransactionData {
// Constructors
+ // For JAX-RS
+ protected GenesisTransactionData() {
+ }
+
public GenesisTransactionData(String recipient, BigDecimal amount, long timestamp, byte[] signature) {
// Zero fee
super(TransactionType.GENESIS, BigDecimal.ZERO, GenesisAccount.PUBLIC_KEY, timestamp, null, signature);
diff --git a/src/data/transaction/PaymentTransactionData.java b/src/data/transaction/PaymentTransactionData.java
index 98afb3ab..e93ada3a 100644
--- a/src/data/transaction/PaymentTransactionData.java
+++ b/src/data/transaction/PaymentTransactionData.java
@@ -2,8 +2,15 @@ package data.transaction;
import java.math.BigDecimal;
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+
+import io.swagger.v3.oas.annotations.media.Schema;
import qora.transaction.Transaction.TransactionType;
+// All properties to be converted to JSON via JAX-RS
+@XmlAccessorType(XmlAccessType.FIELD)
+@Schema( allOf = { TransactionData.class } )
public class PaymentTransactionData extends TransactionData {
// Properties
@@ -13,6 +20,10 @@ public class PaymentTransactionData extends TransactionData {
// Constructors
+ // For JAX-RS
+ protected PaymentTransactionData() {
+ }
+
public PaymentTransactionData(byte[] senderPublicKey, String recipient, BigDecimal amount, BigDecimal fee, long timestamp, byte[] reference,
byte[] signature) {
super(TransactionType.PAYMENT, fee, senderPublicKey, timestamp, reference, signature);
diff --git a/src/data/transaction/TransactionData.java b/src/data/transaction/TransactionData.java
index 428223c5..820d54c8 100644
--- a/src/data/transaction/TransactionData.java
+++ b/src/data/transaction/TransactionData.java
@@ -23,6 +23,10 @@ public abstract class TransactionData {
// Constructors
+ // For JAX-RS
+ protected TransactionData() {
+ }
+
public TransactionData(TransactionType type, BigDecimal fee, byte[] creatorPublicKey, long timestamp, byte[] reference, byte[] signature) {
this.fee = fee;
this.type = type;
diff --git a/src/qora/account/Account.java b/src/qora/account/Account.java
index 23053a31..78dca2eb 100644
--- a/src/qora/account/Account.java
+++ b/src/qora/account/Account.java
@@ -1,7 +1,6 @@
package qora.account;
import java.math.BigDecimal;
-import java.util.Arrays;
import java.util.List;
import org.apache.logging.log4j.LogManager;
diff --git a/src/repository/hsqldb/HSQLDBRepositoryFactory.java b/src/repository/hsqldb/HSQLDBRepositoryFactory.java
index b5e368bc..ed35a067 100644
--- a/src/repository/hsqldb/HSQLDBRepositoryFactory.java
+++ b/src/repository/hsqldb/HSQLDBRepositoryFactory.java
@@ -20,6 +20,12 @@ public class HSQLDBRepositoryFactory implements RepositoryFactory {
// one-time initialization goes in here
this.connectionUrl = connectionUrl;
+ // Check no-one else is accessing database
+ try (Connection connection = DriverManager.getConnection(this.connectionUrl)) {
+ } catch (SQLException e) {
+ throw new DataException("Unable to open repository: " + e.getMessage());
+ }
+
this.connectionPool = new JDBCPool();
this.connectionPool.setUrl(this.connectionUrl);