Work on API

Rejigged pom.xml, extracting common dependency versions as properties.
Removed extraneous HSQLDB dependency (v2.4.1) as we're using svn r5836 for now.

Removed calls to Security.checkApiCallAllowed() for all API calls EXCEPT /admin/stop.
Throws error if remote IP is not localhost.

Added 'global' OpenAPI parameters to fake /admin/dud endpoint to save copy&pasting.
This will need more tidying in the future, or at least future support from swagger-core.
Code added in AnnotationPostProcessor to insert global parameters in top-level
  OpenAPI components section.

/block-explorer.html hidden from API UI

BlocksResource now expects Base64 block signatures instead of Base58.
Endpoints that return block data also accept optional "includeTransactions"
    query param which does exactly that.
BlockWithTransactions API model added for above.

Some attempt to get transaction-specific data returned but no luck as yet.
(TransactionData, GenesisTransactionData, PaymentTransactionData touched).
See https://github.com/swagger-api/swagger-core/issues/3046

TransactionsResource now has support for optional query params "limit" and "offset"
    so that only a subset of large results can be requested.

UtilsResource added to provide convenient Base64<->Base58 conversions.

/admin/uptime fixed to return uptime from application launch instead of
    instantiation of AdminResource class!

Controller improved to detect repository and API startup failures.

HSQLDBRepositoryFactory now detects when it can't open database and throws.
(Before it would simply hang).

Removed extraneous import from qora.account.Account
This commit is contained in:
catbref 2018-12-07 17:42:31 +00:00
parent df2a414cf4
commit b5c02f49ce
18 changed files with 458 additions and 319 deletions

241
pom.xml
View File

