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>
<properties>
<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>
</properties>
<build>
@ -156,105 +163,144 @@
</repository>
</repositories>
<dependencies>
<!-- HSQLDB for repository -->
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>r5836</version>
<version>${hsqldb.version}</version>
</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>
<groupId>com.googlecode.json-simple</groupId>
<artifactId>json-simple</artifactId>
<version>1.1.1</version>
</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>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.4</version>
</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>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-moxy</artifactId>
<version>2.27</version>
<version>${jersey.version}</version>
</dependency>
<dependency>
<groupId>org.ciyam</groupId>
<artifactId>at</artifactId>
<version>1.0</version>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-multipart</artifactId>
<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>
<groupId>org.hsqldb</groupId>
<artifactId>sqltool</artifactId>
<version>2.4.1</version>
<scope>test</scope>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-jaxrs2-servlet-initializer</artifactId>
<version>${swagger-api.version}</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>swagger-ui</artifactId>
<version>${swagger-ui.version}</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
@ -265,48 +311,11 @@
<artifactId>hamcrest-library</artifactId>
<version>1.3</version>
</dependency>
<dependency>
<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 -->
<!-- BouncyCastle for crypto, including TLS secure networking -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.60</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>
<version>${bouncycastle.version}</version>
</dependency>
</dependencies>
</project>

View File

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

View File

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

View File

@ -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<String, String> 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();

View File

@ -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

View File

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

View File

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

View File

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

View File

@ -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<TransactionData> getAddressTransactions(@PathParam("address") String address) {
Security.checkApiCallAllowed("GET transactions/address", request);
public List<TransactionData> 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<byte[]> 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<TransactionData> transactions = new ArrayList<TransactionData>(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<TransactionData> getBlockTransactions(@PathParam("signature") String signature) {
Security.checkApiCallAllowed("GET transactions/block", request);
public List<TransactionData> 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);
}
}
}

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 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

View File

@ -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,

View File

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

View File

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

View File

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

View File

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

View File

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