diff --git a/README.md b/README.md
index 88cbab7..d4b030f 100644
--- a/README.md
+++ b/README.md
@@ -2,22 +2,107 @@
[](https://central.sonatype.com/artifact/dev.andstuff.kraken/kraken-api)
-Query the [Kraken API][1] in Java.
+Query the [Kraken REST API][1] in Java.
-### Examples
+## Maven
-Run the examples with:
+```xml
+
+ dev.andstuff.kraken
+ kraken-api
+ 1.0.0
+
+```
+
+## Usage
+
+> Note: the following code examples are for the current state of the repository and not for the v1.0.0. See [here][3] for v1.0.0 code examples.
+
+### Public endpoints
+
+Public endpoints that have been implemented by the library, can be queried in the following way:
+
+```java
+KrakenAPI api = new KrakenAPI();
+
+Map assets = api.assetInfo(List.of("BTC", "ETH"));
+// {BTC=AssetInfo[assetClass=currency, alternateName=XBT, maxDecimals=10, …
+
+Map pairs = api.assetPairs(List.of("ETH/BTC", "ETH/USD"));
+// {ETH/BTC=AssetPair[alternateName=ETHXBT, webSocketName=ETH/XBT, …
+```
+
+If the endpoint has not yet been implemented (feel free to submit a PR!), the generic `query` method can be used, which will return a `JsonNode` of the [Jackson][2] deserialization libary:
+
+```java
+JsonNode ticker = api.query(KrakenAPI.Public.TICKER, Map.of("pair", "XBTEUR"));
+// {"XXBTZEUR":{"a":["62650.00000","1","1.000"],"b":["62649.90000","6","6.000"], …
+```
+
+It's also possible to specify the path directly, in case a new endpoint has been added by Kraken and not yet added in the library:
+
+```java
+JsonNode trades = api.queryPublic("Trades", Map.of("pair", "XBTUSD", "count", "1"));
+// {"XXBTZUSD":[["68515.60000","0.00029628",1.7100231295628998E9,"s","m","",68007835]], …
+```
+
+### Private endpoints
+
+Private endpoints can be queried in the same way as the public ones, but an API key and secret must be provided to the `KrakenAPI` instance:
+
+```java
+KrakenAPI api = new KrakenAPI("my key", "my secret");
+
+JsonNode balance = api.query(KrakenAPI.Private.BALANCE);
+// {"XXBT":"1234.8396278900", … :)
+```
+
+If the Kraken API call returns an error, an unchecked exception of type `KrakenException` is thrown:
+
+```java
+// API key doesn't have the permission to create orders
+JsonNode order = api.query(KrakenAPI.Private.ADD_ORDER, Map.of(
+ "ordertype", "limit", "type", "sell",
+ "volume", "1", "pair", "XLTCZUSD",
+ "price", "1000", "oflags", "post,fciq",
+ "validate", "true"));
+// Exception in thread "main" KrakenException(errors=[EGeneral:Permission denied])
+```
+
+### Custom REST requester
+
+The current implementation of the library uses the JDK's HttpsURLConnection to make HTTP request. If that doesn't suit your needs and which to use something else (e.g. Spring RestTemplate, Apache HttpComponents, OkHttp), you can implement the KrakenRestRequester interface and pass it to the KrakenAPI constructor:
+
+```java
+public class MyRestTemplateRestRequester implements KrakenRestRequester {
+ public T execute(PublicEndpoint endpoint) { /* your implementation */ }
+ public T execute(PrivateEndpoint endpoint) { /* your implementation */ }
+}
+
+KrakenAPI api = new KrakenAPI(MyRestTemplateRestRequest(apiKey, apiSecret));
+```
+
+See `DefaultKrakenRestRequester` for the default implementation.
+
+### Custom nonce generator (not yet implemented)
+
+
+
+## Examples
+
+The `examples` Maven module contains some examples that might be worth checking (e.g. total staking rewards summary). The examples can be run directly from your IDE, or from the command line.
+
+For private endpoints, you need to rename `api-keys.properties.example` (located in `examples/src/main/resources/`) to `api-keys.properties` and fill in your API keys.
```shell
-# input your API keys in api-keys.properties
-cp examples/src/main/resources/api-keys.properties.example \
- examples/src/main/resources/api-keys.properties
-
# build project
mvn clean install
-# run Examples class
+# run example classes
mvn -q -pl examples exec:java -Dexec.mainClass=dev.andstuff.kraken.example.Examples
+mvn -q -pl examples exec:java -Dexec.mainClass=dev.andstuff.kraken.example.TotalRewards
```
[1]: https://docs.kraken.com/rest/
+[2]: https://github.com/FasterXML/jackson
+[3]: https://github.com/nyg/kraken-api-java/blob/v1.0.0/examples/src/main/java/dev/andstuff/kraken/example/Examples.java
diff --git a/examples/src/main/java/dev/andstuff/kraken/example/Examples.java b/examples/src/main/java/dev/andstuff/kraken/example/Examples.java
index 91825d8..624c764 100644
--- a/examples/src/main/java/dev/andstuff/kraken/example/Examples.java
+++ b/examples/src/main/java/dev/andstuff/kraken/example/Examples.java
@@ -1,54 +1,69 @@
package dev.andstuff.kraken.example;
-import static dev.andstuff.kraken.example.ExampleHelper.readPropertiesFromFile;
+import static dev.andstuff.kraken.example.PropertiesHelper.readFromFile;
-import java.io.IOException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Properties;
import com.fasterxml.jackson.databind.JsonNode;
-import dev.andstuff.kraken.api.KrakenApi;
+import dev.andstuff.kraken.api.KrakenAPI;
+import dev.andstuff.kraken.api.model.endpoint.market.response.AssetInfo;
+import dev.andstuff.kraken.api.model.endpoint.market.response.AssetPair;
+import dev.andstuff.kraken.api.model.endpoint.market.response.ServerTime;
+import dev.andstuff.kraken.api.model.endpoint.market.response.SystemStatus;
public class Examples {
- public static void main(String[] args) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
-
- Properties apiKeys = readPropertiesFromFile("/api-keys.properties");
-
- KrakenApi api = new KrakenApi();
- api.setKey(apiKeys.getProperty("key"));
- api.setSecret(apiKeys.getProperty("secret"));
-
- JsonNode response;
- Map input = new HashMap<>();
-
- input.put("pair", "XBTEUR");
- response = api.queryPublic(KrakenApi.Method.TICKER, input);
- System.out.println(response);
-
- input.clear();
- input.put("pair", "XBTUSD,XLTCZUSD");
- response = api.queryPublic(KrakenApi.Method.ASSET_PAIRS, input);
- System.out.println(response);
-
- input.clear();
- input.put("asset", "ZEUR");
- response = api.queryPrivate(KrakenApi.Method.BALANCE, input);
- System.out.println(response);
-
- input.clear();
- input.put("ordertype", "limit");
- input.put("type", "sell");
- input.put("volume", "1");
- input.put("pair", "XLTCZUSD");
- input.put("price", "1000");
- input.put("oflags", "post,fciq");
- input.put("validate", "true");
- response = api.queryPrivate(KrakenApi.Method.ADD_ORDER, input);
- System.out.println(response);
+ public static void main(String[] args) {
+
+
+ /* Public endpoint examples */
+
+ KrakenAPI publicAPI = new KrakenAPI();
+
+ ServerTime serverTime = publicAPI.serverTime();
+ System.out.println(serverTime);
+
+ SystemStatus systemStatus = publicAPI.systemStatus();
+ System.out.println(systemStatus);
+
+ Map assets1 = publicAPI.assetInfo(List.of("BTC", "ETH"));
+ System.out.println(assets1);
+
+ Map assets2 = publicAPI.assetInfo(List.of("DOT", "ADA"), "currency");
+ System.out.println(assets2);
+
+ Map pairs1 = publicAPI.assetPairs(List.of("ETH/BTC", "ETH/USD"));
+ System.out.println(pairs1);
+
+ Map pairs2 = publicAPI.assetPairs(List.of("DOT/USD", "ADA/USD"), AssetPair.Info.MARGIN);
+ System.out.println(pairs2);
+
+ JsonNode ticker = publicAPI.query(KrakenAPI.Public.TICKER, Map.of("pair", "XBTEUR"));
+ System.out.println(ticker);
+
+ JsonNode trades = publicAPI.queryPublic("Trades", Map.of("pair", "XBTUSD", "count", "1"));
+ System.out.println(trades);
+
+ /* Private endpoint example */
+
+ Properties apiKeys = readFromFile("/api-keys.properties");
+
+ KrakenAPI api = new KrakenAPI(apiKeys.getProperty("key"), apiKeys.getProperty("secret"));
+
+ JsonNode balance = api.query(KrakenAPI.Private.BALANCE);
+ System.out.println(balance);
+
+ JsonNode order = api.query(KrakenAPI.Private.ADD_ORDER, Map.of(
+ "ordertype", "limit",
+ "type", "sell",
+ "volume", "1",
+ "pair", "XLTCZUSD",
+ "price", "1000",
+ "oflags", "post,fciq",
+ "validate", "true"));
+ System.out.println(order);
}
}
diff --git a/examples/src/main/java/dev/andstuff/kraken/example/JacksonTest.java b/examples/src/main/java/dev/andstuff/kraken/example/JacksonTest.java
new file mode 100644
index 0000000..a9c46ed
--- /dev/null
+++ b/examples/src/main/java/dev/andstuff/kraken/example/JacksonTest.java
@@ -0,0 +1,34 @@
+package dev.andstuff.kraken.example;
+
+import java.util.Optional;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.MapperFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+
+public class JacksonTest {
+
+ private static final ObjectMapper OBJECT_MAPPER;
+
+ static {
+ OBJECT_MAPPER = JsonMapper.builder()
+ .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
+ .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
+ .addModules(new JavaTimeModule(), new Jdk8Module())
+ .build();
+ }
+
+ public static void main(String[] args) throws JsonProcessingException {
+ MyRecord myRecord = OBJECT_MAPPER.readValue("{}", MyRecord.class);
+ System.out.println(myRecord);
+ System.out.println(myRecord.result().isEmpty());
+ }
+
+ public record MyRecord(Optional result) {}
+
+ public class MyClass {}
+}
diff --git a/examples/src/main/java/dev/andstuff/kraken/example/ExampleHelper.java b/examples/src/main/java/dev/andstuff/kraken/example/PropertiesHelper.java
similarity index 80%
rename from examples/src/main/java/dev/andstuff/kraken/example/ExampleHelper.java
rename to examples/src/main/java/dev/andstuff/kraken/example/PropertiesHelper.java
index 6a32665..fb44f12 100644
--- a/examples/src/main/java/dev/andstuff/kraken/example/ExampleHelper.java
+++ b/examples/src/main/java/dev/andstuff/kraken/example/PropertiesHelper.java
@@ -8,9 +8,9 @@
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
-public final class ExampleHelper {
+public final class PropertiesHelper {
- public static Properties readPropertiesFromFile(String path) {
+ public static Properties readFromFile(String path) {
try {
InputStream stream = Examples.class.getResourceAsStream(path);
Properties properties = new Properties();
@@ -18,7 +18,7 @@ public static Properties readPropertiesFromFile(String path) {
return properties;
}
catch (IOException e) {
- throw new RuntimeException(String.format("Could not read properties file: %s", path));
+ throw new RuntimeException(String.format("Could not read properties from file: %s", path));
}
}
}
diff --git a/examples/src/main/java/dev/andstuff/kraken/example/TotalRewards.java b/examples/src/main/java/dev/andstuff/kraken/example/TotalRewards.java
index 7fa3243..d59daaa 100644
--- a/examples/src/main/java/dev/andstuff/kraken/example/TotalRewards.java
+++ b/examples/src/main/java/dev/andstuff/kraken/example/TotalRewards.java
@@ -1,6 +1,6 @@
package dev.andstuff.kraken.example;
-import static dev.andstuff.kraken.example.ExampleHelper.readPropertiesFromFile;
+import static dev.andstuff.kraken.example.PropertiesHelper.readFromFile;
import static java.util.Arrays.asList;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.groupingBy;
@@ -21,7 +21,7 @@
import com.fasterxml.jackson.databind.JsonNode;
-import dev.andstuff.kraken.api.KrakenApi;
+import dev.andstuff.kraken.api.KrakenAPI;
/**
* TODO Group by year
@@ -30,23 +30,20 @@ public class TotalRewards {
public static void main(String[] args) throws IOException, NoSuchAlgorithmException, InvalidKeyException, InterruptedException {
- Properties apiKeys = readPropertiesFromFile("/api-keys.properties");
+ Properties apiKeys = readFromFile("/api-keys.properties");
+ KrakenAPI api = new KrakenAPI(apiKeys.getProperty("key"), apiKeys.getProperty("secret"));
- KrakenApi api = new KrakenApi();
- api.setKey(apiKeys.getProperty("key"));
- api.setSecret(apiKeys.getProperty("secret"));
-
- Map params = new HashMap<>();
- params.put("type", "staking");
- params.put("without_count", "true");
- params.put("ofs", "0");
+ Map params = Map.of(
+ "type", "staking",
+ "without_count", "true",
+ "ofs", "0");
Map rewards = new HashMap<>();
boolean hasNext = true;
while (hasNext) {
- JsonNode response = api.queryPrivate(KrakenApi.Method.LEDGERS, params);
+ JsonNode response = api.query(KrakenAPI.Private.LEDGERS, params);
params.put("ofs", String.valueOf(Integer.parseInt(params.get("ofs")) + 50));
System.out.printf("Fetched %s rewards%n", params.get("ofs"));
@@ -107,12 +104,12 @@ public static void main(String[] args) throws IOException, NoSuchAlgorithmExcept
System.out.printf("Total USD: %s%n", totalRewardAmountUsd);
}
- private static BigDecimal fetchRate(String asset, KrakenApi api) {
+ private static BigDecimal fetchRate(String asset, KrakenAPI api) {
try {
Map tickerParams = new HashMap<>();
tickerParams.put("pair", asset + "USD");
- JsonNode tickerResponse = api.queryPublic(KrakenApi.Method.TICKER, tickerParams).findValue("result");
+ JsonNode tickerResponse = api.query(KrakenAPI.Public.TICKER, tickerParams).findValue("result");
return new BigDecimal(tickerResponse.findValue(tickerResponse.fieldNames().next()).findValue("c").get(0).textValue());
}
catch (Exception e) {
diff --git a/library/src/main/java/dev/andstuff/kraken/api/ApiRequest.java b/library/src/main/java/dev/andstuff/kraken/api/ApiRequest.java
deleted file mode 100644
index 8eca230..0000000
--- a/library/src/main/java/dev/andstuff/kraken/api/ApiRequest.java
+++ /dev/null
@@ -1,196 +0,0 @@
-package dev.andstuff.kraken.api;
-
-import java.io.IOException;
-import java.io.OutputStreamWriter;
-import java.io.UnsupportedEncodingException;
-import java.net.MalformedURLException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URL;
-import java.util.Map;
-import java.util.Map.Entry;
-
-import javax.net.ssl.HttpsURLConnection;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-/**
- * Represents an HTTPS request for querying the Kraken API.
- *
- * @author nyg
- */
-class ApiRequest {
-
- private static final String ERROR_NULL_METHOD = "The API method can't be null.";
- private static final String ERROR_NULL_SIGNATURE = "The signature can't be null.";
- private static final String ERROR_NULL_KEY = "The key can't be null.";
- private static final String ERROR_NO_PARAMETERS = "The parameters can't be null or empty.";
- private static final String ERROR_INCOMPLETE_PRIVATE_METHOD =
- "A private method request requires the API key, the message signature and the method parameters.";
-
- private static final String GITHUB_NYG = "github.nyg";
- private static final String REQUEST_API_SIGN = "API-Sign";
- private static final String REQUEST_API_KEY = "API-Key";
- private static final String REQUEST_USER_AGENT = "User-Agent";
- private static final String REQUEST_POST = "POST";
-
- private static final String PUBLIC_URL = "https://api.kraken.com/0/public/";
- private static final String PRIVATE_URL = "https://api.kraken.com/0/private/";
-
- private static final String AMPERSAND = "&";
- private static final String EQUAL_SIGN = "=";
-
- private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
- /**
- * The request URL.
- */
- private URL url;
-
- /**
- * The request message signature.
- */
- private String signature;
-
- /**
- * The API key.
- */
- private String key;
-
- /**
- * The request's POST data.
- */
- private StringBuilder postData;
-
- /**
- * Tells whether the API method is public or private.
- */
- private boolean isPublic;
-
- /**
- * Executes the request and returns its response.
- *
- * @return the request's response
- * @throws IOException if the underlying {@link HttpsURLConnection} could
- * not be set up or executed
- */
- public JsonNode execute() throws IOException {
-
- HttpsURLConnection connection = null;
- try {
- connection = (HttpsURLConnection) url.openConnection();
- connection.setRequestMethod(REQUEST_POST);
- connection.addRequestProperty(REQUEST_USER_AGENT, GITHUB_NYG);
-
- // set key & signature is method is private
- if (!isPublic) {
-
- if (key == null || signature == null || postData == null) {
- throw new IllegalStateException(ERROR_INCOMPLETE_PRIVATE_METHOD);
- }
-
- connection.addRequestProperty(REQUEST_API_KEY, key);
- connection.addRequestProperty(REQUEST_API_SIGN, signature);
- }
-
- // write POST data to request
- if (postData != null && !postData.toString().isEmpty()) {
-
- connection.setDoOutput(true);
-
- try (OutputStreamWriter out = new OutputStreamWriter(connection.getOutputStream())) {
- out.write(postData.toString());
- }
- }
-
- // execute request and read response
- return OBJECT_MAPPER.readTree(connection.getInputStream());
- }
- finally {
- if (connection != null) {
- connection.disconnect();
- }
- }
- }
-
- /**
- * Sets the API method of the request.
- *
- * @param method the API method
- * @return the path of the request taking the method into account
- */
- public String setMethod(KrakenApi.Method method) {
-
- if (method == null) {
- throw new IllegalArgumentException(ERROR_NULL_METHOD);
- }
-
- isPublic = method.isPublic;
- try {
- url = new URI((isPublic ? PUBLIC_URL : PRIVATE_URL) + method.name).toURL();
- return url.getPath();
- }
- catch (MalformedURLException | URISyntaxException e) {
- throw new IllegalStateException("Could not set method URL", e);
- }
- }
-
- /**
- * Sets the parameters of the API method. Only supports "1-dimension" map.
- * Nulls for keys or values are converted to the string "null".
- *
- * @param parameters a map containing parameter names and values.
- * @return the parameters in POST data format, or null if the parameters are
- * null or empty
- * @throws UnsupportedEncodingException if the named encoding is not
- * supported
- * @throws IllegalArgumentException if the map is null of empty
- */
- public String setParameters(Map parameters) throws UnsupportedEncodingException {
-
- if (parameters == null || parameters.isEmpty()) {
- throw new IllegalArgumentException(ERROR_NO_PARAMETERS);
- }
-
- postData = new StringBuilder();
- for (Entry entry : parameters.entrySet()) {
- postData.append(entry.getKey())
- .append(EQUAL_SIGN)
- .append(KrakenUtils.urlEncode(entry.getValue()))
- .append(AMPERSAND);
- }
-
- return postData.toString();
- }
-
- /**
- * Sets the value of the API-Key request property.
- *
- * @param key the key
- * @throws IllegalArgumentException if the key is null
- */
- public void setKey(String key) {
-
- if (key == null) {
- throw new IllegalArgumentException(ERROR_NULL_KEY);
- }
-
- this.key = key;
- }
-
- /**
- * Sets the value of the API-Sign request property.
- *
- * @param signature the signature
- * @throws IllegalArgumentException if the signature is null
- */
- public void setSignature(String signature) {
-
- if (signature == null) {
- throw new IllegalArgumentException(ERROR_NULL_SIGNATURE);
- }
-
- this.signature = signature;
- }
-}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java b/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java
new file mode 100644
index 0000000..265eaf4
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java
@@ -0,0 +1,172 @@
+package dev.andstuff.kraken.api;
+
+import java.util.List;
+import java.util.Map;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import dev.andstuff.kraken.api.model.endpoint.market.AssetInfoEndpoint;
+import dev.andstuff.kraken.api.model.endpoint.market.AssetPairEndpoint;
+import dev.andstuff.kraken.api.model.endpoint.market.ServerTimeEndpoint;
+import dev.andstuff.kraken.api.model.endpoint.market.SystemStatusEndpoint;
+import dev.andstuff.kraken.api.model.endpoint.market.response.AssetInfo;
+import dev.andstuff.kraken.api.model.endpoint.market.response.AssetPair;
+import dev.andstuff.kraken.api.model.endpoint.market.response.ServerTime;
+import dev.andstuff.kraken.api.model.endpoint.market.response.SystemStatus;
+import dev.andstuff.kraken.api.model.endpoint.priv.JsonPrivateEndpoint;
+import dev.andstuff.kraken.api.model.endpoint.pub.JsonPublicEndpoint;
+import dev.andstuff.kraken.api.rest.DefaultKrakenRestRequester;
+import dev.andstuff.kraken.api.rest.KrakenRestRequester;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+public class KrakenAPI {
+
+ private final KrakenRestRequester restRequester;
+
+ public KrakenAPI() {
+ this(new DefaultKrakenRestRequester());
+ }
+
+ public KrakenAPI(String key, String secret) {
+ this(new DefaultKrakenRestRequester(key, secret));
+ }
+
+ public KrakenAPI(KrakenRestRequester restRequester) {
+ this.restRequester = restRequester;
+ }
+
+ /* Implemented public endpoints */
+
+ public ServerTime serverTime() {
+ return restRequester.execute(new ServerTimeEndpoint());
+ }
+
+ public SystemStatus systemStatus() {
+ return restRequester.execute(new SystemStatusEndpoint());
+ }
+
+ public Map assetInfo(List assets) {
+ return restRequester.execute(new AssetInfoEndpoint(assets));
+ }
+
+ public Map assetInfo(List assets, String assetClass) {
+ return restRequester.execute(new AssetInfoEndpoint(assets, assetClass));
+ }
+
+ public Map assetPairs(List pair) {
+ return restRequester.execute(new AssetPairEndpoint(pair));
+ }
+
+ public Map assetPairs(List pair, AssetPair.Info info) {
+ return restRequester.execute(new AssetPairEndpoint(pair, info));
+ }
+
+ /* Query unimplemented endpoints */
+
+ public JsonNode query(Public endpoint) {
+ return restRequester.execute(new JsonPublicEndpoint(endpoint.getPath()));
+ }
+
+ public JsonNode query(Public endpoint, Map queryParams) {
+ return restRequester.execute(new JsonPublicEndpoint(endpoint.getPath(), queryParams));
+ }
+
+ public JsonNode queryPublic(String path) {
+ return restRequester.execute(new JsonPublicEndpoint(path));
+ }
+
+ public JsonNode queryPublic(String path, Map queryParams) {
+ return restRequester.execute(new JsonPublicEndpoint(path, queryParams));
+ }
+
+ public JsonNode query(Private endpoint) {
+ return restRequester.execute(new JsonPrivateEndpoint(endpoint.getPath()));
+ }
+
+ public JsonNode query(Private endpoint, Map params) {
+ return restRequester.execute(new JsonPrivateEndpoint(endpoint.getPath(), params));
+ }
+
+ public JsonNode queryPrivate(String path) {
+ return restRequester.execute(new JsonPrivateEndpoint(path));
+ }
+
+ public JsonNode queryPrivate(String path, Map params) {
+ return restRequester.execute(new JsonPrivateEndpoint(path, params));
+ }
+
+ /* All endpoints */
+
+ @Getter
+ @RequiredArgsConstructor
+ public enum Public {
+
+ ASSETS("Assets"),
+ ASSET_PAIRS("AssetPairs"),
+ DEPTH("Depth"),
+ OHLC("OHLC"),
+ SPREAD("Spread"),
+ SYSTEM_STATUS("SystemStatus"),
+ TICKER("Ticker"),
+ TIME("Time"),
+ TRADES("Trades");
+
+ private final String path;
+ }
+
+ @Getter
+ @RequiredArgsConstructor
+ public enum Private {
+
+ ACCOUNT_TRANSFER("AccountTransfer"),
+ ADD_EXPORT("AddExport"),
+ ADD_ORDER("AddOrder"),
+ ADD_ORDER_BATCH("AddOrderBatch"),
+ BALANCE("Balance"),
+ BALANCE_EX("BalanceEx"),
+ CANCEL_ALL("CancelAll"),
+ CANCEL_ALL_ORDERS_AFTER("CancelAllOrdersAfter"),
+ CANCEL_ORDER("CancelOrder"),
+ CANCEL_ORDER_BATCH("CancelOrderBatch"),
+ CLOSED_ORDERS("ClosedOrders"),
+ CREATE_SUB_ACCOUNT("CreateSubaccount"),
+ DEPOSIT_ADDRESSES("DepositAddresses"),
+ DEPOSIT_METHODS("DepositMethods"),
+ DEPOSIT_STATUS("DepositStatus"),
+ EARN_ALLOCATE("Earn/Allocate"),
+ EARN_ALLOCATE_STATUS("Earn/AllocateStatus"),
+ EARN_ALLOCATIONS("Earn/Allocations"),
+ EARN_DEALLOCATE("Earn/Deallocate"),
+ EARN_DEALLOCATE_STATUS("Earn/DeallocateStatus"),
+ EARN_STRATEGIES("Earn/Strategies"),
+ EDIT_ORDER("EditOrder"),
+ EXPORT_STATUS("ExportStatus"),
+ GET_WEBSOCKETS_TOKEN("GetWebSocketsToken"),
+ LEDGERS("Ledgers"),
+ OPEN_ORDERS("OpenOrders"),
+ OPEN_POSITIONS("OpenPositions"),
+ QUERY_LEDGERS("QueryLedgers"),
+ QUERY_ORDERS("QueryOrders"),
+ QUERY_TRADES("QueryTrades"),
+ REMOVE_EXPORT("RemoveExport"),
+ RETRIEVE_EXPORT("RetrieveExport"),
+ STAKE("Stake"),
+ STAKING_ASSETS("Staking/Assets"),
+ STAKING_PENDING("Staking/Pending"),
+ STAKING_TRANSACTIONS("Staking/Transactions"),
+ TRADES_HISTORY("TradesHistory"),
+ TRADE_BALANCE("TradeBalance"),
+ TRADE_VOLUME("TradeVolume"),
+ UNSTAKE("Unstake"),
+ WALLET_TRANSFER("WalletTransfer"),
+ WITHDRAW("Withdraw"),
+ WITHDRAW_ADDRESSES("WithdrawAddresses"),
+ WITHDRAW_CANCEL("WithdrawCancel"),
+ WITHDRAW_INFO("WithdrawInfo"),
+ WITHDRAW_METHODS("WithdrawMethods"),
+ WITHDRAW_STATUS("WithdrawStatus");
+
+ public final String path;
+ }
+}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/KrakenApi.java b/library/src/main/java/dev/andstuff/kraken/api/KrakenApi.java
deleted file mode 100644
index 01cf071..0000000
--- a/library/src/main/java/dev/andstuff/kraken/api/KrakenApi.java
+++ /dev/null
@@ -1,216 +0,0 @@
-package dev.andstuff.kraken.api;
-
-import java.io.IOException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.util.HashMap;
-import java.util.Map;
-
-import com.fasterxml.jackson.databind.JsonNode;
-
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-import lombok.Setter;
-
-/**
- * A KrakenApi instance allows querying the Kraken API.
- *
- * @author nyg
- */
-@Getter
-@Setter
-@NoArgsConstructor
-public class KrakenApi {
-
- private static final String OTP = "otp";
- private static final String NONCE = "nonce";
- private static final String MICRO_SECONDS = "000";
-
- private String key;
- private String secret;
-
- /**
- * Query a public method of the API with the given parameters.
- *
- * @param method the API method
- * @param parameters the method parameters
- * @return the API response
- * @throws IllegalArgumentException if the API method is null
- * @throws IOException if the request could not be created or executed
- */
- public JsonNode queryPublic(Method method, Map parameters) throws IOException {
-
- ApiRequest request = new ApiRequest();
- request.setMethod(method);
-
- if (parameters != null) {
- request.setParameters(parameters);
- }
-
- return request.execute();
- }
-
- /**
- * Query a public method of the API without any parameters.
- *
- * @param method the public API method
- * @return the API response
- * @throws IOException if the request could not be created or executed
- */
- public JsonNode queryPublic(Method method) throws IOException {
- return queryPublic(method, null);
- }
-
- /**
- * Query a private method of the API with the given parameters.
- *
- * @param method the private API method
- * @param otp the one-time password
- * @param parameters the method parameters
- * @return the API response
- * @throws IOException if the request could not be created or executed
- * @throws NoSuchAlgorithmException if the SHA-256 or HmacSha512 algorithm
- * could not be found
- * @throws InvalidKeyException if the HMAC key is invalid
- */
- public JsonNode queryPrivate(Method method, String otp, Map parameters)
- throws IOException, NoSuchAlgorithmException, InvalidKeyException {
-
- ApiRequest request = new ApiRequest();
- request.setKey(key);
-
- // clone parameter map
- parameters = parameters == null ? new HashMap<>() : new HashMap<>(parameters);
-
- // set OTP parameter
- if (otp != null) {
- parameters.put(OTP, otp);
- }
-
- // generate nonce
- String nonce = System.currentTimeMillis() + MICRO_SECONDS;
- parameters.put(NONCE, nonce);
-
- // set the parameters and retrieve the POST data
- String postData = request.setParameters(parameters);
-
- // create SHA-256 hash of the nonce and the POST data
- byte[] sha256 = KrakenUtils.sha256(nonce + postData);
-
- // set the API method and retrieve the path
- byte[] path = KrakenUtils.stringToBytes(request.setMethod(method));
-
- // decode the API secret, it's the HMAC key
- byte[] hmacKey = KrakenUtils.base64Decode(secret);
-
- // create the HMAC message from the path and the previous hash
- byte[] hmacMessage = KrakenUtils.concatArrays(path, sha256);
-
- // create the HMAC-SHA512 digest, encode it and set it as the request signature
- String hmacDigest = KrakenUtils.base64Encode(KrakenUtils.hmacSha512(hmacKey, hmacMessage));
- request.setSignature(hmacDigest);
-
- return request.execute();
- }
-
- /**
- * @see #queryPrivate(Method, String, Map)
- */
- public JsonNode queryPrivate(Method method) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
- return queryPrivate(method, null, null);
- }
-
- /**
- * @see #queryPrivate(Method, String, Map)
- */
- public JsonNode queryPrivate(Method method, String otp) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
- return queryPrivate(method, otp, null);
- }
-
- /**
- * @see #queryPrivate(Method, String, Map)
- */
- public JsonNode queryPrivate(Method method, Map parameters)
- throws IOException, InvalidKeyException, NoSuchAlgorithmException {
- return queryPrivate(method, null, parameters);
- }
-
- /**
- * Represents an API method.
- *
- * @author nyg
- */
- public enum Method {
-
- /* Public methods */
- ASSET_PAIRS("AssetPairs", true),
- ASSETS("Assets", true),
- DEPTH("Depth", true),
- OHLC("OHLC", true),
- SPREAD("Spread", true),
- SYSTEM_STATUS("SystemStatus", true),
- TICKER("Ticker", true),
- TIME("Time", true),
- TRADES("Trades", true),
-
- // TODO Missing methods
- // "/private/AccountTransfer",
- // "/private/AddExport",
- // "/private/AddOrderBatch",
- // "/private/BalanceEx",
- // "/private/CancelAll",
- // "/private/CancelAllOrdersAfter",
- // "/private/CancelOrderBatch",
- // "/private/CreateSubaccount",
- // "/private/DepositMethods",
- // "/private/Earn/Allocate",
- // "/private/Earn/AllocateStatus",
- // "/private/Earn/Allocations",
- // "/private/Earn/Deallocate",
- // "/private/Earn/DeallocateStatus",
- // "/private/Earn/Strategies",
- // "/private/EditOrder",
- // "/private/ExportStatus",
- // "/private/GetWebSocketsToken",
- // "/private/RemoveExport",
- // "/private/RetrieveExport",
- // "/private/Stake",
- // "/private/Staking/Assets",
- // "/private/Staking/Pending",
- // "/private/Staking/Transactions",
- // "/private/Unstake",
- // "/private/WalletTransfer",
- // "/private/WithdrawAddresses",
- // "/private/WithdrawMethods",
-
- /* Private methods */
- ADD_ORDER("AddOrder", false),
- BALANCE("Balance", false),
- CANCEL_ORDER("CancelOrder", false),
- CLOSED_ORDERS("ClosedOrders", false),
- DEPOSIT_ADDRESSES("DepositAddresses", false),
- DEPOSIT_METHODS("DepositMethods", false),
- DEPOSIT_STATUS("DepositStatus", false),
- LEDGERS("Ledgers", false),
- OPEN_ORDERS("OpenOrders", false),
- OPEN_POSITIONS("OpenPositions", false),
- QUERY_LEDGERS("QueryLedgers", false),
- QUERY_ORDERS("QueryOrders", false),
- QUERY_TRADES("QueryTrades", false),
- TRADES_HISTORY("TradesHistory", false),
- TRADE_BALANCE("TradeBalance", false),
- TRADE_VOLUME("TradeVolume", false),
- WITHDRAW("Withdraw", false),
- WITHDRAW_CANCEL("WithdrawCancel", false),
- WITHDRAW_INFO("WithdrawInfo", false),
- WITHDRAW_STATUS("WithdrawStatus", false);
-
- public final String name;
- public final boolean isPublic;
-
- Method(String name, boolean isPublic) {
- this.name = name;
- this.isPublic = isPublic;
- }
- }
-}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/KrakenUtils.java b/library/src/main/java/dev/andstuff/kraken/api/KrakenUtils.java
deleted file mode 100644
index 94af149..0000000
--- a/library/src/main/java/dev/andstuff/kraken/api/KrakenUtils.java
+++ /dev/null
@@ -1,76 +0,0 @@
-package dev.andstuff.kraken.api;
-
-import java.io.UnsupportedEncodingException;
-import java.net.URLEncoder;
-import java.nio.charset.StandardCharsets;
-import java.security.InvalidKeyException;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.Base64;
-
-import javax.crypto.Mac;
-import javax.crypto.spec.SecretKeySpec;
-
-/**
- * Provides basic utilities for hash, byte array and string manipulation.
- *
- * @author nyg
- */
-final class KrakenUtils {
-
- private static final String ERROR_NULL_INPUT = "Input can't be null.";
- private static final String ERROR_NULL_ARRAYS = "Given arrays can't be null.";
-
- private static final String SHA256 = "SHA-256";
- private static final String HMAC_SHA512 = "HmacSHA512";
-
- public static byte[] base64Decode(String input) {
- return Base64.getDecoder().decode(input);
- }
-
- public static String base64Encode(byte[] data) {
- return Base64.getEncoder().encodeToString(data);
- }
-
- public static byte[] concatArrays(byte[] a, byte[] b) {
-
- if (a == null || b == null) {
- throw new IllegalArgumentException(ERROR_NULL_ARRAYS);
- }
-
- byte[] concat = new byte[a.length + b.length];
- for (int i = 0; i < concat.length; i++) {
- concat[i] = i < a.length ? a[i] : b[i - a.length];
- }
-
- return concat;
- }
-
- public static byte[] hmacSha512(byte[] key, byte[] message) throws NoSuchAlgorithmException, InvalidKeyException {
- Mac mac = Mac.getInstance(HMAC_SHA512);
- mac.init(new SecretKeySpec(key, HMAC_SHA512));
- return mac.doFinal(message);
- }
-
- public static byte[] sha256(String message) throws NoSuchAlgorithmException {
- MessageDigest md = MessageDigest.getInstance(SHA256);
- return md.digest(stringToBytes(message));
- }
-
- public static byte[] stringToBytes(String input) {
-
- if (input == null) {
- throw new IllegalArgumentException(ERROR_NULL_INPUT);
- }
-
- return input.getBytes(StandardCharsets.UTF_8);
- }
-
- public static String urlEncode(String input) throws UnsupportedEncodingException {
- return URLEncoder.encode(input, "UTF-8");
- }
-
- private KrakenUtils() {
- throw new IllegalStateException();
- }
-}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/model/KrakenCredentials.java b/library/src/main/java/dev/andstuff/kraken/api/model/KrakenCredentials.java
new file mode 100644
index 0000000..205d2f9
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/model/KrakenCredentials.java
@@ -0,0 +1,64 @@
+package dev.andstuff.kraken.api.model;
+
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Base64;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+public class KrakenCredentials {
+
+ @Getter
+ @NonNull
+ private final String key;
+
+ @NonNull
+ private final String secret;
+
+ public String sign(URL url, String nonce, String urlEncodedParams) {
+
+ byte[] hmacKey = Base64.getDecoder().decode(secret);
+
+ byte[] sha256 = sha256(nonce + urlEncodedParams);
+ byte[] hmacMessage = concat(url.getPath().getBytes(StandardCharsets.UTF_8), sha256);
+
+ byte[] hmac = hmacSha512(hmacKey, hmacMessage);
+ return Base64.getEncoder().encodeToString(hmac);
+ }
+
+ public static byte[] concat(byte[] a, byte[] b) {
+ byte[] concat = new byte[a.length + b.length];
+ System.arraycopy(a, 0, concat, 0, a.length);
+ System.arraycopy(b, 0, concat, a.length, b.length);
+ return concat;
+ }
+
+ public static byte[] hmacSha512(byte[] key, byte[] message) {
+ try {
+ Mac mac = Mac.getInstance("HmacSHA512");
+ mac.init(new SecretKeySpec(key, "HmacSHA512"));
+ return mac.doFinal(message);
+ }
+ catch (InvalidKeyException | NoSuchAlgorithmException e) {
+ throw new IllegalStateException("Could not compute HMAC digest");
+ }
+ }
+
+ private static byte[] sha256(String message) {
+ try {
+ return MessageDigest.getInstance("SHA-256").digest(message.getBytes(StandardCharsets.UTF_8));
+ }
+ catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException("Could not compute SHA-256 digest", e);
+ }
+ }
+}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/model/KrakenException.java b/library/src/main/java/dev/andstuff/kraken/api/model/KrakenException.java
new file mode 100644
index 0000000..5f9dcc3
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/model/KrakenException.java
@@ -0,0 +1,17 @@
+package dev.andstuff.kraken.api.model;
+
+import java.util.List;
+
+import lombok.Getter;
+import lombok.ToString;
+
+@Getter
+@ToString
+public class KrakenException extends RuntimeException {
+
+ private final List errors;
+
+ public KrakenException(List errors) {
+ this.errors = errors;
+ }
+}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/model/KrakenResponse.java b/library/src/main/java/dev/andstuff/kraken/api/model/KrakenResponse.java
new file mode 100644
index 0000000..7373f93
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/model/KrakenResponse.java
@@ -0,0 +1,15 @@
+package dev.andstuff.kraken.api.model;
+
+import java.util.List;
+import java.util.Optional;
+
+import com.fasterxml.jackson.databind.node.NullNode;
+
+public record KrakenResponse(List error,
+ Optional result) {
+
+ public Optional result() {
+ // FIXME looks like an issue with jackson which returns Optional.of(NullNode.instance) instead of Optional.empty
+ return result.map(res -> res.equals(NullNode.getInstance()) ? null : res);
+ }
+}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/Endpoint.java b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/Endpoint.java
new file mode 100644
index 0000000..5d1ec4f
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/Endpoint.java
@@ -0,0 +1,30 @@
+package dev.andstuff.kraken.api.model.endpoint;
+
+import java.net.URL;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.type.TypeFactory;
+
+import dev.andstuff.kraken.api.model.KrakenResponse;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+public abstract class Endpoint {
+
+ @Getter
+ private final String httpMethod;
+
+ protected final String path;
+
+ @Getter
+ private final TypeReference responseType;
+
+ public abstract URL buildURL();
+
+ public JavaType wrappedResponseType(TypeFactory typeFactory) {
+ return typeFactory.constructParametricType(
+ KrakenResponse.class, typeFactory.constructType(responseType.getType()));
+ }
+}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/AssetInfoEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/AssetInfoEndpoint.java
new file mode 100644
index 0000000..69fb6dc
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/AssetInfoEndpoint.java
@@ -0,0 +1,21 @@
+package dev.andstuff.kraken.api.model.endpoint.market;
+
+import java.util.List;
+import java.util.Map;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+
+import dev.andstuff.kraken.api.model.endpoint.market.params.AssetInfoParams;
+import dev.andstuff.kraken.api.model.endpoint.market.response.AssetInfo;
+import dev.andstuff.kraken.api.model.endpoint.pub.PublicEndpoint;
+
+public class AssetInfoEndpoint extends PublicEndpoint