@ -7,6 +7,13 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<bouncycastle.version>1.60</bouncycastle.version>
<hsqldb.version>r5836</hsqldb.version>
<jetty.version>9.4.11.v20180605</jetty.version>
<jersey.version>2.27</jersey.version>
<log4j.version>2.11.0</log4j.version>
<slf4j.version>1.7.12</slf4j.version>
<swagger-api.version>2.0.6</swagger-api.version>
<swagger-ui.version>3.19.0</swagger-ui.version> <swagger-ui.version>3.19.0</swagger-ui.version>
</properties> </properties>
<build> <build>
@ -156,105 +163,144 @@
</repository> </repository>
</repositories> </repositories>
<dependencies> <dependencies>
<!-- HSQLDB for repository -->
<dependency> <dependency>
<groupId>org.hsqldb</groupId> <groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId> <artifactId>hsqldb</artifactId>
<version>r5836</version> <version>${hsqldb.version}</version>
</dependency> </dependency>
<!-- CIYAM AT (automated transactions) -->
<dependency>
<groupId>org.ciyam</groupId>
<artifactId>at</artifactId>
<version>1.0</version>
</dependency>
<!-- Bitcoin support -->
<dependency>
<groupId>org.bitcoinj</groupId>
<artifactId>bitcoinj-core</artifactId>
<version>0.14.7</version>
</dependency>
<!-- Utilities -->
<dependency> <dependency>
<groupId>com.googlecode.json-simple</groupId> <groupId>com.googlecode.json-simple</groupId>
<artifactId>json-simple</artifactId> <artifactId>json-simple</artifactId>
<version>1.1.1</version> <version>1.1.1</version>
</dependency> </dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>25.0-jre</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>3.3</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-server</artifactId>
<version>2.27</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>9.4.11.v20180605</version>
<classifier>config</classifier>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-servlet-core</artifactId>
<version>2.27</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<version>9.4.11.v20180605</version>
<type>jar</type>
</dependency>
<!-- https://mvnrepository.com/artifact/org.eclipse.jetty/jetty-servlets -->
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlets</artifactId>
<version>9.4.11.v20180605</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId>
<version>2.27</version>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-jaxrs2</artifactId>
<version>2.0.4</version>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-jaxrs2-servlet-initializer</artifactId>
<version>2.0.4</version>
</dependency>
<dependency> <dependency>
<groupId>org.apache.commons</groupId> <groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId> <artifactId>commons-text</artifactId>
<version>1.4</version> <version>1.4</version>
</dependency> </dependency>
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>3.3</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>25.0-jre</version>
</dependency>
<!-- Logging: log4j2 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>${log4j.version}</version>
</dependency>
<!-- Logging: slf4j used by Jetty -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>${slf4j.version}</version>
</dependency>
<!-- Servlet related -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
<version>1.5.0-b01</version>
</dependency>
<!-- Jetty -->
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>${jetty.version}</version>
<classifier>config</classifier>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<version>${jetty.version}</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlets</artifactId>
<version>${jetty.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-rewrite</artifactId>
<version>${jetty.version}</version>
</dependency>
<!-- Jersey -->
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-server</artifactId>
<version>${jersey.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-servlet-core</artifactId>
<version>${jersey.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId>
<version>${jersey.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.glassfish.jersey.media</groupId> <groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-moxy</artifactId> <artifactId>jersey-media-moxy</artifactId>
<version>2.27</version> <version>${jersey.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.ciyam</groupId> <groupId>org.glassfish.jersey.media</groupId>
<artifactId>at</artifactId> <artifactId>jersey-media-multipart</artifactId>
<version>1.0</version> <version>${jersey.version}</version>
</dependency>
<!-- Swagger OpenAPI implementation -->
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-jaxrs2</artifactId>
<version>${swagger-api.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.hsqldb</groupId> <groupId>io.swagger.core.v3</groupId>
<artifactId>sqltool</artifactId> <artifactId>swagger-jaxrs2-servlet-initializer</artifactId>
<version>2.4.1</version> <version>${swagger-api.version}</version>
<scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>swagger-ui</artifactId>
<version>${swagger-ui.version}</version>
</dependency>
<!-- Testing -->
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId> <artifactId>junit-jupiter-engine</artifactId>
@ -265,48 +311,11 @@
<artifactId>hamcrest-library</artifactId> <artifactId>hamcrest-library</artifactId>
<version>1.3</version> <version>1.3</version>
</dependency> </dependency>
<dependency> <!-- BouncyCastle for crypto, including TLS secure networking -->
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-multipart</artifactId>
<version>2.27</version>
</dependency>
<dependency>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
<version>1.5.0-b01</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>swagger-ui</artifactId>
<version>${swagger-ui.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-rewrite</artifactId>
<version>9.4.11.v20180605</version>
</dependency>
<dependency>
<groupId>org.bitcoinj</groupId>
<artifactId>bitcoinj-core</artifactId>
<version>0.14.7</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk15on -->
<dependency> <dependency>
<groupId>org.bouncycastle</groupId> <groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId> <artifactId>bcprov-jdk15on</artifactId>
<version>1.60</version> <version>${bouncycastle.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.12</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-simple -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.12</version>
</dependency> </dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -81,8 +81,6 @@ public class AddressesResource {
public String getLastReference( public String getLastReference(
@Parameter(description = "a base58-encoded address", required = true) @PathParam("address") String address @Parameter(description = "a base58-encoded address", required = true) @PathParam("address") String address
) { ) {
Security.checkApiCallAllowed("GET addresses/lastreference", request);
if (!Crypto.isValidAddress(address)) if (!Crypto.isValidAddress(address))
throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS); throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS);
@ -130,8 +128,6 @@ public class AddressesResource {
} }
) )
public String getLastReferenceUnconfirmed(@PathParam("address") String address) { public String getLastReferenceUnconfirmed(@PathParam("address") String address) {
Security.checkApiCallAllowed("GET addresses/lastreference", request);
if (!Crypto.isValidAddress(address)) if (!Crypto.isValidAddress(address))
throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS); throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS);
@ -177,8 +173,6 @@ public class AddressesResource {
} }
) )
public boolean validate(@PathParam("address") String address) { public boolean validate(@PathParam("address") String address) {
Security.checkApiCallAllowed("GET addresses/validate", request);
return Crypto.isValidAddress(address); return Crypto.isValidAddress(address);
} }
@ -208,8 +202,6 @@ public class AddressesResource {
} }
) )
public BigDecimal getGeneratingBalanceOfAddress(@PathParam("address") String address) { public BigDecimal getGeneratingBalanceOfAddress(@PathParam("address") String address) {
Security.checkApiCallAllowed("GET addresses/generatingbalance", request);
if (!Crypto.isValidAddress(address)) if (!Crypto.isValidAddress(address))
throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS); throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS);
@ -250,8 +242,6 @@ public class AddressesResource {
} }
) )
public BigDecimal getGeneratingBalance(@PathParam("address") String address) { public BigDecimal getGeneratingBalance(@PathParam("address") String address) {
Security.checkApiCallAllowed("GET addresses/balance", request);
if (!Crypto.isValidAddress(address)) if (!Crypto.isValidAddress(address))
throw this.apiErrorFactory.createError(ApiError.INVALID_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) { public BigDecimal getAssetBalance(@PathParam("assetid") long assetid, @PathParam("address") String address) {
Security.checkApiCallAllowed("GET addresses/assetbalance", request);
if (!Crypto.isValidAddress(address)) if (!Crypto.isValidAddress(address))
throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS); throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS);
@ -332,8 +320,6 @@ public class AddressesResource {
} }
) )
public List<AccountBalanceData> getAssets(@PathParam("address") String address) { public List<AccountBalanceData> getAssets(@PathParam("address") String address) {
Security.checkApiCallAllowed("GET addresses/assets", request);
if (!Crypto.isValidAddress(address)) if (!Crypto.isValidAddress(address))
throw this.apiErrorFactory.createError(ApiError.INVALID_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) { public String getGeneratingBalance(@PathParam("address") String address, @PathParam("confirmations") int confirmations) {
Security.checkApiCallAllowed("GET addresses/balance", request);
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
@ -403,8 +387,6 @@ public class AddressesResource {
} }
) )
public String getPublicKey(@PathParam("address") String address) { public String getPublicKey(@PathParam("address") String address) {
Security.checkApiCallAllowed("GET addresses/publickey", request);
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }

View File

@ -1,7 +1,8 @@
package api; package api;
import globalization.Translator;
import io.swagger.v3.oas.annotations.Operation; 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.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty; import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Content;
@ -30,16 +31,14 @@ public class AdminResource {
@Context @Context
HttpServletRequest request; HttpServletRequest request;
private static final long startTime = System.currentTimeMillis(); @GET
@Path("/dud")
private ApiErrorFactory apiErrorFactory; @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"))
public AdminResource() { @Parameter(in = ParameterIn.QUERY, name = "offset", description = "Starting entry in results", schema = @Schema(type = "integer"))
this(new ApiErrorFactory(Translator.getInstance())); @Parameter(in = ParameterIn.QUERY, name = "includeTransactions", description = "Include associated transactions in results", schema = @Schema(type = "boolean"))
} public String globalParameters() {
return "";
public AdminResource(ApiErrorFactory apiErrorFactory) {
this.apiErrorFactory = apiErrorFactory;
} }
@GET @GET
@ -65,9 +64,7 @@ public class AdminResource {
} }
) )
public String uptime() { public String uptime() {
Security.checkApiCallAllowed("GET admin/uptime", request); return Long.toString(System.currentTimeMillis() - Controller.startTime);
return Long.toString(System.currentTimeMillis() - startTime);
} }
@GET @GET
@ -93,16 +90,16 @@ public class AdminResource {
} }
) )
public String shutdown() { public String shutdown() {
Security.checkApiCallAllowed("GET admin/stop", request); Security.checkApiCallAllowed(request);
new Thread(new Runnable() { new Thread(new Runnable() {
@Override @Override
public void run() { public void run() {
Controller.shutdown(); Controller.shutdown();
} }
}); // disabled for now: .start(); }).start();
return "false"; return "true";
} }
} }

