diff --git a/pom.xml b/pom.xml
index eb218575..3e58ba75 100644
--- a/pom.xml
+++ b/pom.xml
@@ -49,5 +49,63 @@
commons-net
3.3
+
+ org.hsqldb
+ hsqldb
+ 2.4.0
+ jar
+
+
+ org.glassfish.jersey.core
+ jersey-server
+ 2.27
+
+
+ javax.servlet
+ javax.servlet-api
+ 4.0.1
+
+
+ org.eclipse.jetty
+ jetty-server
+ 9.4.11.v20180605
+ config
+
+
+ org.glassfish.jersey.containers
+ jersey-container-servlet-core
+ 2.27
+
+
+ org.eclipse.jetty
+ jetty-servlet
+ 9.4.11.v20180605
+ jar
+
+
+ org.glassfish.jersey.inject
+ jersey-hk2
+ 2.27
+
+
+ io.swagger.core.v3
+ swagger-jaxrs2
+ 2.0.4
+
+
+ io.swagger.core.v3
+ swagger-jaxrs2-servlet-initializer
+ 2.0.4
+
+
+ org.apache.commons
+ commons-text
+ 1.4
+
+
+ org.glassfish.jersey.media
+ jersey-media-moxy
+ 2.27
+
\ No newline at end of file
diff --git a/src/Start.java b/src/Start.java
new file mode 100644
index 00000000..585f65c6
--- /dev/null
+++ b/src/Start.java
@@ -0,0 +1,25 @@
+
+import api.ApiClient;
+import api.ApiService;
+import repository.DataException;
+import repository.RepositoryFactory;
+import repository.RepositoryManager;
+import repository.hsqldb.HSQLDBRepositoryFactory;
+
+public class Start {
+
+ private static final String connectionUrl = "jdbc:hsqldb:mem:db/test;create=true;close_result=true;sql.strict_exec=true;sql.enforce_names=true;sql.syntax_mys=true";
+
+ public static void main(String args[]) throws DataException {
+ RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(connectionUrl);
+ RepositoryManager.setRepositoryFactory(repositoryFactory);
+
+ ApiService apiService = ApiService.getInstance();
+ apiService.start();
+
+ //// testing the API client
+ //ApiClient client = ApiClient.getInstance();
+ //String test = client.executeCommand("GET blocks/height");
+ //System.out.println(test);
+ }
+}
diff --git a/src/api/ApiClient.java b/src/api/ApiClient.java
new file mode 100644
index 00000000..49d9cd8a
--- /dev/null
+++ b/src/api/ApiClient.java
@@ -0,0 +1,201 @@
+package api;
+
+import globalization.Translator;
+import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource;
+import io.swagger.v3.oas.annotations.Operation;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.ws.rs.Path;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.PATCH;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.Invocation;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import org.apache.commons.lang3.StringUtils;
+import org.glassfish.jersey.client.HttpUrlConnectorProvider;
+import settings.Settings;
+
+public class ApiClient {
+
+ private class HelpString {
+
+ public final Pattern pattern;
+ public final String fullPath;
+ public final String description;
+
+ public HelpString(Pattern pattern, String fullPath, String description) {
+ this.pattern = pattern;
+ this.fullPath = fullPath;
+ this.description = description;
+ }
+ }
+
+ private static final Pattern COMMAND_PATTERN = Pattern.compile("^ *(?GET|POST|PUT|PATCH|DELETE) *(?.*)$");
+ private static final Pattern HELP_COMMAND_PATTERN = Pattern.compile("^ *help *(?.*)$", Pattern.CASE_INSENSITIVE);
+ private static final List> HTTP_METHOD_ANNOTATIONS = Arrays.asList(
+ GET.class,
+ POST.class,
+ PUT.class,
+ PATCH.class,
+ DELETE.class
+ );
+
+ ApiService apiService;
+ private Translator translator;
+ List helpStrings;
+
+ public ApiClient(ApiService apiService, Translator translator) {
+ this.apiService = apiService;
+ this.helpStrings = getHelpStrings(apiService.getResources());
+ }
+
+ //XXX: replace singleton pattern by dependency injection?
+ private static ApiClient instance;
+
+ public static ApiClient getInstance() {
+ if (instance == null) {
+ instance = new ApiClient(ApiService.getInstance(), Translator.getInstance());
+ }
+
+ return instance;
+ }
+
+ private List getHelpStrings(Iterable> resources) {
+ List result = new ArrayList<>();
+
+ // scan each resource class
+ for (Class> resource : resources) {
+ if (OpenApiResource.class.isAssignableFrom(resource)) {
+ continue; // ignore swagger resources
+ }
+ Path resourcePath = resource.getDeclaredAnnotation(Path.class);
+ if (resourcePath == null) {
+ continue;
+ }
+
+ String resourcePathString = resourcePath.value();
+
+ // scan each method
+ for (Method method : resource.getDeclaredMethods()) {
+ Operation operationAnnotation = method.getAnnotation(Operation.class);
+ if (operationAnnotation == null) {
+ continue;
+ }
+
+ String description = operationAnnotation.description();
+
+ Path methodPath = method.getDeclaredAnnotation(Path.class);
+ String methodPathString = (methodPath != null) ? methodPath.value() : "";
+
+ // scan for each potential http method
+ for (Class extends Annotation> restMethodAnnotation : HTTP_METHOD_ANNOTATIONS) {
+ Annotation annotation = method.getDeclaredAnnotation(restMethodAnnotation);
+ if (annotation == null) {
+ continue;
+ }
+
+ HttpMethod httpMethod = annotation.annotationType().getDeclaredAnnotation(HttpMethod.class);
+ String httpMethodString = httpMethod.value();
+
+ String fullPath = httpMethodString + " " + resourcePathString + methodPathString;
+ Pattern pattern = Pattern.compile("^ *(" + httpMethodString + " *)?" + getHelpPatternForPath(resourcePathString + methodPathString));
+ result.add(new HelpString(pattern, fullPath, description));
+ }
+ }
+ }
+
+ // sort by path
+ result.sort((h1, h2) -> h1.fullPath.compareTo(h2.fullPath));
+
+ return result;
+ }
+
+ private String getHelpPatternForPath(String path) {
+ path = path
+ .replaceAll("\\.", "\\.") // escapes "." as "\."
+ .replaceAll("\\{.*?\\}", ".*?"); // replace placeholders "{...}" by the "ungreedy match anything" pattern ".*?"
+
+ // arrange the regex pattern so that it also matches partial
+ StringBuilder result = new StringBuilder();
+ String[] parts = path.split("/");
+ for (int i = 0; i < parts.length; i++) {
+ if (i != 0) {
+ result.append("(/"); // opening bracket
+ }
+ result.append(parts[i]);
+ }
+ for (int i = 0; i < parts.length - 1; i++) {
+ result.append(")?"); // closing bracket
+ }
+ return result.toString();
+ }
+
+ public String executeCommand(String command) {
+ // check if this is a help command
+ Matcher match = HELP_COMMAND_PATTERN.matcher(command);
+ if (match.matches()) {
+ command = match.group("command");
+ StringBuilder result = new StringBuilder();
+
+ boolean showAll = command.trim().equalsIgnoreCase("all");
+ for (HelpString helpString : helpStrings) {
+ if (showAll || helpString.pattern.matcher(command).matches()) {
+ appendHelp(result, helpString);
+ }
+ }
+
+ return result.toString();
+ }
+
+
+ match = COMMAND_PATTERN.matcher(command);
+ if(!match.matches())
+ return this.translator.translate(Locale.getDefault(), "ApiClient: INVALID_COMMAND", "Invalid command! \nType help to get a list of commands.");
+
+ // send the command to the API service
+ String method = match.group("method");
+ String path = match.group("path");
+ String url = String.format("http://127.0.0.1:%d/%s", Settings.getInstance().getRpcPort(), path);
+
+ Client client = ClientBuilder.newClient();
+ client.property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true); // workaround for non-standard HTTP methods like PATCH
+ WebTarget wt = client.target(url);
+ Invocation.Builder builder = wt.request(MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN);
+ Response response = builder.method(method);
+
+ // send back result
+ final String body = response.readEntity(String.class);
+ final int status = response.getStatus();
+ StringBuilder result = new StringBuilder();
+ if(status >= 400) {
+ result.append("HTTP Status ");
+ result.append(status);
+ if(!StringUtils.isBlank(body)) {
+ result.append(": ");
+ result.append(body);
+ }
+ result.append("\nType help to get a list of commands.");
+ } else {
+ result.append(body);
+ }
+ return result.toString();
+ }
+
+ private void appendHelp(StringBuilder builder, HelpString helpString) {
+ builder.append(helpString.fullPath + "\n");
+ builder.append(helpString.description + "\n");
+ }
+}
diff --git a/src/api/ApiError.java b/src/api/ApiError.java
new file mode 100644
index 00000000..471fa9f2
--- /dev/null
+++ b/src/api/ApiError.java
@@ -0,0 +1,120 @@
+
+package api;
+
+public enum ApiError {
+ //COMMON
+ UNKNOWN(0, 500),
+ JSON(1, 400),
+ NO_BALANCE(2, 422),
+ NOT_YET_RELEASED(3, 422),
+
+ //VALIDATION
+ INVALID_SIGNATURE(101, 400),
+ INVALID_ADDRESS(102, 400),
+ INVALID_SEED(103, 400),
+ INVALID_AMOUNT(104, 400),
+ INVALID_FEE(105, 400),
+ INVALID_SENDER(106, 400),
+ INVALID_RECIPIENT(107, 400),
+ INVALID_NAME_LENGTH(108, 400),
+ INVALID_VALUE_LENGTH(109, 400),
+ INVALID_NAME_OWNER(110, 400),
+ INVALID_BUYER(111, 400),
+ INVALID_PUBLIC_KEY(112, 400),
+ INVALID_OPTIONS_LENGTH(113, 400),
+ INVALID_OPTION_LENGTH(114, 400),
+ INVALID_DATA(115, 400),
+ INVALID_DATA_LENGTH(116, 400),
+ INVALID_UPDATE_VALUE(117, 400),
+ KEY_ALREADY_EXISTS(118, 422),
+ KEY_NOT_EXISTS(119, 404),
+ LAST_KEY_IS_DEFAULT_KEY_ERROR(120, 422),
+ FEE_LESS_REQUIRED(121, 422),
+ WALLET_NOT_IN_SYNC(122, 422),
+ INVALID_NETWORK_ADDRESS(123, 404),
+
+ //WALLET
+ WALLET_NO_EXISTS(201, 404),
+ WALLET_ADDRESS_NO_EXISTS(202, 404),
+ WALLET_LOCKED(203, 422),
+ WALLET_ALREADY_EXISTS(204, 422),
+ WALLET_API_CALL_FORBIDDEN_BY_USER(205, 403),
+
+ //BLOCKS
+ BLOCK_NO_EXISTS(301, 404),
+
+ //TRANSACTIONS
+ TRANSACTION_NO_EXISTS(311, 404),
+ PUBLIC_KEY_NOT_FOUND(304, 404),
+
+ //NAMING
+ NAME_NO_EXISTS(401, 404),
+ NAME_ALREADY_EXISTS(402, 422),
+ NAME_ALREADY_FOR_SALE(403, 422),
+ NAME_NOT_LOWER_CASE(404, 422),
+ NAME_SALE_NO_EXISTS(410, 404),
+ BUYER_ALREADY_OWNER(411, 422),
+
+ //POLLS
+ POLL_NO_EXISTS(501, 404),
+ POLL_ALREADY_EXISTS(502, 422),
+ DUPLICATE_OPTION(503, 422),
+ POLL_OPTION_NO_EXISTS(504, 404),
+ ALREADY_VOTED_FOR_THAT_OPTION(505, 422),
+
+ //ASSET
+ INVALID_ASSET_ID(601, 400),
+
+ //NAME PAYMENTS
+ NAME_NOT_REGISTERED(701, 422),
+ NAME_FOR_SALE(702, 422),
+ NAME_WITH_SPACE(703, 422),
+
+ //ATs
+ INVALID_DESC_LENGTH(801, 400),
+ EMPTY_CODE(802, 400),
+ DATA_SIZE(803, 400),
+ NULL_PAGES(804, 400),
+ INVALID_TYPE_LENGTH(805, 400),
+ INVALID_TAGS_LENGTH(806, 400),
+ INVALID_CREATION_BYTES(809, 400),
+
+ //BLOG/Namestorage
+ BODY_EMPTY(901, 400),
+ BLOG_DISABLED(902, 403),
+ NAME_NOT_OWNER(903, 422),
+ TX_AMOUNT(904, 400),
+ BLOG_ENTRY_NO_EXISTS(905, 404),
+ BLOG_EMPTY(906, 404),
+ POSTID_EMPTY(907, 400),
+ POST_NOT_EXISTING(908, 404),
+ COMMENTING_DISABLED(909, 403),
+ COMMENT_NOT_EXISTING(910, 404),
+ INVALID_COMMENT_OWNER(911, 422),
+
+ //Messages
+ MESSAGE_FORMAT_NOT_HEX(1001, 400),
+ MESSAGE_BLANK(1002, 400),
+ NO_PUBLIC_KEY(1003, 422),
+ MESSAGESIZE_EXCEEDED(1004, 400);
+
+ private final int code; // API error code
+ private final int status; // HTTP status code
+
+ private ApiError(int code) {
+ this(code, 400); // defaults to "400 - BAD REQUEST"
+ }
+
+ private ApiError(int code, int status) {
+ this.code = code;
+ this.status = status;
+ }
+
+ int getCode() {
+ return this.code;
+ }
+
+ int getStatus() {
+ return this.status;
+ }
+}
\ No newline at end of file
diff --git a/src/api/ApiErrorFactory.java b/src/api/ApiErrorFactory.java
new file mode 100644
index 00000000..ad7c7f7e
--- /dev/null
+++ b/src/api/ApiErrorFactory.java
@@ -0,0 +1,181 @@
+package api;
+
+import globalization.Translator;
+import java.util.AbstractMap;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+public class ApiErrorFactory {
+
+ private class ErrorMessageEntry {
+
+ String templateKey;
+ String defaultTemplate;
+ AbstractMap.Entry[] templateValues;
+
+ public ErrorMessageEntry(String templateKey, String defaultTemplate, AbstractMap.Entry... templateValues) {
+ this.templateKey = templateKey;
+ this.defaultTemplate = defaultTemplate;
+ this.templateValues = templateValues;
+ }
+ }
+
+ private Translator translator;
+ private Map errorMessages;
+
+ public ApiErrorFactory(Translator translator) {
+ this.translator = translator;
+
+ this.errorMessages = new HashMap();
+
+ //COMMON
+ this.errorMessages.put(ApiError.UNKNOWN, createErrorMessageEntry(ApiError.UNKNOWN, "unknown error"));
+ this.errorMessages.put(ApiError.JSON, createErrorMessageEntry(ApiError.JSON, "failed to parse json message"));
+ this.errorMessages.put(ApiError.NO_BALANCE, createErrorMessageEntry(ApiError.NO_BALANCE, "not enough balance"));
+ this.errorMessages.put(ApiError.NOT_YET_RELEASED, createErrorMessageEntry(ApiError.NOT_YET_RELEASED, "that feature is not yet released"));
+
+ //VALIDATION
+ this.errorMessages.put(ApiError.INVALID_SIGNATURE, createErrorMessageEntry(ApiError.INVALID_SIGNATURE, "invalid signature"));
+ this.errorMessages.put(ApiError.INVALID_ADDRESS, createErrorMessageEntry(ApiError.INVALID_ADDRESS, "invalid address"));
+ this.errorMessages.put(ApiError.INVALID_SEED, createErrorMessageEntry(ApiError.INVALID_SEED, "invalid seed"));
+ this.errorMessages.put(ApiError.INVALID_AMOUNT, createErrorMessageEntry(ApiError.INVALID_AMOUNT, "invalid amount"));
+ this.errorMessages.put(ApiError.INVALID_FEE, createErrorMessageEntry(ApiError.INVALID_FEE, "invalid fee"));
+ this.errorMessages.put(ApiError.INVALID_SENDER, createErrorMessageEntry(ApiError.INVALID_SENDER, "invalid sender"));
+ this.errorMessages.put(ApiError.INVALID_RECIPIENT, createErrorMessageEntry(ApiError.INVALID_RECIPIENT, "invalid recipient"));
+ this.errorMessages.put(ApiError.INVALID_NAME_LENGTH, createErrorMessageEntry(ApiError.INVALID_NAME_LENGTH, "invalid name length"));
+ this.errorMessages.put(ApiError.INVALID_VALUE_LENGTH, createErrorMessageEntry(ApiError.INVALID_VALUE_LENGTH, "invalid value length"));
+ this.errorMessages.put(ApiError.INVALID_NAME_OWNER, createErrorMessageEntry(ApiError.INVALID_NAME_OWNER, "invalid name owner"));
+ this.errorMessages.put(ApiError.INVALID_BUYER, createErrorMessageEntry(ApiError.INVALID_BUYER, "invalid buyer"));
+ this.errorMessages.put(ApiError.INVALID_PUBLIC_KEY, createErrorMessageEntry(ApiError.INVALID_PUBLIC_KEY, "invalid public key"));
+ this.errorMessages.put(ApiError.INVALID_OPTIONS_LENGTH, createErrorMessageEntry(ApiError.INVALID_OPTIONS_LENGTH, "invalid options length"));
+ this.errorMessages.put(ApiError.INVALID_OPTION_LENGTH, createErrorMessageEntry(ApiError.INVALID_OPTION_LENGTH, "invalid option length"));
+ this.errorMessages.put(ApiError.INVALID_DATA, createErrorMessageEntry(ApiError.INVALID_DATA, "invalid data"));
+ this.errorMessages.put(ApiError.INVALID_DATA_LENGTH, createErrorMessageEntry(ApiError.INVALID_DATA_LENGTH, "invalid data length"));
+ this.errorMessages.put(ApiError.INVALID_UPDATE_VALUE, createErrorMessageEntry(ApiError.INVALID_UPDATE_VALUE, "invalid update value"));
+ this.errorMessages.put(ApiError.KEY_ALREADY_EXISTS, createErrorMessageEntry(ApiError.KEY_ALREADY_EXISTS, "key already exists, edit is false"));
+ this.errorMessages.put(ApiError.KEY_NOT_EXISTS, createErrorMessageEntry(ApiError.KEY_NOT_EXISTS, "the key does not exist"));
+// TODO
+// this.errorMessages.put(ApiError.LAST_KEY_IS_DEFAULT_KEY_ERROR, createErrorMessageEntry(ApiError.LAST_KEY_IS_DEFAULT_KEY_ERROR,
+// "you can't delete the key \"${key}\" if it is the only key",
+// new AbstractMap.SimpleEntry("key", Qorakeys.DEFAULT.toString())));
+ this.errorMessages.put(ApiError.FEE_LESS_REQUIRED, createErrorMessageEntry(ApiError.FEE_LESS_REQUIRED, "fee less required"));
+ this.errorMessages.put(ApiError.WALLET_NOT_IN_SYNC, createErrorMessageEntry(ApiError.WALLET_NOT_IN_SYNC, "wallet needs to be synchronized"));
+ this.errorMessages.put(ApiError.INVALID_NETWORK_ADDRESS, createErrorMessageEntry(ApiError.INVALID_NETWORK_ADDRESS, "invalid network address"));
+
+ //WALLET
+ this.errorMessages.put(ApiError.WALLET_NO_EXISTS, createErrorMessageEntry(ApiError.WALLET_NO_EXISTS, "wallet does not exist"));
+ this.errorMessages.put(ApiError.WALLET_ADDRESS_NO_EXISTS, createErrorMessageEntry(ApiError.WALLET_ADDRESS_NO_EXISTS, "address does not exist in wallet"));
+ this.errorMessages.put(ApiError.WALLET_LOCKED, createErrorMessageEntry(ApiError.WALLET_LOCKED, "wallet is locked"));
+ this.errorMessages.put(ApiError.WALLET_ALREADY_EXISTS, createErrorMessageEntry(ApiError.WALLET_ALREADY_EXISTS, "wallet already exists"));
+ this.errorMessages.put(ApiError.WALLET_API_CALL_FORBIDDEN_BY_USER, createErrorMessageEntry(ApiError.WALLET_API_CALL_FORBIDDEN_BY_USER, "user denied api call"));
+
+ //BLOCK
+ this.errorMessages.put(ApiError.BLOCK_NO_EXISTS, createErrorMessageEntry(ApiError.BLOCK_NO_EXISTS, "block does not exist"));
+
+ //TRANSACTIONS
+ this.errorMessages.put(ApiError.TRANSACTION_NO_EXISTS, createErrorMessageEntry(ApiError.TRANSACTION_NO_EXISTS, "transactions does not exist"));
+ this.errorMessages.put(ApiError.PUBLIC_KEY_NOT_FOUND, createErrorMessageEntry(ApiError.PUBLIC_KEY_NOT_FOUND, "public key not found"));
+
+ //NAMING
+ this.errorMessages.put(ApiError.NAME_NO_EXISTS, createErrorMessageEntry(ApiError.NAME_NO_EXISTS, "name does not exist"));
+ this.errorMessages.put(ApiError.NAME_ALREADY_EXISTS, createErrorMessageEntry(ApiError.NAME_ALREADY_EXISTS, "name already exists"));
+ this.errorMessages.put(ApiError.NAME_ALREADY_FOR_SALE, createErrorMessageEntry(ApiError.NAME_ALREADY_FOR_SALE, "name already for sale"));
+ this.errorMessages.put(ApiError.NAME_NOT_LOWER_CASE, createErrorMessageEntry(ApiError.NAME_NOT_LOWER_CASE, "name must be lower case"));
+ this.errorMessages.put(ApiError.NAME_SALE_NO_EXISTS, createErrorMessageEntry(ApiError.NAME_SALE_NO_EXISTS, "namesale does not exist"));
+ this.errorMessages.put(ApiError.BUYER_ALREADY_OWNER, createErrorMessageEntry(ApiError.BUYER_ALREADY_OWNER, "buyer is already owner"));
+
+ //POLLS
+ this.errorMessages.put(ApiError.POLL_NO_EXISTS, createErrorMessageEntry(ApiError.POLL_NO_EXISTS, "poll does not exist"));
+ this.errorMessages.put(ApiError.POLL_ALREADY_EXISTS, createErrorMessageEntry(ApiError.POLL_ALREADY_EXISTS, "poll already exists"));
+ this.errorMessages.put(ApiError.DUPLICATE_OPTION, createErrorMessageEntry(ApiError.DUPLICATE_OPTION, "not all options are unique"));
+ this.errorMessages.put(ApiError.POLL_OPTION_NO_EXISTS, createErrorMessageEntry(ApiError.POLL_OPTION_NO_EXISTS, "option does not exist"));
+ this.errorMessages.put(ApiError.ALREADY_VOTED_FOR_THAT_OPTION, createErrorMessageEntry(ApiError.ALREADY_VOTED_FOR_THAT_OPTION, "already voted for that option"));
+
+ //ASSETS
+ this.errorMessages.put(ApiError.INVALID_ASSET_ID, createErrorMessageEntry(ApiError.INVALID_ASSET_ID, "invalid asset id"));
+
+ //NAME PAYMENTS
+// TODO
+// this.errorMessages.put(ApiError.NAME_NOT_REGISTERED, createErrorMessageEntry(ApiError.NAME_NOT_REGISTERED, NameResult.NAME_NOT_REGISTERED.getStatusMessage()));
+// this.errorMessages.put(ApiError.NAME_FOR_SALE, createErrorMessageEntry(ApiError.NAME_FOR_SALE, NameResult.NAME_FOR_SALE.getStatusMessage()));
+// this.errorMessages.put(ApiError.NAME_WITH_SPACE, createErrorMessageEntry(ApiError.NAME_WITH_SPACE, NameResult.NAME_WITH_SPACE.getStatusMessage()));
+ //AT
+ this.errorMessages.put(ApiError.INVALID_CREATION_BYTES, createErrorMessageEntry(ApiError.INVALID_CREATION_BYTES, "error in creation bytes"));
+// TODO
+// this.errorMessages.put(ApiError.INVALID_DESC_LENGTH, createErrorMessageEntry(ApiError.INVALID_DESC_LENGTH,
+// "invalid description length. max length ${MAX_LENGTH}",
+// new AbstractMap.SimpleEntry("MAX_LENGTH", AT_Constants.DESC_MAX_LENGTH));
+ this.errorMessages.put(ApiError.EMPTY_CODE, createErrorMessageEntry(ApiError.EMPTY_CODE, "code is empty"));
+ this.errorMessages.put(ApiError.DATA_SIZE, createErrorMessageEntry(ApiError.DATA_SIZE, "invalid data length"));
+ this.errorMessages.put(ApiError.INVALID_TYPE_LENGTH, createErrorMessageEntry(ApiError.INVALID_TYPE_LENGTH, "invalid type length"));
+ this.errorMessages.put(ApiError.INVALID_TAGS_LENGTH, createErrorMessageEntry(ApiError.INVALID_TAGS_LENGTH, "invalid tags length"));
+ this.errorMessages.put(ApiError.NULL_PAGES, createErrorMessageEntry(ApiError.NULL_PAGES, "invalid pages"));
+
+ //BLOG
+ this.errorMessages.put(ApiError.BODY_EMPTY, createErrorMessageEntry(ApiError.BODY_EMPTY, "invalid body it must not be empty"));
+ this.errorMessages.put(ApiError.BLOG_DISABLED, createErrorMessageEntry(ApiError.BLOG_DISABLED, "this blog is disabled"));
+ this.errorMessages.put(ApiError.NAME_NOT_OWNER, createErrorMessageEntry(ApiError.NAME_NOT_OWNER, "the creator address does not own the author name"));
+// this.errorMessages.put(ApiError.TX_AMOUNT, createErrorMessageEntry(ApiError.TX_AMOUNT,
+// "the data size is too large - currently only ${BATCH_TX_AMOUNT} arbitrary transactions are allowed at once!",
+// new AbstractMap.SimpleEntry("BATCH_TX_AMOUNT", BATCH_TX_AMOUNT)));
+ this.errorMessages.put(ApiError.BLOG_ENTRY_NO_EXISTS, createErrorMessageEntry(ApiError.BLOG_ENTRY_NO_EXISTS, "transaction with this signature contains no entries!"));
+ this.errorMessages.put(ApiError.BLOG_EMPTY, createErrorMessageEntry(ApiError.BLOG_EMPTY, "this blog is empty"));
+ this.errorMessages.put(ApiError.POSTID_EMPTY, createErrorMessageEntry(ApiError.POSTID_EMPTY, "the attribute postid is empty! this is the signature of the post you want to comment"));
+ this.errorMessages.put(ApiError.POST_NOT_EXISTING, createErrorMessageEntry(ApiError.POST_NOT_EXISTING, "for the given postid no blogpost to comment was found"));
+ this.errorMessages.put(ApiError.COMMENTING_DISABLED, createErrorMessageEntry(ApiError.COMMENTING_DISABLED, "commenting is for this blog disabled"));
+ this.errorMessages.put(ApiError.COMMENT_NOT_EXISTING, createErrorMessageEntry(ApiError.COMMENT_NOT_EXISTING, "for the given signature no comment was found"));
+ this.errorMessages.put(ApiError.INVALID_COMMENT_OWNER, createErrorMessageEntry(ApiError.INVALID_COMMENT_OWNER, "invalid comment owner"));
+
+ //MESSAGES
+ this.errorMessages.put(ApiError.MESSAGE_FORMAT_NOT_HEX, createErrorMessageEntry(ApiError.MESSAGE_FORMAT_NOT_HEX, "the Message format is not hex - correct the text or use isTextMessage = true"));
+ this.errorMessages.put(ApiError.MESSAGE_BLANK, createErrorMessageEntry(ApiError.MESSAGE_BLANK, "The message attribute is missing or content is blank"));
+ this.errorMessages.put(ApiError.NO_PUBLIC_KEY, createErrorMessageEntry(ApiError.NO_PUBLIC_KEY, "The recipient has not yet performed any action in the blockchain.\nYou can't send an encrypted message to him."));
+ this.errorMessages.put(ApiError.MESSAGESIZE_EXCEEDED, createErrorMessageEntry(ApiError.MESSAGESIZE_EXCEEDED, "Message size exceeded!"));
+
+ }
+
+ //XXX: replace singleton pattern by dependency injection?
+ private static ApiErrorFactory instance;
+
+ public static ApiErrorFactory getInstance() {
+ if (instance == null) {
+ instance = new ApiErrorFactory(Translator.getInstance());
+ }
+
+ return instance;
+ }
+
+ private ErrorMessageEntry createErrorMessageEntry(ApiError errorCode, String defaultTemplate, AbstractMap.SimpleEntry... templateValues) {
+ String templateKey = String.format("%s: ApiError.%s message", ApiErrorFactory.class.getSimpleName(), errorCode.name());
+ return new ErrorMessageEntry(templateKey, defaultTemplate, templateValues);
+ }
+
+ public ApiException createError(ApiError error) {
+ return createError(error, null);
+ }
+
+ public ApiException createError(ApiError error, Throwable throwable) {
+ Locale locale = Locale.ENGLISH; // XXX: should this be in local language?
+
+ // TODO: handle AT errors
+// old AT error handling
+// JSONObject jsonObject = new JSONObject();
+// jsonObject.put("error", error);
+// if ( error > Transaction.AT_ERROR )
+// {
+// jsonObject.put("message", AT_Error.getATError(error - Transaction.AT_ERROR) );
+// }
+// else
+// {
+// jsonObject.put("message", this.errorMessages.get(error));
+// }
+//
+//
+// return new WebApplicationException(Response.status(Response.Status.BAD_REQUEST).entity(jsonObject.toJSONString()).build());
+ ErrorMessageEntry errorMessage = this.errorMessages.get(error);
+ String message = this.translator.translate(locale, errorMessage.templateKey, errorMessage.defaultTemplate, errorMessage.templateValues);
+
+ return new ApiException(error.getStatus(), error.getCode(), message, throwable);
+ }
+}
diff --git a/src/api/ApiErrorMessage.java b/src/api/ApiErrorMessage.java
new file mode 100644
index 00000000..0c84fe4a
--- /dev/null
+++ b/src/api/ApiErrorMessage.java
@@ -0,0 +1,22 @@
+package api;
+
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+
+@XmlRootElement
+public class ApiErrorMessage {
+
+ @XmlElement(name = "error")
+ public int error;
+
+ @XmlElement(name = "message")
+ public String message;
+
+ ApiErrorMessage() {
+ }
+
+ ApiErrorMessage(int errorCode, String message) {
+ this.error = errorCode;
+ this.message = message;
+ }
+}
diff --git a/src/api/ApiException.java b/src/api/ApiException.java
new file mode 100644
index 00000000..b8419cc7
--- /dev/null
+++ b/src/api/ApiException.java
@@ -0,0 +1,36 @@
+package api;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
+public class ApiException extends WebApplicationException {
+ // HTTP status code
+
+ int status;
+
+ // API error code
+ int error;
+
+ String message;
+
+ public ApiException(int status, int error, String message) {
+ this(status, error, message, null);
+ }
+
+ public ApiException(int status, int error, String message, Throwable throwable) {
+ super(
+ message,
+ throwable,
+ Response.status(Status.fromStatusCode(status))
+ .entity(new ApiErrorMessage(error, message))
+ .type(MediaType.APPLICATION_JSON)
+ .build()
+ );
+
+ this.status = status;
+ this.error = error;
+ this.message = message;
+ }
+}
diff --git a/src/api/ApiService.java b/src/api/ApiService.java
new file mode 100644
index 00000000..afa236ec
--- /dev/null
+++ b/src/api/ApiService.java
@@ -0,0 +1,82 @@
+package api;
+
+import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.handler.InetAccessHandler;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.servlet.ServletContainer;
+
+import settings.Settings;
+
+public class ApiService {
+
+ private final Server server;
+ private final Set> resources;
+
+ public ApiService() {
+ // resources to register
+ this.resources = new HashSet>();
+ this.resources.add(BlocksResource.class);
+ this.resources.add(OpenApiResource.class); // swagger
+ ResourceConfig config = new ResourceConfig(this.resources);
+
+ // create RPC server
+ this.server = new Server(Settings.getInstance().getRpcPort());
+
+ // whitelist
+ InetAccessHandler accessHandler = new InetAccessHandler();
+ for (String pattern : Settings.getInstance().getRpcAllowed()) {
+ accessHandler.include(pattern);
+ }
+ this.server.setHandler(accessHandler);
+
+ // context
+ ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
+ context.setContextPath("/");
+ accessHandler.setHandler(context);
+
+ // API servlet
+ ServletContainer container = new ServletContainer(config);
+ ServletHolder apiServlet = new ServletHolder(container);
+ apiServlet.setInitOrder(1);
+ context.addServlet(apiServlet, "/*");
+ }
+
+ //XXX: replace singleton pattern by dependency injection?
+ private static ApiService instance;
+
+ public static ApiService getInstance() {
+ if (instance == null) {
+ instance = new ApiService();
+ }
+
+ return instance;
+ }
+
+ Iterable> getResources() {
+ return resources;
+ }
+
+ public void start() {
+ try {
+ //START RPC
+ server.start();
+ } catch (Exception e) {
+ //FAILED TO START RPC
+ }
+ }
+
+ public void stop() {
+ try {
+ //STOP RPC
+ server.stop();
+ } catch (Exception e) {
+ //FAILED TO STOP RPC
+ }
+ }
+}
diff --git a/src/api/BlocksResource.java b/src/api/BlocksResource.java
new file mode 100644
index 00000000..ae79734d
--- /dev/null
+++ b/src/api/BlocksResource.java
@@ -0,0 +1,312 @@
+package api;
+
+import globalization.Translator;
+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 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;
+
+import repository.Repository;
+import repository.RepositoryManager;
+
+@Path("blocks")
+@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
+public class BlocksResource {
+
+ @Context
+ HttpServletRequest request;
+
+ private ApiErrorFactory apiErrorFactory;
+
+ public BlocksResource() {
+ this(new ApiErrorFactory(new Translator()));
+ }
+
+ public BlocksResource(ApiErrorFactory apiErrorFactory) {
+ this.apiErrorFactory = apiErrorFactory;
+ }
+
+ @GET
+ @Operation(
+ description = "Returns an array of the 50 last blocks generated by your accounts",
+ responses = {
+ @ApiResponse(
+ description = "The blocks"
+ //content = @Content(schema = @Schema(implementation = ???))
+ ),
+ @ApiResponse(
+ responseCode = "422",
+ description = "Wallet does not exist"
+ )
+ }
+ )
+ public String getBlocks() {
+ Security.checkApiCallAllowed("GET blocks", request);
+
+ throw new UnsupportedOperationException();
+ }
+
+ @GET
+ @Path("/address/{address}")
+ @Operation(
+ description = "Returns an array of the 50 last blocks generated by a specific address in your wallet",
+ responses = {
+ @ApiResponse(
+ description = "The blocks"
+ //content = @Content(schema = @Schema(implementation = ???))
+ ),
+ @ApiResponse(
+ responseCode = "400",
+ description = "Invalid address"
+ ),
+ @ApiResponse(
+ responseCode = "422",
+ description = "Wallet does not exist"
+ ),
+ @ApiResponse(
+ responseCode = "422",
+ description = "Address does not exist in wallet"
+ )
+ }
+ )
+ public String getBlocks(@PathParam("address") String address) {
+ Security.checkApiCallAllowed("GET blocks/address/" + address, request);
+
+ throw new UnsupportedOperationException();
+ }
+
+ @GET
+ @Path("/{signature}")
+ @Operation(
+ description = "Returns the block that matches the given signature",
+ responses = {
+ @ApiResponse(
+ description = "The block"
+ //content = @Content(schema = @Schema(implementation = ???))
+ ),
+ @ApiResponse(
+ responseCode = "400",
+ description = "Invalid signature"
+ ),
+ @ApiResponse(
+ responseCode = "422",
+ description = "Block does not exist"
+ )
+ }
+ )
+ public String getBlock(@PathParam("signature") String signature) {
+ Security.checkApiCallAllowed("GET blocks", request);
+
+ throw new UnsupportedOperationException();
+ }
+
+ @GET
+ @Path("/first")
+ @Operation(
+ description = "Returns the genesis block",
+ responses = {
+ @ApiResponse(
+ description = "The block"
+ //content = @Content(schema = @Schema(implementation = ???))
+ )
+ }
+ )
+ public String getFirstBlock() {
+ Security.checkApiCallAllowed("GET blocks/first", request);
+
+ throw new UnsupportedOperationException();
+ }
+
+ @GET
+ @Path("/last")
+ @Operation(
+ description = "Returns the last valid block",
+ responses = {
+ @ApiResponse(
+ description = "The block"
+ //content = @Content(schema = @Schema(implementation = ???))
+ )
+ }
+ )
+ public String getLastBlock() {
+ Security.checkApiCallAllowed("GET blocks/last", request);
+
+ throw new UnsupportedOperationException();
+ }
+
+ @GET
+ @Path("/child/{signature}")
+ @Operation(
+ description = "Returns the child block of the block that matches the given signature",
+ responses = {
+ @ApiResponse(
+ description = "The block"
+ //content = @Content(schema = @Schema(implementation = ???))
+ ),
+ @ApiResponse(
+ responseCode = "400",
+ description = "Invalid signature"
+ ),
+ @ApiResponse(
+ responseCode = "422",
+ description = "Block does not exist"
+ )
+ }
+ )
+ public String getChild(@PathParam("signature") String signature) {
+ Security.checkApiCallAllowed("GET blocks/child", request);
+
+ throw new UnsupportedOperationException();
+ }
+
+ @GET
+ @Path("/generatingbalance")
+ @Operation(
+ description = "Calculates the generating balance of the block that will follow the last block",
+ responses = {
+ @ApiResponse(
+ description = "The generating balance",
+ content = @Content(schema = @Schema(implementation = long.class))
+ )
+ }
+ )
+ public long getGeneratingBalance() {
+ Security.checkApiCallAllowed("GET blocks/generatingbalance", request);
+
+ throw new UnsupportedOperationException();
+ }
+
+ @GET
+ @Path("/generatingbalance/{signature}")
+ @Operation(
+ description = "Calculates the generating balance of the block that will follow the block that matches the signature",
+ responses = {
+ @ApiResponse(
+ description = "The block",
+ content = @Content(schema = @Schema(implementation = long.class))
+ ),
+ @ApiResponse(
+ responseCode = "400",
+ description = "Invalid signature"
+ ),
+ @ApiResponse(
+ responseCode = "422",
+ description = "Block does not exist"
+ )
+ }
+ )
+ public long getGeneratingBalance(@PathParam("signature") String signature) {
+ Security.checkApiCallAllowed("GET blocks/generatingbalance", request);
+
+ throw new UnsupportedOperationException();
+ }
+
+ @GET
+ @Path("/time")
+ @Operation(
+ description = "Calculates the time it should take for the network to generate the next block",
+ responses = {
+ @ApiResponse(
+ description = "The time", // in seconds?
+ content = @Content(schema = @Schema(implementation = long.class))
+ )
+ }
+ )
+ public long getTimePerBlock() {
+ Security.checkApiCallAllowed("GET blocks/time", request);
+
+ throw new UnsupportedOperationException();
+ }
+
+ @GET
+ @Path("/time/{generatingbalance}")
+ @Operation(
+ description = "Calculates the time it should take for the network to generate blocks when the current generating balance in the network is the specified generating balance",
+ responses = {
+ @ApiResponse(
+ description = "The time", // in seconds?
+ content = @Content(schema = @Schema(implementation = long.class))
+ )
+ }
+ )
+ public String getTimePerBlock(@PathParam("generating") long generatingbalance) {
+ Security.checkApiCallAllowed("GET blocks/time", request);
+
+ throw new UnsupportedOperationException();
+ }
+
+ @GET
+ @Path("/height")
+ @Operation(
+ description = "Returns the block height of the last block.",
+ responses = {
+ @ApiResponse(
+ description = "The height",
+ content = @Content(schema = @Schema(implementation = int.class))
+ )
+ }
+ )
+ public int getHeight() {
+ Security.checkApiCallAllowed("GET blocks/height", request);
+
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ return repository.getBlockRepository().getBlockchainHeight();
+ } catch (Exception e) {
+ throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e);
+ }
+ }
+
+ @GET
+ @Path("/height/{signature}")
+ @Operation(
+ description = "Returns the block height of the block that matches the given signature",
+ responses = {
+ @ApiResponse(
+ description = "The height",
+ content = @Content(schema = @Schema(implementation = int.class))
+ ),
+ @ApiResponse(
+ responseCode = "400",
+ description = "Invalid signature"
+ ),
+ @ApiResponse(
+ responseCode = "422",
+ description = "Block does not exist"
+ )
+ }
+ )
+ public int getHeight(@PathParam("signature") String signature) {
+ Security.checkApiCallAllowed("GET blocks/height", request);
+
+ throw new UnsupportedOperationException();
+ }
+
+ @GET
+ @Path("/byheight/{height}")
+ @Operation(
+ description = "Returns the block whith given height",
+ responses = {
+ @ApiResponse(
+ description = "The block"
+ //content = @Content(schema = @Schema(implementation = ???))
+ ),
+ @ApiResponse(
+ responseCode = "422",
+ description = "Block does not exist"
+ )
+ }
+ )
+ public String getbyHeight(@PathParam("height") int height) {
+ Security.checkApiCallAllowed("GET blocks/byheight", request);
+
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/src/api/Security.java b/src/api/Security.java
new file mode 100644
index 00000000..d05a41af
--- /dev/null
+++ b/src/api/Security.java
@@ -0,0 +1,10 @@
+package api;
+
+import javax.servlet.http.HttpServletRequest;
+
+public class Security {
+
+ public static void checkApiCallAllowed(final String messageToDisplay, HttpServletRequest request) {
+ // TODO
+ }
+}
diff --git a/src/globalization/Translator.java b/src/globalization/Translator.java
new file mode 100644
index 00000000..da61c595
--- /dev/null
+++ b/src/globalization/Translator.java
@@ -0,0 +1,52 @@
+package globalization;
+
+import java.util.AbstractMap;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import org.apache.commons.text.StringSubstitutor;
+
+public class Translator {
+
+ private Map createMap(Map.Entry[] entries) {
+ HashMap map = new HashMap<>();
+ for (AbstractMap.Entry entry : entries) {
+ map.put(entry.getKey(), entry.getValue());
+ }
+ return map;
+ }
+
+ //XXX: replace singleton pattern by dependency injection?
+ private static Translator instance;
+
+ public static Translator getInstance() {
+ if (instance == null) {
+ instance = new Translator();
+ }
+
+ return instance;
+ }
+
+ public String translate(Locale locale, String templateKey, AbstractMap.Entry... templateValues) {
+ Map map = createMap(templateValues);
+ return translate(locale, templateKey, map);
+ }
+
+ public String translate(Locale locale, String templateKey, Map templateValues) {
+ return translate(locale, templateKey, null, templateValues);
+ }
+
+ public String translate(Locale locale, String templateKey, String defaultTemplate, AbstractMap.Entry... templateValues) {
+ Map map = createMap(templateValues);
+ return translate(locale, templateKey, defaultTemplate, map);
+ }
+
+ public String translate(Locale locale, String templateKey, String defaultTemplate, Map templateValues) {
+ String template = defaultTemplate; // TODO: get template for the given locale if available
+
+ StringSubstitutor sub = new StringSubstitutor(templateValues);
+ String result = sub.replace(template);
+
+ return result;
+ }
+}
diff --git a/src/settings/Settings.java b/src/settings/Settings.java
index be62d2a9..288020b6 100644
--- a/src/settings/Settings.java
+++ b/src/settings/Settings.java
@@ -2,8 +2,12 @@ package settings;
import java.io.File;
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
+import java.util.Set;
+import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
@@ -20,9 +24,15 @@ public class Settings {
private int maxBytePerFee = 1024;
private String userpath = "";
+ //RPC
+ private int rpcPort = 9085;
+ private List rpcAllowed = new ArrayList(Arrays.asList("127.0.0.1", "::1")); // ipv4, ipv6
+ private boolean rpcEnabled = true;
+
// Constants
private static final String SETTINGS_FILENAME = "settings.json";
+
// Constructors
private Settings() {
@@ -102,6 +112,23 @@ public class Settings {
this.genesisTimestamp = ((Long) json.get("testnetstamp")).longValue();
}
}
+
+ // RPC
+ if(json.containsKey("rpcport"))
+ {
+ this.rpcPort = ((Long) json.get("rpcport")).intValue();
+ }
+
+ if(json.containsKey("rpcallowed"))
+ {
+ JSONArray allowedArray = (JSONArray) json.get("rpcallowed");
+ this.rpcAllowed = new ArrayList(allowedArray);
+ }
+
+ if(json.containsKey("rpcenabled"))
+ {
+ this.rpcEnabled = ((Boolean) json.get("rpcenabled")).booleanValue();
+ }
}
public boolean isTestNet() {
@@ -122,4 +149,18 @@ public class Settings {
return this.userpath;
}
+ public int getRpcPort()
+ {
+ return this.rpcPort;
+ }
+
+ public List getRpcAllowed()
+ {
+ return this.rpcAllowed;
+ }
+
+ public boolean isRpcEnabled()
+ {
+ return this.rpcEnabled;
+ }
}