diff --git a/AGENTS.md b/AGENTS.md
index 4963f39..b253fa2 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -11,7 +11,7 @@ mvn -pl library package # build only the library module
mvn -pl examples package # build only the examples module
```
-There are no tests in this project. CI runs `mvn clean package` on PRs targeting `master`.
+The library's JUnit 5 tests use JSON fixtures and Mockito mocks of `KrakenRestRequester`, without network access or API keys. Run them with `mvn -pl library test`. CI runs `mvn clean package` on PRs targeting `master`.
Java 25 with Temurin is required (configured via `maven-compiler-plugin` with `25`).
diff --git a/README.md b/README.md
index 1127e88..8193e26 100644
--- a/README.md
+++ b/README.md
@@ -103,6 +103,26 @@ JsonNode order = api.query(KrakenAPI.Private.ADD_ORDER, Map.of(
// Exception in thread "main" KrakenException(errors=[EGeneral:Permission denied])
```
+### Account data
+
+All Account Data operations have typed methods, including balances, credit lines, orders, amendments, trades, positions, fee tiers, API key information, wallets, ledgers, and report exports. Optional settings use the corresponding `...Params.builder()`; omitted values keep Kraken's defaults.
+
+```java
+Map balances = api.accountBalance();
+OpenOrders orders = api.openOrders(OpenOrdersParams.builder().trades(true).build());
+ClosedOrders page = api.closedOrders(ClosedOrdersParams.builder().offset(50).withoutCount(true).build());
+Map trades = api.queryTrades(QueryTradesParams.builder().transactionIds(List.of("THVRQM-33VKH-UCI7BS")).build());
+TradeVolume fees = api.tradeVolume(TradeVolumeParams.builder()
+ .pairsWithClass(List.of(new TradeVolumeParams.Pair("TSLAx/USD", "equity_pair")))
+ .feeSchedule(true).build());
+```
+
+`closedOrders` and `tradesHistory` expose the returned count, which is null when omitted by Kraken. Their `start` and `end` filters accept timestamp strings or transaction IDs. Monetary values use `BigDecimal`, timestamps use `Instant`, including fractional trade times and amendment times decoded from epoch nanoseconds. `creditLines` returns `Optional.empty()` when Kraken reports no credit lines. `accountBalance` can select a wallet using `AccountBalanceParams.accountId`, while `walletAccounts` lists the available wallets.
+
+`TradeVolumeParams` encodes requests as JSON to support class-qualified pairs. Custom REST requesters should send `endpoint.encodedParamsWith(nonce)` unchanged with `endpoint.getContentType()` and use `endpoint.unwrapResponse(response)` to handle endpoint-specific nullable results.
+
+Custom `KrakenNonceGenerator` implementations must produce increasing unsigned 64-bit integers as canonical decimal strings. `TradeVolume` encodes the nonce as a JSON number and rejects malformed, out-of-range or noncanonical values with an `IllegalStateException` that names the generator contract. Canonical formatting keeps the nonce used for signing identical to the number in the JSON body.
+
### Custom endpoints
An endpoint the library doesn't implement can also be given a proper type, instead of falling back to `JsonNode`. Extend `PublicEndpoint`, or `PrivateEndpoint` for a private one, and pass your endpoint to `query`:
diff --git a/library/pom.xml b/library/pom.xml
index f44f4cd..5700858 100644
--- a/library/pom.xml
+++ b/library/pom.xml
@@ -12,4 +12,22 @@
kraken-api
Java Kraken API Client
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+ org.assertj
+ assertj-core
+ test
+
+
+ org.mockito
+ mockito-junit-jupiter
+ test
+
+
+
diff --git a/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java b/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java
index 67cf598..0f0d12d 100644
--- a/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java
+++ b/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java
@@ -1,29 +1,72 @@
package dev.andstuff.kraken.api;
+import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import com.fasterxml.jackson.databind.JsonNode;
import dev.andstuff.kraken.api.endpoint.KrakenException;
+import dev.andstuff.kraken.api.endpoint.account.AccountBalanceEndpoint;
+import dev.andstuff.kraken.api.endpoint.account.ApiKeyInfoEndpoint;
+import dev.andstuff.kraken.api.endpoint.account.ClosedOrdersEndpoint;
+import dev.andstuff.kraken.api.endpoint.account.CreditLinesEndpoint;
+import dev.andstuff.kraken.api.endpoint.account.ExtendedBalanceEndpoint;
import dev.andstuff.kraken.api.endpoint.account.LedgerEntriesEndpoint;
import dev.andstuff.kraken.api.endpoint.account.LedgerInfoEndpoint;
+import dev.andstuff.kraken.api.endpoint.account.OpenOrdersEndpoint;
+import dev.andstuff.kraken.api.endpoint.account.OpenPositionsEndpoint;
+import dev.andstuff.kraken.api.endpoint.account.OrderAmendsEndpoint;
+import dev.andstuff.kraken.api.endpoint.account.QueryOrdersEndpoint;
+import dev.andstuff.kraken.api.endpoint.account.QueryTradesEndpoint;
import dev.andstuff.kraken.api.endpoint.account.RemoveReportEndpoint;
import dev.andstuff.kraken.api.endpoint.account.ReportDataEndpoint;
import dev.andstuff.kraken.api.endpoint.account.ReportsStatusesEndpoint;
import dev.andstuff.kraken.api.endpoint.account.RequestReportEndpoint;
+import dev.andstuff.kraken.api.endpoint.account.TradeBalanceEndpoint;
+import dev.andstuff.kraken.api.endpoint.account.TradeVolumeEndpoint;
+import dev.andstuff.kraken.api.endpoint.account.TradesHistoryEndpoint;
+import dev.andstuff.kraken.api.endpoint.account.WalletAccountsEndpoint;
+import dev.andstuff.kraken.api.endpoint.account.params.AccountBalanceParams;
+import dev.andstuff.kraken.api.endpoint.account.params.ApiKeyInfoParams;
+import dev.andstuff.kraken.api.endpoint.account.params.ClosedOrdersParams;
+import dev.andstuff.kraken.api.endpoint.account.params.CreditLinesParams;
+import dev.andstuff.kraken.api.endpoint.account.params.ExtendedBalanceParams;
import dev.andstuff.kraken.api.endpoint.account.params.LedgerEntriesParams;
import dev.andstuff.kraken.api.endpoint.account.params.LedgerInfoParams;
+import dev.andstuff.kraken.api.endpoint.account.params.OpenOrdersParams;
+import dev.andstuff.kraken.api.endpoint.account.params.OpenPositionsParams;
+import dev.andstuff.kraken.api.endpoint.account.params.OrderAmendsParams;
+import dev.andstuff.kraken.api.endpoint.account.params.QueryOrdersParams;
+import dev.andstuff.kraken.api.endpoint.account.params.QueryTradesParams;
import dev.andstuff.kraken.api.endpoint.account.params.RemovalType;
import dev.andstuff.kraken.api.endpoint.account.params.RemoveReportParams;
import dev.andstuff.kraken.api.endpoint.account.params.ReportDataParams;
import dev.andstuff.kraken.api.endpoint.account.params.ReportType;
import dev.andstuff.kraken.api.endpoint.account.params.ReportsStatusesParams;
import dev.andstuff.kraken.api.endpoint.account.params.RequestReportParams;
+import dev.andstuff.kraken.api.endpoint.account.params.TradeBalanceParams;
+import dev.andstuff.kraken.api.endpoint.account.params.TradeVolumeParams;
+import dev.andstuff.kraken.api.endpoint.account.params.TradesHistoryParams;
+import dev.andstuff.kraken.api.endpoint.account.params.WalletAccountsParams;
+import dev.andstuff.kraken.api.endpoint.account.response.AccountTrade;
+import dev.andstuff.kraken.api.endpoint.account.response.ApiKeyInfo;
+import dev.andstuff.kraken.api.endpoint.account.response.ClosedOrders;
+import dev.andstuff.kraken.api.endpoint.account.response.CreditLines;
+import dev.andstuff.kraken.api.endpoint.account.response.ExtendedBalance;
import dev.andstuff.kraken.api.endpoint.account.response.LedgerEntry;
import dev.andstuff.kraken.api.endpoint.account.response.LedgerInfo;
+import dev.andstuff.kraken.api.endpoint.account.response.OpenOrders;
+import dev.andstuff.kraken.api.endpoint.account.response.OpenPosition;
+import dev.andstuff.kraken.api.endpoint.account.response.Order;
+import dev.andstuff.kraken.api.endpoint.account.response.OrderAmends;
import dev.andstuff.kraken.api.endpoint.account.response.Report;
import dev.andstuff.kraken.api.endpoint.account.response.ReportRequest;
+import dev.andstuff.kraken.api.endpoint.account.response.TradeBalance;
+import dev.andstuff.kraken.api.endpoint.account.response.TradeVolume;
+import dev.andstuff.kraken.api.endpoint.account.response.TradesHistory;
+import dev.andstuff.kraken.api.endpoint.account.response.WalletAccounts;
import dev.andstuff.kraken.api.endpoint.earn.EarnAllocateEndpoint;
import dev.andstuff.kraken.api.endpoint.earn.EarnAllocateStatusEndpoint;
import dev.andstuff.kraken.api.endpoint.earn.EarnAllocationsEndpoint;
@@ -299,6 +342,306 @@ public Map ledgerEntries(LedgerEntriesParams params) {
return query(new LedgerEntriesEndpoint(params));
}
+ /**
+ * Queries the {@code Balance} endpoint using default options.
+ *
+ * @return the account balance returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public Map accountBalance() {
+ return query(new AccountBalanceEndpoint());
+ }
+
+ /**
+ * Queries the {@code Balance} endpoint.
+ *
+ * @param params the request options
+ * @return the account balance returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public Map accountBalance(AccountBalanceParams params) {
+ return query(new AccountBalanceEndpoint(params));
+ }
+
+ /**
+ * Queries the {@code BalanceEx} endpoint using default options.
+ *
+ * @return the extended balance returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public Map extendedBalance() {
+ return query(new ExtendedBalanceEndpoint());
+ }
+
+ /**
+ * Queries the {@code BalanceEx} endpoint.
+ *
+ * @param params the request options
+ * @return the extended balance returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public Map extendedBalance(ExtendedBalanceParams params) {
+ return query(new ExtendedBalanceEndpoint(params));
+ }
+
+ /**
+ * Queries the {@code CreditLines} endpoint using default options.
+ *
+ * @return the credit lines returned by Kraken, or an empty optional when no credit lines exist
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public Optional creditLines() {
+ return query(new CreditLinesEndpoint());
+ }
+
+ /**
+ * Queries the {@code CreditLines} endpoint.
+ *
+ * @param params the request options
+ * @return the credit lines returned by Kraken, or an empty optional when no credit lines exist
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public Optional creditLines(CreditLinesParams params) {
+ return query(new CreditLinesEndpoint(params));
+ }
+
+ /**
+ * Queries the {@code TradeBalance} endpoint using default options.
+ *
+ * @return the trade balance returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public TradeBalance tradeBalance() {
+ return query(new TradeBalanceEndpoint());
+ }
+
+ /**
+ * Queries the {@code TradeBalance} endpoint.
+ *
+ * @param params the request options
+ * @return the trade balance returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public TradeBalance tradeBalance(TradeBalanceParams params) {
+ return query(new TradeBalanceEndpoint(params));
+ }
+
+ /**
+ * Queries the {@code OpenOrders} endpoint using default options.
+ *
+ * @return the open orders returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public OpenOrders openOrders() {
+ return query(new OpenOrdersEndpoint());
+ }
+
+ /**
+ * Queries the {@code OpenOrders} endpoint.
+ *
+ * @param params the request options
+ * @return the open orders returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public OpenOrders openOrders(OpenOrdersParams params) {
+ return query(new OpenOrdersEndpoint(params));
+ }
+
+ /**
+ * Queries the {@code ClosedOrders} endpoint using default options.
+ *
+ * @return the closed orders returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public ClosedOrders closedOrders() {
+ return query(new ClosedOrdersEndpoint());
+ }
+
+ /**
+ * Queries the {@code ClosedOrders} endpoint.
+ *
+ * @param params the request options
+ * @return the closed orders returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public ClosedOrders closedOrders(ClosedOrdersParams params) {
+ return query(new ClosedOrdersEndpoint(params));
+ }
+
+ /**
+ * Queries the {@code QueryOrders} endpoint.
+ *
+ * @param params the request options
+ * @return the query orders returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public Map queryOrders(QueryOrdersParams params) {
+ return query(new QueryOrdersEndpoint(params));
+ }
+
+ /**
+ * Queries the {@code OrderAmends} endpoint using default options.
+ *
+ * @return the order amends returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public OrderAmends orderAmends() {
+ return query(new OrderAmendsEndpoint());
+ }
+
+ /**
+ * Queries the {@code OrderAmends} endpoint.
+ *
+ * @param params the request options
+ * @return the order amends returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public OrderAmends orderAmends(OrderAmendsParams params) {
+ return query(new OrderAmendsEndpoint(params));
+ }
+
+ /**
+ * Queries the {@code TradesHistory} endpoint using default options.
+ *
+ * @return the trades history returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public TradesHistory tradesHistory() {
+ return query(new TradesHistoryEndpoint());
+ }
+
+ /**
+ * Queries the {@code TradesHistory} endpoint.
+ *
+ * @param params the request options
+ * @return the trades history returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public TradesHistory tradesHistory(TradesHistoryParams params) {
+ return query(new TradesHistoryEndpoint(params));
+ }
+
+ /**
+ * Queries the {@code QueryTrades} endpoint.
+ *
+ * @param params the request options
+ * @return the query trades returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public Map queryTrades(QueryTradesParams params) {
+ return query(new QueryTradesEndpoint(params));
+ }
+
+ /**
+ * Queries the {@code OpenPositions} endpoint using default options.
+ *
+ * @return the open positions returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public Map openPositions() {
+ return query(new OpenPositionsEndpoint());
+ }
+
+ /**
+ * Queries the {@code OpenPositions} endpoint.
+ *
+ * @param params the request options
+ * @return the open positions returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public Map openPositions(OpenPositionsParams params) {
+ return query(new OpenPositionsEndpoint(params));
+ }
+
+ /**
+ * Queries the {@code TradeVolume} endpoint using default options.
+ *
+ * @return the trade volume returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public TradeVolume tradeVolume() {
+ return query(new TradeVolumeEndpoint());
+ }
+
+ /**
+ * Queries the {@code TradeVolume} endpoint.
+ *
+ * @param params the request options
+ * @return the trade volume returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public TradeVolume tradeVolume(TradeVolumeParams params) {
+ return query(new TradeVolumeEndpoint(params));
+ }
+
+ /**
+ * Queries the {@code GetApiKeyInfo} endpoint using default options.
+ *
+ * @return the api key info returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public ApiKeyInfo apiKeyInfo() {
+ return query(new ApiKeyInfoEndpoint());
+ }
+
+ /**
+ * Queries the {@code GetApiKeyInfo} endpoint.
+ *
+ * @param params the request options
+ * @return the api key info returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public ApiKeyInfo apiKeyInfo(ApiKeyInfoParams params) {
+ return query(new ApiKeyInfoEndpoint(params));
+ }
+
+ /**
+ * Queries the {@code ListWalletAccounts} endpoint using default options.
+ *
+ * @return the wallet accounts returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public WalletAccounts walletAccounts() {
+ return query(new WalletAccountsEndpoint());
+ }
+
+ /**
+ * Queries the {@code ListWalletAccounts} endpoint.
+ *
+ * @param params the request options
+ * @return the wallet accounts returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public WalletAccounts walletAccounts(WalletAccountsParams params) {
+ return query(new WalletAccountsEndpoint(params));
+ }
+
/**
* Queries the private {@code AddExport} endpoint, asking Kraken to generate a report. The report is generated asynchronously: use {@link #reportsStatuses(ReportType)} to know when it is ready and {@link #reportData(String)} to download it.
*
@@ -644,6 +987,7 @@ public enum Private {
GET_WEBSOCKETS_TOKEN("GetWebSocketsToken"),
LEDGERS("Ledgers"),
LEVEL3("Level3"),
+ LIST_WALLET_ACCOUNTS("ListWalletAccounts"),
OPEN_ORDERS("OpenOrders"),
OPEN_POSITIONS("OpenPositions"),
ORDER_AMENDS("OrderAmends"),
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/Endpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/Endpoint.java
index 3b109f4..c500667 100644
--- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/Endpoint.java
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/Endpoint.java
@@ -55,6 +55,17 @@ public JavaType wrappedResponseType(TypeFactory typeFactory) {
KrakenResponse.class, typeFactory.constructType(responseType.getType()));
}
+ /**
+ * Unwraps a JSON response or raises the Kraken error associated with an absent result.
+ *
+ * @param response the deserialized response envelope
+ * @return the endpoint result
+ * @throws KrakenException if the response has no result
+ */
+ public T unwrapResponse(KrakenResponse response) {
+ return response.result().orElseThrow(() -> new KrakenException(response.error()));
+ }
+
/**
* Reads the response of endpoints answering with a ZIP archive instead of JSON, e.g. report exports. Endpoints that can return such a response override this method.
*
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/KrakenResponse.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/KrakenResponse.java
index fce3c4e..e79c74a 100644
--- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/KrakenResponse.java
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/KrakenResponse.java
@@ -10,7 +10,7 @@
*
* @param the type the {@code result} field is deserialized into
* @param error the errors returned by Kraken, empty when the request succeeded
- * @param result the payload of the response, empty when the request failed
+ * @param result the payload of the response, empty when the request failed or the endpoint returns a nullable result
*/
public record KrakenResponse(List error,
Optional result) {
@@ -18,7 +18,7 @@ public record KrakenResponse(List error,
/**
* Returns the payload of the response.
*
- * @return the payload, or an empty optional if the request failed
+ * @return the payload, or an empty optional if the request failed or its result is null
*/
public Optional result() {
// TODO looks like an issue with jackson which returns Optional.of(NullNode.instance) instead of Optional.empty
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/AccountBalanceEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/AccountBalanceEndpoint.java
new file mode 100644
index 0000000..65178f0
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/AccountBalanceEndpoint.java
@@ -0,0 +1,55 @@
+package dev.andstuff.kraken.api.endpoint.account;
+
+import java.math.BigDecimal;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+
+import dev.andstuff.kraken.api.endpoint.account.params.AccountBalanceParams;
+import dev.andstuff.kraken.api.endpoint.priv.PrivateEndpoint;
+
+/**
+ * The private {@code Balance} endpoint for account balance.
+ */
+public class AccountBalanceEndpoint extends PrivateEndpoint