View File

@ -6,23 +6,31 @@ import globalization.Translator;
import io.swagger.v3.core.converter.ModelConverters; import io.swagger.v3.core.converter.ModelConverters;
import io.swagger.v3.jaxrs2.Reader; import io.swagger.v3.jaxrs2.Reader;
import io.swagger.v3.jaxrs2.ReaderListener; 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.OpenAPI;
import io.swagger.v3.oas.models.Operation; import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem; import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.examples.Example; import io.swagger.v3.oas.models.examples.Example;
import io.swagger.v3.oas.models.info.Info; 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.MediaType;
import io.swagger.v3.oas.models.media.Schema; 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 io.swagger.v3.oas.models.responses.ApiResponse;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class AnnotationPostProcessor implements ReaderListener { public class AnnotationPostProcessor implements ReaderListener {
private static final Logger LOGGER = LogManager.getLogger(AnnotationPostProcessor.class);
private class ContextInformation { private class ContextInformation {
public String path; public String path;
public Map<String, String> keys; public Map<String, String> keys;
@ -41,10 +49,22 @@ public class AnnotationPostProcessor implements ReaderListener {
} }
@Override @Override
public void beforeScan(Reader reader, OpenAPI openAPI) {} public void beforeScan(Reader reader, OpenAPI openAPI) {
LOGGER.info("beforeScan");
}
@Override @Override
public void afterScan(Reader reader, OpenAPI openAPI) { 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 // use context path and keys from "x-translation" extension annotations
// to translate supported annotations and finally remove "x-translation" extensions // to translate supported annotations and finally remove "x-translation" extensions
Info resourceInfo = openAPI.getInfo(); Info resourceInfo = openAPI.getInfo();

View File

@ -31,10 +31,12 @@ public class ApiService {
this.resources.add(AdminResource.class); this.resources.add(AdminResource.class);
this.resources.add(BlocksResource.class); this.resources.add(BlocksResource.class);
this.resources.add(TransactionsResource.class); this.resources.add(TransactionsResource.class);
this.resources.add(BlockExplorerResource.class); this.resources.add(UtilsResource.class);
this.resources.add(OpenApiResource.class); // swagger
this.resources.add(ApiDefinition.class); // for API definition this.resources.add(BlockExplorerResource.class); // block-explorer.html
this.resources.add(AnnotationPostProcessor.class); // for API resource annotations 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); ResourceConfig config = new ResourceConfig(this.resources);
// Create RPC server // Create RPC server

View File

@ -7,12 +7,11 @@ import java.nio.file.Files;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context; import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import io.swagger.v3.oas.annotations.Operation;
@Path("/") @Path("/")
@Produces({ MediaType.TEXT_HTML })
public class BlockExplorerResource { public class BlockExplorerResource {
@Context @Context
@ -23,6 +22,7 @@ public class BlockExplorerResource {
@GET @GET
@Path("/block-explorer.html") @Path("/block-explorer.html")
@Operation(hidden = true)
public String getBlockExplorer() { public String getBlockExplorer() {
try { try {
byte[] htmlBytes = Files.readAllBytes(FileSystems.getDefault().getPath("block-explorer.html")); byte[] htmlBytes = Files.readAllBytes(FileSystems.getDefault().getPath("block-explorer.html"));

View File

@ -3,6 +3,7 @@ package api;
import data.block.BlockData; import data.block.BlockData;
import globalization.Translator; import globalization.Translator;
import io.swagger.v3.oas.annotations.Operation; 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.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty; import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.media.Content; 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 io.swagger.v3.oas.annotations.tags.Tag;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Base64;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam; import javax.ws.rs.PathParam;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context; import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import api.models.BlockWithTransactions;
import qora.block.Block; import qora.block.Block;
import repository.DataException; import repository.DataException;
import repository.Repository; import repository.Repository;
import repository.RepositoryManager; import repository.RepositoryManager;
import utils.Base58;
@Path("blocks") @Path("blocks")
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) @Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
@ -50,7 +55,7 @@ public class BlocksResource {
@GET @GET
@Path("/{signature}") @Path("/{signature}")
@Operation( @Operation(
summary = "Fetch block using base58 signature", summary = "Fetch block using base64 signature",
description = "returns the block that matches the given signature", description = "returns the block that matches the given signature",
extensions = { extensions = {
@Extension(name = "translation", properties = { @Extension(name = "translation", properties = {
@ -64,7 +69,7 @@ public class BlocksResource {
responses = { responses = {
@ApiResponse( @ApiResponse(
description = "the block", description = "the block",
content = @Content(schema = @Schema(implementation = BlockData.class)), content = @Content(schema = @Schema(implementation = BlockWithTransactions.class)),
extensions = { extensions = {
@Extension(name = "translation", properties = { @Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description") @ExtensionProperty(name="description.key", value="success_response:description")
@ -73,34 +78,23 @@ public class BlocksResource {
) )
} }
) )
public BlockData getBlock(@PathParam("signature") String signature) { public BlockWithTransactions getBlock(@PathParam("signature") String signature, @Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) {
Security.checkApiCallAllowed("GET blocks", request); // Decode signature
// decode signature
byte[] signatureBytes; byte[] signatureBytes;
try try {
{ signatureBytes = Base64.getDecoder().decode(signature);
signatureBytes = Base58.decode(signature); } catch (NumberFormatException e) {
} throw this.apiErrorFactory.createError(ApiError.INVALID_SIGNATURE, e);
catch(Exception e)
{
throw this.apiErrorFactory.createError(ApiError.INVALID_SIGNATURE, e);
} }
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().fromSignature(signatureBytes); BlockData blockData = repository.getBlockRepository().fromSignature(signatureBytes);
return new BlockWithTransactions(repository, blockData, includeTransactions);
// check if block exists
if(blockData == null)
throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS);
return blockData;
} catch (ApiException e) { } catch (ApiException e) {
throw e; throw e;
} catch (Exception e) { } catch (DataException e) {
throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e); throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
} }
} }
@GET @GET
@ -115,7 +109,7 @@ public class BlocksResource {
responses = { responses = {
@ApiResponse( @ApiResponse(
description = "the block", description = "the block",
content = @Content(schema = @Schema(implementation = BlockData.class)), content = @Content(schema = @Schema(implementation = BlockWithTransactions.class)),
extensions = { extensions = {
@Extension(name = "translation", properties = { @Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description") @ExtensionProperty(name="description.key", value="success_response:description")
@ -124,18 +118,15 @@ public class BlocksResource {
) )
} }
) )
public BlockData getFirstBlock() { public BlockWithTransactions getFirstBlock(@Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) {
Security.checkApiCallAllowed("GET blocks/first", request); try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().fromHeight(1);
try (final Repository repository = RepositoryManager.getRepository()) { return new BlockWithTransactions(repository, blockData, includeTransactions);
BlockData blockData = repository.getBlockRepository().fromHeight(1);
return blockData;
} catch (ApiException e) { } catch (ApiException e) {
throw e; throw e;
} catch (Exception e) { } catch (DataException e) {
throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e); throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
} }
} }
@GET @GET
@ -150,7 +141,7 @@ public class BlocksResource {
responses = { responses = {
@ApiResponse( @ApiResponse(
description = "the block", description = "the block",
content = @Content(schema = @Schema(implementation = BlockData.class)), content = @Content(schema = @Schema(implementation = BlockWithTransactions.class)),
extensions = { extensions = {
@Extension(name = "translation", properties = { @Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description") @ExtensionProperty(name="description.key", value="success_response:description")
@ -159,24 +150,21 @@ public class BlocksResource {
) )
} }
) )
public BlockData getLastBlock() { public BlockWithTransactions getLastBlock(@Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) {
Security.checkApiCallAllowed("GET blocks/last", request); try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().getLastBlock();
try (final Repository repository = RepositoryManager.getRepository()) { return new BlockWithTransactions(repository, blockData, includeTransactions);
BlockData blockData = repository.getBlockRepository().getLastBlock();
return blockData;
} catch (ApiException e) { } catch (ApiException e) {
throw e; throw e;
} catch (Exception e) { } catch (DataException e) {
throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e); throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
} }
} }
@GET @GET
@Path("/child/{signature}") @Path("/child/{signature}")
@Operation( @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", description = "returns the child block of the block that matches the given signature",
extensions = { extensions = {
@Extension(name = "translation", properties = { @Extension(name = "translation", properties = {
@ -190,7 +178,7 @@ public class BlocksResource {
responses = { responses = {
@ApiResponse( @ApiResponse(
description = "the block", description = "the block",
content = @Content(schema = @Schema(implementation = BlockData.class)), content = @Content(schema = @Schema(implementation = BlockWithTransactions.class)),
extensions = { extensions = {
@Extension(name = "translation", properties = { @Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description") @ExtensionProperty(name="description.key", value="success_response:description")
@ -199,13 +187,11 @@ public class BlocksResource {
) )
} }
) )
public BlockData getChild(@PathParam("signature") String signature) { public BlockWithTransactions getChild(@PathParam("signature") String signature, @Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) {
Security.checkApiCallAllowed("GET blocks/child", request); // Decode signature
// decode signature
byte[] signatureBytes; byte[] signatureBytes;
try { try {
signatureBytes = Base58.decode(signature); signatureBytes = Base64.getDecoder().decode(signature);
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
throw this.apiErrorFactory.createError(ApiError.INVALID_SIGNATURE, e); throw this.apiErrorFactory.createError(ApiError.INVALID_SIGNATURE, e);
} }
@ -213,17 +199,17 @@ public class BlocksResource {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().fromSignature(signatureBytes); BlockData blockData = repository.getBlockRepository().fromSignature(signatureBytes);
// check if block exists // Check block exists
if(blockData == null) if(blockData == null)
throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS); throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS);
BlockData childBlockData = repository.getBlockRepository().fromReference(signatureBytes); BlockData childBlockData = repository.getBlockRepository().fromReference(signatureBytes);
// check if child exists // Check child exists
if(childBlockData == null) if(childBlockData == null)
throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS); throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS);
return childBlockData; return new BlockWithTransactions(repository, childBlockData, includeTransactions);
} catch (ApiException e) { } catch (ApiException e) {
throw e; throw e;
} catch (DataException e) { } catch (DataException e) {
@ -252,18 +238,15 @@ public class BlocksResource {
} }
) )
public BigDecimal getGeneratingBalance() { 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); Block block = new Block(repository, blockData);
return block.calcNextBlockGeneratingBalance(); return block.calcNextBlockGeneratingBalance();
} catch (ApiException e) { } catch (ApiException e) {
throw e; throw e;
} catch (Exception e) { } catch (DataException e) {
throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e); throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
} }
} }
@GET @GET
@ -292,34 +275,28 @@ public class BlocksResource {
} }
) )
public BigDecimal getGeneratingBalance(@PathParam("signature") String signature) { public BigDecimal getGeneratingBalance(@PathParam("signature") String signature) {
Security.checkApiCallAllowed("GET blocks/generatingbalance", request); // Decode signature
// decode signature
byte[] signatureBytes; byte[] signatureBytes;
try try {
{ signatureBytes = Base64.getDecoder().decode(signature);
signatureBytes = Base58.decode(signature); } catch (NumberFormatException e) {
} throw this.apiErrorFactory.createError(ApiError.INVALID_SIGNATURE, e);
catch(Exception e)
{
throw this.apiErrorFactory.createError(ApiError.INVALID_SIGNATURE, e);
} }
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().fromSignature(signatureBytes); BlockData blockData = repository.getBlockRepository().fromSignature(signatureBytes);
// check if block exists // Check block exists
if(blockData == null) if (blockData == null)
throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS); throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS);
Block block = new Block(repository, blockData); Block block = new Block(repository, blockData);
return block.calcNextBlockGeneratingBalance(); return block.calcNextBlockGeneratingBalance();
} catch (ApiException e) { } catch (ApiException e) {
throw e; throw e;
} catch (Exception e) { } catch (DataException e) {
throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e); throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
} }
} }
@GET @GET
@ -343,17 +320,14 @@ public class BlocksResource {
} }
) )
public long getTimePerBlock() { 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()); return Block.calcForgingDelay(blockData.getGeneratingBalance());
} catch (ApiException e) { } catch (ApiException e) {
throw e; throw e;
} catch (Exception e) { } catch (DataException e) {
throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e); throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
} }
} }
@GET @GET
@ -377,8 +351,6 @@ public class BlocksResource {
} }
) )
public long getTimePerBlock(@PathParam("generating") BigDecimal generatingbalance) { public long getTimePerBlock(@PathParam("generating") BigDecimal generatingbalance) {
Security.checkApiCallAllowed("GET blocks/time", request);
return Block.calcForgingDelay(generatingbalance); return Block.calcForgingDelay(generatingbalance);
} }
@ -403,16 +375,13 @@ public class BlocksResource {
} }
) )
public int getHeight() { 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) { } catch (ApiException e) {
throw e; throw e;
} catch (Exception e) { } catch (DataException e) {
throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e); throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
} }
} }
@GET @GET
@ -441,33 +410,27 @@ public class BlocksResource {
} }
) )
public int getHeight(@PathParam("signature") String signature) { public int getHeight(@PathParam("signature") String signature) {
Security.checkApiCallAllowed("GET blocks/height", request); // Decode signature
// decode signature
byte[] signatureBytes; byte[] signatureBytes;
try try {
{ signatureBytes = Base64.getDecoder().decode(signature);
signatureBytes = Base58.decode(signature); } catch (NumberFormatException e) {
} throw this.apiErrorFactory.createError(ApiError.INVALID_SIGNATURE, e);
catch(Exception e)
{
throw this.apiErrorFactory.createError(ApiError.INVALID_SIGNATURE, e);
} }
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().fromSignature(signatureBytes); BlockData blockData = repository.getBlockRepository().fromSignature(signatureBytes);
// check if block exists // Check block exists
if(blockData == null) if (blockData == null)
throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS); throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS);
return blockData.getHeight(); return blockData.getHeight();
} catch (ApiException e) { } catch (ApiException e) {
throw e; throw e;
} catch (Exception e) { } catch (DataException e) {
throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e); throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
} }
} }
@GET @GET
@ -486,7 +449,7 @@ public class BlocksResource {
responses = { responses = {
@ApiResponse( @ApiResponse(
description = "the block", description = "the block",
content = @Content(schema = @Schema(implementation = BlockData.class)), content = @Content(schema = @Schema(implementation = BlockWithTransactions.class)),
extensions = { extensions = {
@Extension(name = "translation", properties = { @Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description") @ExtensionProperty(name="description.key", value="success_response:description")
@ -495,22 +458,15 @@ public class BlocksResource {
) )
} }
) )
public BlockData getbyHeight(@PathParam("height") int height) { public BlockWithTransactions getbyHeight(@PathParam("height") int height, @Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) {
Security.checkApiCallAllowed("GET blocks/byheight", request); try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().fromHeight(height);
try (final Repository repository = RepositoryManager.getRepository()) { return new BlockWithTransactions(repository, blockData, includeTransactions);
BlockData blockData = repository.getBlockRepository().fromHeight(height);
// check if block exists
if(blockData == null)
throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS);
return blockData;
} catch (ApiException e) { } catch (ApiException e) {
throw e; throw e;
} catch (Exception e) { } catch (DataException e) {
throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e); throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
} }
} }
} }

View File

@ -1,12 +1,22 @@
package api; package api;
import java.net.InetAddress;
import java.net.UnknownHostException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
public class Security { public class Security {
public static void checkApiCallAllowed(final String messageToDisplay, HttpServletRequest request) { // TODO: replace with proper authentication
// TODO 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);
} }
} }

View File

@ -2,6 +2,8 @@ package api;
import globalization.Translator; import globalization.Translator;
import io.swagger.v3.oas.annotations.Operation; 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.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty; import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.media.ArraySchema; 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.Path;
import javax.ws.rs.PathParam; import javax.ws.rs.PathParam;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context; import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import data.transaction.GenesisTransactionData;
import data.transaction.PaymentTransactionData;
import data.transaction.TransactionData; import data.transaction.TransactionData;
import repository.DataException; import repository.DataException;
import repository.Repository; import repository.Repository;
@ -56,6 +61,9 @@ public class TransactionsResource {
@Operation( @Operation(
summary = "Fetch transactions involving address", summary = "Fetch transactions involving address",
description = "Returns list of transactions", description = "Returns list of transactions",
parameters = {
@Parameter(in = ParameterIn.PATH, name = "address", description = "Account's address", schema = @Schema(type = "string"))
},
extensions = { extensions = {
@Extension(name = "translation", properties = { @Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="GET block:signature"), @ExtensionProperty(name="path", value="GET block:signature"),
@ -77,9 +85,7 @@ public class TransactionsResource {
) )
} }
) )
public List<TransactionData> getAddressTransactions(@PathParam("address") String address) { public List<TransactionData> getAddressTransactions(@PathParam("address") String address, @Parameter(ref = "limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) {
Security.checkApiCallAllowed("GET transactions/address", request);
if (!Crypto.isValidAddress(address)) if (!Crypto.isValidAddress(address))
throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS); throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS);
@ -89,6 +95,9 @@ public class TransactionsResource {
List<byte[]> signatures = txRepo.getAllSignaturesInvolvingAddress(address); List<byte[]> signatures = txRepo.getAllSignaturesInvolvingAddress(address);
// Pagination would take effect here (or as part of the repository access) // 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 // Expand signatures to transactions
List<TransactionData> transactions = new ArrayList<TransactionData>(signatures.size()); List<TransactionData> transactions = new ArrayList<TransactionData>(signatures.size());
@ -101,13 +110,12 @@ public class TransactionsResource {
} catch (DataException e) { } catch (DataException e) {
throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e); throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
} }
} }
@GET @GET
@Path("/block/{signature}") @Path("/block/{signature}")
@Operation( @Operation(
summary = "Fetch transactions via block signature", summary = "Fetch transactions using block signature",
description = "Returns list of transactions", description = "Returns list of transactions",
extensions = { extensions = {
@Extension(name = "translation", properties = { @Extension(name = "translation", properties = {
@ -121,7 +129,9 @@ public class TransactionsResource {
responses = { responses = {
@ApiResponse( @ApiResponse(
description = "list of transactions", 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 = { extensions = {
@Extension(name = "translation", properties = { @Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description") @ExtensionProperty(name="description.key", value="success_response:description")
@ -130,9 +140,7 @@ public class TransactionsResource {
) )
} }
) )
public List<TransactionData> getBlockTransactions(@PathParam("signature") String signature) { public List<TransactionData> getBlockTransactions(@PathParam("signature") String signature, @Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) {
Security.checkApiCallAllowed("GET transactions/block", request);
// decode signature // decode signature
byte[] signatureBytes; byte[] signatureBytes;
try { try {
@ -148,13 +156,17 @@ public class TransactionsResource {
if(transactions == null) if(transactions == null)
throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS); 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; return transactions;
} catch (ApiException e) { } catch (ApiException e) {
throw e; throw e;
} catch (DataException e) { } catch (DataException e) {
throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e); throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
} }
} }
} }

View File

@ -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);
}
}
}

View File

@ -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<TransactionData> 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());
}
}
}

View File

@ -14,19 +14,31 @@ public class Controller {
private static final Logger LOGGER = LogManager.getLogger(Controller.class); private static final Logger LOGGER = LogManager.getLogger(Controller.class);
private static final String connectionUrl = "jdbc:hsqldb:file:db/test;create=true"; 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 final Object shutdownLock = new Object();
private static boolean isStopping = false; 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 up...");
LOGGER.info("Starting repository"); LOGGER.info("Starting repository");
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(connectionUrl); try {
RepositoryManager.setRepositoryFactory(repositoryFactory); 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"); LOGGER.info("Starting API");
ApiService apiService = ApiService.getInstance(); try {
apiService.start(); ApiService apiService = ApiService.getInstance();
apiService.start();
} catch (Exception e) {
LOGGER.error("Unable to start API", e);
System.exit(1);
}
Runtime.getRuntime().addShutdownHook(new Thread() { Runtime.getRuntime().addShutdownHook(new Thread() {
@Override @Override

View File

@ -29,8 +29,7 @@ public class BlockData implements Serializable {
private BigDecimal atFees; private BigDecimal atFees;
// necessary for JAX-RS serialization // necessary for JAX-RS serialization
@SuppressWarnings("unused") protected BlockData() {
private BlockData() {
} }
public BlockData(int version, byte[] reference, int transactionCount, BigDecimal totalFees, byte[] transactionsSignature, Integer height, long timestamp, public BlockData(int version, byte[] reference, int transactionCount, BigDecimal totalFees, byte[] transactionsSignature, Integer height, long timestamp,

View File

@ -2,9 +2,16 @@ package data.transaction;
import java.math.BigDecimal; 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.account.GenesisAccount;
import qora.transaction.Transaction.TransactionType; 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 { public class GenesisTransactionData extends TransactionData {
// Properties // Properties
@ -13,6 +20,10 @@ public class GenesisTransactionData extends TransactionData {
// Constructors // Constructors
// For JAX-RS
protected GenesisTransactionData() {
}
public GenesisTransactionData(String recipient, BigDecimal amount, long timestamp, byte[] signature) { public GenesisTransactionData(String recipient, BigDecimal amount, long timestamp, byte[] signature) {
// Zero fee // Zero fee
super(TransactionType.GENESIS, BigDecimal.ZERO, GenesisAccount.PUBLIC_KEY, timestamp, null, signature); super(TransactionType.GENESIS, BigDecimal.ZERO, GenesisAccount.PUBLIC_KEY, timestamp, null, signature);

View File

@ -2,8 +2,15 @@ package data.transaction;
import java.math.BigDecimal; 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; 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 { public class PaymentTransactionData extends TransactionData {
// Properties // Properties
@ -13,6 +20,10 @@ public class PaymentTransactionData extends TransactionData {
// Constructors // Constructors
// For JAX-RS
protected PaymentTransactionData() {
}
public PaymentTransactionData(byte[] senderPublicKey, String recipient, BigDecimal amount, BigDecimal fee, long timestamp, byte[] reference, public PaymentTransactionData(byte[] senderPublicKey, String recipient, BigDecimal amount, BigDecimal fee, long timestamp, byte[] reference,
byte[] signature) { byte[] signature) {
super(TransactionType.PAYMENT, fee, senderPublicKey, timestamp, reference, signature); super(TransactionType.PAYMENT, fee, senderPublicKey, timestamp, reference, signature);

View File

@ -23,6 +23,10 @@ public abstract class TransactionData {
// Constructors // Constructors
// For JAX-RS
protected TransactionData() {
}
public TransactionData(TransactionType type, BigDecimal fee, byte[] creatorPublicKey, long timestamp, byte[] reference, byte[] signature) { public TransactionData(TransactionType type, BigDecimal fee, byte[] creatorPublicKey, long timestamp, byte[] reference, byte[] signature) {
this.fee = fee; this.fee = fee;
this.type = type; this.type = type;

View File

@ -1,7 +1,6 @@
package qora.account; package qora.account;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List; import java.util.List;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;

View File

@ -20,6 +20,12 @@ public class HSQLDBRepositoryFactory implements RepositoryFactory {
// one-time initialization goes in here // one-time initialization goes in here
this.connectionUrl = connectionUrl; 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 = new JDBCPool();
this.connectionPool.setUrl(this.connectionUrl); this.connectionPool.setUrl(this.connectionUrl);