From 278516bde90bcfa4699ff4dddb6eb1ff653ba5b7 Mon Sep 17 00:00:00 2001 From: nyg Date: Tue, 8 Sep 2026 07:46:33 +0200 Subject: [PATCH 1/3] feat: complete typed Account Data API Support nullable credit results and JSON trade-volume requests while preserving decimal precision and public API documentation. Refs #82 --- AGENTS.md | 2 +- README.md | 18 + library/pom.xml | 21 + .../dev/andstuff/kraken/api/KrakenAPI.java | 344 ++++++++++++ .../kraken/api/endpoint/Endpoint.java | 11 + .../kraken/api/endpoint/KrakenResponse.java | 4 +- .../account/AccountBalanceEndpoint.java | 57 ++ .../endpoint/account/ApiKeyInfoEndpoint.java | 29 + .../account/ClosedOrdersEndpoint.java | 29 + .../endpoint/account/CreditLinesEndpoint.java | 46 ++ .../account/ExtendedBalanceEndpoint.java | 31 ++ .../endpoint/account/OpenOrdersEndpoint.java | 29 + .../account/OpenPositionsEndpoint.java | 31 ++ .../endpoint/account/OrderAmendsEndpoint.java | 29 + .../endpoint/account/QueryOrdersEndpoint.java | 34 ++ .../endpoint/account/QueryTradesEndpoint.java | 34 ++ .../account/TradeBalanceEndpoint.java | 29 + .../endpoint/account/TradeVolumeEndpoint.java | 85 +++ .../account/TradesHistoryEndpoint.java | 29 + .../account/WalletAccountsEndpoint.java | 29 + .../account/params/AccountBalanceParams.java | 34 ++ .../account/params/ApiKeyInfoParams.java | 29 + .../endpoint/account/params/AssetClass.java | 33 ++ .../endpoint/account/params/CloseTime.java | 31 ++ .../account/params/ClosedOrdersParams.java | 83 +++ .../account/params/Consolidation.java | 29 + .../account/params/CreditLinesParams.java | 29 + .../account/params/ExtendedBalanceParams.java | 29 + .../account/params/OpenOrdersParams.java | 47 ++ .../account/params/OpenPositionsParams.java | 48 ++ .../account/params/OrderAmendsParams.java | 35 ++ .../account/params/QueryOrdersParams.java | 56 ++ .../account/params/QueryTradesParams.java | 44 ++ .../account/params/RebaseMultiplier.java | 30 ++ .../account/params/TradeBalanceParams.java | 35 ++ .../endpoint/account/params/TradeType.java | 33 ++ .../account/params/TradeVolumeParams.java | 77 +++ .../account/params/TradesHistoryParams.java | 95 ++++ .../account/params/WalletAccountsParams.java | 23 + .../account/response/AccountTrade.java | 63 +++ .../endpoint/account/response/ApiKeyInfo.java | 36 ++ .../account/response/ClosedOrders.java | 14 + .../account/response/CreditLines.java | 50 ++ .../account/response/ExtendedBalance.java | 18 + .../endpoint/account/response/OpenOrders.java | 12 + .../account/response/OpenPosition.java | 56 ++ .../api/endpoint/account/response/Order.java | 142 +++++ .../account/response/OrderAmends.java | 52 ++ .../account/response/TradeBalance.java | 32 ++ .../account/response/TradeVolume.java | 117 +++++ .../account/response/TradesHistory.java | 14 + .../account/response/WalletAccounts.java | 69 +++ .../api/endpoint/priv/PrivateEndpoint.java | 12 +- .../api/rest/DefaultKrakenRestRequester.java | 5 +- .../kraken/api/AccountKrakenAPITest.java | 497 ++++++++++++++++++ .../account/AccountBalanceEndpointTest.java | 79 +++ .../account/ApiKeyInfoEndpointTest.java | 72 +++ .../account/ClosedOrdersEndpointTest.java | 81 +++ .../account/CreditLinesEndpointTest.java | 89 ++++ .../account/ExtendedBalanceEndpointTest.java | 72 +++ .../account/OpenOrdersEndpointTest.java | 93 ++++ .../account/OpenPositionsEndpointTest.java | 75 +++ .../account/OrderAmendsEndpointTest.java | 74 +++ .../account/QueryOrdersEndpointTest.java | 73 +++ .../account/QueryTradesEndpointTest.java | 71 +++ .../account/TradeBalanceEndpointTest.java | 73 +++ .../account/TradeVolumeEndpointTest.java | 107 ++++ .../account/TradesHistoryEndpointTest.java | 83 +++ .../account/WalletAccountsEndpointTest.java | 71 +++ .../api/rest/AccountRestRequesterTest.java | 109 ++++ .../src/test/resources/account/Balance.json | 15 + .../src/test/resources/account/BalanceEx.json | 13 + .../test/resources/account/ClosedOrders.json | 68 +++ .../test/resources/account/CreditLines.json | 27 + .../test/resources/account/GetApiKeyInfo.json | 23 + .../resources/account/ListWalletAccounts.json | 20 + .../test/resources/account/OpenOrders.json | 64 +++ .../test/resources/account/OpenPositions.json | 100 ++++ .../test/resources/account/OrderAmends.json | 32 ++ .../test/resources/account/QueryOrders.json | 70 +++ .../test/resources/account/QueryTrades.json | 37 ++ library/src/test/resources/account/README.md | 1 + .../test/resources/account/TradeBalance.json | 14 + .../test/resources/account/TradeVolume.json | 33 ++ .../test/resources/account/TradesHistory.json | 39 ++ pom.xml | 9 + 86 files changed, 4711 insertions(+), 6 deletions(-) create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/AccountBalanceEndpoint.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/ApiKeyInfoEndpoint.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/ClosedOrdersEndpoint.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/CreditLinesEndpoint.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/ExtendedBalanceEndpoint.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/OpenOrdersEndpoint.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/OpenPositionsEndpoint.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/OrderAmendsEndpoint.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/QueryOrdersEndpoint.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/QueryTradesEndpoint.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/TradeBalanceEndpoint.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/TradeVolumeEndpoint.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/TradesHistoryEndpoint.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/WalletAccountsEndpoint.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/AccountBalanceParams.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/ApiKeyInfoParams.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/AssetClass.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/CloseTime.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/ClosedOrdersParams.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/Consolidation.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/CreditLinesParams.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/ExtendedBalanceParams.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/OpenOrdersParams.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/OpenPositionsParams.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/OrderAmendsParams.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/QueryOrdersParams.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/QueryTradesParams.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/RebaseMultiplier.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradeBalanceParams.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradeType.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradeVolumeParams.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradesHistoryParams.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/WalletAccountsParams.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/AccountTrade.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/ApiKeyInfo.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/ClosedOrders.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/CreditLines.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/ExtendedBalance.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/OpenOrders.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/OpenPosition.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/Order.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/OrderAmends.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/TradeBalance.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/TradeVolume.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/TradesHistory.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/WalletAccounts.java create mode 100644 library/src/test/java/dev/andstuff/kraken/api/AccountKrakenAPITest.java create mode 100644 library/src/test/java/dev/andstuff/kraken/api/endpoint/account/AccountBalanceEndpointTest.java create mode 100644 library/src/test/java/dev/andstuff/kraken/api/endpoint/account/ApiKeyInfoEndpointTest.java create mode 100644 library/src/test/java/dev/andstuff/kraken/api/endpoint/account/ClosedOrdersEndpointTest.java create mode 100644 library/src/test/java/dev/andstuff/kraken/api/endpoint/account/CreditLinesEndpointTest.java create mode 100644 library/src/test/java/dev/andstuff/kraken/api/endpoint/account/ExtendedBalanceEndpointTest.java create mode 100644 library/src/test/java/dev/andstuff/kraken/api/endpoint/account/OpenOrdersEndpointTest.java create mode 100644 library/src/test/java/dev/andstuff/kraken/api/endpoint/account/OpenPositionsEndpointTest.java create mode 100644 library/src/test/java/dev/andstuff/kraken/api/endpoint/account/OrderAmendsEndpointTest.java create mode 100644 library/src/test/java/dev/andstuff/kraken/api/endpoint/account/QueryOrdersEndpointTest.java create mode 100644 library/src/test/java/dev/andstuff/kraken/api/endpoint/account/QueryTradesEndpointTest.java create mode 100644 library/src/test/java/dev/andstuff/kraken/api/endpoint/account/TradeBalanceEndpointTest.java create mode 100644 library/src/test/java/dev/andstuff/kraken/api/endpoint/account/TradeVolumeEndpointTest.java create mode 100644 library/src/test/java/dev/andstuff/kraken/api/endpoint/account/TradesHistoryEndpointTest.java create mode 100644 library/src/test/java/dev/andstuff/kraken/api/endpoint/account/WalletAccountsEndpointTest.java create mode 100644 library/src/test/java/dev/andstuff/kraken/api/rest/AccountRestRequesterTest.java create mode 100644 library/src/test/resources/account/Balance.json create mode 100644 library/src/test/resources/account/BalanceEx.json create mode 100644 library/src/test/resources/account/ClosedOrders.json create mode 100644 library/src/test/resources/account/CreditLines.json create mode 100644 library/src/test/resources/account/GetApiKeyInfo.json create mode 100644 library/src/test/resources/account/ListWalletAccounts.json create mode 100644 library/src/test/resources/account/OpenOrders.json create mode 100644 library/src/test/resources/account/OpenPositions.json create mode 100644 library/src/test/resources/account/OrderAmends.json create mode 100644 library/src/test/resources/account/QueryOrders.json create mode 100644 library/src/test/resources/account/QueryTrades.json create mode 100644 library/src/test/resources/account/README.md create mode 100644 library/src/test/resources/account/TradeBalance.json create mode 100644 library/src/test/resources/account/TradeVolume.json create mode 100644 library/src/test/resources/account/TradesHistory.json 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..1aa6063 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,24 @@ 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`, fractional trade timestamps retain their precision, and amendment timestamps retain the nanosecond integers in Kraken's responses. `creditLines` returns null when Kraken reports no credit lines. `accountBalance` can select a wallet using `AccountBalanceParams.accountId`, while `walletAccounts` lists the available wallets. + +`TradeVolumeEndpoint` 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 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..f977d31 100644 --- a/library/pom.xml +++ b/library/pom.xml @@ -12,4 +12,25 @@ kraken-api Java Kraken API Client + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + org.assertj + assertj-core + ${assertj.version} + test + + + org.mockito + mockito-junit-jupiter + ${mockito.version} + 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..a5673a3 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,71 @@ package dev.andstuff.kraken.api; +import java.math.BigDecimal; import java.util.List; import java.util.Map; 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; @@ -68,6 +110,7 @@ import dev.andstuff.kraken.api.rest.KrakenCredentials; import dev.andstuff.kraken.api.rest.KrakenNonceGenerator; import dev.andstuff.kraken.api.rest.KrakenRestRequester; + import lombok.Builder; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -277,6 +320,306 @@ public PostTrade postTrade(PostTradeParams params) { /* Implemented private endpoints */ + /** + * 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 null when no credit lines exist + * @throws KrakenException if Kraken rejects the request + * @throws IllegalStateException if credentials are missing + */ + public CreditLines creditLines() { + return query(new CreditLinesEndpoint()); + } + + /** + * Queries the {@code CreditLines} endpoint. + * + * @param params the request options + * @return the credit lines returned by Kraken, or null when no credit lines exist + * @throws KrakenException if Kraken rejects the request + * @throws IllegalStateException if credentials are missing + */ + public CreditLines 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 Ledgers} endpoint, returning at most 50 ledger entries per call. * @@ -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..89c5227 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/AccountBalanceEndpoint.java @@ -0,0 +1,57 @@ +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> { + + private final AccountBalanceParams params; + + /** + * Creates the {@code Balance} endpoint with default options. + */ + public AccountBalanceEndpoint() { + this(AccountBalanceParams.builder().build()); + } + + /** + * Creates the {@code Balance} endpoint. + * + * @param params the request parameters + */ + public AccountBalanceEndpoint(AccountBalanceParams params) { + super("Balance", params, new TypeReference<>() {}); + this.params = params; + } + + /** + * Builds the {@code Balance} URL with the optional wallet selector. + * + * @return the URL with an encoded {@code account_id} query parameter when set + */ + @Override + public URL buildURL() { + if (params.getAccountId() == null) { + return super.buildURL(); + } + try { + return URI.create(super.buildURL() + "?account_id=" + URLEncoder.encode(params.getAccountId(), StandardCharsets.UTF_8)).toURL(); + } + catch (MalformedURLException e) { + throw new IllegalStateException("Invalid Balance URL", e); + } + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/ApiKeyInfoEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/ApiKeyInfoEndpoint.java new file mode 100644 index 0000000..c3b6238 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/ApiKeyInfoEndpoint.java @@ -0,0 +1,29 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import com.fasterxml.jackson.core.type.TypeReference; + +import dev.andstuff.kraken.api.endpoint.account.params.ApiKeyInfoParams; +import dev.andstuff.kraken.api.endpoint.account.response.ApiKeyInfo; +import dev.andstuff.kraken.api.endpoint.priv.PrivateEndpoint; + +/** + * The private {@code GetApiKeyInfo} endpoint for api key info. + */ +public class ApiKeyInfoEndpoint extends PrivateEndpoint { + + /** + * Creates the {@code GetApiKeyInfo} endpoint with default options. + */ + public ApiKeyInfoEndpoint() { + this(ApiKeyInfoParams.builder().build()); + } + + /** + * Creates the {@code GetApiKeyInfo} endpoint. + * + * @param params the request parameters + */ + public ApiKeyInfoEndpoint(ApiKeyInfoParams params) { + super("GetApiKeyInfo", params, new TypeReference<>() {}); + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/ClosedOrdersEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/ClosedOrdersEndpoint.java new file mode 100644 index 0000000..933abf2 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/ClosedOrdersEndpoint.java @@ -0,0 +1,29 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import com.fasterxml.jackson.core.type.TypeReference; + +import dev.andstuff.kraken.api.endpoint.account.params.ClosedOrdersParams; +import dev.andstuff.kraken.api.endpoint.account.response.ClosedOrders; +import dev.andstuff.kraken.api.endpoint.priv.PrivateEndpoint; + +/** + * The private {@code ClosedOrders} endpoint for closed orders. + */ +public class ClosedOrdersEndpoint extends PrivateEndpoint { + + /** + * Creates the {@code ClosedOrders} endpoint with default options. + */ + public ClosedOrdersEndpoint() { + this(ClosedOrdersParams.builder().build()); + } + + /** + * Creates the {@code ClosedOrders} endpoint. + * + * @param params the request parameters + */ + public ClosedOrdersEndpoint(ClosedOrdersParams params) { + super("ClosedOrders", params, new TypeReference<>() {}); + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/CreditLinesEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/CreditLinesEndpoint.java new file mode 100644 index 0000000..78edf74 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/CreditLinesEndpoint.java @@ -0,0 +1,46 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import com.fasterxml.jackson.core.type.TypeReference; + +import dev.andstuff.kraken.api.endpoint.KrakenException; +import dev.andstuff.kraken.api.endpoint.KrakenResponse; +import dev.andstuff.kraken.api.endpoint.account.params.CreditLinesParams; +import dev.andstuff.kraken.api.endpoint.account.response.CreditLines; +import dev.andstuff.kraken.api.endpoint.priv.PrivateEndpoint; + +/** + * The private {@code CreditLines} endpoint for credit lines. + */ +public class CreditLinesEndpoint extends PrivateEndpoint { + + /** + * Creates the {@code CreditLines} endpoint with default options. + */ + public CreditLinesEndpoint() { + this(CreditLinesParams.builder().build()); + } + + /** + * Creates the {@code CreditLines} endpoint. + * + * @param params the request parameters + */ + public CreditLinesEndpoint(CreditLinesParams params) { + super("CreditLines", params, new TypeReference<>() {}); + } + + /** + * Unwraps {@code CreditLines}, whose successful result can be null. + * + * @param response the response envelope + * @return the credit details, or null when Kraken returns no credit lines + * @throws KrakenException if Kraken reports an error + */ + @Override + public CreditLines unwrapResponse(KrakenResponse response) { + if (!response.error().isEmpty()) { + throw new KrakenException(response.error()); + } + return response.result().orElse(null); + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/ExtendedBalanceEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/ExtendedBalanceEndpoint.java new file mode 100644 index 0000000..8bc22d1 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/ExtendedBalanceEndpoint.java @@ -0,0 +1,31 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import java.util.Map; + +import com.fasterxml.jackson.core.type.TypeReference; + +import dev.andstuff.kraken.api.endpoint.account.params.ExtendedBalanceParams; +import dev.andstuff.kraken.api.endpoint.account.response.ExtendedBalance; +import dev.andstuff.kraken.api.endpoint.priv.PrivateEndpoint; + +/** + * The private {@code BalanceEx} endpoint for extended balance. + */ +public class ExtendedBalanceEndpoint extends PrivateEndpoint> { + + /** + * Creates the {@code BalanceEx} endpoint with default options. + */ + public ExtendedBalanceEndpoint() { + this(ExtendedBalanceParams.builder().build()); + } + + /** + * Creates the {@code BalanceEx} endpoint. + * + * @param params the request parameters + */ + public ExtendedBalanceEndpoint(ExtendedBalanceParams params) { + super("BalanceEx", params, new TypeReference<>() {}); + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/OpenOrdersEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/OpenOrdersEndpoint.java new file mode 100644 index 0000000..c9b9c0a --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/OpenOrdersEndpoint.java @@ -0,0 +1,29 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import com.fasterxml.jackson.core.type.TypeReference; + +import dev.andstuff.kraken.api.endpoint.account.params.OpenOrdersParams; +import dev.andstuff.kraken.api.endpoint.account.response.OpenOrders; +import dev.andstuff.kraken.api.endpoint.priv.PrivateEndpoint; + +/** + * The private {@code OpenOrders} endpoint for open orders. + */ +public class OpenOrdersEndpoint extends PrivateEndpoint { + + /** + * Creates the {@code OpenOrders} endpoint with default options. + */ + public OpenOrdersEndpoint() { + this(OpenOrdersParams.builder().build()); + } + + /** + * Creates the {@code OpenOrders} endpoint. + * + * @param params the request parameters + */ + public OpenOrdersEndpoint(OpenOrdersParams params) { + super("OpenOrders", params, new TypeReference<>() {}); + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/OpenPositionsEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/OpenPositionsEndpoint.java new file mode 100644 index 0000000..9e33271 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/OpenPositionsEndpoint.java @@ -0,0 +1,31 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import java.util.Map; + +import com.fasterxml.jackson.core.type.TypeReference; + +import dev.andstuff.kraken.api.endpoint.account.params.OpenPositionsParams; +import dev.andstuff.kraken.api.endpoint.account.response.OpenPosition; +import dev.andstuff.kraken.api.endpoint.priv.PrivateEndpoint; + +/** + * The private {@code OpenPositions} endpoint for open positions. + */ +public class OpenPositionsEndpoint extends PrivateEndpoint> { + + /** + * Creates the {@code OpenPositions} endpoint with default options. + */ + public OpenPositionsEndpoint() { + this(OpenPositionsParams.builder().build()); + } + + /** + * Creates the {@code OpenPositions} endpoint. + * + * @param params the request parameters + */ + public OpenPositionsEndpoint(OpenPositionsParams params) { + super("OpenPositions", params, new TypeReference<>() {}); + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/OrderAmendsEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/OrderAmendsEndpoint.java new file mode 100644 index 0000000..4b7550f --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/OrderAmendsEndpoint.java @@ -0,0 +1,29 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import com.fasterxml.jackson.core.type.TypeReference; + +import dev.andstuff.kraken.api.endpoint.account.params.OrderAmendsParams; +import dev.andstuff.kraken.api.endpoint.account.response.OrderAmends; +import dev.andstuff.kraken.api.endpoint.priv.PrivateEndpoint; + +/** + * The private {@code OrderAmends} endpoint for order amends. + */ +public class OrderAmendsEndpoint extends PrivateEndpoint { + + /** + * Creates the {@code OrderAmends} endpoint with default options. + */ + public OrderAmendsEndpoint() { + this(OrderAmendsParams.builder().build()); + } + + /** + * Creates the {@code OrderAmends} endpoint. + * + * @param params the request parameters + */ + public OrderAmendsEndpoint(OrderAmendsParams params) { + super("OrderAmends", params, new TypeReference<>() {}); + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/QueryOrdersEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/QueryOrdersEndpoint.java new file mode 100644 index 0000000..99fadb7 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/QueryOrdersEndpoint.java @@ -0,0 +1,34 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.core.type.TypeReference; + +import dev.andstuff.kraken.api.endpoint.account.params.QueryOrdersParams; +import dev.andstuff.kraken.api.endpoint.account.response.Order; +import dev.andstuff.kraken.api.endpoint.priv.PrivateEndpoint; + +/** + * The private {@code QueryOrders} endpoint for query orders. + */ +public class QueryOrdersEndpoint extends PrivateEndpoint> { + + /** + * Creates the {@code QueryOrders} endpoint for the selected transaction IDs. + * + * @param transactionIds the transaction IDs to query + */ + public QueryOrdersEndpoint(List transactionIds) { + this(QueryOrdersParams.builder().transactionIds(transactionIds).build()); + } + + /** + * Creates the {@code QueryOrders} endpoint. + * + * @param params the request parameters + */ + public QueryOrdersEndpoint(QueryOrdersParams params) { + super("QueryOrders", params, new TypeReference<>() {}); + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/QueryTradesEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/QueryTradesEndpoint.java new file mode 100644 index 0000000..5d834e9 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/QueryTradesEndpoint.java @@ -0,0 +1,34 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.core.type.TypeReference; + +import dev.andstuff.kraken.api.endpoint.account.params.QueryTradesParams; +import dev.andstuff.kraken.api.endpoint.account.response.AccountTrade; +import dev.andstuff.kraken.api.endpoint.priv.PrivateEndpoint; + +/** + * The private {@code QueryTrades} endpoint for query trades. + */ +public class QueryTradesEndpoint extends PrivateEndpoint> { + + /** + * Creates the {@code QueryTrades} endpoint for the selected transaction IDs. + * + * @param transactionIds the transaction IDs to query + */ + public QueryTradesEndpoint(List transactionIds) { + this(QueryTradesParams.builder().transactionIds(transactionIds).build()); + } + + /** + * Creates the {@code QueryTrades} endpoint. + * + * @param params the request parameters + */ + public QueryTradesEndpoint(QueryTradesParams params) { + super("QueryTrades", params, new TypeReference<>() {}); + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/TradeBalanceEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/TradeBalanceEndpoint.java new file mode 100644 index 0000000..f3eb1a0 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/TradeBalanceEndpoint.java @@ -0,0 +1,29 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import com.fasterxml.jackson.core.type.TypeReference; + +import dev.andstuff.kraken.api.endpoint.account.params.TradeBalanceParams; +import dev.andstuff.kraken.api.endpoint.account.response.TradeBalance; +import dev.andstuff.kraken.api.endpoint.priv.PrivateEndpoint; + +/** + * The private {@code TradeBalance} endpoint for trade balance. + */ +public class TradeBalanceEndpoint extends PrivateEndpoint { + + /** + * Creates the {@code TradeBalance} endpoint with default options. + */ + public TradeBalanceEndpoint() { + this(TradeBalanceParams.builder().build()); + } + + /** + * Creates the {@code TradeBalance} endpoint. + * + * @param params the request parameters + */ + public TradeBalanceEndpoint(TradeBalanceParams params) { + super("TradeBalance", params, new TypeReference<>() {}); + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/TradeVolumeEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/TradeVolumeEndpoint.java new file mode 100644 index 0000000..3cd5e13 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/TradeVolumeEndpoint.java @@ -0,0 +1,85 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import java.math.BigInteger; +import java.util.LinkedHashMap; +import java.util.Map; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.json.JsonMapper; + +import dev.andstuff.kraken.api.endpoint.account.params.TradeVolumeParams; +import dev.andstuff.kraken.api.endpoint.account.response.TradeVolume; +import dev.andstuff.kraken.api.endpoint.priv.PrivateEndpoint; + +/** + * The private {@code TradeVolume} endpoint for trade volume. + */ +public class TradeVolumeEndpoint extends PrivateEndpoint { + + private final TradeVolumeParams params; + + /** + * Creates the {@code TradeVolume} endpoint with default options. + */ + public TradeVolumeEndpoint() { + this(TradeVolumeParams.builder().build()); + } + + /** + * Creates the {@code TradeVolume} endpoint. + * + * @param params the request parameters + */ + public TradeVolumeEndpoint(TradeVolumeParams params) { + super("TradeVolume", params, new TypeReference<>() {}); + this.params = params; + } + + /** + * Encodes {@code TradeVolume} as JSON so class-qualified pair arrays retain their structure. + * + * @param nonce the request nonce + * @return the JSON request body that must be signed and sent unchanged + * @throws IllegalArgumentException if both pair formats are supplied + */ + @Override + public String encodedParamsWith(String nonce) { + if (params.getPairs() != null && params.getPairsWithClass() != null) { + throw new IllegalArgumentException("Specify pairs or pairsWithClass, not both"); + } + Map body = new LinkedHashMap<>(); + body.put("nonce", new BigInteger(nonce)); + if (params.getPairs() != null) { + body.put("pair", String.join(",", params.getPairs())); + } + if (params.getPairsWithClass() != null) { + body.put("pair", params.getPairsWithClass()); + } + if (params.getFeeInfo() != null) { + body.put("fee-info", params.getFeeInfo()); + } + if (params.getFeeSchedule() != null) { + body.put("fee_schedule", params.getFeeSchedule()); + } + if (params.getRebaseMultiplier() != null) { + body.put("rebase_multiplier", params.getRebaseMultiplier().getValue()); + } + try { + return JsonMapper.builder().build().writeValueAsString(body); + } + catch (JsonProcessingException e) { + throw new IllegalStateException("Cannot encode TradeVolume parameters", e); + } + } + + /** + * Returns the media type used by the {@code TradeVolume} request body. + * + * @return {@code application/json} + */ + @Override + public String getContentType() { + return "application/json"; + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/TradesHistoryEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/TradesHistoryEndpoint.java new file mode 100644 index 0000000..f7e778d --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/TradesHistoryEndpoint.java @@ -0,0 +1,29 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import com.fasterxml.jackson.core.type.TypeReference; + +import dev.andstuff.kraken.api.endpoint.account.params.TradesHistoryParams; +import dev.andstuff.kraken.api.endpoint.account.response.TradesHistory; +import dev.andstuff.kraken.api.endpoint.priv.PrivateEndpoint; + +/** + * The private {@code TradesHistory} endpoint for trades history. + */ +public class TradesHistoryEndpoint extends PrivateEndpoint { + + /** + * Creates the {@code TradesHistory} endpoint with default options. + */ + public TradesHistoryEndpoint() { + this(TradesHistoryParams.builder().build()); + } + + /** + * Creates the {@code TradesHistory} endpoint. + * + * @param params the request parameters + */ + public TradesHistoryEndpoint(TradesHistoryParams params) { + super("TradesHistory", params, new TypeReference<>() {}); + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/WalletAccountsEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/WalletAccountsEndpoint.java new file mode 100644 index 0000000..9f8fb8f --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/WalletAccountsEndpoint.java @@ -0,0 +1,29 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import com.fasterxml.jackson.core.type.TypeReference; + +import dev.andstuff.kraken.api.endpoint.account.params.WalletAccountsParams; +import dev.andstuff.kraken.api.endpoint.account.response.WalletAccounts; +import dev.andstuff.kraken.api.endpoint.priv.PrivateEndpoint; + +/** + * The private {@code ListWalletAccounts} endpoint for wallet accounts. + */ +public class WalletAccountsEndpoint extends PrivateEndpoint { + + /** + * Creates the {@code ListWalletAccounts} endpoint with default options. + */ + public WalletAccountsEndpoint() { + this(WalletAccountsParams.builder().build()); + } + + /** + * Creates the {@code ListWalletAccounts} endpoint. + * + * @param params the request parameters + */ + public WalletAccountsEndpoint(WalletAccountsParams params) { + super("ListWalletAccounts", params, new TypeReference<>() {}); + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/AccountBalanceParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/AccountBalanceParams.java new file mode 100644 index 0000000..60c3d47 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/AccountBalanceParams.java @@ -0,0 +1,34 @@ +package dev.andstuff.kraken.api.endpoint.account.params; + +import java.util.HashMap; +import java.util.Map; + +import dev.andstuff.kraken.api.endpoint.priv.PostParams; + +import lombok.Builder; +import lombok.Getter; + +/** + * The parameters of {@code Balance}; omitted options use Kraken's defaults. + */ +@Getter +@Builder(toBuilder = true) +public class AccountBalanceParams extends PostParams { + + /** + * The rebase multiplier for {@code Balance}. Kraken defaults to {@code rebased} when omitted. + */ + private final RebaseMultiplier rebaseMultiplier; + + /** + * The wallet ID, sent in the URL query; omit to use the default wallet. + */ + private final String accountId; + + @Override + protected Map params() { + Map params = new HashMap<>(); + putIfNonNull(params, "rebase_multiplier", rebaseMultiplier, RebaseMultiplier::getValue); + return params; + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/ApiKeyInfoParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/ApiKeyInfoParams.java new file mode 100644 index 0000000..52fdcab --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/ApiKeyInfoParams.java @@ -0,0 +1,29 @@ +package dev.andstuff.kraken.api.endpoint.account.params; + +import java.util.HashMap; +import java.util.Map; + +import dev.andstuff.kraken.api.endpoint.priv.PostParams; + +import lombok.Builder; +import lombok.Getter; + +/** + * The parameters of {@code GetApiKeyInfo}; omitted options use Kraken's defaults. + */ +@Getter +@Builder(toBuilder = true) +public class ApiKeyInfoParams extends PostParams { + + /** + * The one-time password when API-key two-factor authentication is enabled. + */ + private final String otp; + + @Override + protected Map params() { + Map params = new HashMap<>(); + putIfNonNull(params, "otp", otp); + return params; + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/AssetClass.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/AssetClass.java new file mode 100644 index 0000000..e071037 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/AssetClass.java @@ -0,0 +1,33 @@ +package dev.andstuff.kraken.api.endpoint.account.params; + +import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * The asset class values accepted by {@code TradesHistory}. + */ +public enum AssetClass { + @JsonProperty("forex") FOREX("forex"), + @JsonProperty("equity_pair") EQUITY_PAIR("equity_pair"), + @JsonProperty("futures_contract") FUTURES_CONTRACT("futures_contract"), + @JsonProperty("synthetic_pair") SYNTHETIC_PAIR("synthetic_pair"), + @JsonProperty("external_pair") EXTERNAL_PAIR("external_pair"), + @JsonEnumDefaultValue UNKNOWN("unknown"); + + private final String value; + + AssetClass(String value) { + this.value = value; + } + + /** + * Returns the value sent to Kraken. + * + * @return the API value + */ + @JsonValue + public String getValue() { + return value; + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/CloseTime.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/CloseTime.java new file mode 100644 index 0000000..ef40de6 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/CloseTime.java @@ -0,0 +1,31 @@ +package dev.andstuff.kraken.api.endpoint.account.params; + +import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * The close time values accepted by {@code ClosedOrders}. + */ +public enum CloseTime { + @JsonProperty("open") OPEN("open"), + @JsonProperty("close") CLOSE("close"), + @JsonProperty("both") BOTH("both"), + @JsonEnumDefaultValue UNKNOWN("unknown"); + + private final String value; + + CloseTime(String value) { + this.value = value; + } + + /** + * Returns the value sent to Kraken. + * + * @return the API value + */ + @JsonValue + public String getValue() { + return value; + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/ClosedOrdersParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/ClosedOrdersParams.java new file mode 100644 index 0000000..e5454e4 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/ClosedOrdersParams.java @@ -0,0 +1,83 @@ +package dev.andstuff.kraken.api.endpoint.account.params; + +import java.util.HashMap; +import java.util.Map; + +import dev.andstuff.kraken.api.endpoint.priv.PostParams; + +import lombok.Builder; +import lombok.Getter; + +/** + * The parameters of {@code ClosedOrders}; omitted options use Kraken's defaults. + */ +@Getter +@Builder(toBuilder = true) +public class ClosedOrdersParams extends PostParams { + + /** + * The trades for {@code ClosedOrders}. Kraken defaults to {@code false} when omitted. + */ + private final Boolean trades; + + /** + * The user reference for {@code ClosedOrders}. + */ + private final Long userReference; + + /** + * The client order id for {@code ClosedOrders}. + */ + private final String clientOrderId; + + /** + * The start boundary as Unix seconds or a transaction ID. + */ + private final String start; + + /** + * The end boundary as Unix seconds or a transaction ID. + */ + private final String end; + + /** + * The offset for {@code ClosedOrders}. + */ + private final Integer offset; + + /** + * The close time for {@code ClosedOrders}. Kraken defaults to {@code both} when omitted. + */ + private final CloseTime closeTime; + + /** + * The consolidate taker for {@code ClosedOrders}. Kraken defaults to {@code true} when omitted. + */ + private final Boolean consolidateTaker; + + /** + * The without count for {@code ClosedOrders}. Kraken defaults to {@code false} when omitted. + */ + private final Boolean withoutCount; + + /** + * The rebase multiplier for {@code ClosedOrders}. Kraken defaults to {@code rebased} when omitted. + */ + private final RebaseMultiplier rebaseMultiplier; + + @Override + protected Map params() { + Map params = new HashMap<>(); + putIfNonNull(params, "trades", trades); + putIfNonNull(params, "userref", userReference); + putIfNonNull(params, "cl_ord_id", clientOrderId); + putIfNonNull(params, "start", start); + putIfNonNull(params, "end", end); + putIfNonNull(params, "ofs", offset); + putIfNonNull(params, "closetime", closeTime, CloseTime::getValue); + putIfNonNull(params, "consolidate_taker", consolidateTaker); + putIfNonNull(params, "without_count", withoutCount); + putIfNonNull(params, "rebase_multiplier", rebaseMultiplier, RebaseMultiplier::getValue); + return params; + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/Consolidation.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/Consolidation.java new file mode 100644 index 0000000..f849fd4 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/Consolidation.java @@ -0,0 +1,29 @@ +package dev.andstuff.kraken.api.endpoint.account.params; + +import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * The consolidation values accepted by {@code OpenPositions}. + */ +public enum Consolidation { + @JsonProperty("market") MARKET("market"), + @JsonEnumDefaultValue UNKNOWN("unknown"); + + private final String value; + + Consolidation(String value) { + this.value = value; + } + + /** + * Returns the value sent to Kraken. + * + * @return the API value + */ + @JsonValue + public String getValue() { + return value; + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/CreditLinesParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/CreditLinesParams.java new file mode 100644 index 0000000..6195e73 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/CreditLinesParams.java @@ -0,0 +1,29 @@ +package dev.andstuff.kraken.api.endpoint.account.params; + +import java.util.HashMap; +import java.util.Map; + +import dev.andstuff.kraken.api.endpoint.priv.PostParams; + +import lombok.Builder; +import lombok.Getter; + +/** + * The parameters of {@code CreditLines}; omitted options use Kraken's defaults. + */ +@Getter +@Builder(toBuilder = true) +public class CreditLinesParams extends PostParams { + + /** + * The rebase multiplier for {@code CreditLines}. Kraken defaults to {@code rebased} when omitted. + */ + private final RebaseMultiplier rebaseMultiplier; + + @Override + protected Map params() { + Map params = new HashMap<>(); + putIfNonNull(params, "rebase_multiplier", rebaseMultiplier, RebaseMultiplier::getValue); + return params; + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/ExtendedBalanceParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/ExtendedBalanceParams.java new file mode 100644 index 0000000..c767f09 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/ExtendedBalanceParams.java @@ -0,0 +1,29 @@ +package dev.andstuff.kraken.api.endpoint.account.params; + +import java.util.HashMap; +import java.util.Map; + +import dev.andstuff.kraken.api.endpoint.priv.PostParams; + +import lombok.Builder; +import lombok.Getter; + +/** + * The parameters of {@code BalanceEx}; omitted options use Kraken's defaults. + */ +@Getter +@Builder(toBuilder = true) +public class ExtendedBalanceParams extends PostParams { + + /** + * The rebase multiplier for {@code BalanceEx}. Kraken defaults to {@code rebased} when omitted. + */ + private final RebaseMultiplier rebaseMultiplier; + + @Override + protected Map params() { + Map params = new HashMap<>(); + putIfNonNull(params, "rebase_multiplier", rebaseMultiplier, RebaseMultiplier::getValue); + return params; + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/OpenOrdersParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/OpenOrdersParams.java new file mode 100644 index 0000000..6c9657b --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/OpenOrdersParams.java @@ -0,0 +1,47 @@ +package dev.andstuff.kraken.api.endpoint.account.params; + +import java.util.HashMap; +import java.util.Map; + +import dev.andstuff.kraken.api.endpoint.priv.PostParams; + +import lombok.Builder; +import lombok.Getter; + +/** + * The parameters of {@code OpenOrders}; omitted options use Kraken's defaults. + */ +@Getter +@Builder(toBuilder = true) +public class OpenOrdersParams extends PostParams { + + /** + * The trades for {@code OpenOrders}. Kraken defaults to {@code false} when omitted. + */ + private final Boolean trades; + + /** + * The user reference for {@code OpenOrders}. + */ + private final Long userReference; + + /** + * The client order id for {@code OpenOrders}. + */ + private final String clientOrderId; + + /** + * The rebase multiplier for {@code OpenOrders}. Kraken defaults to {@code rebased} when omitted. + */ + private final RebaseMultiplier rebaseMultiplier; + + @Override + protected Map params() { + Map params = new HashMap<>(); + putIfNonNull(params, "trades", trades); + putIfNonNull(params, "userref", userReference); + putIfNonNull(params, "cl_ord_id", clientOrderId); + putIfNonNull(params, "rebase_multiplier", rebaseMultiplier, RebaseMultiplier::getValue); + return params; + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/OpenPositionsParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/OpenPositionsParams.java new file mode 100644 index 0000000..d12e634 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/OpenPositionsParams.java @@ -0,0 +1,48 @@ +package dev.andstuff.kraken.api.endpoint.account.params; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import dev.andstuff.kraken.api.endpoint.priv.PostParams; + +import lombok.Builder; +import lombok.Getter; + +/** + * The parameters of {@code OpenPositions}; omitted options use Kraken's defaults. + */ +@Getter +@Builder(toBuilder = true) +public class OpenPositionsParams extends PostParams { + + /** + * The transaction ids for {@code OpenPositions}. + */ + private final List transactionIds; + + /** + * The calculate values for {@code OpenPositions}. Kraken defaults to {@code false} when omitted. + */ + private final Boolean calculateValues; + + /** + * The consolidation for {@code OpenPositions}. + */ + private final Consolidation consolidation; + + /** + * The rebase multiplier for {@code OpenPositions}. Kraken defaults to {@code rebased} when omitted. + */ + private final RebaseMultiplier rebaseMultiplier; + + @Override + protected Map params() { + Map params = new HashMap<>(); + putIfNonNull(params, "txid", transactionIds, v -> String.join(",", v)); + putIfNonNull(params, "docalcs", calculateValues); + putIfNonNull(params, "consolidation", consolidation, Consolidation::getValue); + putIfNonNull(params, "rebase_multiplier", rebaseMultiplier, RebaseMultiplier::getValue); + return params; + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/OrderAmendsParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/OrderAmendsParams.java new file mode 100644 index 0000000..94c6816 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/OrderAmendsParams.java @@ -0,0 +1,35 @@ +package dev.andstuff.kraken.api.endpoint.account.params; + +import java.util.HashMap; +import java.util.Map; + +import dev.andstuff.kraken.api.endpoint.priv.PostParams; + +import lombok.Builder; +import lombok.Getter; + +/** + * The parameters of {@code OrderAmends}; omitted options use Kraken's defaults. + */ +@Getter +@Builder(toBuilder = true) +public class OrderAmendsParams extends PostParams { + + /** + * The order id for {@code OrderAmends}. + */ + private final String orderId; + + /** + * The rebase multiplier for {@code OrderAmends}. Kraken defaults to {@code rebased} when omitted. + */ + private final RebaseMultiplier rebaseMultiplier; + + @Override + protected Map params() { + Map params = new HashMap<>(); + putIfNonNull(params, "order_id", orderId); + putIfNonNull(params, "rebase_multiplier", rebaseMultiplier, RebaseMultiplier::getValue); + return params; + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/QueryOrdersParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/QueryOrdersParams.java new file mode 100644 index 0000000..7ff57a3 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/QueryOrdersParams.java @@ -0,0 +1,56 @@ +package dev.andstuff.kraken.api.endpoint.account.params; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import dev.andstuff.kraken.api.endpoint.priv.PostParams; + +import lombok.Builder; +import lombok.Getter; +import lombok.NonNull; + +/** + * The parameters of {@code QueryOrders}; omitted options use Kraken's defaults. + */ +@Getter +@Builder(toBuilder = true) +public class QueryOrdersParams extends PostParams { + + /** + * The trades for {@code QueryOrders}. Kraken defaults to {@code false} when omitted. + */ + private final Boolean trades; + + /** + * The user reference for {@code QueryOrders}. + */ + private final Long userReference; + + /** + * The transaction ids for {@code QueryOrders}. + */ + @NonNull + private final List transactionIds; + + /** + * The consolidate taker for {@code QueryOrders}. Kraken defaults to {@code true} when omitted. + */ + private final Boolean consolidateTaker; + + /** + * The rebase multiplier for {@code QueryOrders}. Kraken defaults to {@code rebased} when omitted. + */ + private final RebaseMultiplier rebaseMultiplier; + + @Override + protected Map params() { + Map params = new HashMap<>(); + putIfNonNull(params, "trades", trades); + putIfNonNull(params, "userref", userReference); + putIfNonNull(params, "txid", transactionIds, v -> String.join(",", v)); + putIfNonNull(params, "consolidate_taker", consolidateTaker); + putIfNonNull(params, "rebase_multiplier", rebaseMultiplier, RebaseMultiplier::getValue); + return params; + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/QueryTradesParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/QueryTradesParams.java new file mode 100644 index 0000000..bf6bef5 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/QueryTradesParams.java @@ -0,0 +1,44 @@ +package dev.andstuff.kraken.api.endpoint.account.params; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import dev.andstuff.kraken.api.endpoint.priv.PostParams; + +import lombok.Builder; +import lombok.Getter; +import lombok.NonNull; + +/** + * The parameters of {@code QueryTrades}; omitted options use Kraken's defaults. + */ +@Getter +@Builder(toBuilder = true) +public class QueryTradesParams extends PostParams { + + /** + * The transaction ids for {@code QueryTrades}. + */ + @NonNull + private final List transactionIds; + + /** + * The trades for {@code QueryTrades}. Kraken defaults to {@code false} when omitted. + */ + private final Boolean trades; + + /** + * The rebase multiplier for {@code QueryTrades}. Kraken defaults to {@code rebased} when omitted. + */ + private final RebaseMultiplier rebaseMultiplier; + + @Override + protected Map params() { + Map params = new HashMap<>(); + putIfNonNull(params, "txid", transactionIds, v -> String.join(",", v)); + putIfNonNull(params, "trades", trades); + putIfNonNull(params, "rebase_multiplier", rebaseMultiplier, RebaseMultiplier::getValue); + return params; + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/RebaseMultiplier.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/RebaseMultiplier.java new file mode 100644 index 0000000..6c65f2c --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/RebaseMultiplier.java @@ -0,0 +1,30 @@ +package dev.andstuff.kraken.api.endpoint.account.params; + +import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * The rebase multiplier values accepted by {@code Balance}. + */ +public enum RebaseMultiplier { + @JsonProperty("rebased") REBASED("rebased"), + @JsonProperty("base") BASE("base"), + @JsonEnumDefaultValue UNKNOWN("unknown"); + + private final String value; + + RebaseMultiplier(String value) { + this.value = value; + } + + /** + * Returns the value sent to Kraken. + * + * @return the API value + */ + @JsonValue + public String getValue() { + return value; + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradeBalanceParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradeBalanceParams.java new file mode 100644 index 0000000..5a104e5 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradeBalanceParams.java @@ -0,0 +1,35 @@ +package dev.andstuff.kraken.api.endpoint.account.params; + +import java.util.HashMap; +import java.util.Map; + +import dev.andstuff.kraken.api.endpoint.priv.PostParams; + +import lombok.Builder; +import lombok.Getter; + +/** + * The parameters of {@code TradeBalance}; omitted options use Kraken's defaults. + */ +@Getter +@Builder(toBuilder = true) +public class TradeBalanceParams extends PostParams { + + /** + * The asset for {@code TradeBalance}. Kraken defaults to {@code zusd} when omitted. + */ + private final String asset; + + /** + * The rebase multiplier for {@code TradeBalance}. Kraken defaults to {@code rebased} when omitted. + */ + private final RebaseMultiplier rebaseMultiplier; + + @Override + protected Map params() { + Map params = new HashMap<>(); + putIfNonNull(params, "asset", asset); + putIfNonNull(params, "rebase_multiplier", rebaseMultiplier, RebaseMultiplier::getValue); + return params; + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradeType.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradeType.java new file mode 100644 index 0000000..f0627cc --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradeType.java @@ -0,0 +1,33 @@ +package dev.andstuff.kraken.api.endpoint.account.params; + +import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * The trade type values accepted by {@code TradesHistory}. + */ +public enum TradeType { + @JsonProperty("all") ALL("all"), + @JsonProperty("any position") ANY_POSITION("any position"), + @JsonProperty("closed position") CLOSED_POSITION("closed position"), + @JsonProperty("closing position") CLOSING_POSITION("closing position"), + @JsonProperty("no position") NO_POSITION("no position"), + @JsonEnumDefaultValue UNKNOWN("unknown"); + + private final String value; + + TradeType(String value) { + this.value = value; + } + + /** + * Returns the value sent to Kraken. + * + * @return the API value + */ + @JsonValue + public String getValue() { + return value; + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradeVolumeParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradeVolumeParams.java new file mode 100644 index 0000000..a7dabcc --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradeVolumeParams.java @@ -0,0 +1,77 @@ +package dev.andstuff.kraken.api.endpoint.account.params; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.json.JsonMapper; + +import dev.andstuff.kraken.api.endpoint.priv.PostParams; + +import lombok.Builder; +import lombok.Getter; +import lombok.NonNull; + +/** + * The parameters of {@code TradeVolume}; omitted options use Kraken's defaults. + */ +@Getter +@Builder(toBuilder = true) +public class TradeVolumeParams extends PostParams { + + /** + * The pairs for {@code TradeVolume}. + */ + private final List pairs; + + /** + * Class-qualified pairs, used instead of {@code pairs} when selecting non-forex pairs. + */ + private final List pairsWithClass; + + /** + * The fee info for {@code TradeVolume}. Kraken defaults to {@code false} when omitted. + */ + private final Boolean feeInfo; + + /** + * The fee schedule for {@code TradeVolume}. + */ + private final Boolean feeSchedule; + + /** + * The rebase multiplier for {@code TradeVolume}. Kraken defaults to {@code rebased} when omitted. + */ + private final RebaseMultiplier rebaseMultiplier; + + @Override + protected Map params() { + if (pairs != null && pairsWithClass != null) { + throw new IllegalArgumentException("Specify pairs or pairsWithClass, not both"); + } + Map params = new HashMap<>(); + if (pairsWithClass != null) { + try { + params.put("pair", JsonMapper.builder().build().writeValueAsString(pairsWithClass)); + } + catch (JsonProcessingException e) { + throw new IllegalStateException("Cannot encode class-qualified pairs", e); + } + } + putIfNonNull(params, "pair", pairs, v -> String.join(",", v)); + putIfNonNull(params, "fee-info", feeInfo); + putIfNonNull(params, "fee_schedule", feeSchedule); + putIfNonNull(params, "rebase_multiplier", rebaseMultiplier, RebaseMultiplier::getValue); + return params; + } + + /** + * A class-qualified trading pair for {@code TradeVolume}. + * + * @param asset the pair name + * @param assetClass the Kraken asset class, such as {@code forex} or {@code derivatives} + */ + public record Pair(@NonNull String asset, @NonNull @JsonProperty("aclass") String assetClass) {} +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradesHistoryParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradesHistoryParams.java new file mode 100644 index 0000000..d9266fc --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradesHistoryParams.java @@ -0,0 +1,95 @@ +package dev.andstuff.kraken.api.endpoint.account.params; + +import java.util.HashMap; +import java.util.Map; + +import dev.andstuff.kraken.api.endpoint.priv.PostParams; + +import lombok.Builder; +import lombok.Getter; + +/** + * The parameters of {@code TradesHistory}; omitted options use Kraken's defaults. + */ +@Getter +@Builder(toBuilder = true) +public class TradesHistoryParams extends PostParams { + + /** + * The type for {@code TradesHistory}. Kraken defaults to {@code all} when omitted. + */ + private final TradeType type; + + /** + * The trades for {@code TradesHistory}. Kraken defaults to {@code false} when omitted. + */ + private final Boolean trades; + + /** + * The start boundary as Unix seconds or a transaction ID. + */ + private final String start; + + /** + * The end boundary as Unix seconds or a transaction ID. + */ + private final String end; + + /** + * The offset for {@code TradesHistory}. + */ + private final Integer offset; + + /** + * The without count for {@code TradesHistory}. Kraken defaults to {@code false} when omitted. + */ + private final Boolean withoutCount; + + /** + * The consolidate taker for {@code TradesHistory}. Kraken defaults to {@code true} when omitted. + */ + private final Boolean consolidateTaker; + + /** + * The ledgers for {@code TradesHistory}. Kraken defaults to {@code false} when omitted. + */ + private final Boolean ledgers; + + /** + * The rebase multiplier for {@code TradesHistory}. Kraken defaults to {@code rebased} when omitted. + */ + private final RebaseMultiplier rebaseMultiplier; + + /** + * The asset class for {@code TradesHistory}. Kraken defaults to {@code forex} when omitted. + */ + private final AssetClass assetClass; + + /** + * The pair for {@code TradesHistory}. + */ + private final String pair; + + /** + * The limit for {@code TradesHistory}. Kraken defaults to {@code 50} when omitted. + */ + private final Integer limit; + + @Override + protected Map params() { + Map params = new HashMap<>(); + putIfNonNull(params, "type", type, TradeType::getValue); + putIfNonNull(params, "trades", trades); + putIfNonNull(params, "start", start); + putIfNonNull(params, "end", end); + putIfNonNull(params, "ofs", offset); + putIfNonNull(params, "without_count", withoutCount); + putIfNonNull(params, "consolidate_taker", consolidateTaker); + putIfNonNull(params, "ledgers", ledgers); + putIfNonNull(params, "rebase_multiplier", rebaseMultiplier, RebaseMultiplier::getValue); + putIfNonNull(params, "aclass", assetClass, AssetClass::getValue); + putIfNonNull(params, "pair", pair); + putIfNonNull(params, "limit", limit); + return params; + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/WalletAccountsParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/WalletAccountsParams.java new file mode 100644 index 0000000..4781dd9 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/WalletAccountsParams.java @@ -0,0 +1,23 @@ +package dev.andstuff.kraken.api.endpoint.account.params; + +import java.util.HashMap; +import java.util.Map; + +import dev.andstuff.kraken.api.endpoint.priv.PostParams; + +import lombok.Builder; +import lombok.Getter; + +/** + * The parameters of {@code ListWalletAccounts}; omitted options use Kraken's defaults. + */ +@Getter +@Builder(toBuilder = true) +public class WalletAccountsParams extends PostParams { + + @Override + protected Map params() { + Map params = new HashMap<>(); + return params; + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/AccountTrade.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/AccountTrade.java new file mode 100644 index 0000000..8d86db2 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/AccountTrade.java @@ -0,0 +1,63 @@ +package dev.andstuff.kraken.api.endpoint.account.response; + +import java.math.BigDecimal; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * The account trade returned by {@code TradesHistory and QueryTrades}. + * + * @param orderId the order id + * @param positionId the position id + * @param pair the pair + * @param time the time as Unix seconds + * @param type the type + * @param orderType the order type + * @param price the price + * @param cost the cost + * @param fee the fee + * @param volume the volume + * @param margin the margin + * @param leverage the leverage + * @param miscellaneous the miscellaneous + * @param ledgers the ledgers + * @param tradeId the trade id + * @param maker the maker + * @param assetClass the asset class + * @param tradeOrderType the trade order type + * @param positionStatus the position status + * @param closedPrice the closed price + * @param closedCost the closed cost + * @param closedFee the closed fee + * @param closedVolume the closed volume + * @param closedMargin the closed margin + * @param net the net + * @param trades the trades + */ +public record AccountTrade(@JsonProperty("ordertxid") String orderId, + @JsonProperty("postxid") String positionId, + String pair, + BigDecimal time, + String type, + @JsonProperty("ordertype") String orderType, + BigDecimal price, + BigDecimal cost, + BigDecimal fee, + @JsonProperty("vol") BigDecimal volume, + BigDecimal margin, + String leverage, + @JsonProperty("misc") String miscellaneous, + List ledgers, + @JsonProperty("trade_id") Long tradeId, + Boolean maker, + @JsonProperty("aclass") String assetClass, + @JsonProperty("tradeordertype") String tradeOrderType, + @JsonProperty("posstatus") String positionStatus, + @JsonProperty("cprice") BigDecimal closedPrice, + @JsonProperty("ccost") BigDecimal closedCost, + @JsonProperty("cfee") BigDecimal closedFee, + @JsonProperty("cvol") BigDecimal closedVolume, + @JsonProperty("cmargin") BigDecimal closedMargin, + BigDecimal net, + List trades) {} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/ApiKeyInfo.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/ApiKeyInfo.java new file mode 100644 index 0000000..d14a535 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/ApiKeyInfo.java @@ -0,0 +1,36 @@ +package dev.andstuff.kraken.api.endpoint.account.response; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * The api key info returned by {@code GetApiKeyInfo}. + * + * @param name the name + * @param apiKey the api key + * @param nonce the nonce + * @param nonceWindow the nonce window + * @param permissions the permissions + * @param iban the iban + * @param validUntil the valid until as Unix seconds, preserving Kraken's string representation + * @param queryFrom the query from as Unix seconds, preserving Kraken's string representation + * @param queryTo the query to as Unix seconds, preserving Kraken's string representation + * @param createdTime the created time as Unix seconds, preserving Kraken's string representation + * @param modifiedTime the modified time as Unix seconds, preserving Kraken's string representation + * @param ipAllowlist the ip allowlist + * @param lastUsed the last used as Unix seconds, preserving Kraken's string representation + */ +public record ApiKeyInfo(@JsonProperty("apiKeyName") String name, + String apiKey, + String nonce, + Long nonceWindow, + List permissions, + String iban, + String validUntil, + String queryFrom, + String queryTo, + String createdTime, + String modifiedTime, + List ipAllowlist, + String lastUsed) {} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/ClosedOrders.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/ClosedOrders.java new file mode 100644 index 0000000..eaca451 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/ClosedOrders.java @@ -0,0 +1,14 @@ +package dev.andstuff.kraken.api.endpoint.account.response; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * The closed orders returned by {@code ClosedOrders}. + * + * @param closed the closed + * @param count the count; null when the count is omitted + */ +public record ClosedOrders(Map closed, + Long count) {} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/CreditLines.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/CreditLines.java new file mode 100644 index 0000000..1e1e181 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/CreditLines.java @@ -0,0 +1,50 @@ +package dev.andstuff.kraken.api.endpoint.account.response; + +import java.math.BigDecimal; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * The credit lines returned by {@code CreditLines}. + * + * @param assetDetails the asset details + * @param limitsMonitor the limits monitor + */ +public record CreditLines(@JsonProperty("asset_details") Map assetDetails, + @JsonProperty("limits_monitor") LimitsMonitor limitsMonitor) { + + /** + * The asset details returned by {@code CreditLines}. + * + * @param balance the balance + * @param holdTrade the hold trade + * @param collateralValue the collateral value + * @param creditLimit the credit limit + * @param creditUsed the credit used + * @param availableCredit the available credit + */ + public record AssetDetails(BigDecimal balance, + @JsonProperty("hold_trade") BigDecimal holdTrade, + @JsonProperty("collateral_value") BigDecimal collateralValue, + @JsonProperty("credit_limit") BigDecimal creditLimit, + @JsonProperty("credit_used") BigDecimal creditUsed, + @JsonProperty("available_credit") BigDecimal availableCredit) {} + + /** + * The limits monitor returned by {@code CreditLines}. + * + * @param totalCreditUsd the total credit usd + * @param totalCreditUsedUsd the total credit used usd + * @param totalCollateralValueUsd the total collateral value usd + * @param equityUsd the equity usd + * @param ongoingBalance the ongoing balance + * @param debtToEquity the debt to equity + */ + public record LimitsMonitor(@JsonProperty("total_credit_usd") BigDecimal totalCreditUsd, + @JsonProperty("total_credit_used_usd") BigDecimal totalCreditUsedUsd, + @JsonProperty("total_collateral_value_usd") BigDecimal totalCollateralValueUsd, + @JsonProperty("equity_usd") BigDecimal equityUsd, + @JsonProperty("ongoing_balance") BigDecimal ongoingBalance, + @JsonProperty("debt_to_equity") BigDecimal debtToEquity) {} +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/ExtendedBalance.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/ExtendedBalance.java new file mode 100644 index 0000000..a1020ac --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/ExtendedBalance.java @@ -0,0 +1,18 @@ +package dev.andstuff.kraken.api.endpoint.account.response; + +import java.math.BigDecimal; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * The extended balance returned by {@code BalanceEx}. + * + * @param balance the balance + * @param credit the credit + * @param creditUsed the credit used + * @param holdTrade the hold trade + */ +public record ExtendedBalance(BigDecimal balance, + BigDecimal credit, + @JsonProperty("credit_used") BigDecimal creditUsed, + @JsonProperty("hold_trade") BigDecimal holdTrade) {} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/OpenOrders.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/OpenOrders.java new file mode 100644 index 0000000..390f9e4 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/OpenOrders.java @@ -0,0 +1,12 @@ +package dev.andstuff.kraken.api.endpoint.account.response; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * The open orders returned by {@code OpenOrders}. + * + * @param open the open + */ +public record OpenOrders(Map open) {} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/OpenPosition.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/OpenPosition.java new file mode 100644 index 0000000..1a1b994 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/OpenPosition.java @@ -0,0 +1,56 @@ +package dev.andstuff.kraken.api.endpoint.account.response; + +import java.math.BigDecimal; + +import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * The open position returned by {@code OpenPositions}. + * + * @param orderId the order id + * @param assetClass the asset class + * @param positionStatus the position status + * @param pair the pair + * @param time the time as Unix seconds + * @param type the type + * @param orderType the order type + * @param cost the cost + * @param fee the fee + * @param volume the volume + * @param closedVolume the closed volume + * @param margin the margin + * @param value the value + * @param net the net + * @param terms the terms + * @param rolloverTime the rollover time as Unix seconds, preserving Kraken's string representation + * @param miscellaneous the miscellaneous + * @param orderFlags the order flags + */ +public record OpenPosition(@JsonProperty("ordertxid") String orderId, + @JsonProperty("class") String assetClass, + @JsonProperty("posstatus") PositionStatus positionStatus, + String pair, + BigDecimal time, + String type, + @JsonProperty("ordertype") String orderType, + BigDecimal cost, + BigDecimal fee, + @JsonProperty("vol") BigDecimal volume, + @JsonProperty("vol_closed") BigDecimal closedVolume, + BigDecimal margin, + BigDecimal value, + BigDecimal net, + String terms, + @JsonProperty("rollovertm") String rolloverTime, + @JsonProperty("misc") String miscellaneous, + @JsonProperty("oflags") String orderFlags) { + + /** + * The position status values used by the {@code OpenPositions} endpoint. + */ + public enum PositionStatus { + @JsonProperty("open") OPEN, + @JsonEnumDefaultValue UNKNOWN + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/Order.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/Order.java new file mode 100644 index 0000000..3c2eec0 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/Order.java @@ -0,0 +1,142 @@ +package dev.andstuff.kraken.api.endpoint.account.response; + +import java.math.BigDecimal; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * The order returned by {@code OpenOrders, ClosedOrders and QueryOrders}. + * + * @param referenceId the reference id + * @param userReference the user reference + * @param clientOrderId the client order id + * @param status the status + * @param openTime the open time as Unix seconds + * @param startTime the start time as Unix seconds + * @param expireTime the expire time as Unix seconds + * @param description the description + * @param timeInForce the time in force + * @param volume the volume + * @param executedVolume the executed volume + * @param cost the cost + * @param fee the fee + * @param price the price + * @param stopPrice the stop price + * @param limitPrice the limit price + * @param trigger the trigger + * @param margin whether this is a margin order + * @param miscellaneous the miscellaneous + * @param senderSubId the sender sub id + * @param orderFlags the order flags + * @param trades the trades + * @param closeTime the close time as Unix seconds + * @param reason the reason + */ +public record Order(@JsonProperty("refid") String referenceId, + @JsonProperty("userref") Long userReference, + @JsonProperty("cl_ord_id") String clientOrderId, + Status status, + @JsonProperty("opentm") BigDecimal openTime, + @JsonProperty("starttm") BigDecimal startTime, + @JsonProperty("expiretm") BigDecimal expireTime, + @JsonProperty("descr") Description description, + @JsonProperty("time_in_force") TimeInForce timeInForce, + @JsonProperty("vol") BigDecimal volume, + @JsonProperty("vol_exec") BigDecimal executedVolume, + BigDecimal cost, + BigDecimal fee, + BigDecimal price, + @JsonProperty("stopprice") BigDecimal stopPrice, + @JsonProperty("limitprice") BigDecimal limitPrice, + Trigger trigger, + Boolean margin, + @JsonProperty("misc") String miscellaneous, + @JsonProperty("sender_sub_id") String senderSubId, + @JsonProperty("oflags") String orderFlags, + List trades, + @JsonProperty("closetm") BigDecimal closeTime, + String reason) { + + /** + * The status values used by the {@code OpenOrders, ClosedOrders and QueryOrders} endpoint. + */ + public enum Status { + @JsonProperty("pending") PENDING, + @JsonProperty("open") OPEN, + @JsonProperty("closed") CLOSED, + @JsonProperty("canceled") CANCELED, + @JsonProperty("expired") EXPIRED, + @JsonEnumDefaultValue UNKNOWN + } + + /** + * The type values used by the {@code OpenOrders, ClosedOrders and QueryOrders} endpoint. + */ + public enum Type { + @JsonProperty("buy") BUY, + @JsonProperty("sell") SELL, + @JsonEnumDefaultValue UNKNOWN + } + + /** + * The order type values used by the {@code OpenOrders, ClosedOrders and QueryOrders} endpoint. + */ + public enum OrderType { + @JsonProperty("market") MARKET, + @JsonProperty("limit") LIMIT, + @JsonProperty("iceberg") ICEBERG, + @JsonProperty("stop-loss") STOP_LOSS, + @JsonProperty("take-profit") TAKE_PROFIT, + @JsonProperty("trailing-stop") TRAILING_STOP, + @JsonProperty("stop-loss-limit") STOP_LOSS_LIMIT, + @JsonProperty("take-profit-limit") TAKE_PROFIT_LIMIT, + @JsonProperty("trailing-stop-limit") TRAILING_STOP_LIMIT, + @JsonProperty("settle-position") SETTLE_POSITION, + @JsonEnumDefaultValue UNKNOWN + } + + /** + * The description returned by {@code OpenOrders, ClosedOrders and QueryOrders}. + * + * @param pair the pair + * @param type the type + * @param orderType the order type + * @param price the price + * @param price2 the price2 + * @param leverage the leverage + * @param order the order + * @param close the close + * @param assetClass the asset class + */ + public record Description(String pair, + Type type, + @JsonProperty("ordertype") OrderType orderType, + BigDecimal price, + BigDecimal price2, + String leverage, + String order, + String close, + @JsonProperty("aclass") String assetClass) {} + + /** + * The time in force values used by the {@code OpenOrders, ClosedOrders and QueryOrders} endpoint. + */ + public enum TimeInForce { + @JsonProperty("gtc") GTC, + @JsonProperty("ioc") IOC, + @JsonProperty("gtd") GTD, + @JsonProperty("fok") FOK, + @JsonEnumDefaultValue UNKNOWN + } + + /** + * The trigger values used by the {@code OpenOrders, ClosedOrders and QueryOrders} endpoint. + */ + public enum Trigger { + @JsonProperty("last") LAST, + @JsonProperty("index") INDEX, + @JsonEnumDefaultValue UNKNOWN + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/OrderAmends.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/OrderAmends.java new file mode 100644 index 0000000..f87268c --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/OrderAmends.java @@ -0,0 +1,52 @@ +package dev.andstuff.kraken.api.endpoint.account.response; + +import java.math.BigDecimal; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * The order amends returned by {@code OrderAmends}. + * + * @param count the count; null when the count is omitted + * @param amends the amends + */ +public record OrderAmends(Long count, + List amends) { + + /** + * The amend type values used by the {@code OrderAmends} endpoint. + */ + public enum AmendType { + @JsonProperty("original") ORIGINAL, + @JsonProperty("user") USER, + @JsonProperty("restated") RESTATED, + @JsonEnumDefaultValue UNKNOWN + } + + /** + * The amendment returned by {@code OrderAmends}. + * + * @param amendId the amend id + * @param amendType the amend type + * @param orderQty the order qty + * @param displayQty the display qty + * @param remainingQty the remaining qty + * @param limitPrice the limit price + * @param triggerPrice the trigger price + * @param reason the reason + * @param postOnly the post only + * @param timestamp the timestamp in nanoseconds since the Unix epoch + */ + public record Amendment(@JsonProperty("amend_id") String amendId, + @JsonProperty("amend_type") AmendType amendType, + @JsonProperty("order_qty") BigDecimal orderQty, + @JsonProperty("display_qty") BigDecimal displayQty, + @JsonProperty("remaining_qty") BigDecimal remainingQty, + @JsonProperty("limit_price") BigDecimal limitPrice, + @JsonProperty("trigger_price") BigDecimal triggerPrice, + String reason, + @JsonProperty("post_only") Boolean postOnly, + Long timestamp) {} +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/TradeBalance.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/TradeBalance.java new file mode 100644 index 0000000..91fde9b --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/TradeBalance.java @@ -0,0 +1,32 @@ +package dev.andstuff.kraken.api.endpoint.account.response; + +import java.math.BigDecimal; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * The trade balance returned by {@code TradeBalance}. + * + * @param equivalentBalance the equivalent balance + * @param tradeBalance the trade balance + * @param margin the margin + * @param unrealizedNet the unrealized net + * @param costBasis the cost basis + * @param floatingValue the floating value + * @param equity the equity + * @param freeMargin the free margin + * @param freeMarginForOrders the free margin for orders + * @param marginLevel the margin level + * @param unexecutedValue the unexecuted value + */ +public record TradeBalance(@JsonProperty("eb") BigDecimal equivalentBalance, + @JsonProperty("tb") BigDecimal tradeBalance, + @JsonProperty("m") BigDecimal margin, + @JsonProperty("n") BigDecimal unrealizedNet, + @JsonProperty("c") BigDecimal costBasis, + @JsonProperty("v") BigDecimal floatingValue, + @JsonProperty("e") BigDecimal equity, + @JsonProperty("mf") BigDecimal freeMargin, + @JsonProperty("mfo") BigDecimal freeMarginForOrders, + @JsonProperty("ml") BigDecimal marginLevel, + @JsonProperty("uv") BigDecimal unexecutedValue) {} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/TradeVolume.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/TradeVolume.java new file mode 100644 index 0000000..0d45637 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/TradeVolume.java @@ -0,0 +1,117 @@ +package dev.andstuff.kraken.api.endpoint.account.response; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * The trade volume returned by {@code TradeVolume}. + * + * @param currency the currency + * @param assetClass the asset class + * @param volume the volume + * @param inputs the inputs + * @param fees the fees + * @param feesMaker the fees maker + * @param volumeSubaccounts the volume subaccounts + * @param schedules the schedules + */ +public record TradeVolume(String currency, + @JsonProperty("asset_class") AssetClass assetClass, + BigDecimal volume, + Inputs inputs, + Map fees, + @JsonProperty("fees_maker") Map feesMaker, + @JsonProperty("volume_subaccounts") List volumeSubaccounts, + List schedules) { + + /** + * The asset class values used by the {@code TradeVolume} endpoint. + */ + public enum AssetClass { + @JsonProperty("volume") VOLUME, + @JsonProperty("currency") CURRENCY, + @JsonProperty("forex") FOREX, + @JsonProperty("equity") EQUITY, + @JsonProperty("equity_pair") EQUITY_PAIR, + @JsonProperty("nft") NFT, + @JsonProperty("derivatives") DERIVATIVES, + @JsonProperty("tokenized_asset") TOKENIZED_ASSET, + @JsonProperty("futures_contract") FUTURES_CONTRACT, + @JsonEnumDefaultValue UNKNOWN + } + + /** + * The inputs returned by {@code TradeVolume}. + * + * @param domainSpotVolume30d the domain spot volume30d + * @param domainFuturesVolume30d the domain futures volume30d + * @param domainAssetsOnPlatform the domain assets on platform + */ + public record Inputs(@JsonProperty("domain_spot_volume_30d") BigDecimal domainSpotVolume30d, + @JsonProperty("domain_futures_volume_30d") BigDecimal domainFuturesVolume30d, + @JsonProperty("domain_assets_on_platform") BigDecimal domainAssetsOnPlatform) {} + + /** + * The fee tier returned by {@code TradeVolume}. + * + * @param fee the fee + * @param minimumFee the minimum fee + * @param maximumFee the maximum fee + * @param nextFee the next fee + * @param tierVolume the tier volume + * @param tierFuturesVolume the tier futures volume + * @param nextVolume the next volume + * @param nextFuturesVolume the next futures volume + * @param volumeOffset the volume offset + */ + public record FeeTier(BigDecimal fee, + @JsonProperty("minfee") BigDecimal minimumFee, + @JsonProperty("maxfee") BigDecimal maximumFee, + @JsonProperty("nextfee") BigDecimal nextFee, + @JsonProperty("tiervolume") BigDecimal tierVolume, + @JsonProperty("tierfuturesvolume") BigDecimal tierFuturesVolume, + @JsonProperty("nextvolume") BigDecimal nextVolume, + @JsonProperty("nextfuturesvolume") BigDecimal nextFuturesVolume, + @JsonProperty("volumeoffset") BigDecimal volumeOffset) {} + + /** + * The subaccount volume returned by {@code TradeVolume}. + * + * @param iiban the iiban + * @param volume the volume + */ + public record SubaccountVolume(String iiban, + BigDecimal volume) {} + + /** + * The tier returned by {@code TradeVolume}. + * + * @param makerFee the maker fee + * @param takerFee the taker fee + * @param minSpotVolume the min spot volume + * @param minFuturesVolume the min futures volume + * @param minAssetsOnPlatform the min assets on platform + * @param active the active + */ + public record Tier(@JsonProperty("maker_fee") BigDecimal makerFee, + @JsonProperty("taker_fee") BigDecimal takerFee, + @JsonProperty("min_spot_volume") BigDecimal minSpotVolume, + @JsonProperty("min_futures_volume") BigDecimal minFuturesVolume, + @JsonProperty("min_assets_on_platform") BigDecimal minAssetsOnPlatform, + Boolean active) {} + + /** + * The fee schedule returned by {@code TradeVolume}. + * + * @param pair the pair + * @param assetClass the asset class + * @param tiers the tiers + */ + public record FeeSchedule(String pair, + @JsonProperty("class") AssetClass assetClass, + List tiers) {} +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/TradesHistory.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/TradesHistory.java new file mode 100644 index 0000000..163ac31 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/TradesHistory.java @@ -0,0 +1,14 @@ +package dev.andstuff.kraken.api.endpoint.account.response; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * The trades history returned by {@code TradesHistory}. + * + * @param trades the trades + * @param count the count; null when the count is omitted + */ +public record TradesHistory(Map trades, + Long count) {} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/WalletAccounts.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/WalletAccounts.java new file mode 100644 index 0000000..bc4a155 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/WalletAccounts.java @@ -0,0 +1,69 @@ +package dev.andstuff.kraken.api.endpoint.account.response; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * The wallet accounts returned by {@code ListWalletAccounts}. + * + * @param accounts the accounts + * @param cursor the cursor + */ +public record WalletAccounts(List accounts, + Cursor cursor) { + + /** + * The flags returned by {@code ListWalletAccounts}. + * + * @param userDefined the user defined + * @param active the active + */ + public record Flags(@JsonProperty("user_defined") Boolean userDefined, + Boolean active) {} + + /** + * The status values used by the {@code ListWalletAccounts} endpoint. + */ + public enum Status { + @JsonProperty("active") ACTIVE, + @JsonProperty("disabled") DISABLED, + @JsonProperty("closed") CLOSED, + @JsonEnumDefaultValue UNKNOWN + } + + /** + * The type values used by the {@code ListWalletAccounts} endpoint. + */ + public enum Type { + @JsonProperty("main") MAIN, + @JsonProperty("spot") SPOT, + @JsonProperty("pay") PAY, + @JsonProperty("prop_paper") PROP_PAPER, + @JsonProperty("prop_real") PROP_REAL, + @JsonEnumDefaultValue UNKNOWN + } + + /** + * The account returned by {@code ListWalletAccounts}. + * + * @param accountId the account id + * @param flags the flags + * @param status the status + * @param type the type + * @param name the name + */ + public record Account(@JsonProperty("account_id") String accountId, + Flags flags, + Status status, + Type type, + String name) {} + + /** + * The cursor returned by {@code ListWalletAccounts}. + * + * @param next the next + */ + public record Cursor(String next) {} +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/priv/PrivateEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/priv/PrivateEndpoint.java index 5f41abd..7018ddd 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/priv/PrivateEndpoint.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/priv/PrivateEndpoint.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import dev.andstuff.kraken.api.endpoint.Endpoint; + import lombok.Getter; /** @@ -36,13 +37,22 @@ public PrivateEndpoint(String path, PostParams postParams, TypeReference resp * Returns the request body, built from the endpoint parameters and the given nonce. * * @param nonce the nonce of the request, which Kraken requires to be ever-increasing - * @return the URL encoded request body + * @return the request body, form-encoded unless an endpoint overrides the encoding */ public String encodedParamsWith(String nonce) { postParams.setNonce(nonce); return postParams.encoded(); } + /** + * Returns the media type of the signed request body. + * + * @return the form media type, unless an endpoint uses a different body encoding + */ + public String getContentType() { + return "application/x-www-form-urlencoded"; + } + /** * Builds the endpoint URL. Parameters are sent in the request body, not in the URL. * diff --git a/library/src/main/java/dev/andstuff/kraken/api/rest/DefaultKrakenRestRequester.java b/library/src/main/java/dev/andstuff/kraken/api/rest/DefaultKrakenRestRequester.java index efc4f55..f0db3a3 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/rest/DefaultKrakenRestRequester.java +++ b/library/src/main/java/dev/andstuff/kraken/api/rest/DefaultKrakenRestRequester.java @@ -15,10 +15,10 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import dev.andstuff.kraken.api.endpoint.Endpoint; -import dev.andstuff.kraken.api.endpoint.KrakenException; import dev.andstuff.kraken.api.endpoint.KrakenResponse; import dev.andstuff.kraken.api.endpoint.priv.PrivateEndpoint; import dev.andstuff.kraken.api.endpoint.pub.PublicEndpoint; + import lombok.extern.slf4j.Slf4j; /** @@ -72,6 +72,7 @@ public T execute(PrivateEndpoint endpoint, KrakenCredentials credentials, log.info("Fetching private endpoint: {}", connection.getURL()); connection.addRequestProperty("API-Key", credentials.getKey()); connection.addRequestProperty("API-Sign", credentials.sign(connection.getURL(), nonce, postData)); + connection.addRequestProperty("Content-Type", endpoint.getContentType()); connection.setDoOutput(true); try (OutputStreamWriter out = new OutputStreamWriter(connection.getOutputStream())) { @@ -97,7 +98,7 @@ private static T parseResponse(HttpsURLConnection connection, Endpoint en if ("application/json".equals(contentType)) { JavaType krakenResponseType = endpoint.wrappedResponseType(OBJECT_MAPPER.getTypeFactory()); KrakenResponse response = OBJECT_MAPPER.readValue(connection.getInputStream(), krakenResponseType); - return response.result().orElseThrow(() -> new KrakenException(response.error())); + return endpoint.unwrapResponse(response); } else if ("application/zip".equals(contentType)) { try (ZipInputStream zipStream = new ZipInputStream(connection.getInputStream())) { diff --git a/library/src/test/java/dev/andstuff/kraken/api/AccountKrakenAPITest.java b/library/src/test/java/dev/andstuff/kraken/api/AccountKrakenAPITest.java new file mode 100644 index 0000000..8d5680d --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/AccountKrakenAPITest.java @@ -0,0 +1,497 @@ +package dev.andstuff.kraken.api; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +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.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.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.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.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.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.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.rest.KrakenCredentials; +import dev.andstuff.kraken.api.rest.KrakenNonceGenerator; +import dev.andstuff.kraken.api.rest.KrakenRestRequester; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class AccountKrakenAPITest { + + @Mock private KrakenCredentials credentials; + @Mock private KrakenNonceGenerator nonceGenerator; + @Mock private KrakenRestRequester requester; + @Mock private Map accountBalanceResponse; + @Mock private Map extendedBalanceResponse; + @Mock private CreditLines creditLinesResponse; + @Mock private TradeBalance tradeBalanceResponse; + @Mock private OpenOrders openOrdersResponse; + @Mock private ClosedOrders closedOrdersResponse; + @Mock private Map queryOrdersResponse; + @Mock private OrderAmends orderAmendsResponse; + @Mock private TradesHistory tradesHistoryResponse; + @Mock private Map queryTradesResponse; + @Mock private Map openPositionsResponse; + @Mock private TradeVolume tradeVolumeResponse; + @Mock private ApiKeyInfo apiKeyInfoResponse; + @Mock private WalletAccounts walletAccountsResponse; + + @Test + void should_route_accountBalance_options_when_called() { + KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); + AccountBalanceParams params = AccountBalanceParams.builder().build(); + when(requester.execute(any(AccountBalanceEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(accountBalanceResponse); + + Map result = unit.accountBalance(params); + + assertThat(result).isSameAs(accountBalanceResponse); + verify(requester).execute(argThat((AccountBalanceEndpoint endpoint) -> endpoint.getPostParams() == params), same(credentials), same(nonceGenerator)); + } + + @Test + void should_route_accountBalance_defaults_when_called() { + KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); + when(requester.execute(any(AccountBalanceEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(accountBalanceResponse); + + Map result = unit.accountBalance(); + + assertThat(result).isSameAs(accountBalanceResponse); + verify(requester).execute(any(AccountBalanceEndpoint.class), same(credentials), same(nonceGenerator)); + } + + @Test + void should_reject_accountBalance_when_credentials_are_missing() { + KrakenAPI unit = new KrakenAPI(null, nonceGenerator, requester); + + assertThatThrownBy(() -> unit.accountBalance()).isInstanceOf(IllegalStateException.class); + verifyNoInteractions(requester); + } + + @Test + void should_route_extendedBalance_options_when_called() { + KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); + ExtendedBalanceParams params = ExtendedBalanceParams.builder().build(); + when(requester.execute(any(ExtendedBalanceEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(extendedBalanceResponse); + + Map result = unit.extendedBalance(params); + + assertThat(result).isSameAs(extendedBalanceResponse); + verify(requester).execute(argThat((ExtendedBalanceEndpoint endpoint) -> endpoint.getPostParams() == params), same(credentials), same(nonceGenerator)); + } + + @Test + void should_route_extendedBalance_defaults_when_called() { + KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); + when(requester.execute(any(ExtendedBalanceEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(extendedBalanceResponse); + + Map result = unit.extendedBalance(); + + assertThat(result).isSameAs(extendedBalanceResponse); + verify(requester).execute(any(ExtendedBalanceEndpoint.class), same(credentials), same(nonceGenerator)); + } + + @Test + void should_reject_extendedBalance_when_credentials_are_missing() { + KrakenAPI unit = new KrakenAPI(null, nonceGenerator, requester); + + assertThatThrownBy(() -> unit.extendedBalance()).isInstanceOf(IllegalStateException.class); + verifyNoInteractions(requester); + } + + @Test + void should_route_creditLines_options_when_called() { + KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); + CreditLinesParams params = CreditLinesParams.builder().build(); + when(requester.execute(any(CreditLinesEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(creditLinesResponse); + + CreditLines result = unit.creditLines(params); + + assertThat(result).isSameAs(creditLinesResponse); + verify(requester).execute(argThat((CreditLinesEndpoint endpoint) -> endpoint.getPostParams() == params), same(credentials), same(nonceGenerator)); + } + + @Test + void should_route_creditLines_defaults_when_called() { + KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); + when(requester.execute(any(CreditLinesEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(creditLinesResponse); + + CreditLines result = unit.creditLines(); + + assertThat(result).isSameAs(creditLinesResponse); + verify(requester).execute(any(CreditLinesEndpoint.class), same(credentials), same(nonceGenerator)); + } + + @Test + void should_reject_creditLines_when_credentials_are_missing() { + KrakenAPI unit = new KrakenAPI(null, nonceGenerator, requester); + + assertThatThrownBy(() -> unit.creditLines()).isInstanceOf(IllegalStateException.class); + verifyNoInteractions(requester); + } + + @Test + void should_route_tradeBalance_options_when_called() { + KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); + TradeBalanceParams params = TradeBalanceParams.builder().build(); + when(requester.execute(any(TradeBalanceEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(tradeBalanceResponse); + + TradeBalance result = unit.tradeBalance(params); + + assertThat(result).isSameAs(tradeBalanceResponse); + verify(requester).execute(argThat((TradeBalanceEndpoint endpoint) -> endpoint.getPostParams() == params), same(credentials), same(nonceGenerator)); + } + + @Test + void should_route_tradeBalance_defaults_when_called() { + KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); + when(requester.execute(any(TradeBalanceEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(tradeBalanceResponse); + + TradeBalance result = unit.tradeBalance(); + + assertThat(result).isSameAs(tradeBalanceResponse); + verify(requester).execute(any(TradeBalanceEndpoint.class), same(credentials), same(nonceGenerator)); + } + + @Test + void should_reject_tradeBalance_when_credentials_are_missing() { + KrakenAPI unit = new KrakenAPI(null, nonceGenerator, requester); + + assertThatThrownBy(() -> unit.tradeBalance()).isInstanceOf(IllegalStateException.class); + verifyNoInteractions(requester); + } + + @Test + void should_route_openOrders_options_when_called() { + KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); + OpenOrdersParams params = OpenOrdersParams.builder().build(); + when(requester.execute(any(OpenOrdersEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(openOrdersResponse); + + OpenOrders result = unit.openOrders(params); + + assertThat(result).isSameAs(openOrdersResponse); + verify(requester).execute(argThat((OpenOrdersEndpoint endpoint) -> endpoint.getPostParams() == params), same(credentials), same(nonceGenerator)); + } + + @Test + void should_route_openOrders_defaults_when_called() { + KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); + when(requester.execute(any(OpenOrdersEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(openOrdersResponse); + + OpenOrders result = unit.openOrders(); + + assertThat(result).isSameAs(openOrdersResponse); + verify(requester).execute(any(OpenOrdersEndpoint.class), same(credentials), same(nonceGenerator)); + } + + @Test + void should_reject_openOrders_when_credentials_are_missing() { + KrakenAPI unit = new KrakenAPI(null, nonceGenerator, requester); + + assertThatThrownBy(() -> unit.openOrders()).isInstanceOf(IllegalStateException.class); + verifyNoInteractions(requester); + } + + @Test + void should_route_closedOrders_options_when_called() { + KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); + ClosedOrdersParams params = ClosedOrdersParams.builder().build(); + when(requester.execute(any(ClosedOrdersEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(closedOrdersResponse); + + ClosedOrders result = unit.closedOrders(params); + + assertThat(result).isSameAs(closedOrdersResponse); + verify(requester).execute(argThat((ClosedOrdersEndpoint endpoint) -> endpoint.getPostParams() == params), same(credentials), same(nonceGenerator)); + } + + @Test + void should_route_closedOrders_defaults_when_called() { + KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); + when(requester.execute(any(ClosedOrdersEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(closedOrdersResponse); + + ClosedOrders result = unit.closedOrders(); + + assertThat(result).isSameAs(closedOrdersResponse); + verify(requester).execute(any(ClosedOrdersEndpoint.class), same(credentials), same(nonceGenerator)); + } + + @Test + void should_reject_closedOrders_when_credentials_are_missing() { + KrakenAPI unit = new KrakenAPI(null, nonceGenerator, requester); + + assertThatThrownBy(() -> unit.closedOrders()).isInstanceOf(IllegalStateException.class); + verifyNoInteractions(requester); + } + + @Test + void should_route_queryOrders_options_when_called() { + KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); + QueryOrdersParams params = QueryOrdersParams.builder().transactionIds(List.of("ID-1", "ID+2")).build(); + when(requester.execute(any(QueryOrdersEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(queryOrdersResponse); + + Map result = unit.queryOrders(params); + + assertThat(result).isSameAs(queryOrdersResponse); + verify(requester).execute(argThat((QueryOrdersEndpoint endpoint) -> endpoint.getPostParams() == params), same(credentials), same(nonceGenerator)); + } + + @Test + void should_reject_queryOrders_when_credentials_are_missing() { + KrakenAPI unit = new KrakenAPI(null, nonceGenerator, requester); + QueryOrdersParams params = QueryOrdersParams.builder().transactionIds(List.of("ID-1", "ID+2")).build(); + + assertThatThrownBy(() -> unit.queryOrders(params)).isInstanceOf(IllegalStateException.class); + verifyNoInteractions(requester); + } + + @Test + void should_route_orderAmends_options_when_called() { + KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); + OrderAmendsParams params = OrderAmendsParams.builder().build(); + when(requester.execute(any(OrderAmendsEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(orderAmendsResponse); + + OrderAmends result = unit.orderAmends(params); + + assertThat(result).isSameAs(orderAmendsResponse); + verify(requester).execute(argThat((OrderAmendsEndpoint endpoint) -> endpoint.getPostParams() == params), same(credentials), same(nonceGenerator)); + } + + @Test + void should_route_orderAmends_defaults_when_called() { + KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); + when(requester.execute(any(OrderAmendsEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(orderAmendsResponse); + + OrderAmends result = unit.orderAmends(); + + assertThat(result).isSameAs(orderAmendsResponse); + verify(requester).execute(any(OrderAmendsEndpoint.class), same(credentials), same(nonceGenerator)); + } + + @Test + void should_reject_orderAmends_when_credentials_are_missing() { + KrakenAPI unit = new KrakenAPI(null, nonceGenerator, requester); + + assertThatThrownBy(() -> unit.orderAmends()).isInstanceOf(IllegalStateException.class); + verifyNoInteractions(requester); + } + + @Test + void should_route_tradesHistory_options_when_called() { + KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); + TradesHistoryParams params = TradesHistoryParams.builder().build(); + when(requester.execute(any(TradesHistoryEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(tradesHistoryResponse); + + TradesHistory result = unit.tradesHistory(params); + + assertThat(result).isSameAs(tradesHistoryResponse); + verify(requester).execute(argThat((TradesHistoryEndpoint endpoint) -> endpoint.getPostParams() == params), same(credentials), same(nonceGenerator)); + } + + @Test + void should_route_tradesHistory_defaults_when_called() { + KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); + when(requester.execute(any(TradesHistoryEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(tradesHistoryResponse); + + TradesHistory result = unit.tradesHistory(); + + assertThat(result).isSameAs(tradesHistoryResponse); + verify(requester).execute(any(TradesHistoryEndpoint.class), same(credentials), same(nonceGenerator)); + } + + @Test + void should_reject_tradesHistory_when_credentials_are_missing() { + KrakenAPI unit = new KrakenAPI(null, nonceGenerator, requester); + + assertThatThrownBy(() -> unit.tradesHistory()).isInstanceOf(IllegalStateException.class); + verifyNoInteractions(requester); + } + + @Test + void should_route_queryTrades_options_when_called() { + KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); + QueryTradesParams params = QueryTradesParams.builder().transactionIds(List.of("ID-1", "ID+2")).build(); + when(requester.execute(any(QueryTradesEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(queryTradesResponse); + + Map result = unit.queryTrades(params); + + assertThat(result).isSameAs(queryTradesResponse); + verify(requester).execute(argThat((QueryTradesEndpoint endpoint) -> endpoint.getPostParams() == params), same(credentials), same(nonceGenerator)); + } + + @Test + void should_reject_queryTrades_when_credentials_are_missing() { + KrakenAPI unit = new KrakenAPI(null, nonceGenerator, requester); + QueryTradesParams params = QueryTradesParams.builder().transactionIds(List.of("ID-1", "ID+2")).build(); + + assertThatThrownBy(() -> unit.queryTrades(params)).isInstanceOf(IllegalStateException.class); + verifyNoInteractions(requester); + } + + @Test + void should_route_openPositions_options_when_called() { + KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); + OpenPositionsParams params = OpenPositionsParams.builder().build(); + when(requester.execute(any(OpenPositionsEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(openPositionsResponse); + + Map result = unit.openPositions(params); + + assertThat(result).isSameAs(openPositionsResponse); + verify(requester).execute(argThat((OpenPositionsEndpoint endpoint) -> endpoint.getPostParams() == params), same(credentials), same(nonceGenerator)); + } + + @Test + void should_route_openPositions_defaults_when_called() { + KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); + when(requester.execute(any(OpenPositionsEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(openPositionsResponse); + + Map result = unit.openPositions(); + + assertThat(result).isSameAs(openPositionsResponse); + verify(requester).execute(any(OpenPositionsEndpoint.class), same(credentials), same(nonceGenerator)); + } + + @Test + void should_reject_openPositions_when_credentials_are_missing() { + KrakenAPI unit = new KrakenAPI(null, nonceGenerator, requester); + + assertThatThrownBy(() -> unit.openPositions()).isInstanceOf(IllegalStateException.class); + verifyNoInteractions(requester); + } + + @Test + void should_route_tradeVolume_options_when_called() { + KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); + TradeVolumeParams params = TradeVolumeParams.builder().build(); + when(requester.execute(any(TradeVolumeEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(tradeVolumeResponse); + + TradeVolume result = unit.tradeVolume(params); + + assertThat(result).isSameAs(tradeVolumeResponse); + verify(requester).execute(argThat((TradeVolumeEndpoint endpoint) -> endpoint.getPostParams() == params), same(credentials), same(nonceGenerator)); + } + + @Test + void should_route_tradeVolume_defaults_when_called() { + KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); + when(requester.execute(any(TradeVolumeEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(tradeVolumeResponse); + + TradeVolume result = unit.tradeVolume(); + + assertThat(result).isSameAs(tradeVolumeResponse); + verify(requester).execute(any(TradeVolumeEndpoint.class), same(credentials), same(nonceGenerator)); + } + + @Test + void should_reject_tradeVolume_when_credentials_are_missing() { + KrakenAPI unit = new KrakenAPI(null, nonceGenerator, requester); + + assertThatThrownBy(() -> unit.tradeVolume()).isInstanceOf(IllegalStateException.class); + verifyNoInteractions(requester); + } + + @Test + void should_route_apiKeyInfo_options_when_called() { + KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); + ApiKeyInfoParams params = ApiKeyInfoParams.builder().build(); + when(requester.execute(any(ApiKeyInfoEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(apiKeyInfoResponse); + + ApiKeyInfo result = unit.apiKeyInfo(params); + + assertThat(result).isSameAs(apiKeyInfoResponse); + verify(requester).execute(argThat((ApiKeyInfoEndpoint endpoint) -> endpoint.getPostParams() == params), same(credentials), same(nonceGenerator)); + } + + @Test + void should_route_apiKeyInfo_defaults_when_called() { + KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); + when(requester.execute(any(ApiKeyInfoEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(apiKeyInfoResponse); + + ApiKeyInfo result = unit.apiKeyInfo(); + + assertThat(result).isSameAs(apiKeyInfoResponse); + verify(requester).execute(any(ApiKeyInfoEndpoint.class), same(credentials), same(nonceGenerator)); + } + + @Test + void should_reject_apiKeyInfo_when_credentials_are_missing() { + KrakenAPI unit = new KrakenAPI(null, nonceGenerator, requester); + + assertThatThrownBy(() -> unit.apiKeyInfo()).isInstanceOf(IllegalStateException.class); + verifyNoInteractions(requester); + } + + @Test + void should_route_walletAccounts_options_when_called() { + KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); + WalletAccountsParams params = WalletAccountsParams.builder().build(); + when(requester.execute(any(WalletAccountsEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(walletAccountsResponse); + + WalletAccounts result = unit.walletAccounts(params); + + assertThat(result).isSameAs(walletAccountsResponse); + verify(requester).execute(argThat((WalletAccountsEndpoint endpoint) -> endpoint.getPostParams() == params), same(credentials), same(nonceGenerator)); + } + + @Test + void should_route_walletAccounts_defaults_when_called() { + KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); + when(requester.execute(any(WalletAccountsEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(walletAccountsResponse); + + WalletAccounts result = unit.walletAccounts(); + + assertThat(result).isSameAs(walletAccountsResponse); + verify(requester).execute(any(WalletAccountsEndpoint.class), same(credentials), same(nonceGenerator)); + } + + @Test + void should_reject_walletAccounts_when_credentials_are_missing() { + KrakenAPI unit = new KrakenAPI(null, nonceGenerator, requester); + + assertThatThrownBy(() -> unit.walletAccounts()).isInstanceOf(IllegalStateException.class); + verifyNoInteractions(requester); + } + +} diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/AccountBalanceEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/AccountBalanceEndpointTest.java new file mode 100644 index 0000000..e9494b7 --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/AccountBalanceEndpointTest.java @@ -0,0 +1,79 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import java.math.BigDecimal; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; + +import dev.andstuff.kraken.api.endpoint.KrakenException; +import dev.andstuff.kraken.api.endpoint.KrakenResponse; +import dev.andstuff.kraken.api.endpoint.account.params.*; +import dev.andstuff.kraken.api.endpoint.account.response.*; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@ExtendWith(MockitoExtension.class) +class AccountBalanceEndpointTest { + + @Test + void should_encode_all_options_when_supplied() { + AccountBalanceEndpoint unit = new AccountBalanceEndpoint(AccountBalanceParams.builder().rebaseMultiplier(RebaseMultiplier.BASE).accountId("id +/&=").build()); + + Map result = Arrays.stream(unit.encodedParamsWith("123456789").split("&")) + .map(value -> value.split("=", 2)) + .collect(Collectors.toMap(value -> value[0], value -> URLDecoder.decode(value[1], StandardCharsets.UTF_8))); + + assertThat(result).containsExactlyInAnyOrderEntriesOf(Map.ofEntries( + Map.entry("nonce", "123456789"), + Map.entry("rebase_multiplier", "base"))); + assertThat(unit.buildURL().getPath()).isEqualTo("/0/private/Balance"); + assertThat(unit.getHttpMethod()).isEqualTo("POST"); + } + + @Test + void should_omit_optional_values_when_not_supplied() { + AccountBalanceEndpoint unit = new AccountBalanceEndpoint(); + + assertThat(unit.encodedParamsWith("123")).isEqualTo("nonce=123"); + } + + @Test + void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exception { + AccountBalanceEndpoint unit = new AccountBalanceEndpoint(AccountBalanceParams.builder().build()); + JsonMapper mapper = JsonMapper.builder() + .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) + .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .addModule(new Jdk8Module()).build(); + String json = Files.readString(Path.of("src/test/resources/account/Balance.json")); + + KrakenResponse> response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + Map result = unit.unwrapResponse(response); + + assertThat(result.get("ZUSD")).isEqualByComparingTo("171288.6158"); + assertThat(result.get("XXBT")).isEqualByComparingTo("1011.1908877900"); + } + + @Test + void should_encode_wallet_in_query_when_wallet_is_selected() { + AccountBalanceEndpoint unit = new AccountBalanceEndpoint(AccountBalanceParams.builder().accountId("wallet +/&=").build()); + + assertThat(unit.buildURL().getQuery()).isEqualTo("account_id=wallet+%2B%2F%26%3D"); + assertThat(unit.encodedParamsWith("123")).isEqualTo("nonce=123"); + } +} diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/ApiKeyInfoEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/ApiKeyInfoEndpointTest.java new file mode 100644 index 0000000..bef7dd0 --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/ApiKeyInfoEndpointTest.java @@ -0,0 +1,72 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import java.math.BigDecimal; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; + +import dev.andstuff.kraken.api.endpoint.KrakenException; +import dev.andstuff.kraken.api.endpoint.KrakenResponse; +import dev.andstuff.kraken.api.endpoint.account.params.*; +import dev.andstuff.kraken.api.endpoint.account.response.*; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@ExtendWith(MockitoExtension.class) +class ApiKeyInfoEndpointTest { + + @Test + void should_encode_all_options_when_supplied() { + ApiKeyInfoEndpoint unit = new ApiKeyInfoEndpoint(ApiKeyInfoParams.builder().otp("id +/&=").build()); + + Map result = Arrays.stream(unit.encodedParamsWith("123456789").split("&")) + .map(value -> value.split("=", 2)) + .collect(Collectors.toMap(value -> value[0], value -> URLDecoder.decode(value[1], StandardCharsets.UTF_8))); + + assertThat(result).containsExactlyInAnyOrderEntriesOf(Map.ofEntries( + Map.entry("nonce", "123456789"), + Map.entry("otp", "id +/&="))); + assertThat(unit.buildURL().getPath()).isEqualTo("/0/private/GetApiKeyInfo"); + assertThat(unit.getHttpMethod()).isEqualTo("POST"); + } + + @Test + void should_omit_optional_values_when_not_supplied() { + ApiKeyInfoEndpoint unit = new ApiKeyInfoEndpoint(); + + assertThat(unit.encodedParamsWith("123")).isEqualTo("nonce=123"); + } + + @Test + void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exception { + ApiKeyInfoEndpoint unit = new ApiKeyInfoEndpoint(ApiKeyInfoParams.builder().build()); + JsonMapper mapper = JsonMapper.builder() + .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) + .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .addModule(new Jdk8Module()).build(); + String json = Files.readString(Path.of("src/test/resources/account/GetApiKeyInfo.json")); + + KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + ApiKeyInfo result = unit.unwrapResponse(response); + + assertThat(result.apiKey()).isEqualTo("documentation-test-key"); + assertThat(result.nonce()).isEqualTo("1772627060997"); + assertThat(result.lastUsed()).isEqualTo("1772627061"); + } +} diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/ClosedOrdersEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/ClosedOrdersEndpointTest.java new file mode 100644 index 0000000..6316b49 --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/ClosedOrdersEndpointTest.java @@ -0,0 +1,81 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import java.math.BigDecimal; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; + +import dev.andstuff.kraken.api.endpoint.KrakenException; +import dev.andstuff.kraken.api.endpoint.KrakenResponse; +import dev.andstuff.kraken.api.endpoint.account.params.*; +import dev.andstuff.kraken.api.endpoint.account.response.*; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@ExtendWith(MockitoExtension.class) +class ClosedOrdersEndpointTest { + + @Test + void should_encode_all_options_when_supplied() { + ClosedOrdersEndpoint unit = new ClosedOrdersEndpoint(ClosedOrdersParams.builder().trades(false).userReference(0L).clientOrderId("id +/&=").start("1700000000").end("1700000000").offset(0).closeTime(CloseTime.BOTH).consolidateTaker(false).withoutCount(false).rebaseMultiplier(RebaseMultiplier.BASE).build()); + + Map result = Arrays.stream(unit.encodedParamsWith("123456789").split("&")) + .map(value -> value.split("=", 2)) + .collect(Collectors.toMap(value -> value[0], value -> URLDecoder.decode(value[1], StandardCharsets.UTF_8))); + + assertThat(result).containsExactlyInAnyOrderEntriesOf(Map.ofEntries( + Map.entry("nonce", "123456789"), + Map.entry("trades", "false"), + Map.entry("userref", "0"), + Map.entry("cl_ord_id", "id +/&="), + Map.entry("start", "1700000000"), + Map.entry("end", "1700000000"), + Map.entry("ofs", "0"), + Map.entry("closetime", "both"), + Map.entry("consolidate_taker", "false"), + Map.entry("without_count", "false"), + Map.entry("rebase_multiplier", "base"))); + assertThat(unit.buildURL().getPath()).isEqualTo("/0/private/ClosedOrders"); + assertThat(unit.getHttpMethod()).isEqualTo("POST"); + } + + @Test + void should_omit_optional_values_when_not_supplied() { + ClosedOrdersEndpoint unit = new ClosedOrdersEndpoint(); + + assertThat(unit.encodedParamsWith("123")).isEqualTo("nonce=123"); + } + + @Test + void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exception { + ClosedOrdersEndpoint unit = new ClosedOrdersEndpoint(ClosedOrdersParams.builder().build()); + JsonMapper mapper = JsonMapper.builder() + .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) + .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .addModule(new Jdk8Module()).build(); + String json = Files.readString(Path.of("src/test/resources/account/ClosedOrders.json")); + + KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + ClosedOrders result = unit.unwrapResponse(response); + + assertThat(result.count()).isEqualTo(2L); + assertThat(result.closed().get("O37652-RJWRT-IMO74O").status()).isEqualTo(Order.Status.CANCELED); + assertThat(result.closed().get("O37652-RJWRT-IMO74O").closeTime()).isEqualByComparingTo("1688148610.0482"); + } +} diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/CreditLinesEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/CreditLinesEndpointTest.java new file mode 100644 index 0000000..c761c1e --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/CreditLinesEndpointTest.java @@ -0,0 +1,89 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import java.math.BigDecimal; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; + +import dev.andstuff.kraken.api.endpoint.KrakenException; +import dev.andstuff.kraken.api.endpoint.KrakenResponse; +import dev.andstuff.kraken.api.endpoint.account.params.*; +import dev.andstuff.kraken.api.endpoint.account.response.*; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@ExtendWith(MockitoExtension.class) +class CreditLinesEndpointTest { + + @Test + void should_encode_all_options_when_supplied() { + CreditLinesEndpoint unit = new CreditLinesEndpoint(CreditLinesParams.builder().rebaseMultiplier(RebaseMultiplier.BASE).build()); + + Map result = Arrays.stream(unit.encodedParamsWith("123456789").split("&")) + .map(value -> value.split("=", 2)) + .collect(Collectors.toMap(value -> value[0], value -> URLDecoder.decode(value[1], StandardCharsets.UTF_8))); + + assertThat(result).containsExactlyInAnyOrderEntriesOf(Map.ofEntries( + Map.entry("nonce", "123456789"), + Map.entry("rebase_multiplier", "base"))); + assertThat(unit.buildURL().getPath()).isEqualTo("/0/private/CreditLines"); + assertThat(unit.getHttpMethod()).isEqualTo("POST"); + } + + @Test + void should_omit_optional_values_when_not_supplied() { + CreditLinesEndpoint unit = new CreditLinesEndpoint(); + + assertThat(unit.encodedParamsWith("123")).isEqualTo("nonce=123"); + } + + @Test + void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exception { + CreditLinesEndpoint unit = new CreditLinesEndpoint(CreditLinesParams.builder().build()); + JsonMapper mapper = JsonMapper.builder() + .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) + .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .addModule(new Jdk8Module()).build(); + String json = Files.readString(Path.of("src/test/resources/account/CreditLines.json")); + + KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + CreditLines result = unit.unwrapResponse(response); + + assertThat(result.assetDetails().get("USD").availableCredit()).isEqualByComparingTo("37500.0000"); + assertThat(result.limitsMonitor().debtToEquity()).isEqualByComparingTo("0.2000"); + } + + @Test + void should_return_null_when_no_credit_lines_exist() throws Exception { + CreditLinesEndpoint unit = new CreditLinesEndpoint(); + JsonMapper mapper = JsonMapper.builder().addModule(new Jdk8Module()).build(); + KrakenResponse response = mapper.readValue("{\"error\":[],\"result\":null}", unit.wrappedResponseType(mapper.getTypeFactory())); + + assertThat(unit.unwrapResponse(response)).isNull(); + } + + @Test + void should_throw_kraken_error_when_credit_request_is_rejected() throws Exception { + CreditLinesEndpoint unit = new CreditLinesEndpoint(); + JsonMapper mapper = JsonMapper.builder().addModule(new Jdk8Module()).build(); + KrakenResponse response = mapper.readValue("{\"error\":[\"EGeneral:Permission denied\"]}", unit.wrappedResponseType(mapper.getTypeFactory())); + + assertThatThrownBy(() -> unit.unwrapResponse(response)).isInstanceOf(KrakenException.class); + } +} diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/ExtendedBalanceEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/ExtendedBalanceEndpointTest.java new file mode 100644 index 0000000..c1bfc00 --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/ExtendedBalanceEndpointTest.java @@ -0,0 +1,72 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import java.math.BigDecimal; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; + +import dev.andstuff.kraken.api.endpoint.KrakenException; +import dev.andstuff.kraken.api.endpoint.KrakenResponse; +import dev.andstuff.kraken.api.endpoint.account.params.*; +import dev.andstuff.kraken.api.endpoint.account.response.*; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@ExtendWith(MockitoExtension.class) +class ExtendedBalanceEndpointTest { + + @Test + void should_encode_all_options_when_supplied() { + ExtendedBalanceEndpoint unit = new ExtendedBalanceEndpoint(ExtendedBalanceParams.builder().rebaseMultiplier(RebaseMultiplier.BASE).build()); + + Map result = Arrays.stream(unit.encodedParamsWith("123456789").split("&")) + .map(value -> value.split("=", 2)) + .collect(Collectors.toMap(value -> value[0], value -> URLDecoder.decode(value[1], StandardCharsets.UTF_8))); + + assertThat(result).containsExactlyInAnyOrderEntriesOf(Map.ofEntries( + Map.entry("nonce", "123456789"), + Map.entry("rebase_multiplier", "base"))); + assertThat(unit.buildURL().getPath()).isEqualTo("/0/private/BalanceEx"); + assertThat(unit.getHttpMethod()).isEqualTo("POST"); + } + + @Test + void should_omit_optional_values_when_not_supplied() { + ExtendedBalanceEndpoint unit = new ExtendedBalanceEndpoint(); + + assertThat(unit.encodedParamsWith("123")).isEqualTo("nonce=123"); + } + + @Test + void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exception { + ExtendedBalanceEndpoint unit = new ExtendedBalanceEndpoint(ExtendedBalanceParams.builder().build()); + JsonMapper mapper = JsonMapper.builder() + .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) + .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .addModule(new Jdk8Module()).build(); + String json = Files.readString(Path.of("src/test/resources/account/BalanceEx.json")); + + KrakenResponse> response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + Map result = unit.unwrapResponse(response); + + assertThat(result.get("ZUSD").holdTrade()).isEqualByComparingTo("8249.76"); + assertThat(result.get("XXBT").balance()).isEqualByComparingTo("1.2435"); + assertThat(result.get("ZUSD").credit()).isNull(); + } +} diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/OpenOrdersEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/OpenOrdersEndpointTest.java new file mode 100644 index 0000000..690d87e --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/OpenOrdersEndpointTest.java @@ -0,0 +1,93 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import java.math.BigDecimal; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; + +import dev.andstuff.kraken.api.endpoint.KrakenException; +import dev.andstuff.kraken.api.endpoint.KrakenResponse; +import dev.andstuff.kraken.api.endpoint.account.params.*; +import dev.andstuff.kraken.api.endpoint.account.response.*; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@ExtendWith(MockitoExtension.class) +class OpenOrdersEndpointTest { + + @Test + void should_encode_all_options_when_supplied() { + OpenOrdersEndpoint unit = new OpenOrdersEndpoint(OpenOrdersParams.builder().trades(false).userReference(0L).clientOrderId("id +/&=").rebaseMultiplier(RebaseMultiplier.BASE).build()); + + Map result = Arrays.stream(unit.encodedParamsWith("123456789").split("&")) + .map(value -> value.split("=", 2)) + .collect(Collectors.toMap(value -> value[0], value -> URLDecoder.decode(value[1], StandardCharsets.UTF_8))); + + assertThat(result).containsExactlyInAnyOrderEntriesOf(Map.ofEntries( + Map.entry("nonce", "123456789"), + Map.entry("trades", "false"), + Map.entry("userref", "0"), + Map.entry("cl_ord_id", "id +/&="), + Map.entry("rebase_multiplier", "base"))); + assertThat(unit.buildURL().getPath()).isEqualTo("/0/private/OpenOrders"); + assertThat(unit.getHttpMethod()).isEqualTo("POST"); + } + + @Test + void should_omit_optional_values_when_not_supplied() { + OpenOrdersEndpoint unit = new OpenOrdersEndpoint(); + + assertThat(unit.encodedParamsWith("123")).isEqualTo("nonce=123"); + } + + @Test + void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exception { + OpenOrdersEndpoint unit = new OpenOrdersEndpoint(OpenOrdersParams.builder().build()); + JsonMapper mapper = JsonMapper.builder() + .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) + .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .addModule(new Jdk8Module()).build(); + String json = Files.readString(Path.of("src/test/resources/account/OpenOrders.json")); + + KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + OpenOrders result = unit.unwrapResponse(response); + + assertThat(result.open().get("OQCLML-BW3P3-BUCMWZ").executedVolume()).isEqualByComparingTo("0.37500000"); + assertThat(result.open().get("OQCLML-BW3P3-BUCMWZ").openTime()).isEqualByComparingTo("1688666559.8974"); + assertThat(result.open().get("OQCLML-BW3P3-BUCMWZ").description().orderType()).isEqualTo(Order.OrderType.LIMIT); + } + + @Test + void should_preserve_margin_and_unknown_enums_when_new_order_values_are_returned() throws Exception { + OpenOrdersEndpoint unit = new OpenOrdersEndpoint(); + JsonMapper mapper = JsonMapper.builder().addModule(new Jdk8Module()) + .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES).build(); + String json = """ + {"error":[],"result":{"open":{"order":{"margin":true,"status":"future-status","descr":{"ordertype":"future-type"},"future_field":123}}}} + """; + + KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + Order result = unit.unwrapResponse(response).open().get("order"); + + assertThat(result.margin()).isTrue(); + assertThat(result.status()).isEqualTo(Order.Status.UNKNOWN); + assertThat(result.description().orderType()).isEqualTo(Order.OrderType.UNKNOWN); + } +} diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/OpenPositionsEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/OpenPositionsEndpointTest.java new file mode 100644 index 0000000..a6fae6a --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/OpenPositionsEndpointTest.java @@ -0,0 +1,75 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import java.math.BigDecimal; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; + +import dev.andstuff.kraken.api.endpoint.KrakenException; +import dev.andstuff.kraken.api.endpoint.KrakenResponse; +import dev.andstuff.kraken.api.endpoint.account.params.*; +import dev.andstuff.kraken.api.endpoint.account.response.*; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@ExtendWith(MockitoExtension.class) +class OpenPositionsEndpointTest { + + @Test + void should_encode_all_options_when_supplied() { + OpenPositionsEndpoint unit = new OpenPositionsEndpoint(OpenPositionsParams.builder().transactionIds(List.of("ID-1", "ID+2")).calculateValues(false).consolidation(Consolidation.MARKET).rebaseMultiplier(RebaseMultiplier.BASE).build()); + + Map result = Arrays.stream(unit.encodedParamsWith("123456789").split("&")) + .map(value -> value.split("=", 2)) + .collect(Collectors.toMap(value -> value[0], value -> URLDecoder.decode(value[1], StandardCharsets.UTF_8))); + + assertThat(result).containsExactlyInAnyOrderEntriesOf(Map.ofEntries( + Map.entry("nonce", "123456789"), + Map.entry("txid", "ID-1,ID+2"), + Map.entry("docalcs", "false"), + Map.entry("consolidation", "market"), + Map.entry("rebase_multiplier", "base"))); + assertThat(unit.buildURL().getPath()).isEqualTo("/0/private/OpenPositions"); + assertThat(unit.getHttpMethod()).isEqualTo("POST"); + } + + @Test + void should_omit_optional_values_when_not_supplied() { + OpenPositionsEndpoint unit = new OpenPositionsEndpoint(); + + assertThat(unit.encodedParamsWith("123")).isEqualTo("nonce=123"); + } + + @Test + void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exception { + OpenPositionsEndpoint unit = new OpenPositionsEndpoint(OpenPositionsParams.builder().build()); + JsonMapper mapper = JsonMapper.builder() + .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) + .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .addModule(new Jdk8Module()).build(); + String json = Files.readString(Path.of("src/test/resources/account/OpenPositions.json")); + + KrakenResponse> response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + Map result = unit.unwrapResponse(response); + + assertThat(result.get("TF5GVO-T7ZZ2-6NBKBI").net()).isEqualByComparingTo("154186.9728"); + assertThat(result.get("TF5GVO-T7ZZ2-6NBKBI").closedVolume()).isEqualByComparingTo("0.20200000"); + assertThat(result.get("TF5GVO-T7ZZ2-6NBKBI").rolloverTime()).isEqualTo("1616672637"); + } +} diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/OrderAmendsEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/OrderAmendsEndpointTest.java new file mode 100644 index 0000000..32c513f --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/OrderAmendsEndpointTest.java @@ -0,0 +1,74 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import java.math.BigDecimal; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; + +import dev.andstuff.kraken.api.endpoint.KrakenException; +import dev.andstuff.kraken.api.endpoint.KrakenResponse; +import dev.andstuff.kraken.api.endpoint.account.params.*; +import dev.andstuff.kraken.api.endpoint.account.response.*; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@ExtendWith(MockitoExtension.class) +class OrderAmendsEndpointTest { + + @Test + void should_encode_all_options_when_supplied() { + OrderAmendsEndpoint unit = new OrderAmendsEndpoint(OrderAmendsParams.builder().orderId("id +/&=").rebaseMultiplier(RebaseMultiplier.BASE).build()); + + Map result = Arrays.stream(unit.encodedParamsWith("123456789").split("&")) + .map(value -> value.split("=", 2)) + .collect(Collectors.toMap(value -> value[0], value -> URLDecoder.decode(value[1], StandardCharsets.UTF_8))); + + assertThat(result).containsExactlyInAnyOrderEntriesOf(Map.ofEntries( + Map.entry("nonce", "123456789"), + Map.entry("order_id", "id +/&="), + Map.entry("rebase_multiplier", "base"))); + assertThat(unit.buildURL().getPath()).isEqualTo("/0/private/OrderAmends"); + assertThat(unit.getHttpMethod()).isEqualTo("POST"); + } + + @Test + void should_omit_optional_values_when_not_supplied() { + OrderAmendsEndpoint unit = new OrderAmendsEndpoint(); + + assertThat(unit.encodedParamsWith("123")).isEqualTo("nonce=123"); + } + + @Test + void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exception { + OrderAmendsEndpoint unit = new OrderAmendsEndpoint(OrderAmendsParams.builder().build()); + JsonMapper mapper = JsonMapper.builder() + .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) + .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .addModule(new Jdk8Module()).build(); + String json = Files.readString(Path.of("src/test/resources/account/OrderAmends.json")); + + KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + OrderAmends result = unit.unwrapResponse(response); + + assertThat(result.count()).isEqualTo(3L); + assertThat(result.amends().getFirst().timestamp()).isEqualTo(1724158070287558000L); + assertThat(result.amends().getFirst().amendType()).isEqualTo(OrderAmends.AmendType.ORIGINAL); + assertThat(result.amends().getFirst().orderQty()).isEqualByComparingTo("0.01000000"); + } +} diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/QueryOrdersEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/QueryOrdersEndpointTest.java new file mode 100644 index 0000000..ac99332 --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/QueryOrdersEndpointTest.java @@ -0,0 +1,73 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import java.math.BigDecimal; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; + +import dev.andstuff.kraken.api.endpoint.KrakenException; +import dev.andstuff.kraken.api.endpoint.KrakenResponse; +import dev.andstuff.kraken.api.endpoint.account.params.*; +import dev.andstuff.kraken.api.endpoint.account.response.*; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@ExtendWith(MockitoExtension.class) +class QueryOrdersEndpointTest { + + @Test + void should_encode_all_options_when_supplied() { + QueryOrdersEndpoint unit = new QueryOrdersEndpoint(QueryOrdersParams.builder().trades(false).userReference(0L).transactionIds(List.of("ID-1", "ID+2")).consolidateTaker(false).rebaseMultiplier(RebaseMultiplier.BASE).build()); + + Map result = Arrays.stream(unit.encodedParamsWith("123456789").split("&")) + .map(value -> value.split("=", 2)) + .collect(Collectors.toMap(value -> value[0], value -> URLDecoder.decode(value[1], StandardCharsets.UTF_8))); + + assertThat(result).containsExactlyInAnyOrderEntriesOf(Map.ofEntries( + Map.entry("nonce", "123456789"), + Map.entry("trades", "false"), + Map.entry("userref", "0"), + Map.entry("txid", "ID-1,ID+2"), + Map.entry("consolidate_taker", "false"), + Map.entry("rebase_multiplier", "base"))); + assertThat(unit.buildURL().getPath()).isEqualTo("/0/private/QueryOrders"); + assertThat(unit.getHttpMethod()).isEqualTo("POST"); + } + + @Test + void should_reject_missing_transactionIds_when_building_parameters() { + assertThatThrownBy(() -> QueryOrdersParams.builder().build()).isInstanceOf(NullPointerException.class).hasMessageContaining("transactionIds"); + } + + @Test + void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exception { + QueryOrdersEndpoint unit = new QueryOrdersEndpoint(QueryOrdersParams.builder().transactionIds(List.of("ID-1", "ID+2")).build()); + JsonMapper mapper = JsonMapper.builder() + .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) + .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .addModule(new Jdk8Module()).build(); + String json = Files.readString(Path.of("src/test/resources/account/QueryOrders.json")); + + KrakenResponse> response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + Map result = unit.unwrapResponse(response); + + assertThat(result.get("OBCMZD-JIEE7-77TH3F").closeTime()).isEqualByComparingTo("1688665499.1922"); + assertThat(result.get("OBCMZD-JIEE7-77TH3F").reason()).isNull(); + } +} diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/QueryTradesEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/QueryTradesEndpointTest.java new file mode 100644 index 0000000..5116656 --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/QueryTradesEndpointTest.java @@ -0,0 +1,71 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import java.math.BigDecimal; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; + +import dev.andstuff.kraken.api.endpoint.KrakenException; +import dev.andstuff.kraken.api.endpoint.KrakenResponse; +import dev.andstuff.kraken.api.endpoint.account.params.*; +import dev.andstuff.kraken.api.endpoint.account.response.*; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@ExtendWith(MockitoExtension.class) +class QueryTradesEndpointTest { + + @Test + void should_encode_all_options_when_supplied() { + QueryTradesEndpoint unit = new QueryTradesEndpoint(QueryTradesParams.builder().transactionIds(List.of("ID-1", "ID+2")).trades(false).rebaseMultiplier(RebaseMultiplier.BASE).build()); + + Map result = Arrays.stream(unit.encodedParamsWith("123456789").split("&")) + .map(value -> value.split("=", 2)) + .collect(Collectors.toMap(value -> value[0], value -> URLDecoder.decode(value[1], StandardCharsets.UTF_8))); + + assertThat(result).containsExactlyInAnyOrderEntriesOf(Map.ofEntries( + Map.entry("nonce", "123456789"), + Map.entry("txid", "ID-1,ID+2"), + Map.entry("trades", "false"), + Map.entry("rebase_multiplier", "base"))); + assertThat(unit.buildURL().getPath()).isEqualTo("/0/private/QueryTrades"); + assertThat(unit.getHttpMethod()).isEqualTo("POST"); + } + + @Test + void should_reject_missing_transactionIds_when_building_parameters() { + assertThatThrownBy(() -> QueryTradesParams.builder().build()).isInstanceOf(NullPointerException.class).hasMessageContaining("transactionIds"); + } + + @Test + void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exception { + QueryTradesEndpoint unit = new QueryTradesEndpoint(QueryTradesParams.builder().transactionIds(List.of("ID-1", "ID+2")).build()); + JsonMapper mapper = JsonMapper.builder() + .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) + .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .addModule(new Jdk8Module()).build(); + String json = Files.readString(Path.of("src/test/resources/account/QueryTrades.json")); + + KrakenResponse> response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + Map result = unit.unwrapResponse(response); + + assertThat(result.get("TTEUX3-HDAAA-RC2RUO").tradeId()).isEqualTo(74625834L); + assertThat(result.get("TTEUX3-HDAAA-RC2RUO").volume()).isEqualByComparingTo("0.00020000"); + } +} diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/TradeBalanceEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/TradeBalanceEndpointTest.java new file mode 100644 index 0000000..a375019 --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/TradeBalanceEndpointTest.java @@ -0,0 +1,73 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import java.math.BigDecimal; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; + +import dev.andstuff.kraken.api.endpoint.KrakenException; +import dev.andstuff.kraken.api.endpoint.KrakenResponse; +import dev.andstuff.kraken.api.endpoint.account.params.*; +import dev.andstuff.kraken.api.endpoint.account.response.*; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@ExtendWith(MockitoExtension.class) +class TradeBalanceEndpointTest { + + @Test + void should_encode_all_options_when_supplied() { + TradeBalanceEndpoint unit = new TradeBalanceEndpoint(TradeBalanceParams.builder().asset("id +/&=").rebaseMultiplier(RebaseMultiplier.BASE).build()); + + Map result = Arrays.stream(unit.encodedParamsWith("123456789").split("&")) + .map(value -> value.split("=", 2)) + .collect(Collectors.toMap(value -> value[0], value -> URLDecoder.decode(value[1], StandardCharsets.UTF_8))); + + assertThat(result).containsExactlyInAnyOrderEntriesOf(Map.ofEntries( + Map.entry("nonce", "123456789"), + Map.entry("asset", "id +/&="), + Map.entry("rebase_multiplier", "base"))); + assertThat(unit.buildURL().getPath()).isEqualTo("/0/private/TradeBalance"); + assertThat(unit.getHttpMethod()).isEqualTo("POST"); + } + + @Test + void should_omit_optional_values_when_not_supplied() { + TradeBalanceEndpoint unit = new TradeBalanceEndpoint(); + + assertThat(unit.encodedParamsWith("123")).isEqualTo("nonce=123"); + } + + @Test + void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exception { + TradeBalanceEndpoint unit = new TradeBalanceEndpoint(TradeBalanceParams.builder().build()); + JsonMapper mapper = JsonMapper.builder() + .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) + .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .addModule(new Jdk8Module()).build(); + String json = Files.readString(Path.of("src/test/resources/account/TradeBalance.json")); + + KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + TradeBalance result = unit.unwrapResponse(response); + + assertThat(result.equivalentBalance()).isEqualByComparingTo("1101.3425"); + assertThat(result.unrealizedNet()).isEqualByComparingTo("-10.0232"); + assertThat(result.marginLevel()).isEqualByComparingTo("5432.57"); + } +} diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/TradeVolumeEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/TradeVolumeEndpointTest.java new file mode 100644 index 0000000..ea12009 --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/TradeVolumeEndpointTest.java @@ -0,0 +1,107 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import java.math.BigDecimal; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; + +import dev.andstuff.kraken.api.endpoint.KrakenException; +import dev.andstuff.kraken.api.endpoint.KrakenResponse; +import dev.andstuff.kraken.api.endpoint.account.params.*; +import dev.andstuff.kraken.api.endpoint.account.response.*; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@ExtendWith(MockitoExtension.class) +class TradeVolumeEndpointTest { + + @Test + void should_omit_optional_values_when_not_supplied() { + TradeVolumeEndpoint unit = new TradeVolumeEndpoint(); + + assertThat(unit.encodedParamsWith("123")).isEqualTo("{\"nonce\":123}"); + } + + @Test + void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exception { + TradeVolumeEndpoint unit = new TradeVolumeEndpoint(TradeVolumeParams.builder().build()); + JsonMapper mapper = JsonMapper.builder() + .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) + .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .addModule(new Jdk8Module()).build(); + String json = Files.readString(Path.of("src/test/resources/account/TradeVolume.json")); + + KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + TradeVolume result = unit.unwrapResponse(response); + + assertThat(result.fees().get("XXBTZUSD").fee()).isEqualByComparingTo("0.1000"); + assertThat(result.fees().get("XXBTZUSD").nextVolume()).isNull(); + assertThat(result.feesMaker().get("XXBTZUSD").fee()).isEqualByComparingTo("0.0000"); + } + + @Test + void should_encode_structured_pairs_when_requesting_non_forex_fees() throws Exception { + TradeVolumeEndpoint unit = new TradeVolumeEndpoint(TradeVolumeParams.builder() + .pairsWithClass(List.of(new TradeVolumeParams.Pair("TSLAx/USD", "equity_pair"))) + .feeInfo(false).feeSchedule(true).rebaseMultiplier(RebaseMultiplier.BASE).build()); + JsonMapper mapper = JsonMapper.builder().build(); + + assertThat(mapper.readTree(unit.encodedParamsWith("12345678901234567890"))).isEqualTo(mapper.readTree(""" + {"nonce":12345678901234567890,"pair":[{"asset":"TSLAx/USD","aclass":"equity_pair"}],"fee-info":false,"fee_schedule":true,"rebase_multiplier":"base"} + """)); + assertThat(unit.getContentType()).isEqualTo("application/json"); + } + + @Test + void should_encode_pair_names_when_requesting_forex_fees() throws Exception { + TradeVolumeEndpoint unit = new TradeVolumeEndpoint(TradeVolumeParams.builder().pairs(List.of("XBTUSD", "ETHUSD")).build()); + JsonMapper mapper = JsonMapper.builder().build(); + + assertThat(mapper.readTree(unit.encodedParamsWith("123"))).isEqualTo(mapper.readTree(""" + {"nonce":123,"pair":"XBTUSD,ETHUSD"} + """)); + } + + @Test + void should_reject_ambiguous_pairs_when_both_formats_are_supplied() { + TradeVolumeEndpoint unit = new TradeVolumeEndpoint(TradeVolumeParams.builder() + .pairs(List.of("XBTUSD")).pairsWithClass(List.of(new TradeVolumeParams.Pair("XBTUSD", "forex"))).build()); + + assertThatThrownBy(() -> unit.encodedParamsWith("123")).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void should_parse_fee_schedules_when_requested() throws Exception { + TradeVolumeEndpoint unit = new TradeVolumeEndpoint(); + JsonMapper mapper = JsonMapper.builder().addModule(new Jdk8Module()) + .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE).build(); + String json = """ + {"error":[],"result":{"asset_class":"future-class","volume_subaccounts":[{"iiban":"subaccount","volume":"0.0000000000123456789"}],"schedules":[{"pair":"XBTUSD","class":"volume","tiers":[{"maker_fee":"0.25","taker_fee":"0.4","min_spot_volume":"0","min_futures_volume":"10000","min_assets_on_platform":"0","active":true}]}]}} + """; + + KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + TradeVolume result = unit.unwrapResponse(response); + + assertThat(result.assetClass()).isEqualTo(TradeVolume.AssetClass.UNKNOWN); + assertThat(result.volumeSubaccounts().getFirst().volume()).isEqualByComparingTo("0.0000000000123456789"); + assertThat(result.schedules().getFirst().assetClass()).isEqualTo(TradeVolume.AssetClass.VOLUME); + assertThat(result.schedules().getFirst().tiers().getFirst().active()).isTrue(); + assertThat(result.schedules().getFirst().tiers().getFirst().makerFee()).isEqualByComparingTo("0.25"); + } +} diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/TradesHistoryEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/TradesHistoryEndpointTest.java new file mode 100644 index 0000000..f95c3d9 --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/TradesHistoryEndpointTest.java @@ -0,0 +1,83 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import java.math.BigDecimal; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; + +import dev.andstuff.kraken.api.endpoint.KrakenException; +import dev.andstuff.kraken.api.endpoint.KrakenResponse; +import dev.andstuff.kraken.api.endpoint.account.params.*; +import dev.andstuff.kraken.api.endpoint.account.response.*; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@ExtendWith(MockitoExtension.class) +class TradesHistoryEndpointTest { + + @Test + void should_encode_all_options_when_supplied() { + TradesHistoryEndpoint unit = new TradesHistoryEndpoint(TradesHistoryParams.builder().type(TradeType.NO_POSITION).trades(false).start("1700000000").end("1700000000").offset(0).withoutCount(false).consolidateTaker(false).ledgers(false).rebaseMultiplier(RebaseMultiplier.BASE).assetClass(AssetClass.EXTERNAL_PAIR).pair("id +/&=").limit(0).build()); + + Map result = Arrays.stream(unit.encodedParamsWith("123456789").split("&")) + .map(value -> value.split("=", 2)) + .collect(Collectors.toMap(value -> value[0], value -> URLDecoder.decode(value[1], StandardCharsets.UTF_8))); + + assertThat(result).containsExactlyInAnyOrderEntriesOf(Map.ofEntries( + Map.entry("nonce", "123456789"), + Map.entry("type", "no position"), + Map.entry("trades", "false"), + Map.entry("start", "1700000000"), + Map.entry("end", "1700000000"), + Map.entry("ofs", "0"), + Map.entry("without_count", "false"), + Map.entry("consolidate_taker", "false"), + Map.entry("ledgers", "false"), + Map.entry("rebase_multiplier", "base"), + Map.entry("aclass", "external_pair"), + Map.entry("pair", "id +/&="), + Map.entry("limit", "0"))); + assertThat(unit.buildURL().getPath()).isEqualTo("/0/private/TradesHistory"); + assertThat(unit.getHttpMethod()).isEqualTo("POST"); + } + + @Test + void should_omit_optional_values_when_not_supplied() { + TradesHistoryEndpoint unit = new TradesHistoryEndpoint(); + + assertThat(unit.encodedParamsWith("123")).isEqualTo("nonce=123"); + } + + @Test + void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exception { + TradesHistoryEndpoint unit = new TradesHistoryEndpoint(TradesHistoryParams.builder().build()); + JsonMapper mapper = JsonMapper.builder() + .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) + .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .addModule(new Jdk8Module()).build(); + String json = Files.readString(Path.of("src/test/resources/account/TradesHistory.json")); + + KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + TradesHistory result = unit.unwrapResponse(response); + + assertThat(result.trades().get("THVRQM-33VKH-UCI7BS").time()).isEqualByComparingTo("1688667796.8802"); + assertThat(result.trades().get("THVRQM-33VKH-UCI7BS").volume()).isEqualByComparingTo("0.02000000"); + assertThat(result.count()).isNull(); + } +} diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/WalletAccountsEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/WalletAccountsEndpointTest.java new file mode 100644 index 0000000..8dd12b4 --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/WalletAccountsEndpointTest.java @@ -0,0 +1,71 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import java.math.BigDecimal; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; + +import dev.andstuff.kraken.api.endpoint.KrakenException; +import dev.andstuff.kraken.api.endpoint.KrakenResponse; +import dev.andstuff.kraken.api.endpoint.account.params.*; +import dev.andstuff.kraken.api.endpoint.account.response.*; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@ExtendWith(MockitoExtension.class) +class WalletAccountsEndpointTest { + + @Test + void should_encode_all_options_when_supplied() { + WalletAccountsEndpoint unit = new WalletAccountsEndpoint(WalletAccountsParams.builder().build()); + + Map result = Arrays.stream(unit.encodedParamsWith("123456789").split("&")) + .map(value -> value.split("=", 2)) + .collect(Collectors.toMap(value -> value[0], value -> URLDecoder.decode(value[1], StandardCharsets.UTF_8))); + + assertThat(result).containsExactlyInAnyOrderEntriesOf(Map.ofEntries( + Map.entry("nonce", "123456789"))); + assertThat(unit.buildURL().getPath()).isEqualTo("/0/private/ListWalletAccounts"); + assertThat(unit.getHttpMethod()).isEqualTo("POST"); + } + + @Test + void should_omit_optional_values_when_not_supplied() { + WalletAccountsEndpoint unit = new WalletAccountsEndpoint(); + + assertThat(unit.encodedParamsWith("123")).isEqualTo("nonce=123"); + } + + @Test + void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exception { + WalletAccountsEndpoint unit = new WalletAccountsEndpoint(WalletAccountsParams.builder().build()); + JsonMapper mapper = JsonMapper.builder() + .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) + .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .addModule(new Jdk8Module()).build(); + String json = Files.readString(Path.of("src/test/resources/account/ListWalletAccounts.json")); + + KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + WalletAccounts result = unit.unwrapResponse(response); + + assertThat(result.accounts().getFirst().flags().active()).isTrue(); + assertThat(result.accounts().getFirst().status()).isEqualTo(WalletAccounts.Status.ACTIVE); + assertThat(result.cursor().next()).isNull(); + } +} diff --git a/library/src/test/java/dev/andstuff/kraken/api/rest/AccountRestRequesterTest.java b/library/src/test/java/dev/andstuff/kraken/api/rest/AccountRestRequesterTest.java new file mode 100644 index 0000000..dfe9d02 --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/rest/AccountRestRequesterTest.java @@ -0,0 +1,109 @@ +package dev.andstuff.kraken.api.rest; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; + +import javax.net.ssl.HttpsURLConnection; + +import com.fasterxml.jackson.databind.type.TypeFactory; + +import dev.andstuff.kraken.api.endpoint.KrakenException; +import dev.andstuff.kraken.api.endpoint.account.CreditLinesEndpoint; +import dev.andstuff.kraken.api.endpoint.account.TradeVolumeEndpoint; +import dev.andstuff.kraken.api.endpoint.account.response.CreditLines; +import dev.andstuff.kraken.api.endpoint.account.response.TradeVolume; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class AccountRestRequesterTest { + + @InjectMocks private DefaultKrakenRestRequester unit; + @Mock private TradeVolumeEndpoint tradeVolume; + @Mock private CreditLinesEndpoint creditLines; + @Mock private HttpsURLConnection connection; + @Mock private URL url; + @Mock private KrakenCredentials credentials; + @Mock private KrakenNonceGenerator nonceGenerator; + + @Test + void should_sign_and_send_identical_json_when_requesting_trade_volume() throws Exception { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + String body = "{\"nonce\":123,\"pair\":[{\"asset\":\"TSLAx/USD\",\"aclass\":\"equity_pair\"}]}"; + when(nonceGenerator.generate()).thenReturn("123"); + when(tradeVolume.encodedParamsWith("123")).thenReturn(body); + when(tradeVolume.buildURL()).thenReturn(url); + when(url.openConnection()).thenReturn(connection); + when(connection.getURL()).thenReturn(url); + when(tradeVolume.getHttpMethod()).thenReturn("POST"); + when(tradeVolume.getContentType()).thenReturn("application/json"); + when(credentials.getKey()).thenReturn("test-key"); + when(credentials.sign(url, "123", body)).thenReturn("test-signature"); + when(connection.getOutputStream()).thenReturn(output); + when(connection.getHeaderField("Content-Type")).thenReturn("application/json"); + when(connection.getInputStream()).thenReturn(new ByteArrayInputStream("{\"error\":[],\"result\":{\"volume\":\"0.0000000000123456789\"}}".getBytes(StandardCharsets.UTF_8))); + when(tradeVolume.wrappedResponseType(any())).thenReturn(new TradeVolumeEndpoint().wrappedResponseType(TypeFactory.createDefaultInstance())); + when(tradeVolume.unwrapResponse(any())).thenCallRealMethod(); + + TradeVolume result = unit.execute(tradeVolume, credentials, nonceGenerator); + + assertThat(result.volume()).isEqualByComparingTo("0.0000000000123456789"); + assertThat(output.toString(StandardCharsets.UTF_8)).isEqualTo(body); + verify(connection).setRequestMethod("POST"); + verify(connection).addRequestProperty("Content-Type", "application/json"); + verify(connection).addRequestProperty("API-Key", "test-key"); + verify(connection).addRequestProperty("API-Sign", "test-signature"); + verify(credentials).sign(url, "123", body); + } + + @Test + void should_return_null_when_credit_lines_response_has_no_credit() throws Exception { + when(nonceGenerator.generate()).thenReturn("123"); + when(creditLines.encodedParamsWith("123")).thenReturn("nonce=123"); + when(creditLines.buildURL()).thenReturn(url); + when(url.openConnection()).thenReturn(connection); + when(connection.getURL()).thenReturn(url); + when(creditLines.getHttpMethod()).thenReturn("POST"); + when(creditLines.getContentType()).thenCallRealMethod(); + when(connection.getOutputStream()).thenReturn(new ByteArrayOutputStream()); + when(connection.getHeaderField("Content-Type")).thenReturn("application/json"); + when(connection.getInputStream()).thenReturn(new ByteArrayInputStream("{\"error\":[],\"result\":null}".getBytes(StandardCharsets.UTF_8))); + when(creditLines.wrappedResponseType(any())).thenReturn(new CreditLinesEndpoint().wrappedResponseType(TypeFactory.createDefaultInstance())); + when(creditLines.unwrapResponse(any())).thenCallRealMethod(); + + CreditLines result = unit.execute(creditLines, credentials, nonceGenerator); + + assertThat(result).isNull(); + verify(connection).addRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + } + + @Test + void should_propagate_errors_when_kraken_rejects_trade_volume() throws Exception { + when(nonceGenerator.generate()).thenReturn("123"); + when(tradeVolume.encodedParamsWith("123")).thenReturn("{\"nonce\":123}"); + when(tradeVolume.buildURL()).thenReturn(url); + when(url.openConnection()).thenReturn(connection); + when(connection.getURL()).thenReturn(url); + when(tradeVolume.getHttpMethod()).thenReturn("POST"); + when(tradeVolume.getContentType()).thenReturn("application/json"); + when(connection.getOutputStream()).thenReturn(new ByteArrayOutputStream()); + when(connection.getHeaderField("Content-Type")).thenReturn("application/json"); + when(connection.getInputStream()).thenReturn(new ByteArrayInputStream("{\"error\":[\"EGeneral:Permission denied\"]}".getBytes(StandardCharsets.UTF_8))); + when(tradeVolume.wrappedResponseType(any())).thenReturn(new TradeVolumeEndpoint().wrappedResponseType(TypeFactory.createDefaultInstance())); + when(tradeVolume.unwrapResponse(any())).thenCallRealMethod(); + + assertThatThrownBy(() -> unit.execute(tradeVolume, credentials, nonceGenerator)).isInstanceOf(KrakenException.class); + } +} diff --git a/library/src/test/resources/account/Balance.json b/library/src/test/resources/account/Balance.json new file mode 100644 index 0000000..57f1582 --- /dev/null +++ b/library/src/test/resources/account/Balance.json @@ -0,0 +1,15 @@ +{ + "error": [], + "result": { + "ZUSD": "171288.6158", + "ZEUR": "504861.8946", + "XXBT": "1011.1908877900", + "XETH": "818.5500000000", + "USDT": "500000.00000000", + "DAI": "9999.9999999999", + "DOT": "2.5000000000", + "ETH2.S": "198.3970800000", + "ETH2": "2.5885574330", + "USD.M": "1213029.2780" + } +} diff --git a/library/src/test/resources/account/BalanceEx.json b/library/src/test/resources/account/BalanceEx.json new file mode 100644 index 0000000..479fef0 --- /dev/null +++ b/library/src/test/resources/account/BalanceEx.json @@ -0,0 +1,13 @@ +{ + "error": [], + "result": { + "ZUSD": { + "balance": 25435.21, + "hold_trade": 8249.76 + }, + "XXBT": { + "balance": 1.2435, + "hold_trade": 0.8423 + } + } +} diff --git a/library/src/test/resources/account/ClosedOrders.json b/library/src/test/resources/account/ClosedOrders.json new file mode 100644 index 0000000..96f1c79 --- /dev/null +++ b/library/src/test/resources/account/ClosedOrders.json @@ -0,0 +1,68 @@ +{ + "error": [], + "result": { + "closed": { + "O37652-RJWRT-IMO74O": { + "refid": "None", + "userref": 1, + "status": "canceled", + "reason": "User requested", + "opentm": 1688148493.7708, + "closetm": 1688148610.0482, + "starttm": 0, + "expiretm": 0, + "descr": { + "pair": "XBTGBP", + "type": "buy", + "ordertype": "stop-loss-limit", + "price": "23667.0", + "price2": "0", + "leverage": "none", + "order": "buy 0.00100000 XBTGBP @ limit 23667.0", + "close": "" + }, + "vol": "0.00100000", + "vol_exec": "0.00000000", + "cost": "0.00000", + "fee": "0.00000", + "price": "0.00000", + "stopprice": "0.00000", + "limitprice": "0.00000", + "misc": "", + "oflags": "fciq", + "trigger": "index" + }, + "O6YDQ5-LOMWU-37YKEE": { + "refid": "None", + "userref": 36493663, + "status": "canceled", + "reason": "User requested", + "opentm": 1688148493.7708, + "closetm": 1688148610.0477, + "starttm": 0, + "expiretm": 0, + "descr": { + "pair": "XBTEUR", + "type": "buy", + "ordertype": "take-profit-limit", + "price": "27743.0", + "price2": "0", + "leverage": "none", + "order": "buy 0.00100000 XBTEUR @ limit 27743.0", + "close": "" + }, + "vol": "0.00100000", + "vol_exec": "0.00000000", + "cost": "0.00000", + "fee": "0.00000", + "price": "0.00000", + "stopprice": "0.00000", + "limitprice": "0.00000", + "misc": "", + "oflags": "fciq", + "trigger": "index" + } + }, + "count": 2 + } +} diff --git a/library/src/test/resources/account/CreditLines.json b/library/src/test/resources/account/CreditLines.json new file mode 100644 index 0000000..4ad164b --- /dev/null +++ b/library/src/test/resources/account/CreditLines.json @@ -0,0 +1,27 @@ +{ + "error": [], + "result": { + "asset_details": { + "USD": { + "balance": "1000.5000", + "credit_limit": "50000.0000", + "credit_used": "12500.0000", + "available_credit": "37500.0000" + }, + "EUR": { + "balance": "500.2500", + "credit_limit": "25000.0000", + "credit_used": "5000.0000", + "available_credit": "20000.0000" + } + }, + "limits_monitor": { + "total_credit_usd": "100000.0000", + "total_credit_used_usd": "25000.0000", + "total_collateral_value_usd": "150000.0000", + "equity_usd": "125000.0000", + "ongoing_balance": "1.5000", + "debt_to_equity": "0.2000" + } + } +} diff --git a/library/src/test/resources/account/GetApiKeyInfo.json b/library/src/test/resources/account/GetApiKeyInfo.json new file mode 100644 index 0000000..9015526 --- /dev/null +++ b/library/src/test/resources/account/GetApiKeyInfo.json @@ -0,0 +1,23 @@ +{ + "error": [], + "result": { + "apiKeyName": "my-api-key", + "apiKey": "documentation-test-key", + "nonce": "1772627060997", + "nonceWindow": 0, + "permissions": [ + "query-funds", + "withdraw-funds", + "query-open-trades", + "modify-trades" + ], + "iban": "AA88 N84G WOAK NMOI", + "validUntil": "0", + "queryFrom": "0", + "queryTo": "0", + "createdTime": "1772542900", + "modifiedTime": "1772543095", + "ipAllowlist": [], + "lastUsed": "1772627061" + } +} diff --git a/library/src/test/resources/account/ListWalletAccounts.json b/library/src/test/resources/account/ListWalletAccounts.json new file mode 100644 index 0000000..e482fec --- /dev/null +++ b/library/src/test/resources/account/ListWalletAccounts.json @@ -0,0 +1,20 @@ +{ + "error": [], + "result": { + "accounts": [ + { + "account_id": "WX6V-JUKW-KKPB-QE36", + "flags": { + "user_defined": false, + "active": true + }, + "status": "active", + "type": "main", + "name": null + } + ], + "cursor": { + "next": null + } + } +} diff --git a/library/src/test/resources/account/OpenOrders.json b/library/src/test/resources/account/OpenOrders.json new file mode 100644 index 0000000..f8ae5e3 --- /dev/null +++ b/library/src/test/resources/account/OpenOrders.json @@ -0,0 +1,64 @@ +{ + "error": [], + "result": { + "open": { + "OQCLML-BW3P3-BUCMWZ": { + "refid": "None", + "userref": 0, + "status": "open", + "opentm": 1688666559.8974, + "starttm": 0, + "expiretm": 0, + "descr": { + "pair": "XBTUSD", + "type": "buy", + "ordertype": "limit", + "price": "30010.0", + "price2": "0", + "leverage": "none", + "order": "buy 1.25000000 XBTUSD @ limit 30010.0", + "close": "" + }, + "vol": "1.25000000", + "vol_exec": "0.37500000", + "cost": "11253.7", + "fee": "0.00000", + "price": "30010.0", + "stopprice": "0.00000", + "limitprice": "0.00000", + "misc": "", + "oflags": "fciq", + "trades": [ + "TCCCTY-WE2O6-P3NB37" + ] + }, + "OB5VMB-B4U2U-DK2WRW": { + "refid": "None", + "userref": 45326, + "status": "open", + "opentm": 1688665899.5699, + "starttm": 0, + "expiretm": 0, + "descr": { + "pair": "XBTUSD", + "type": "buy", + "ordertype": "limit", + "price": "14500.0", + "price2": "0", + "leverage": "5:1", + "order": "buy 0.27500000 XBTUSD @ limit 14500.0 with 5:1 leverage", + "close": "" + }, + "vol": "0.27500000", + "vol_exec": "0.00000000", + "cost": "0.00000", + "fee": "0.00000", + "price": "0.00000", + "stopprice": "0.00000", + "limitprice": "0.00000", + "misc": "", + "oflags": "fciq" + } + } + } +} diff --git a/library/src/test/resources/account/OpenPositions.json b/library/src/test/resources/account/OpenPositions.json new file mode 100644 index 0000000..cd6799c --- /dev/null +++ b/library/src/test/resources/account/OpenPositions.json @@ -0,0 +1,100 @@ +{ + "error": [], + "result": { + "TF5GVO-T7ZZ2-6NBKBI": { + "ordertxid": "OLWNFG-LLH4R-D6SFFP", + "posstatus": "open", + "pair": "XXBTZUSD", + "time": 1605280097.8294, + "type": "buy", + "ordertype": "limit", + "cost": "104610.52842", + "fee": "289.06565", + "vol": "8.82412861", + "vol_closed": "0.20200000", + "margin": "20922.10568", + "value": "258797.5", + "net": "+154186.9728", + "terms": "0.0100% per 4 hours", + "rollovertm": "1616672637", + "misc": "", + "oflags": "" + }, + "T24DOR-TAFLM-ID3NYP": { + "ordertxid": "OIVYGZ-M5EHU-ZRUQXX", + "posstatus": "open", + "pair": "XXBTZUSD", + "time": 1607943827.3172, + "type": "buy", + "ordertype": "limit", + "cost": "145756.76856", + "fee": "335.24057", + "vol": "8.00000000", + "vol_closed": "0.00000000", + "margin": "29151.35371", + "value": "240124.0", + "net": "+94367.2314", + "terms": "0.0100% per 4 hours", + "rollovertm": "1616672637", + "misc": "", + "oflags": "" + }, + "TYMRFG-URRG5-2ZTQSD": { + "ordertxid": "OF5WFH-V57DP-QANDAC", + "posstatus": "open", + "pair": "XXBTZUSD", + "time": 1610448039.8374, + "type": "buy", + "ordertype": "limit", + "cost": "0.00240", + "fee": "0.00000", + "vol": "0.00000010", + "vol_closed": "0.00000000", + "margin": "0.00048", + "value": "0", + "net": "+0.0006", + "terms": "0.0100% per 4 hours", + "rollovertm": "1616672637", + "misc": "", + "oflags": "" + }, + "TAFGBN-TZNFC-7CCYIM": { + "ordertxid": "OF5WFH-V57DP-QANDAC", + "posstatus": "open", + "pair": "XXBTZUSD", + "time": 1610448039.8448, + "type": "buy", + "ordertype": "limit", + "cost": "2.40000", + "fee": "0.00264", + "vol": "0.00010000", + "vol_closed": "0.00000000", + "margin": "0.48000", + "value": "3.0", + "net": "+0.6015", + "terms": "0.0100% per 4 hours", + "rollovertm": "1616672637", + "misc": "", + "oflags": "" + }, + "T4O5L3-4VGS4-IRU2UL": { + "ordertxid": "OF5WFH-V57DP-QANDAC", + "posstatus": "open", + "pair": "XXBTZUSD", + "time": 1610448040.7722, + "type": "buy", + "ordertype": "limit", + "cost": "21.59760", + "fee": "0.02376", + "vol": "0.00089990", + "vol_closed": "0.00000000", + "margin": "4.31952", + "value": "27.0", + "net": "+5.4133", + "terms": "0.0100% per 4 hours", + "rollovertm": "1616672637", + "misc": "", + "oflags": "" + } + } +} diff --git a/library/src/test/resources/account/OrderAmends.json b/library/src/test/resources/account/OrderAmends.json new file mode 100644 index 0000000..d3fcd30 --- /dev/null +++ b/library/src/test/resources/account/OrderAmends.json @@ -0,0 +1,32 @@ +{ + "error": [], + "result": { + "amends": [ + { + "amend_id": "TSUN4B-EX2XN-WQ6GKG", + "amend_type": "original", + "order_qty": "0.01000000", + "remaining_qty": "0.01000000", + "limit_price": "61032.8", + "timestamp": 1724158070287558000 + }, + { + "amend_id": "TF6VAW-VUWMX-6SXTCH", + "amend_type": "user", + "order_qty": "0.01000000", + "remaining_qty": "0.01000000", + "limit_price": "61032.7", + "timestamp": 1724158076936755700 + }, + { + "amend_id": "TUMY4K-E4MPE-CSL2N3", + "amend_type": "user", + "order_qty": "0.01000000", + "remaining_qty": "0.01000000", + "limit_price": "61032.6", + "timestamp": 1724158214879660000 + } + ], + "count": 3 + } +} diff --git a/library/src/test/resources/account/QueryOrders.json b/library/src/test/resources/account/QueryOrders.json new file mode 100644 index 0000000..7577943 --- /dev/null +++ b/library/src/test/resources/account/QueryOrders.json @@ -0,0 +1,70 @@ +{ + "error": [], + "result": { + "OBCMZD-JIEE7-77TH3F": { + "refid": "None", + "userref": 0, + "status": "closed", + "reason": null, + "opentm": 1688665496.7808, + "closetm": 1688665499.1922, + "starttm": 0, + "expiretm": 0, + "descr": { + "pair": "XBTUSD", + "type": "buy", + "ordertype": "stop-loss-limit", + "price": "27500.0", + "price2": "0", + "leverage": "none", + "order": "buy 1.25000000 XBTUSD @ limit 27500.0", + "close": "" + }, + "vol": "1.25000000", + "vol_exec": "1.25000000", + "cost": "27526.2", + "fee": "26.2", + "price": "27500.0", + "stopprice": "0.00000", + "limitprice": "0.00000", + "misc": "", + "oflags": "fciq", + "trigger": "index", + "trades": [ + "TZX2WP-XSEOP-FP7WYR" + ] + }, + "OMMDB2-FSB6Z-7W3HPO": { + "refid": "None", + "userref": 0, + "status": "closed", + "reason": null, + "opentm": 1688592012.2317, + "closetm": 1688592012.2335, + "starttm": 0, + "expiretm": 0, + "descr": { + "pair": "XBTUSD", + "type": "sell", + "ordertype": "market", + "price": "0", + "price2": "0", + "leverage": "none", + "order": "sell 0.25000000 XBTUSD @ market", + "close": "" + }, + "vol": "0.25000000", + "vol_exec": "0.25000000", + "cost": "7500.0", + "fee": "7.5", + "price": "30000.0", + "stopprice": "0.00000", + "limitprice": "0.00000", + "misc": "", + "oflags": "fcib", + "trades": [ + "TJUW2K-FLX2N-AR2FLU" + ] + } + } +} diff --git a/library/src/test/resources/account/QueryTrades.json b/library/src/test/resources/account/QueryTrades.json new file mode 100644 index 0000000..c0775aa --- /dev/null +++ b/library/src/test/resources/account/QueryTrades.json @@ -0,0 +1,37 @@ +{ + "error": [], + "result": { + "THVRQM-33VKH-UCI7BS": { + "ordertxid": "OQCLML-BW3P3-BUCMWZ", + "postxid": "TKH2SE-M7IF5-CFI7LT", + "pair": "XXBTZUSD", + "time": 1688667796.8802, + "type": "buy", + "ordertype": "limit", + "price": "30010.00000", + "cost": "600.20000", + "fee": "0.00000", + "vol": "0.02000000", + "margin": "0.00000", + "misc": "", + "trade_id": 93748276, + "maker": true + }, + "TTEUX3-HDAAA-RC2RUO": { + "ordertxid": "OH76VO-UKWAD-PSBDX6", + "postxid": "TKH2SE-M7IF5-CFI7LT", + "pair": "XXBTZEUR", + "time": 1688082549.3138, + "type": "buy", + "ordertype": "limit", + "price": "27732.00000", + "cost": "0.20020", + "fee": "0.00000", + "vol": "0.00020000", + "margin": "0.00000", + "misc": "", + "trade_id": 74625834, + "maker": true + } + } +} diff --git a/library/src/test/resources/account/README.md b/library/src/test/resources/account/README.md new file mode 100644 index 0000000..b132135 --- /dev/null +++ b/library/src/test/resources/account/README.md @@ -0,0 +1 @@ +Fixtures are response examples from [Kraken’s Spot REST OpenAPI specification](https://docs.kraken.com/openapi/spot-rest.yaml), retrieved on 2026-09-08. The redundant `response` wrapper in the OrderAmends documentation example is removed to match the declared response envelope. The API key example is replaced with a test placeholder. Supplemental inline cases cover nullable credit results, fee schedules, unknown enum values, and margin order flags. Tests make no network requests. diff --git a/library/src/test/resources/account/TradeBalance.json b/library/src/test/resources/account/TradeBalance.json new file mode 100644 index 0000000..da13529 --- /dev/null +++ b/library/src/test/resources/account/TradeBalance.json @@ -0,0 +1,14 @@ +{ + "error": [], + "result": { + "eb": "1101.3425", + "tb": "392.2264", + "m": "7.0354", + "n": "-10.0232", + "c": "21.1063", + "v": "31.1297", + "e": "382.2032", + "mf": "375.1678", + "ml": "5432.57" + } +} diff --git a/library/src/test/resources/account/TradeVolume.json b/library/src/test/resources/account/TradeVolume.json new file mode 100644 index 0000000..96ebf73 --- /dev/null +++ b/library/src/test/resources/account/TradeVolume.json @@ -0,0 +1,33 @@ +{ + "error": [], + "result": { + "currency": "ZUSD", + "asset_class": "currency", + "volume": "200709587.4223", + "inputs": { + "domain_spot_volume_30d": "200709587.4223", + "domain_futures_volume_30d": "0.0000", + "domain_assets_on_platform": "0.0000" + }, + "fees": { + "XXBTZUSD": { + "fee": "0.1000", + "minfee": "0.1000", + "maxfee": "0.2600", + "nextfee": null, + "tiervolume": "10000000.0000", + "nextvolume": null + } + }, + "fees_maker": { + "XXBTZUSD": { + "fee": "0.0000", + "minfee": "0.0000", + "maxfee": "0.1600", + "nextfee": null, + "tiervolume": "10000000.0000", + "nextvolume": null + } + } + } +} diff --git a/library/src/test/resources/account/TradesHistory.json b/library/src/test/resources/account/TradesHistory.json new file mode 100644 index 0000000..4117789 --- /dev/null +++ b/library/src/test/resources/account/TradesHistory.json @@ -0,0 +1,39 @@ +{ + "error": [], + "result": { + "trades": { + "THVRQM-33VKH-UCI7BS": { + "ordertxid": "OQCLML-BW3P3-BUCMWZ", + "postxid": "TKH2SE-M7IF5-CFI7LT", + "pair": "XXBTZUSD", + "time": 1688667796.8802, + "type": "buy", + "ordertype": "limit", + "price": "30010.00000", + "cost": "600.20000", + "fee": "0.00000", + "vol": "0.02000000", + "margin": "0.00000", + "misc": "", + "trade_id": 40274859, + "maker": true + }, + "TCWJEG-FL4SZ-3FKGH6": { + "ordertxid": "OQCLML-BW3P3-BUCMWZ", + "postxid": "TKH2SE-M7IF5-CFI7LT", + "pair": "XXBTZUSD", + "time": 1688667769.6396, + "type": "buy", + "ordertype": "limit", + "price": "30010.00000", + "cost": "300.10000", + "fee": "0.00000", + "vol": "0.01000000", + "margin": "0.00000", + "misc": "", + "trade_id": 39482674, + "maker": true + } + } + } +} diff --git a/pom.xml b/pom.xml index 5976be8..b7d6779 100644 --- a/pom.xml +++ b/pom.xml @@ -54,6 +54,9 @@ 2.0.18 2.23.0 5.12.0 + 5.14.0 + 3.27.7 + 5.23.0 3.15.0 @@ -62,6 +65,7 @@ 3.12.0 3.3.1 3.4.0 + 3.5.4 0.11.0 @@ -127,6 +131,11 @@ + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + org.apache.maven.plugins maven-compiler-plugin From 7d6ec3820292d4ec19a30a3890b9446b25d325bf Mon Sep 17 00:00:00 2001 From: nyg Date: Thu, 10 Sep 2026 07:36:07 +0200 Subject: [PATCH 2/3] fix(account): address coverage review findings --- README.md | 2 +- library/pom.xml | 3 - .../dev/andstuff/kraken/api/KrakenAPI.java | 54 ++++++------- .../account/AccountBalanceEndpoint.java | 4 +- .../endpoint/account/CreditLinesEndpoint.java | 10 ++- .../endpoint/account/TradeVolumeEndpoint.java | 6 +- .../account/params/AccountBalanceParams.java | 2 +- .../account/params/ApiKeyInfoParams.java | 1 - .../endpoint/account/params/AssetClass.java | 31 +++----- .../endpoint/account/params/CloseTime.java | 27 +++---- .../account/params/ClosedOrdersParams.java | 2 +- .../account/params/Consolidation.java | 23 ++---- .../account/params/CreditLinesParams.java | 2 +- .../account/params/ExtendedBalanceParams.java | 2 +- .../account/params/OpenOrdersParams.java | 2 +- .../account/params/OpenPositionsParams.java | 2 +- .../account/params/OrderAmendsParams.java | 2 +- .../account/params/QueryOrdersParams.java | 2 +- .../account/params/QueryTradesParams.java | 2 +- .../account/params/RebaseMultiplier.java | 30 ------- .../account/params/TradeBalanceParams.java | 2 +- .../endpoint/account/params/TradeType.java | 31 +++----- .../account/params/TradeVolumeParams.java | 6 +- .../account/params/TradesHistoryParams.java | 2 +- .../account/params/WalletAccountsParams.java | 1 - .../account/response/AccountTrade.java | 53 ++++++------- .../endpoint/account/response/ApiKeyInfo.java | 24 +++--- .../account/response/ClosedOrders.java | 4 +- .../account/response/CreditLines.java | 22 +++--- .../account/response/ExtendedBalance.java | 6 +- .../endpoint/account/response/OpenOrders.java | 2 - .../account/response/OpenPosition.java | 39 +++++----- .../api/endpoint/account/response/Order.java | 71 ++++++++--------- .../account/response/OrderAmends.java | 36 ++++++--- .../account/response/TradeBalance.java | 20 ++--- .../account/response/TradeVolume.java | 50 ++++++------ .../account/response/TradesHistory.java | 4 +- .../account/response/WalletAccounts.java | 12 +-- .../api/endpoint/priv/PrivateEndpoint.java | 1 - .../api/endpoint/priv/RebaseMultiplier.java | 21 +++++ .../api/rest/DefaultKrakenRestRequester.java | 22 +++++- ...APITest.java => KrakenAPIAccountTest.java} | 58 ++++++++------ .../account/AccountBalanceEndpointTest.java | 30 +++---- .../account/ApiKeyInfoEndpointTest.java | 31 ++++---- .../account/ClosedOrdersEndpointTest.java | 37 +++++---- .../account/CreditLinesEndpointTest.java | 47 +++++------ .../account/ExtendedBalanceEndpointTest.java | 32 ++++---- .../account/OpenOrdersEndpointTest.java | 38 ++++----- .../account/OpenPositionsEndpointTest.java | 32 ++++---- .../account/OrderAmendsEndpointTest.java | 35 +++++---- .../account/QueryOrdersEndpointTest.java | 35 +++++---- .../account/QueryTradesEndpointTest.java | 32 ++++---- .../account/TradeBalanceEndpointTest.java | 32 ++++---- .../account/TradeVolumeEndpointTest.java | 45 ++++++----- .../account/TradesHistoryEndpointTest.java | 37 +++++---- .../account/WalletAccountsEndpointTest.java | 31 ++++---- .../account/params/TradeVolumeParamsTest.java | 43 ++++++++++ ...efaultKrakenRestRequesterAccountTest.java} | 78 ++++++++----------- pom.xml | 17 ++++ 59 files changed, 692 insertions(+), 636 deletions(-) delete mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/RebaseMultiplier.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/priv/RebaseMultiplier.java rename library/src/test/java/dev/andstuff/kraken/api/{AccountKrakenAPITest.java => KrakenAPIAccountTest.java} (91%) create mode 100644 library/src/test/java/dev/andstuff/kraken/api/endpoint/account/params/TradeVolumeParamsTest.java rename library/src/test/java/dev/andstuff/kraken/api/rest/{AccountRestRequesterTest.java => DefaultKrakenRestRequesterAccountTest.java} (58%) diff --git a/README.md b/README.md index 1aa6063..e3cdbb4 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ TradeVolume fees = api.tradeVolume(TradeVolumeParams.builder() .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`, fractional trade timestamps retain their precision, and amendment timestamps retain the nanosecond integers in Kraken's responses. `creditLines` returns null when Kraken reports no credit lines. `accountBalance` can select a wallet using `AccountBalanceParams.accountId`, while `walletAccounts` lists the available wallets. +`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. `TradeVolumeEndpoint` 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. diff --git a/library/pom.xml b/library/pom.xml index f977d31..5700858 100644 --- a/library/pom.xml +++ b/library/pom.xml @@ -16,19 +16,16 @@ org.junit.jupiter junit-jupiter - ${junit.version} test org.assertj assertj-core - ${assertj.version} test org.mockito mockito-junit-jupiter - ${mockito.version} 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 a5673a3..0f0d12d 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java +++ b/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java @@ -3,6 +3,7 @@ import java.math.BigDecimal; import java.util.List; import java.util.Map; +import java.util.Optional; import com.fasterxml.jackson.databind.JsonNode; @@ -110,7 +111,6 @@ import dev.andstuff.kraken.api.rest.KrakenCredentials; import dev.andstuff.kraken.api.rest.KrakenNonceGenerator; import dev.andstuff.kraken.api.rest.KrakenRestRequester; - import lombok.Builder; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -320,6 +320,28 @@ public PostTrade postTrade(PostTradeParams params) { /* Implemented private endpoints */ + /** + * Queries the private {@code Ledgers} endpoint, returning at most 50 ledger entries per call. + * + * @param params the filtering and pagination parameters + * @return the matching ledger entries and their total count + * @throws KrakenException if Kraken returns an error + */ + public LedgerInfo ledgerInfo(LedgerInfoParams params) { + return query(new LedgerInfoEndpoint(params)); + } + + /** + * Queries the private {@code QueryLedgers} endpoint, returning specific ledger entries by identifier. + * + * @param params the ledger entry identifiers to retrieve + * @return the ledger entries, by identifier + * @throws KrakenException if Kraken returns an error + */ + public Map ledgerEntries(LedgerEntriesParams params) { + return query(new LedgerEntriesEndpoint(params)); + } + /** * Queries the {@code Balance} endpoint using default options. * @@ -369,11 +391,11 @@ public Map extendedBalance(ExtendedBalanceParams params /** * Queries the {@code CreditLines} endpoint using default options. * - * @return the credit lines returned by Kraken, or null when no credit lines exist + * @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 CreditLines creditLines() { + public Optional creditLines() { return query(new CreditLinesEndpoint()); } @@ -381,11 +403,11 @@ public CreditLines creditLines() { * Queries the {@code CreditLines} endpoint. * * @param params the request options - * @return the credit lines returned by Kraken, or null when no credit lines exist + * @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 CreditLines creditLines(CreditLinesParams params) { + public Optional creditLines(CreditLinesParams params) { return query(new CreditLinesEndpoint(params)); } @@ -620,28 +642,6 @@ public WalletAccounts walletAccounts(WalletAccountsParams params) { return query(new WalletAccountsEndpoint(params)); } - /** - * Queries the private {@code Ledgers} endpoint, returning at most 50 ledger entries per call. - * - * @param params the filtering and pagination parameters - * @return the matching ledger entries and their total count - * @throws KrakenException if Kraken returns an error - */ - public LedgerInfo ledgerInfo(LedgerInfoParams params) { - return query(new LedgerInfoEndpoint(params)); - } - - /** - * Queries the private {@code QueryLedgers} endpoint, returning specific ledger entries by identifier. - * - * @param params the ledger entry identifiers to retrieve - * @return the ledger entries, by identifier - * @throws KrakenException if Kraken returns an error - */ - public Map ledgerEntries(LedgerEntriesParams params) { - return query(new LedgerEntriesEndpoint(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. * 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 index 89c5227..65178f0 100644 --- 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 @@ -18,8 +18,6 @@ */ public class AccountBalanceEndpoint extends PrivateEndpoint> { - private final AccountBalanceParams params; - /** * Creates the {@code Balance} endpoint with default options. */ @@ -34,7 +32,6 @@ public AccountBalanceEndpoint() { */ public AccountBalanceEndpoint(AccountBalanceParams params) { super("Balance", params, new TypeReference<>() {}); - this.params = params; } /** @@ -44,6 +41,7 @@ public AccountBalanceEndpoint(AccountBalanceParams params) { */ @Override public URL buildURL() { + AccountBalanceParams params = (AccountBalanceParams) getPostParams(); if (params.getAccountId() == null) { return super.buildURL(); } diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/CreditLinesEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/CreditLinesEndpoint.java index 78edf74..4e42d9e 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/CreditLinesEndpoint.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/CreditLinesEndpoint.java @@ -1,5 +1,7 @@ package dev.andstuff.kraken.api.endpoint.account; +import java.util.Optional; + import com.fasterxml.jackson.core.type.TypeReference; import dev.andstuff.kraken.api.endpoint.KrakenException; @@ -11,7 +13,7 @@ /** * The private {@code CreditLines} endpoint for credit lines. */ -public class CreditLinesEndpoint extends PrivateEndpoint { +public class CreditLinesEndpoint extends PrivateEndpoint> { /** * Creates the {@code CreditLines} endpoint with default options. @@ -33,14 +35,14 @@ public CreditLinesEndpoint(CreditLinesParams params) { * Unwraps {@code CreditLines}, whose successful result can be null. * * @param response the response envelope - * @return the credit details, or null when Kraken returns no credit lines + * @return the credit details, or an empty optional when Kraken returns no credit lines * @throws KrakenException if Kraken reports an error */ @Override - public CreditLines unwrapResponse(KrakenResponse response) { + public Optional unwrapResponse(KrakenResponse> response) { if (!response.error().isEmpty()) { throw new KrakenException(response.error()); } - return response.result().orElse(null); + return response.result().orElseGet(Optional::empty); } } diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/TradeVolumeEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/TradeVolumeEndpoint.java index 3cd5e13..f56babe 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/TradeVolumeEndpoint.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/TradeVolumeEndpoint.java @@ -17,7 +17,7 @@ */ public class TradeVolumeEndpoint extends PrivateEndpoint { - private final TradeVolumeParams params; + private static final JsonMapper OBJECT_MAPPER = JsonMapper.builder().build(); /** * Creates the {@code TradeVolume} endpoint with default options. @@ -33,7 +33,6 @@ public TradeVolumeEndpoint() { */ public TradeVolumeEndpoint(TradeVolumeParams params) { super("TradeVolume", params, new TypeReference<>() {}); - this.params = params; } /** @@ -45,6 +44,7 @@ public TradeVolumeEndpoint(TradeVolumeParams params) { */ @Override public String encodedParamsWith(String nonce) { + TradeVolumeParams params = (TradeVolumeParams) getPostParams(); if (params.getPairs() != null && params.getPairsWithClass() != null) { throw new IllegalArgumentException("Specify pairs or pairsWithClass, not both"); } @@ -66,7 +66,7 @@ public String encodedParamsWith(String nonce) { body.put("rebase_multiplier", params.getRebaseMultiplier().getValue()); } try { - return JsonMapper.builder().build().writeValueAsString(body); + return OBJECT_MAPPER.writeValueAsString(body); } catch (JsonProcessingException e) { throw new IllegalStateException("Cannot encode TradeVolume parameters", e); diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/AccountBalanceParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/AccountBalanceParams.java index 60c3d47..6a281d9 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/AccountBalanceParams.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/AccountBalanceParams.java @@ -4,7 +4,7 @@ import java.util.Map; import dev.andstuff.kraken.api.endpoint.priv.PostParams; - +import dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; import lombok.Builder; import lombok.Getter; diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/ApiKeyInfoParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/ApiKeyInfoParams.java index 52fdcab..33dbd66 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/ApiKeyInfoParams.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/ApiKeyInfoParams.java @@ -4,7 +4,6 @@ import java.util.Map; import dev.andstuff.kraken.api.endpoint.priv.PostParams; - import lombok.Builder; import lombok.Getter; diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/AssetClass.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/AssetClass.java index e071037..8eefcd0 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/AssetClass.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/AssetClass.java @@ -1,33 +1,24 @@ package dev.andstuff.kraken.api.endpoint.account.params; import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; -import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + /** * The asset class values accepted by {@code TradesHistory}. */ +@Getter +@RequiredArgsConstructor public enum AssetClass { - @JsonProperty("forex") FOREX("forex"), - @JsonProperty("equity_pair") EQUITY_PAIR("equity_pair"), - @JsonProperty("futures_contract") FUTURES_CONTRACT("futures_contract"), - @JsonProperty("synthetic_pair") SYNTHETIC_PAIR("synthetic_pair"), - @JsonProperty("external_pair") EXTERNAL_PAIR("external_pair"), + FOREX("forex"), + EQUITY_PAIR("equity_pair"), + FUTURES_CONTRACT("futures_contract"), + SYNTHETIC_PAIR("synthetic_pair"), + EXTERNAL_PAIR("external_pair"), @JsonEnumDefaultValue UNKNOWN("unknown"); - private final String value; - - AssetClass(String value) { - this.value = value; - } - - /** - * Returns the value sent to Kraken. - * - * @return the API value - */ @JsonValue - public String getValue() { - return value; - } + private final String value; } diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/CloseTime.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/CloseTime.java index ef40de6..e8f1410 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/CloseTime.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/CloseTime.java @@ -1,31 +1,22 @@ package dev.andstuff.kraken.api.endpoint.account.params; import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; -import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + /** * The close time values accepted by {@code ClosedOrders}. */ +@Getter +@RequiredArgsConstructor public enum CloseTime { - @JsonProperty("open") OPEN("open"), - @JsonProperty("close") CLOSE("close"), - @JsonProperty("both") BOTH("both"), + OPEN("open"), + CLOSE("close"), + BOTH("both"), @JsonEnumDefaultValue UNKNOWN("unknown"); - private final String value; - - CloseTime(String value) { - this.value = value; - } - - /** - * Returns the value sent to Kraken. - * - * @return the API value - */ @JsonValue - public String getValue() { - return value; - } + private final String value; } diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/ClosedOrdersParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/ClosedOrdersParams.java index e5454e4..34f65af 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/ClosedOrdersParams.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/ClosedOrdersParams.java @@ -4,7 +4,7 @@ import java.util.Map; import dev.andstuff.kraken.api.endpoint.priv.PostParams; - +import dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; import lombok.Builder; import lombok.Getter; diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/Consolidation.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/Consolidation.java index f849fd4..b39eed6 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/Consolidation.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/Consolidation.java @@ -1,29 +1,20 @@ package dev.andstuff.kraken.api.endpoint.account.params; import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; -import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + /** * The consolidation values accepted by {@code OpenPositions}. */ +@Getter +@RequiredArgsConstructor public enum Consolidation { - @JsonProperty("market") MARKET("market"), + MARKET("market"), @JsonEnumDefaultValue UNKNOWN("unknown"); - private final String value; - - Consolidation(String value) { - this.value = value; - } - - /** - * Returns the value sent to Kraken. - * - * @return the API value - */ @JsonValue - public String getValue() { - return value; - } + private final String value; } diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/CreditLinesParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/CreditLinesParams.java index 6195e73..cd888a9 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/CreditLinesParams.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/CreditLinesParams.java @@ -4,7 +4,7 @@ import java.util.Map; import dev.andstuff.kraken.api.endpoint.priv.PostParams; - +import dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; import lombok.Builder; import lombok.Getter; diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/ExtendedBalanceParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/ExtendedBalanceParams.java index c767f09..81bf328 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/ExtendedBalanceParams.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/ExtendedBalanceParams.java @@ -4,7 +4,7 @@ import java.util.Map; import dev.andstuff.kraken.api.endpoint.priv.PostParams; - +import dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; import lombok.Builder; import lombok.Getter; diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/OpenOrdersParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/OpenOrdersParams.java index 6c9657b..f89d346 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/OpenOrdersParams.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/OpenOrdersParams.java @@ -4,7 +4,7 @@ import java.util.Map; import dev.andstuff.kraken.api.endpoint.priv.PostParams; - +import dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; import lombok.Builder; import lombok.Getter; diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/OpenPositionsParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/OpenPositionsParams.java index d12e634..e31926f 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/OpenPositionsParams.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/OpenPositionsParams.java @@ -5,7 +5,7 @@ import java.util.Map; import dev.andstuff.kraken.api.endpoint.priv.PostParams; - +import dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; import lombok.Builder; import lombok.Getter; diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/OrderAmendsParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/OrderAmendsParams.java index 94c6816..210ce46 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/OrderAmendsParams.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/OrderAmendsParams.java @@ -4,7 +4,7 @@ import java.util.Map; import dev.andstuff.kraken.api.endpoint.priv.PostParams; - +import dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; import lombok.Builder; import lombok.Getter; diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/QueryOrdersParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/QueryOrdersParams.java index 7ff57a3..664cf3a 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/QueryOrdersParams.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/QueryOrdersParams.java @@ -5,7 +5,7 @@ import java.util.Map; import dev.andstuff.kraken.api.endpoint.priv.PostParams; - +import dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; import lombok.Builder; import lombok.Getter; import lombok.NonNull; diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/QueryTradesParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/QueryTradesParams.java index bf6bef5..b62f901 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/QueryTradesParams.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/QueryTradesParams.java @@ -5,7 +5,7 @@ import java.util.Map; import dev.andstuff.kraken.api.endpoint.priv.PostParams; - +import dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; import lombok.Builder; import lombok.Getter; import lombok.NonNull; diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/RebaseMultiplier.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/RebaseMultiplier.java deleted file mode 100644 index 6c65f2c..0000000 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/RebaseMultiplier.java +++ /dev/null @@ -1,30 +0,0 @@ -package dev.andstuff.kraken.api.endpoint.account.params; - -import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonValue; - -/** - * The rebase multiplier values accepted by {@code Balance}. - */ -public enum RebaseMultiplier { - @JsonProperty("rebased") REBASED("rebased"), - @JsonProperty("base") BASE("base"), - @JsonEnumDefaultValue UNKNOWN("unknown"); - - private final String value; - - RebaseMultiplier(String value) { - this.value = value; - } - - /** - * Returns the value sent to Kraken. - * - * @return the API value - */ - @JsonValue - public String getValue() { - return value; - } -} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradeBalanceParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradeBalanceParams.java index 5a104e5..fb8a0df 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradeBalanceParams.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradeBalanceParams.java @@ -4,7 +4,7 @@ import java.util.Map; import dev.andstuff.kraken.api.endpoint.priv.PostParams; - +import dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; import lombok.Builder; import lombok.Getter; diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradeType.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradeType.java index f0627cc..f626d56 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradeType.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradeType.java @@ -1,33 +1,24 @@ package dev.andstuff.kraken.api.endpoint.account.params; import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; -import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + /** * The trade type values accepted by {@code TradesHistory}. */ +@Getter +@RequiredArgsConstructor public enum TradeType { - @JsonProperty("all") ALL("all"), - @JsonProperty("any position") ANY_POSITION("any position"), - @JsonProperty("closed position") CLOSED_POSITION("closed position"), - @JsonProperty("closing position") CLOSING_POSITION("closing position"), - @JsonProperty("no position") NO_POSITION("no position"), + ALL("all"), + ANY_POSITION("any position"), + CLOSED_POSITION("closed position"), + CLOSING_POSITION("closing position"), + NO_POSITION("no position"), @JsonEnumDefaultValue UNKNOWN("unknown"); - private final String value; - - TradeType(String value) { - this.value = value; - } - - /** - * Returns the value sent to Kraken. - * - * @return the API value - */ @JsonValue - public String getValue() { - return value; - } + private final String value; } diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradeVolumeParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradeVolumeParams.java index a7dabcc..4e13229 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradeVolumeParams.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradeVolumeParams.java @@ -9,7 +9,7 @@ import com.fasterxml.jackson.databind.json.JsonMapper; import dev.andstuff.kraken.api.endpoint.priv.PostParams; - +import dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; import lombok.Builder; import lombok.Getter; import lombok.NonNull; @@ -21,6 +21,8 @@ @Builder(toBuilder = true) public class TradeVolumeParams extends PostParams { + private static final JsonMapper OBJECT_MAPPER = JsonMapper.builder().build(); + /** * The pairs for {@code TradeVolume}. */ @@ -54,7 +56,7 @@ protected Map params() { Map params = new HashMap<>(); if (pairsWithClass != null) { try { - params.put("pair", JsonMapper.builder().build().writeValueAsString(pairsWithClass)); + params.put("pair", OBJECT_MAPPER.writeValueAsString(pairsWithClass)); } catch (JsonProcessingException e) { throw new IllegalStateException("Cannot encode class-qualified pairs", e); diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradesHistoryParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradesHistoryParams.java index d9266fc..9f5afb4 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradesHistoryParams.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradesHistoryParams.java @@ -4,7 +4,7 @@ import java.util.Map; import dev.andstuff.kraken.api.endpoint.priv.PostParams; - +import dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; import lombok.Builder; import lombok.Getter; diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/WalletAccountsParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/WalletAccountsParams.java index 4781dd9..15718bd 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/WalletAccountsParams.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/WalletAccountsParams.java @@ -4,7 +4,6 @@ import java.util.Map; import dev.andstuff.kraken.api.endpoint.priv.PostParams; - import lombok.Builder; import lombok.Getter; diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/AccountTrade.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/AccountTrade.java index 8d86db2..7992c79 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/AccountTrade.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/AccountTrade.java @@ -1,6 +1,7 @@ package dev.andstuff.kraken.api.endpoint.account.response; import java.math.BigDecimal; +import java.time.Instant; import java.util.List; import com.fasterxml.jackson.annotation.JsonProperty; @@ -11,7 +12,7 @@ * @param orderId the order id * @param positionId the position id * @param pair the pair - * @param time the time as Unix seconds + * @param time the time as an instant * @param type the type * @param orderType the order type * @param price the price @@ -36,28 +37,28 @@ * @param trades the trades */ public record AccountTrade(@JsonProperty("ordertxid") String orderId, - @JsonProperty("postxid") String positionId, - String pair, - BigDecimal time, - String type, - @JsonProperty("ordertype") String orderType, - BigDecimal price, - BigDecimal cost, - BigDecimal fee, - @JsonProperty("vol") BigDecimal volume, - BigDecimal margin, - String leverage, - @JsonProperty("misc") String miscellaneous, - List ledgers, - @JsonProperty("trade_id") Long tradeId, - Boolean maker, - @JsonProperty("aclass") String assetClass, - @JsonProperty("tradeordertype") String tradeOrderType, - @JsonProperty("posstatus") String positionStatus, - @JsonProperty("cprice") BigDecimal closedPrice, - @JsonProperty("ccost") BigDecimal closedCost, - @JsonProperty("cfee") BigDecimal closedFee, - @JsonProperty("cvol") BigDecimal closedVolume, - @JsonProperty("cmargin") BigDecimal closedMargin, - BigDecimal net, - List trades) {} + @JsonProperty("postxid") String positionId, + String pair, + Instant time, + String type, + @JsonProperty("ordertype") String orderType, + BigDecimal price, + BigDecimal cost, + BigDecimal fee, + @JsonProperty("vol") BigDecimal volume, + BigDecimal margin, + String leverage, + @JsonProperty("misc") String miscellaneous, + List ledgers, + @JsonProperty("trade_id") Long tradeId, + Boolean maker, + @JsonProperty("aclass") String assetClass, + @JsonProperty("tradeordertype") String tradeOrderType, + @JsonProperty("posstatus") String positionStatus, + @JsonProperty("cprice") BigDecimal closedPrice, + @JsonProperty("ccost") BigDecimal closedCost, + @JsonProperty("cfee") BigDecimal closedFee, + @JsonProperty("cvol") BigDecimal closedVolume, + @JsonProperty("cmargin") BigDecimal closedMargin, + BigDecimal net, + List trades) {} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/ApiKeyInfo.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/ApiKeyInfo.java index d14a535..670c57f 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/ApiKeyInfo.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/ApiKeyInfo.java @@ -22,15 +22,15 @@ * @param lastUsed the last used as Unix seconds, preserving Kraken's string representation */ public record ApiKeyInfo(@JsonProperty("apiKeyName") String name, - String apiKey, - String nonce, - Long nonceWindow, - List permissions, - String iban, - String validUntil, - String queryFrom, - String queryTo, - String createdTime, - String modifiedTime, - List ipAllowlist, - String lastUsed) {} + String apiKey, + String nonce, + Long nonceWindow, + List permissions, + String iban, + String validUntil, + String queryFrom, + String queryTo, + String createdTime, + String modifiedTime, + List ipAllowlist, + String lastUsed) {} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/ClosedOrders.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/ClosedOrders.java index eaca451..e97f8a1 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/ClosedOrders.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/ClosedOrders.java @@ -2,8 +2,6 @@ import java.util.Map; -import com.fasterxml.jackson.annotation.JsonProperty; - /** * The closed orders returned by {@code ClosedOrders}. * @@ -11,4 +9,4 @@ * @param count the count; null when the count is omitted */ public record ClosedOrders(Map closed, - Long count) {} + Long count) {} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/CreditLines.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/CreditLines.java index 1e1e181..6f6b695 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/CreditLines.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/CreditLines.java @@ -12,7 +12,7 @@ * @param limitsMonitor the limits monitor */ public record CreditLines(@JsonProperty("asset_details") Map assetDetails, - @JsonProperty("limits_monitor") LimitsMonitor limitsMonitor) { + @JsonProperty("limits_monitor") LimitsMonitor limitsMonitor) { /** * The asset details returned by {@code CreditLines}. @@ -25,11 +25,11 @@ public record CreditLines(@JsonProperty("asset_details") Map trades, - @JsonProperty("closetm") BigDecimal closeTime, - String reason) { + @JsonProperty("userref") Long userReference, + @JsonProperty("cl_ord_id") String clientOrderId, + Status status, + @JsonProperty("opentm") Instant openTime, + @JsonProperty("starttm") Instant startTime, + @JsonProperty("expiretm") Instant expireTime, + @JsonProperty("descr") Description description, + @JsonProperty("time_in_force") TimeInForce timeInForce, + @JsonProperty("vol") BigDecimal volume, + @JsonProperty("vol_exec") BigDecimal executedVolume, + BigDecimal cost, + BigDecimal fee, + BigDecimal price, + @JsonProperty("stopprice") BigDecimal stopPrice, + @JsonProperty("limitprice") BigDecimal limitPrice, + Trigger trigger, + Boolean margin, + @JsonProperty("misc") String miscellaneous, + @JsonProperty("sender_sub_id") String senderSubId, + @JsonProperty("oflags") String orderFlags, + List trades, + @JsonProperty("closetm") Instant closeTime, + String reason) { /** * The status values used by the {@code OpenOrders, ClosedOrders and QueryOrders} endpoint. @@ -111,14 +112,14 @@ public enum OrderType { * @param assetClass the asset class */ public record Description(String pair, - Type type, - @JsonProperty("ordertype") OrderType orderType, - BigDecimal price, - BigDecimal price2, - String leverage, - String order, - String close, - @JsonProperty("aclass") String assetClass) {} + Type type, + @JsonProperty("ordertype") OrderType orderType, + BigDecimal price, + BigDecimal price2, + String leverage, + String order, + String close, + @JsonProperty("aclass") String assetClass) {} /** * The time in force values used by the {@code OpenOrders, ClosedOrders and QueryOrders} endpoint. diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/OrderAmends.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/OrderAmends.java index f87268c..12e00b0 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/OrderAmends.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/OrderAmends.java @@ -1,10 +1,16 @@ package dev.andstuff.kraken.api.endpoint.account.response; +import java.io.IOException; import java.math.BigDecimal; +import java.time.Instant; import java.util.List; import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; /** * The order amends returned by {@code OrderAmends}. @@ -13,7 +19,7 @@ * @param amends the amends */ public record OrderAmends(Long count, - List amends) { + List amends) { /** * The amend type values used by the {@code OrderAmends} endpoint. @@ -37,16 +43,24 @@ public enum AmendType { * @param triggerPrice the trigger price * @param reason the reason * @param postOnly the post only - * @param timestamp the timestamp in nanoseconds since the Unix epoch + * @param timestamp the amendment instant, decoded from epoch nanoseconds */ public record Amendment(@JsonProperty("amend_id") String amendId, - @JsonProperty("amend_type") AmendType amendType, - @JsonProperty("order_qty") BigDecimal orderQty, - @JsonProperty("display_qty") BigDecimal displayQty, - @JsonProperty("remaining_qty") BigDecimal remainingQty, - @JsonProperty("limit_price") BigDecimal limitPrice, - @JsonProperty("trigger_price") BigDecimal triggerPrice, - String reason, - @JsonProperty("post_only") Boolean postOnly, - Long timestamp) {} + @JsonProperty("amend_type") AmendType amendType, + @JsonProperty("order_qty") BigDecimal orderQty, + @JsonProperty("display_qty") BigDecimal displayQty, + @JsonProperty("remaining_qty") BigDecimal remainingQty, + @JsonProperty("limit_price") BigDecimal limitPrice, + @JsonProperty("trigger_price") BigDecimal triggerPrice, + String reason, + @JsonProperty("post_only") Boolean postOnly, + @JsonDeserialize(using = NanosecondsDeserializer.class) Instant timestamp) {} + + static class NanosecondsDeserializer extends JsonDeserializer { + + @Override + public Instant deserialize(JsonParser parser, DeserializationContext context) throws IOException { + return Instant.ofEpochSecond(0, parser.getLongValue()); + } + } } diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/TradeBalance.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/TradeBalance.java index 91fde9b..2bbb980 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/TradeBalance.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/TradeBalance.java @@ -20,13 +20,13 @@ * @param unexecutedValue the unexecuted value */ public record TradeBalance(@JsonProperty("eb") BigDecimal equivalentBalance, - @JsonProperty("tb") BigDecimal tradeBalance, - @JsonProperty("m") BigDecimal margin, - @JsonProperty("n") BigDecimal unrealizedNet, - @JsonProperty("c") BigDecimal costBasis, - @JsonProperty("v") BigDecimal floatingValue, - @JsonProperty("e") BigDecimal equity, - @JsonProperty("mf") BigDecimal freeMargin, - @JsonProperty("mfo") BigDecimal freeMarginForOrders, - @JsonProperty("ml") BigDecimal marginLevel, - @JsonProperty("uv") BigDecimal unexecutedValue) {} + @JsonProperty("tb") BigDecimal tradeBalance, + @JsonProperty("m") BigDecimal margin, + @JsonProperty("n") BigDecimal unrealizedNet, + @JsonProperty("c") BigDecimal costBasis, + @JsonProperty("v") BigDecimal floatingValue, + @JsonProperty("e") BigDecimal equity, + @JsonProperty("mf") BigDecimal freeMargin, + @JsonProperty("mfo") BigDecimal freeMarginForOrders, + @JsonProperty("ml") BigDecimal marginLevel, + @JsonProperty("uv") BigDecimal unexecutedValue) {} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/TradeVolume.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/TradeVolume.java index 0d45637..b10b072 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/TradeVolume.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/TradeVolume.java @@ -20,13 +20,13 @@ * @param schedules the schedules */ public record TradeVolume(String currency, - @JsonProperty("asset_class") AssetClass assetClass, - BigDecimal volume, - Inputs inputs, - Map fees, - @JsonProperty("fees_maker") Map feesMaker, - @JsonProperty("volume_subaccounts") List volumeSubaccounts, - List schedules) { + @JsonProperty("asset_class") AssetClass assetClass, + BigDecimal volume, + Inputs inputs, + Map fees, + @JsonProperty("fees_maker") Map feesMaker, + @JsonProperty("volume_subaccounts") List volumeSubaccounts, + List schedules) { /** * The asset class values used by the {@code TradeVolume} endpoint. @@ -52,8 +52,8 @@ public enum AssetClass { * @param domainAssetsOnPlatform the domain assets on platform */ public record Inputs(@JsonProperty("domain_spot_volume_30d") BigDecimal domainSpotVolume30d, - @JsonProperty("domain_futures_volume_30d") BigDecimal domainFuturesVolume30d, - @JsonProperty("domain_assets_on_platform") BigDecimal domainAssetsOnPlatform) {} + @JsonProperty("domain_futures_volume_30d") BigDecimal domainFuturesVolume30d, + @JsonProperty("domain_assets_on_platform") BigDecimal domainAssetsOnPlatform) {} /** * The fee tier returned by {@code TradeVolume}. @@ -69,14 +69,14 @@ public record Inputs(@JsonProperty("domain_spot_volume_30d") BigDecimal domainSp * @param volumeOffset the volume offset */ public record FeeTier(BigDecimal fee, - @JsonProperty("minfee") BigDecimal minimumFee, - @JsonProperty("maxfee") BigDecimal maximumFee, - @JsonProperty("nextfee") BigDecimal nextFee, - @JsonProperty("tiervolume") BigDecimal tierVolume, - @JsonProperty("tierfuturesvolume") BigDecimal tierFuturesVolume, - @JsonProperty("nextvolume") BigDecimal nextVolume, - @JsonProperty("nextfuturesvolume") BigDecimal nextFuturesVolume, - @JsonProperty("volumeoffset") BigDecimal volumeOffset) {} + @JsonProperty("minfee") BigDecimal minimumFee, + @JsonProperty("maxfee") BigDecimal maximumFee, + @JsonProperty("nextfee") BigDecimal nextFee, + @JsonProperty("tiervolume") BigDecimal tierVolume, + @JsonProperty("tierfuturesvolume") BigDecimal tierFuturesVolume, + @JsonProperty("nextvolume") BigDecimal nextVolume, + @JsonProperty("nextfuturesvolume") BigDecimal nextFuturesVolume, + @JsonProperty("volumeoffset") BigDecimal volumeOffset) {} /** * The subaccount volume returned by {@code TradeVolume}. @@ -85,7 +85,7 @@ public record FeeTier(BigDecimal fee, * @param volume the volume */ public record SubaccountVolume(String iiban, - BigDecimal volume) {} + BigDecimal volume) {} /** * The tier returned by {@code TradeVolume}. @@ -98,11 +98,11 @@ public record SubaccountVolume(String iiban, * @param active the active */ public record Tier(@JsonProperty("maker_fee") BigDecimal makerFee, - @JsonProperty("taker_fee") BigDecimal takerFee, - @JsonProperty("min_spot_volume") BigDecimal minSpotVolume, - @JsonProperty("min_futures_volume") BigDecimal minFuturesVolume, - @JsonProperty("min_assets_on_platform") BigDecimal minAssetsOnPlatform, - Boolean active) {} + @JsonProperty("taker_fee") BigDecimal takerFee, + @JsonProperty("min_spot_volume") BigDecimal minSpotVolume, + @JsonProperty("min_futures_volume") BigDecimal minFuturesVolume, + @JsonProperty("min_assets_on_platform") BigDecimal minAssetsOnPlatform, + Boolean active) {} /** * The fee schedule returned by {@code TradeVolume}. @@ -112,6 +112,6 @@ public record Tier(@JsonProperty("maker_fee") BigDecimal makerFee, * @param tiers the tiers */ public record FeeSchedule(String pair, - @JsonProperty("class") AssetClass assetClass, - List tiers) {} + @JsonProperty("class") AssetClass assetClass, + List tiers) {} } diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/TradesHistory.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/TradesHistory.java index 163ac31..84fc0cc 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/TradesHistory.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/TradesHistory.java @@ -2,8 +2,6 @@ import java.util.Map; -import com.fasterxml.jackson.annotation.JsonProperty; - /** * The trades history returned by {@code TradesHistory}. * @@ -11,4 +9,4 @@ * @param count the count; null when the count is omitted */ public record TradesHistory(Map trades, - Long count) {} + Long count) {} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/WalletAccounts.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/WalletAccounts.java index bc4a155..32b51db 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/WalletAccounts.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/WalletAccounts.java @@ -12,7 +12,7 @@ * @param cursor the cursor */ public record WalletAccounts(List accounts, - Cursor cursor) { + Cursor cursor) { /** * The flags returned by {@code ListWalletAccounts}. @@ -21,7 +21,7 @@ public record WalletAccounts(List accounts, * @param active the active */ public record Flags(@JsonProperty("user_defined") Boolean userDefined, - Boolean active) {} + Boolean active) {} /** * The status values used by the {@code ListWalletAccounts} endpoint. @@ -55,10 +55,10 @@ public enum Type { * @param name the name */ public record Account(@JsonProperty("account_id") String accountId, - Flags flags, - Status status, - Type type, - String name) {} + Flags flags, + Status status, + Type type, + String name) {} /** * The cursor returned by {@code ListWalletAccounts}. diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/priv/PrivateEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/priv/PrivateEndpoint.java index 7018ddd..997ee26 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/priv/PrivateEndpoint.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/priv/PrivateEndpoint.java @@ -8,7 +8,6 @@ import com.fasterxml.jackson.core.type.TypeReference; import dev.andstuff.kraken.api.endpoint.Endpoint; - import lombok.Getter; /** diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/priv/RebaseMultiplier.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/priv/RebaseMultiplier.java new file mode 100644 index 0000000..0f9b0cc --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/priv/RebaseMultiplier.java @@ -0,0 +1,21 @@ +package dev.andstuff.kraken.api.endpoint.priv; + +import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; +import com.fasterxml.jackson.annotation.JsonValue; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * The rebase multiplier values accepted by private endpoints. + */ +@Getter +@RequiredArgsConstructor +public enum RebaseMultiplier { + REBASED("rebased"), + BASE("base"), + @JsonEnumDefaultValue UNKNOWN("unknown"); + + @JsonValue + private final String value; +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/rest/DefaultKrakenRestRequester.java b/library/src/main/java/dev/andstuff/kraken/api/rest/DefaultKrakenRestRequester.java index f0db3a3..e04e6fb 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/rest/DefaultKrakenRestRequester.java +++ b/library/src/main/java/dev/andstuff/kraken/api/rest/DefaultKrakenRestRequester.java @@ -2,8 +2,8 @@ import java.io.IOException; import java.io.OutputStreamWriter; +import java.net.URL; import java.util.zip.ZipInputStream; - import javax.net.ssl.HttpsURLConnection; import com.fasterxml.jackson.databind.DeserializationFeature; @@ -18,7 +18,6 @@ import dev.andstuff.kraken.api.endpoint.KrakenResponse; import dev.andstuff.kraken.api.endpoint.priv.PrivateEndpoint; import dev.andstuff.kraken.api.endpoint.pub.PublicEndpoint; - import lombok.extern.slf4j.Slf4j; /** @@ -40,6 +39,21 @@ public class DefaultKrakenRestRequester implements KrakenRestRequester { .build(); } + private final ConnectionFactory connectionFactory; + + public DefaultKrakenRestRequester() { + this(url -> (HttpsURLConnection) url.openConnection()); + } + + DefaultKrakenRestRequester(ConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; + } + + @FunctionalInterface + interface ConnectionFactory { + HttpsURLConnection open(URL url) throws IOException; + } + /** * {@inheritDoc} * @@ -86,8 +100,8 @@ public T execute(PrivateEndpoint endpoint, KrakenCredentials credentials, } } - private static HttpsURLConnection createHttpsConnection(Endpoint endpoint) throws IOException { - HttpsURLConnection connection = (HttpsURLConnection) endpoint.buildURL().openConnection(); + private HttpsURLConnection createHttpsConnection(Endpoint endpoint) throws IOException { + HttpsURLConnection connection = connectionFactory.open(endpoint.buildURL()); connection.setRequestMethod(endpoint.getHttpMethod()); connection.addRequestProperty("User-Agent", "github.com/nyg/kraken-api-java"); return connection; diff --git a/library/src/test/java/dev/andstuff/kraken/api/AccountKrakenAPITest.java b/library/src/test/java/dev/andstuff/kraken/api/KrakenAPIAccountTest.java similarity index 91% rename from library/src/test/java/dev/andstuff/kraken/api/AccountKrakenAPITest.java rename to library/src/test/java/dev/andstuff/kraken/api/KrakenAPIAccountTest.java index 8d5680d..81257bb 100644 --- a/library/src/test/java/dev/andstuff/kraken/api/AccountKrakenAPITest.java +++ b/library/src/test/java/dev/andstuff/kraken/api/KrakenAPIAccountTest.java @@ -1,8 +1,23 @@ package dev.andstuff.kraken.api; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + import java.math.BigDecimal; import java.util.List; import java.util.Map; +import java.util.Optional; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import dev.andstuff.kraken.api.endpoint.account.AccountBalanceEndpoint; import dev.andstuff.kraken.api.endpoint.account.ApiKeyInfoEndpoint; @@ -49,36 +64,17 @@ import dev.andstuff.kraken.api.rest.KrakenNonceGenerator; import dev.andstuff.kraken.api.rest.KrakenRestRequester; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; - @ExtendWith(MockitoExtension.class) -class AccountKrakenAPITest { +class KrakenAPIAccountTest { @Mock private KrakenCredentials credentials; @Mock private KrakenNonceGenerator nonceGenerator; @Mock private KrakenRestRequester requester; @Mock private Map accountBalanceResponse; @Mock private Map extendedBalanceResponse; - @Mock private CreditLines creditLinesResponse; - @Mock private TradeBalance tradeBalanceResponse; - @Mock private OpenOrders openOrdersResponse; - @Mock private ClosedOrders closedOrdersResponse; @Mock private Map queryOrdersResponse; - @Mock private OrderAmends orderAmendsResponse; - @Mock private TradesHistory tradesHistoryResponse; @Mock private Map queryTradesResponse; @Mock private Map openPositionsResponse; - @Mock private TradeVolume tradeVolumeResponse; - @Mock private ApiKeyInfo apiKeyInfoResponse; - @Mock private WalletAccounts walletAccountsResponse; @Test void should_route_accountBalance_options_when_called() { @@ -144,11 +140,12 @@ void should_reject_extendedBalance_when_credentials_are_missing() { @Test void should_route_creditLines_options_when_called() { + Optional creditLinesResponse = Optional.of(new CreditLines(Map.of(), null)); KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); CreditLinesParams params = CreditLinesParams.builder().build(); when(requester.execute(any(CreditLinesEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(creditLinesResponse); - CreditLines result = unit.creditLines(params); + Optional result = unit.creditLines(params); assertThat(result).isSameAs(creditLinesResponse); verify(requester).execute(argThat((CreditLinesEndpoint endpoint) -> endpoint.getPostParams() == params), same(credentials), same(nonceGenerator)); @@ -156,10 +153,11 @@ void should_route_creditLines_options_when_called() { @Test void should_route_creditLines_defaults_when_called() { + Optional creditLinesResponse = Optional.of(new CreditLines(Map.of(), null)); KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); when(requester.execute(any(CreditLinesEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(creditLinesResponse); - CreditLines result = unit.creditLines(); + Optional result = unit.creditLines(); assertThat(result).isSameAs(creditLinesResponse); verify(requester).execute(any(CreditLinesEndpoint.class), same(credentials), same(nonceGenerator)); @@ -175,6 +173,7 @@ void should_reject_creditLines_when_credentials_are_missing() { @Test void should_route_tradeBalance_options_when_called() { + TradeBalance tradeBalanceResponse = new TradeBalance(null, null, null, null, null, null, null, null, null, null, null); KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); TradeBalanceParams params = TradeBalanceParams.builder().build(); when(requester.execute(any(TradeBalanceEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(tradeBalanceResponse); @@ -187,6 +186,7 @@ void should_route_tradeBalance_options_when_called() { @Test void should_route_tradeBalance_defaults_when_called() { + TradeBalance tradeBalanceResponse = new TradeBalance(null, null, null, null, null, null, null, null, null, null, null); KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); when(requester.execute(any(TradeBalanceEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(tradeBalanceResponse); @@ -206,6 +206,7 @@ void should_reject_tradeBalance_when_credentials_are_missing() { @Test void should_route_openOrders_options_when_called() { + OpenOrders openOrdersResponse = new OpenOrders(Map.of()); KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); OpenOrdersParams params = OpenOrdersParams.builder().build(); when(requester.execute(any(OpenOrdersEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(openOrdersResponse); @@ -218,6 +219,7 @@ void should_route_openOrders_options_when_called() { @Test void should_route_openOrders_defaults_when_called() { + OpenOrders openOrdersResponse = new OpenOrders(Map.of()); KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); when(requester.execute(any(OpenOrdersEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(openOrdersResponse); @@ -237,6 +239,7 @@ void should_reject_openOrders_when_credentials_are_missing() { @Test void should_route_closedOrders_options_when_called() { + ClosedOrders closedOrdersResponse = new ClosedOrders(Map.of(), null); KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); ClosedOrdersParams params = ClosedOrdersParams.builder().build(); when(requester.execute(any(ClosedOrdersEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(closedOrdersResponse); @@ -249,6 +252,7 @@ void should_route_closedOrders_options_when_called() { @Test void should_route_closedOrders_defaults_when_called() { + ClosedOrders closedOrdersResponse = new ClosedOrders(Map.of(), null); KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); when(requester.execute(any(ClosedOrdersEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(closedOrdersResponse); @@ -289,6 +293,7 @@ void should_reject_queryOrders_when_credentials_are_missing() { @Test void should_route_orderAmends_options_when_called() { + OrderAmends orderAmendsResponse = new OrderAmends(null, List.of()); KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); OrderAmendsParams params = OrderAmendsParams.builder().build(); when(requester.execute(any(OrderAmendsEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(orderAmendsResponse); @@ -301,6 +306,7 @@ void should_route_orderAmends_options_when_called() { @Test void should_route_orderAmends_defaults_when_called() { + OrderAmends orderAmendsResponse = new OrderAmends(null, List.of()); KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); when(requester.execute(any(OrderAmendsEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(orderAmendsResponse); @@ -320,6 +326,7 @@ void should_reject_orderAmends_when_credentials_are_missing() { @Test void should_route_tradesHistory_options_when_called() { + TradesHistory tradesHistoryResponse = new TradesHistory(Map.of(), null); KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); TradesHistoryParams params = TradesHistoryParams.builder().build(); when(requester.execute(any(TradesHistoryEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(tradesHistoryResponse); @@ -332,6 +339,7 @@ void should_route_tradesHistory_options_when_called() { @Test void should_route_tradesHistory_defaults_when_called() { + TradesHistory tradesHistoryResponse = new TradesHistory(Map.of(), null); KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); when(requester.execute(any(TradesHistoryEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(tradesHistoryResponse); @@ -403,6 +411,7 @@ void should_reject_openPositions_when_credentials_are_missing() { @Test void should_route_tradeVolume_options_when_called() { + TradeVolume tradeVolumeResponse = new TradeVolume(null, null, null, null, Map.of(), Map.of(), List.of(), List.of()); KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); TradeVolumeParams params = TradeVolumeParams.builder().build(); when(requester.execute(any(TradeVolumeEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(tradeVolumeResponse); @@ -415,6 +424,7 @@ void should_route_tradeVolume_options_when_called() { @Test void should_route_tradeVolume_defaults_when_called() { + TradeVolume tradeVolumeResponse = new TradeVolume(null, null, null, null, Map.of(), Map.of(), List.of(), List.of()); KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); when(requester.execute(any(TradeVolumeEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(tradeVolumeResponse); @@ -434,6 +444,7 @@ void should_reject_tradeVolume_when_credentials_are_missing() { @Test void should_route_apiKeyInfo_options_when_called() { + ApiKeyInfo apiKeyInfoResponse = new ApiKeyInfo(null, null, null, null, List.of(), null, null, null, null, null, null, List.of(), null); KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); ApiKeyInfoParams params = ApiKeyInfoParams.builder().build(); when(requester.execute(any(ApiKeyInfoEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(apiKeyInfoResponse); @@ -446,6 +457,7 @@ void should_route_apiKeyInfo_options_when_called() { @Test void should_route_apiKeyInfo_defaults_when_called() { + ApiKeyInfo apiKeyInfoResponse = new ApiKeyInfo(null, null, null, null, List.of(), null, null, null, null, null, null, List.of(), null); KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); when(requester.execute(any(ApiKeyInfoEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(apiKeyInfoResponse); @@ -465,6 +477,7 @@ void should_reject_apiKeyInfo_when_credentials_are_missing() { @Test void should_route_walletAccounts_options_when_called() { + WalletAccounts walletAccountsResponse = new WalletAccounts(List.of(), null); KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); WalletAccountsParams params = WalletAccountsParams.builder().build(); when(requester.execute(any(WalletAccountsEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(walletAccountsResponse); @@ -477,6 +490,7 @@ void should_route_walletAccounts_options_when_called() { @Test void should_route_walletAccounts_defaults_when_called() { + WalletAccounts walletAccountsResponse = new WalletAccounts(List.of(), null); KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); when(requester.execute(any(WalletAccountsEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(walletAccountsResponse); diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/AccountBalanceEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/AccountBalanceEndpointTest.java index e9494b7..ec97d0e 100644 --- a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/AccountBalanceEndpointTest.java +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/AccountBalanceEndpointTest.java @@ -1,31 +1,28 @@ package dev.andstuff.kraken.api.endpoint.account; +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.InputStream; import java.math.BigDecimal; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.Arrays; -import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import dev.andstuff.kraken.api.endpoint.KrakenException; import dev.andstuff.kraken.api.endpoint.KrakenResponse; -import dev.andstuff.kraken.api.endpoint.account.params.*; -import dev.andstuff.kraken.api.endpoint.account.response.*; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import dev.andstuff.kraken.api.endpoint.account.params.AccountBalanceParams; +import dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; @ExtendWith(MockitoExtension.class) class AccountBalanceEndpointTest { @@ -59,8 +56,11 @@ void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exce .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .addModule(new Jdk8Module()).build(); - String json = Files.readString(Path.of("src/test/resources/account/Balance.json")); + .addModules(new JavaTimeModule(), new Jdk8Module()).build(); + String json; + try (InputStream fixture = getClass().getResourceAsStream("/account/Balance.json")) { + json = new String(fixture.readAllBytes(), StandardCharsets.UTF_8); + } KrakenResponse> response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); Map result = unit.unwrapResponse(response); diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/ApiKeyInfoEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/ApiKeyInfoEndpointTest.java index bef7dd0..ab95da5 100644 --- a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/ApiKeyInfoEndpointTest.java +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/ApiKeyInfoEndpointTest.java @@ -1,31 +1,27 @@ package dev.andstuff.kraken.api.endpoint.account; -import java.math.BigDecimal; +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.InputStream; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.Arrays; -import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import dev.andstuff.kraken.api.endpoint.KrakenException; import dev.andstuff.kraken.api.endpoint.KrakenResponse; -import dev.andstuff.kraken.api.endpoint.account.params.*; -import dev.andstuff.kraken.api.endpoint.account.response.*; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import dev.andstuff.kraken.api.endpoint.account.params.ApiKeyInfoParams; +import dev.andstuff.kraken.api.endpoint.account.response.ApiKeyInfo; @ExtendWith(MockitoExtension.class) class ApiKeyInfoEndpointTest { @@ -59,8 +55,11 @@ void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exce .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .addModule(new Jdk8Module()).build(); - String json = Files.readString(Path.of("src/test/resources/account/GetApiKeyInfo.json")); + .addModules(new JavaTimeModule(), new Jdk8Module()).build(); + String json; + try (InputStream fixture = getClass().getResourceAsStream("/account/GetApiKeyInfo.json")) { + json = new String(fixture.readAllBytes(), StandardCharsets.UTF_8); + } KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); ApiKeyInfo result = unit.unwrapResponse(response); diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/ClosedOrdersEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/ClosedOrdersEndpointTest.java index 6316b49..171d7b5 100644 --- a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/ClosedOrdersEndpointTest.java +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/ClosedOrdersEndpointTest.java @@ -1,31 +1,31 @@ package dev.andstuff.kraken.api.endpoint.account; -import java.math.BigDecimal; +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.InputStream; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; +import java.time.Instant; import java.util.Arrays; -import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import dev.andstuff.kraken.api.endpoint.KrakenException; import dev.andstuff.kraken.api.endpoint.KrakenResponse; -import dev.andstuff.kraken.api.endpoint.account.params.*; -import dev.andstuff.kraken.api.endpoint.account.response.*; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import dev.andstuff.kraken.api.endpoint.account.params.CloseTime; +import dev.andstuff.kraken.api.endpoint.account.params.ClosedOrdersParams; +import dev.andstuff.kraken.api.endpoint.account.response.ClosedOrders; +import dev.andstuff.kraken.api.endpoint.account.response.Order; +import dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; @ExtendWith(MockitoExtension.class) class ClosedOrdersEndpointTest { @@ -68,14 +68,17 @@ void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exce .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .addModule(new Jdk8Module()).build(); - String json = Files.readString(Path.of("src/test/resources/account/ClosedOrders.json")); + .addModules(new JavaTimeModule(), new Jdk8Module()).build(); + String json; + try (InputStream fixture = getClass().getResourceAsStream("/account/ClosedOrders.json")) { + json = new String(fixture.readAllBytes(), StandardCharsets.UTF_8); + } KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); ClosedOrders result = unit.unwrapResponse(response); assertThat(result.count()).isEqualTo(2L); assertThat(result.closed().get("O37652-RJWRT-IMO74O").status()).isEqualTo(Order.Status.CANCELED); - assertThat(result.closed().get("O37652-RJWRT-IMO74O").closeTime()).isEqualByComparingTo("1688148610.0482"); + assertThat(result.closed().get("O37652-RJWRT-IMO74O").closeTime()).isEqualTo(Instant.ofEpochSecond(1688148610L, 48200000L)); } } diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/CreditLinesEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/CreditLinesEndpointTest.java index c761c1e..4f32687 100644 --- a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/CreditLinesEndpointTest.java +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/CreditLinesEndpointTest.java @@ -1,31 +1,31 @@ package dev.andstuff.kraken.api.endpoint.account; -import java.math.BigDecimal; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.InputStream; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.Arrays; -import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import dev.andstuff.kraken.api.endpoint.KrakenException; import dev.andstuff.kraken.api.endpoint.KrakenResponse; -import dev.andstuff.kraken.api.endpoint.account.params.*; -import dev.andstuff.kraken.api.endpoint.account.response.*; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import dev.andstuff.kraken.api.endpoint.account.params.CreditLinesParams; +import dev.andstuff.kraken.api.endpoint.account.response.CreditLines; +import dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; @ExtendWith(MockitoExtension.class) class CreditLinesEndpointTest { @@ -59,11 +59,14 @@ void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exce .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .addModule(new Jdk8Module()).build(); - String json = Files.readString(Path.of("src/test/resources/account/CreditLines.json")); + .addModules(new JavaTimeModule(), new Jdk8Module()).build(); + String json; + try (InputStream fixture = getClass().getResourceAsStream("/account/CreditLines.json")) { + json = new String(fixture.readAllBytes(), StandardCharsets.UTF_8); + } - KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); - CreditLines result = unit.unwrapResponse(response); + KrakenResponse> response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + CreditLines result = unit.unwrapResponse(response).orElseThrow(); assertThat(result.assetDetails().get("USD").availableCredit()).isEqualByComparingTo("37500.0000"); assertThat(result.limitsMonitor().debtToEquity()).isEqualByComparingTo("0.2000"); @@ -72,17 +75,17 @@ void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exce @Test void should_return_null_when_no_credit_lines_exist() throws Exception { CreditLinesEndpoint unit = new CreditLinesEndpoint(); - JsonMapper mapper = JsonMapper.builder().addModule(new Jdk8Module()).build(); - KrakenResponse response = mapper.readValue("{\"error\":[],\"result\":null}", unit.wrappedResponseType(mapper.getTypeFactory())); + JsonMapper mapper = JsonMapper.builder().addModules(new JavaTimeModule(), new Jdk8Module()).build(); + KrakenResponse> response = mapper.readValue("{\"error\":[],\"result\":null}", unit.wrappedResponseType(mapper.getTypeFactory())); - assertThat(unit.unwrapResponse(response)).isNull(); + assertThat(unit.unwrapResponse(response)).isEmpty(); } @Test void should_throw_kraken_error_when_credit_request_is_rejected() throws Exception { CreditLinesEndpoint unit = new CreditLinesEndpoint(); - JsonMapper mapper = JsonMapper.builder().addModule(new Jdk8Module()).build(); - KrakenResponse response = mapper.readValue("{\"error\":[\"EGeneral:Permission denied\"]}", unit.wrappedResponseType(mapper.getTypeFactory())); + JsonMapper mapper = JsonMapper.builder().addModules(new JavaTimeModule(), new Jdk8Module()).build(); + KrakenResponse> response = mapper.readValue("{\"error\":[\"EGeneral:Permission denied\"]}", unit.wrappedResponseType(mapper.getTypeFactory())); assertThatThrownBy(() -> unit.unwrapResponse(response)).isInstanceOf(KrakenException.class); } diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/ExtendedBalanceEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/ExtendedBalanceEndpointTest.java index c1bfc00..3de582b 100644 --- a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/ExtendedBalanceEndpointTest.java +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/ExtendedBalanceEndpointTest.java @@ -1,31 +1,28 @@ package dev.andstuff.kraken.api.endpoint.account; -import java.math.BigDecimal; +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.InputStream; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.Arrays; -import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import dev.andstuff.kraken.api.endpoint.KrakenException; import dev.andstuff.kraken.api.endpoint.KrakenResponse; -import dev.andstuff.kraken.api.endpoint.account.params.*; -import dev.andstuff.kraken.api.endpoint.account.response.*; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import dev.andstuff.kraken.api.endpoint.account.params.ExtendedBalanceParams; +import dev.andstuff.kraken.api.endpoint.account.response.ExtendedBalance; +import dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; @ExtendWith(MockitoExtension.class) class ExtendedBalanceEndpointTest { @@ -59,8 +56,11 @@ void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exce .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .addModule(new Jdk8Module()).build(); - String json = Files.readString(Path.of("src/test/resources/account/BalanceEx.json")); + .addModules(new JavaTimeModule(), new Jdk8Module()).build(); + String json; + try (InputStream fixture = getClass().getResourceAsStream("/account/BalanceEx.json")) { + json = new String(fixture.readAllBytes(), StandardCharsets.UTF_8); + } KrakenResponse> response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); Map result = unit.unwrapResponse(response); diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/OpenOrdersEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/OpenOrdersEndpointTest.java index 690d87e..9ed221b 100644 --- a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/OpenOrdersEndpointTest.java +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/OpenOrdersEndpointTest.java @@ -1,31 +1,30 @@ package dev.andstuff.kraken.api.endpoint.account; -import java.math.BigDecimal; +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.InputStream; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; +import java.time.Instant; import java.util.Arrays; -import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import dev.andstuff.kraken.api.endpoint.KrakenException; import dev.andstuff.kraken.api.endpoint.KrakenResponse; -import dev.andstuff.kraken.api.endpoint.account.params.*; -import dev.andstuff.kraken.api.endpoint.account.response.*; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import dev.andstuff.kraken.api.endpoint.account.params.OpenOrdersParams; +import dev.andstuff.kraken.api.endpoint.account.response.OpenOrders; +import dev.andstuff.kraken.api.endpoint.account.response.Order; +import dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; @ExtendWith(MockitoExtension.class) class OpenOrdersEndpointTest { @@ -62,21 +61,24 @@ void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exce .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .addModule(new Jdk8Module()).build(); - String json = Files.readString(Path.of("src/test/resources/account/OpenOrders.json")); + .addModules(new JavaTimeModule(), new Jdk8Module()).build(); + String json; + try (InputStream fixture = getClass().getResourceAsStream("/account/OpenOrders.json")) { + json = new String(fixture.readAllBytes(), StandardCharsets.UTF_8); + } KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); OpenOrders result = unit.unwrapResponse(response); assertThat(result.open().get("OQCLML-BW3P3-BUCMWZ").executedVolume()).isEqualByComparingTo("0.37500000"); - assertThat(result.open().get("OQCLML-BW3P3-BUCMWZ").openTime()).isEqualByComparingTo("1688666559.8974"); + assertThat(result.open().get("OQCLML-BW3P3-BUCMWZ").openTime()).isEqualTo(Instant.ofEpochSecond(1688666559L, 897400000L)); assertThat(result.open().get("OQCLML-BW3P3-BUCMWZ").description().orderType()).isEqualTo(Order.OrderType.LIMIT); } @Test void should_preserve_margin_and_unknown_enums_when_new_order_values_are_returned() throws Exception { OpenOrdersEndpoint unit = new OpenOrdersEndpoint(); - JsonMapper mapper = JsonMapper.builder().addModule(new Jdk8Module()) + JsonMapper mapper = JsonMapper.builder().addModules(new JavaTimeModule(), new Jdk8Module()) .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES).build(); String json = """ diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/OpenPositionsEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/OpenPositionsEndpointTest.java index a6fae6a..5dab5cc 100644 --- a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/OpenPositionsEndpointTest.java +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/OpenPositionsEndpointTest.java @@ -1,31 +1,30 @@ package dev.andstuff.kraken.api.endpoint.account; -import java.math.BigDecimal; +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.InputStream; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import dev.andstuff.kraken.api.endpoint.KrakenException; import dev.andstuff.kraken.api.endpoint.KrakenResponse; -import dev.andstuff.kraken.api.endpoint.account.params.*; -import dev.andstuff.kraken.api.endpoint.account.response.*; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import dev.andstuff.kraken.api.endpoint.account.params.Consolidation; +import dev.andstuff.kraken.api.endpoint.account.params.OpenPositionsParams; +import dev.andstuff.kraken.api.endpoint.account.response.OpenPosition; +import dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; @ExtendWith(MockitoExtension.class) class OpenPositionsEndpointTest { @@ -62,8 +61,11 @@ void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exce .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .addModule(new Jdk8Module()).build(); - String json = Files.readString(Path.of("src/test/resources/account/OpenPositions.json")); + .addModules(new JavaTimeModule(), new Jdk8Module()).build(); + String json; + try (InputStream fixture = getClass().getResourceAsStream("/account/OpenPositions.json")) { + json = new String(fixture.readAllBytes(), StandardCharsets.UTF_8); + } KrakenResponse> response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); Map result = unit.unwrapResponse(response); diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/OrderAmendsEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/OrderAmendsEndpointTest.java index 32c513f..f0d46c2 100644 --- a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/OrderAmendsEndpointTest.java +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/OrderAmendsEndpointTest.java @@ -1,31 +1,29 @@ package dev.andstuff.kraken.api.endpoint.account; -import java.math.BigDecimal; +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.InputStream; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; +import java.time.Instant; import java.util.Arrays; -import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import dev.andstuff.kraken.api.endpoint.KrakenException; import dev.andstuff.kraken.api.endpoint.KrakenResponse; -import dev.andstuff.kraken.api.endpoint.account.params.*; -import dev.andstuff.kraken.api.endpoint.account.response.*; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import dev.andstuff.kraken.api.endpoint.account.params.OrderAmendsParams; +import dev.andstuff.kraken.api.endpoint.account.response.OrderAmends; +import dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; @ExtendWith(MockitoExtension.class) class OrderAmendsEndpointTest { @@ -60,14 +58,17 @@ void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exce .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .addModule(new Jdk8Module()).build(); - String json = Files.readString(Path.of("src/test/resources/account/OrderAmends.json")); + .addModules(new JavaTimeModule(), new Jdk8Module()).build(); + String json; + try (InputStream fixture = getClass().getResourceAsStream("/account/OrderAmends.json")) { + json = new String(fixture.readAllBytes(), StandardCharsets.UTF_8); + } KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); OrderAmends result = unit.unwrapResponse(response); assertThat(result.count()).isEqualTo(3L); - assertThat(result.amends().getFirst().timestamp()).isEqualTo(1724158070287558000L); + assertThat(result.amends().getFirst().timestamp()).isEqualTo(Instant.ofEpochSecond(0, 1724158070287558000L)); assertThat(result.amends().getFirst().amendType()).isEqualTo(OrderAmends.AmendType.ORIGINAL); assertThat(result.amends().getFirst().orderQty()).isEqualByComparingTo("0.01000000"); } diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/QueryOrdersEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/QueryOrdersEndpointTest.java index ac99332..b00931f 100644 --- a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/QueryOrdersEndpointTest.java +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/QueryOrdersEndpointTest.java @@ -1,31 +1,31 @@ package dev.andstuff.kraken.api.endpoint.account; -import java.math.BigDecimal; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.InputStream; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; +import java.time.Instant; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import dev.andstuff.kraken.api.endpoint.KrakenException; import dev.andstuff.kraken.api.endpoint.KrakenResponse; -import dev.andstuff.kraken.api.endpoint.account.params.*; -import dev.andstuff.kraken.api.endpoint.account.response.*; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import dev.andstuff.kraken.api.endpoint.account.params.QueryOrdersParams; +import dev.andstuff.kraken.api.endpoint.account.response.Order; +import dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; @ExtendWith(MockitoExtension.class) class QueryOrdersEndpointTest { @@ -61,13 +61,16 @@ void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exce .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .addModule(new Jdk8Module()).build(); - String json = Files.readString(Path.of("src/test/resources/account/QueryOrders.json")); + .addModules(new JavaTimeModule(), new Jdk8Module()).build(); + String json; + try (InputStream fixture = getClass().getResourceAsStream("/account/QueryOrders.json")) { + json = new String(fixture.readAllBytes(), StandardCharsets.UTF_8); + } KrakenResponse> response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); Map result = unit.unwrapResponse(response); - assertThat(result.get("OBCMZD-JIEE7-77TH3F").closeTime()).isEqualByComparingTo("1688665499.1922"); + assertThat(result.get("OBCMZD-JIEE7-77TH3F").closeTime()).isEqualTo(Instant.ofEpochSecond(1688665499L, 192200000L)); assertThat(result.get("OBCMZD-JIEE7-77TH3F").reason()).isNull(); } } diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/QueryTradesEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/QueryTradesEndpointTest.java index 5116656..bea070a 100644 --- a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/QueryTradesEndpointTest.java +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/QueryTradesEndpointTest.java @@ -1,31 +1,30 @@ package dev.andstuff.kraken.api.endpoint.account; -import java.math.BigDecimal; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.InputStream; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import dev.andstuff.kraken.api.endpoint.KrakenException; import dev.andstuff.kraken.api.endpoint.KrakenResponse; -import dev.andstuff.kraken.api.endpoint.account.params.*; -import dev.andstuff.kraken.api.endpoint.account.response.*; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import dev.andstuff.kraken.api.endpoint.account.params.QueryTradesParams; +import dev.andstuff.kraken.api.endpoint.account.response.AccountTrade; +import dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; @ExtendWith(MockitoExtension.class) class QueryTradesEndpointTest { @@ -59,8 +58,11 @@ void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exce .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .addModule(new Jdk8Module()).build(); - String json = Files.readString(Path.of("src/test/resources/account/QueryTrades.json")); + .addModules(new JavaTimeModule(), new Jdk8Module()).build(); + String json; + try (InputStream fixture = getClass().getResourceAsStream("/account/QueryTrades.json")) { + json = new String(fixture.readAllBytes(), StandardCharsets.UTF_8); + } KrakenResponse> response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); Map result = unit.unwrapResponse(response); diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/TradeBalanceEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/TradeBalanceEndpointTest.java index a375019..61db587 100644 --- a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/TradeBalanceEndpointTest.java +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/TradeBalanceEndpointTest.java @@ -1,31 +1,28 @@ package dev.andstuff.kraken.api.endpoint.account; -import java.math.BigDecimal; +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.InputStream; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.Arrays; -import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import dev.andstuff.kraken.api.endpoint.KrakenException; import dev.andstuff.kraken.api.endpoint.KrakenResponse; -import dev.andstuff.kraken.api.endpoint.account.params.*; -import dev.andstuff.kraken.api.endpoint.account.response.*; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import dev.andstuff.kraken.api.endpoint.account.params.TradeBalanceParams; +import dev.andstuff.kraken.api.endpoint.account.response.TradeBalance; +import dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; @ExtendWith(MockitoExtension.class) class TradeBalanceEndpointTest { @@ -60,8 +57,11 @@ void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exce .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .addModule(new Jdk8Module()).build(); - String json = Files.readString(Path.of("src/test/resources/account/TradeBalance.json")); + .addModules(new JavaTimeModule(), new Jdk8Module()).build(); + String json; + try (InputStream fixture = getClass().getResourceAsStream("/account/TradeBalance.json")) { + json = new String(fixture.readAllBytes(), StandardCharsets.UTF_8); + } KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); TradeBalance result = unit.unwrapResponse(response); diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/TradeVolumeEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/TradeVolumeEndpointTest.java index ea12009..5b7baae 100644 --- a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/TradeVolumeEndpointTest.java +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/TradeVolumeEndpointTest.java @@ -1,31 +1,27 @@ package dev.andstuff.kraken.api.endpoint.account; -import java.math.BigDecimal; -import java.net.URLDecoder; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.InputStream; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Arrays; import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import dev.andstuff.kraken.api.endpoint.KrakenException; import dev.andstuff.kraken.api.endpoint.KrakenResponse; -import dev.andstuff.kraken.api.endpoint.account.params.*; -import dev.andstuff.kraken.api.endpoint.account.response.*; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import dev.andstuff.kraken.api.endpoint.account.params.AssetClass; +import dev.andstuff.kraken.api.endpoint.account.params.TradeVolumeParams; +import dev.andstuff.kraken.api.endpoint.account.response.TradeVolume; +import dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; @ExtendWith(MockitoExtension.class) class TradeVolumeEndpointTest { @@ -44,8 +40,11 @@ void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exce .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .addModule(new Jdk8Module()).build(); - String json = Files.readString(Path.of("src/test/resources/account/TradeVolume.json")); + .addModules(new JavaTimeModule(), new Jdk8Module()).build(); + String json; + try (InputStream fixture = getClass().getResourceAsStream("/account/TradeVolume.json")) { + json = new String(fixture.readAllBytes(), StandardCharsets.UTF_8); + } KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); TradeVolume result = unit.unwrapResponse(response); @@ -89,7 +88,7 @@ void should_reject_ambiguous_pairs_when_both_formats_are_supplied() { @Test void should_parse_fee_schedules_when_requested() throws Exception { TradeVolumeEndpoint unit = new TradeVolumeEndpoint(); - JsonMapper mapper = JsonMapper.builder().addModule(new Jdk8Module()) + JsonMapper mapper = JsonMapper.builder().addModules(new JavaTimeModule(), new Jdk8Module()) .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE).build(); String json = """ {"error":[],"result":{"asset_class":"future-class","volume_subaccounts":[{"iiban":"subaccount","volume":"0.0000000000123456789"}],"schedules":[{"pair":"XBTUSD","class":"volume","tiers":[{"maker_fee":"0.25","taker_fee":"0.4","min_spot_volume":"0","min_futures_volume":"10000","min_assets_on_platform":"0","active":true}]}]}} @@ -104,4 +103,10 @@ void should_parse_fee_schedules_when_requested() throws Exception { assertThat(result.schedules().getFirst().tiers().getFirst().active()).isTrue(); assertThat(result.schedules().getFirst().tiers().getFirst().makerFee()).isEqualByComparingTo("0.25"); } + @Test + void should_reject_nonnumeric_nonce_when_encoding_json() { + TradeVolumeEndpoint unit = new TradeVolumeEndpoint(); + + assertThatThrownBy(() -> unit.encodedParamsWith("not-a-number")).isInstanceOf(NumberFormatException.class); + } } diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/TradesHistoryEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/TradesHistoryEndpointTest.java index f95c3d9..0f438dd 100644 --- a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/TradesHistoryEndpointTest.java +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/TradesHistoryEndpointTest.java @@ -1,31 +1,31 @@ package dev.andstuff.kraken.api.endpoint.account; -import java.math.BigDecimal; +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.InputStream; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; +import java.time.Instant; import java.util.Arrays; -import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import dev.andstuff.kraken.api.endpoint.KrakenException; import dev.andstuff.kraken.api.endpoint.KrakenResponse; -import dev.andstuff.kraken.api.endpoint.account.params.*; -import dev.andstuff.kraken.api.endpoint.account.response.*; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import dev.andstuff.kraken.api.endpoint.account.params.AssetClass; +import dev.andstuff.kraken.api.endpoint.account.params.TradeType; +import dev.andstuff.kraken.api.endpoint.account.params.TradesHistoryParams; +import dev.andstuff.kraken.api.endpoint.account.response.TradesHistory; +import dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; @ExtendWith(MockitoExtension.class) class TradesHistoryEndpointTest { @@ -70,13 +70,16 @@ void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exce .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .addModule(new Jdk8Module()).build(); - String json = Files.readString(Path.of("src/test/resources/account/TradesHistory.json")); + .addModules(new JavaTimeModule(), new Jdk8Module()).build(); + String json; + try (InputStream fixture = getClass().getResourceAsStream("/account/TradesHistory.json")) { + json = new String(fixture.readAllBytes(), StandardCharsets.UTF_8); + } KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); TradesHistory result = unit.unwrapResponse(response); - assertThat(result.trades().get("THVRQM-33VKH-UCI7BS").time()).isEqualByComparingTo("1688667796.8802"); + assertThat(result.trades().get("THVRQM-33VKH-UCI7BS").time()).isEqualTo(Instant.ofEpochSecond(1688667796L, 880200000L)); assertThat(result.trades().get("THVRQM-33VKH-UCI7BS").volume()).isEqualByComparingTo("0.02000000"); assertThat(result.count()).isNull(); } diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/WalletAccountsEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/WalletAccountsEndpointTest.java index 8dd12b4..a1b8538 100644 --- a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/WalletAccountsEndpointTest.java +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/WalletAccountsEndpointTest.java @@ -1,31 +1,27 @@ package dev.andstuff.kraken.api.endpoint.account; -import java.math.BigDecimal; +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.InputStream; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.Arrays; -import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import dev.andstuff.kraken.api.endpoint.KrakenException; import dev.andstuff.kraken.api.endpoint.KrakenResponse; -import dev.andstuff.kraken.api.endpoint.account.params.*; -import dev.andstuff.kraken.api.endpoint.account.response.*; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import dev.andstuff.kraken.api.endpoint.account.params.WalletAccountsParams; +import dev.andstuff.kraken.api.endpoint.account.response.WalletAccounts; @ExtendWith(MockitoExtension.class) class WalletAccountsEndpointTest { @@ -58,8 +54,11 @@ void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exce .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .addModule(new Jdk8Module()).build(); - String json = Files.readString(Path.of("src/test/resources/account/ListWalletAccounts.json")); + .addModules(new JavaTimeModule(), new Jdk8Module()).build(); + String json; + try (InputStream fixture = getClass().getResourceAsStream("/account/ListWalletAccounts.json")) { + json = new String(fixture.readAllBytes(), StandardCharsets.UTF_8); + } KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); WalletAccounts result = unit.unwrapResponse(response); diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/params/TradeVolumeParamsTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/params/TradeVolumeParamsTest.java new file mode 100644 index 0000000..40306e1 --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/params/TradeVolumeParamsTest.java @@ -0,0 +1,43 @@ +package dev.andstuff.kraken.api.endpoint.account.params; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class TradeVolumeParamsTest { + + @Test + void should_retain_structured_pairs_when_building_form_parameters() { + TradeVolumeParams unit = TradeVolumeParams.builder() + .pairsWithClass(List.of(new TradeVolumeParams.Pair("TSLAx/USD", "equity_pair"))).build(); + + Map result = unit.params(); + + assertThat(result).containsOnlyKeys("pair").containsEntry("pair", "[{\"asset\":\"TSLAx/USD\",\"aclass\":\"equity_pair\"}]"); + } + + @Test + void should_join_pair_names_when_building_form_parameters() { + TradeVolumeParams unit = TradeVolumeParams.builder().pairs(List.of("XBTUSD", "ETHUSD")).build(); + + Map result = unit.params(); + + assertThat(result).containsOnlyKeys("pair").containsEntry("pair", "XBTUSD,ETHUSD"); + } + + @Test + void should_reject_ambiguous_pairs_when_building_form_parameters() { + TradeVolumeParams unit = TradeVolumeParams.builder().pairs(List.of("XBTUSD")) + .pairsWithClass(List.of(new TradeVolumeParams.Pair("TSLAx/USD", "equity_pair"))).build(); + + assertThatThrownBy(unit::params).isInstanceOf(IllegalArgumentException.class) + .hasMessage("Specify pairs or pairsWithClass, not both"); + } +} diff --git a/library/src/test/java/dev/andstuff/kraken/api/rest/AccountRestRequesterTest.java b/library/src/test/java/dev/andstuff/kraken/api/rest/DefaultKrakenRestRequesterAccountTest.java similarity index 58% rename from library/src/test/java/dev/andstuff/kraken/api/rest/AccountRestRequesterTest.java rename to library/src/test/java/dev/andstuff/kraken/api/rest/DefaultKrakenRestRequesterAccountTest.java index dfe9d02..f2131c2 100644 --- a/library/src/test/java/dev/andstuff/kraken/api/rest/AccountRestRequesterTest.java +++ b/library/src/test/java/dev/andstuff/kraken/api/rest/DefaultKrakenRestRequesterAccountTest.java @@ -1,63 +1,57 @@ package dev.andstuff.kraken.api.rest; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.net.URL; import java.nio.charset.StandardCharsets; - +import java.util.List; +import java.util.Optional; import javax.net.ssl.HttpsURLConnection; -import com.fasterxml.jackson.databind.type.TypeFactory; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import dev.andstuff.kraken.api.endpoint.KrakenException; import dev.andstuff.kraken.api.endpoint.account.CreditLinesEndpoint; import dev.andstuff.kraken.api.endpoint.account.TradeVolumeEndpoint; +import dev.andstuff.kraken.api.endpoint.account.params.TradeVolumeParams; import dev.andstuff.kraken.api.endpoint.account.response.CreditLines; import dev.andstuff.kraken.api.endpoint.account.response.TradeVolume; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - @ExtendWith(MockitoExtension.class) -class AccountRestRequesterTest { +class DefaultKrakenRestRequesterAccountTest { - @InjectMocks private DefaultKrakenRestRequester unit; - @Mock private TradeVolumeEndpoint tradeVolume; - @Mock private CreditLinesEndpoint creditLines; + @Mock private DefaultKrakenRestRequester.ConnectionFactory connectionFactory; @Mock private HttpsURLConnection connection; - @Mock private URL url; @Mock private KrakenCredentials credentials; @Mock private KrakenNonceGenerator nonceGenerator; @Test void should_sign_and_send_identical_json_when_requesting_trade_volume() throws Exception { + DefaultKrakenRestRequester unit = new DefaultKrakenRestRequester(connectionFactory); + TradeVolumeEndpoint endpoint = new TradeVolumeEndpoint(TradeVolumeParams.builder() + .pairsWithClass(List.of(new TradeVolumeParams.Pair("TSLAx/USD", "equity_pair"))).build()); + URL url = endpoint.buildURL(); ByteArrayOutputStream output = new ByteArrayOutputStream(); String body = "{\"nonce\":123,\"pair\":[{\"asset\":\"TSLAx/USD\",\"aclass\":\"equity_pair\"}]}"; when(nonceGenerator.generate()).thenReturn("123"); - when(tradeVolume.encodedParamsWith("123")).thenReturn(body); - when(tradeVolume.buildURL()).thenReturn(url); - when(url.openConnection()).thenReturn(connection); + when(connectionFactory.open(any(URL.class))).thenReturn(connection); when(connection.getURL()).thenReturn(url); - when(tradeVolume.getHttpMethod()).thenReturn("POST"); - when(tradeVolume.getContentType()).thenReturn("application/json"); when(credentials.getKey()).thenReturn("test-key"); when(credentials.sign(url, "123", body)).thenReturn("test-signature"); when(connection.getOutputStream()).thenReturn(output); when(connection.getHeaderField("Content-Type")).thenReturn("application/json"); when(connection.getInputStream()).thenReturn(new ByteArrayInputStream("{\"error\":[],\"result\":{\"volume\":\"0.0000000000123456789\"}}".getBytes(StandardCharsets.UTF_8))); - when(tradeVolume.wrappedResponseType(any())).thenReturn(new TradeVolumeEndpoint().wrappedResponseType(TypeFactory.createDefaultInstance())); - when(tradeVolume.unwrapResponse(any())).thenCallRealMethod(); - TradeVolume result = unit.execute(tradeVolume, credentials, nonceGenerator); + TradeVolume result = unit.execute(endpoint, credentials, nonceGenerator); assertThat(result.volume()).isEqualByComparingTo("0.0000000000123456789"); assertThat(output.toString(StandardCharsets.UTF_8)).isEqualTo(body); @@ -69,41 +63,31 @@ void should_sign_and_send_identical_json_when_requesting_trade_volume() throws E } @Test - void should_return_null_when_credit_lines_response_has_no_credit() throws Exception { + void should_return_empty_when_credit_lines_response_has_no_credit() throws Exception { + DefaultKrakenRestRequester unit = new DefaultKrakenRestRequester(connectionFactory); + CreditLinesEndpoint endpoint = new CreditLinesEndpoint(); when(nonceGenerator.generate()).thenReturn("123"); - when(creditLines.encodedParamsWith("123")).thenReturn("nonce=123"); - when(creditLines.buildURL()).thenReturn(url); - when(url.openConnection()).thenReturn(connection); - when(connection.getURL()).thenReturn(url); - when(creditLines.getHttpMethod()).thenReturn("POST"); - when(creditLines.getContentType()).thenCallRealMethod(); + when(connectionFactory.open(any(URL.class))).thenReturn(connection); when(connection.getOutputStream()).thenReturn(new ByteArrayOutputStream()); when(connection.getHeaderField("Content-Type")).thenReturn("application/json"); when(connection.getInputStream()).thenReturn(new ByteArrayInputStream("{\"error\":[],\"result\":null}".getBytes(StandardCharsets.UTF_8))); - when(creditLines.wrappedResponseType(any())).thenReturn(new CreditLinesEndpoint().wrappedResponseType(TypeFactory.createDefaultInstance())); - when(creditLines.unwrapResponse(any())).thenCallRealMethod(); - CreditLines result = unit.execute(creditLines, credentials, nonceGenerator); + Optional result = unit.execute(endpoint, credentials, nonceGenerator); - assertThat(result).isNull(); + assertThat(result).isEmpty(); verify(connection).addRequestProperty("Content-Type", "application/x-www-form-urlencoded"); } @Test void should_propagate_errors_when_kraken_rejects_trade_volume() throws Exception { + DefaultKrakenRestRequester unit = new DefaultKrakenRestRequester(connectionFactory); + TradeVolumeEndpoint endpoint = new TradeVolumeEndpoint(); when(nonceGenerator.generate()).thenReturn("123"); - when(tradeVolume.encodedParamsWith("123")).thenReturn("{\"nonce\":123}"); - when(tradeVolume.buildURL()).thenReturn(url); - when(url.openConnection()).thenReturn(connection); - when(connection.getURL()).thenReturn(url); - when(tradeVolume.getHttpMethod()).thenReturn("POST"); - when(tradeVolume.getContentType()).thenReturn("application/json"); + when(connectionFactory.open(any(URL.class))).thenReturn(connection); when(connection.getOutputStream()).thenReturn(new ByteArrayOutputStream()); when(connection.getHeaderField("Content-Type")).thenReturn("application/json"); when(connection.getInputStream()).thenReturn(new ByteArrayInputStream("{\"error\":[\"EGeneral:Permission denied\"]}".getBytes(StandardCharsets.UTF_8))); - when(tradeVolume.wrappedResponseType(any())).thenReturn(new TradeVolumeEndpoint().wrappedResponseType(TypeFactory.createDefaultInstance())); - when(tradeVolume.unwrapResponse(any())).thenCallRealMethod(); - assertThatThrownBy(() -> unit.execute(tradeVolume, credentials, nonceGenerator)).isInstanceOf(KrakenException.class); + assertThatThrownBy(() -> unit.execute(endpoint, credentials, nonceGenerator)).isInstanceOf(KrakenException.class); } } diff --git a/pom.xml b/pom.xml index b7d6779..4b6a361 100644 --- a/pom.xml +++ b/pom.xml @@ -98,6 +98,23 @@ opencsv ${opencsv.version} + + org.junit + junit-bom + ${junit.version} + import + pom + + + org.assertj + assertj-core + ${assertj.version} + + + org.mockito + mockito-junit-jupiter + ${mockito.version} + From 27ab30b2ffde8eba8fbbbcef66a3a47a9da0a94b Mon Sep 17 00:00:00 2001 From: nyg Date: Thu, 10 Sep 2026 12:23:30 +0200 Subject: [PATCH 3/3] fix(account): unify TradeVolume request encoding Route TradeVolume through PostParams so pair validation and JSON encoding have one owner. Reject noncanonical nonces before signing, because JSON numeric normalization would otherwise differ from the nonce used in the signature. Verify signatures offline with the public Kraken example vector. --- README.md | 4 +- .../endpoint/account/TradeVolumeEndpoint.java | 46 ------------------ .../account/params/TradeVolumeParams.java | 48 +++++++++++++------ .../kraken/api/endpoint/priv/PostParams.java | 14 ++++-- .../account/TradeVolumeEndpointTest.java | 34 +++++++++++-- .../account/params/TradeVolumeParamsTest.java | 12 ++--- ...DefaultKrakenRestRequesterAccountTest.java | 28 ++++++++++- .../api/rest/KrakenCredentialsTest.java | 39 +++++++++++++++ library/src/test/resources/account/README.md | 2 + 9 files changed, 151 insertions(+), 76 deletions(-) create mode 100644 library/src/test/java/dev/andstuff/kraken/api/rest/KrakenCredentialsTest.java diff --git a/README.md b/README.md index e3cdbb4..8193e26 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,9 @@ TradeVolume fees = api.tradeVolume(TradeVolumeParams.builder() `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. -`TradeVolumeEndpoint` 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. +`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 diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/TradeVolumeEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/TradeVolumeEndpoint.java index f56babe..d4ff05c 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/TradeVolumeEndpoint.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/TradeVolumeEndpoint.java @@ -1,12 +1,6 @@ package dev.andstuff.kraken.api.endpoint.account; -import java.math.BigInteger; -import java.util.LinkedHashMap; -import java.util.Map; - -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.json.JsonMapper; import dev.andstuff.kraken.api.endpoint.account.params.TradeVolumeParams; import dev.andstuff.kraken.api.endpoint.account.response.TradeVolume; @@ -17,8 +11,6 @@ */ public class TradeVolumeEndpoint extends PrivateEndpoint { - private static final JsonMapper OBJECT_MAPPER = JsonMapper.builder().build(); - /** * Creates the {@code TradeVolume} endpoint with default options. */ @@ -35,44 +27,6 @@ public TradeVolumeEndpoint(TradeVolumeParams params) { super("TradeVolume", params, new TypeReference<>() {}); } - /** - * Encodes {@code TradeVolume} as JSON so class-qualified pair arrays retain their structure. - * - * @param nonce the request nonce - * @return the JSON request body that must be signed and sent unchanged - * @throws IllegalArgumentException if both pair formats are supplied - */ - @Override - public String encodedParamsWith(String nonce) { - TradeVolumeParams params = (TradeVolumeParams) getPostParams(); - if (params.getPairs() != null && params.getPairsWithClass() != null) { - throw new IllegalArgumentException("Specify pairs or pairsWithClass, not both"); - } - Map body = new LinkedHashMap<>(); - body.put("nonce", new BigInteger(nonce)); - if (params.getPairs() != null) { - body.put("pair", String.join(",", params.getPairs())); - } - if (params.getPairsWithClass() != null) { - body.put("pair", params.getPairsWithClass()); - } - if (params.getFeeInfo() != null) { - body.put("fee-info", params.getFeeInfo()); - } - if (params.getFeeSchedule() != null) { - body.put("fee_schedule", params.getFeeSchedule()); - } - if (params.getRebaseMultiplier() != null) { - body.put("rebase_multiplier", params.getRebaseMultiplier().getValue()); - } - try { - return OBJECT_MAPPER.writeValueAsString(body); - } - catch (JsonProcessingException e) { - throw new IllegalStateException("Cannot encode TradeVolume parameters", e); - } - } - /** * Returns the media type used by the {@code TradeVolume} request body. * diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradeVolumeParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradeVolumeParams.java index 4e13229..9778fa9 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradeVolumeParams.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradeVolumeParams.java @@ -1,6 +1,7 @@ package dev.andstuff.kraken.api.endpoint.account.params; -import java.util.HashMap; +import java.math.BigInteger; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -49,26 +50,45 @@ public class TradeVolumeParams extends PostParams { private final RebaseMultiplier rebaseMultiplier; @Override - protected Map params() { + protected Map params() { if (pairs != null && pairsWithClass != null) { throw new IllegalArgumentException("Specify pairs or pairsWithClass, not both"); } - Map params = new HashMap<>(); - if (pairsWithClass != null) { - try { - params.put("pair", OBJECT_MAPPER.writeValueAsString(pairsWithClass)); - } - catch (JsonProcessingException e) { - throw new IllegalStateException("Cannot encode class-qualified pairs", e); - } + Map params = new LinkedHashMap<>(); + if (pairs != null) { + params.put("pair", String.join(",", pairs)); + } + else if (pairsWithClass != null) { + params.put("pair", pairsWithClass); + } + if (feeInfo != null) { + params.put("fee-info", feeInfo); + } + if (feeSchedule != null) { + params.put("fee_schedule", feeSchedule); + } + if (rebaseMultiplier != null) { + params.put("rebase_multiplier", rebaseMultiplier.getValue()); } - putIfNonNull(params, "pair", pairs, v -> String.join(",", v)); - putIfNonNull(params, "fee-info", feeInfo); - putIfNonNull(params, "fee_schedule", feeSchedule); - putIfNonNull(params, "rebase_multiplier", rebaseMultiplier, RebaseMultiplier::getValue); return params; } + @Override + protected String encode(Map params) { + String nonce = (String) params.get("nonce"); + BigInteger numericNonce = nonce != null && nonce.matches("0|[1-9][0-9]{0,19}") ? new BigInteger(nonce) : null; + if (numericNonce == null || numericNonce.bitLength() > 64) { + throw new IllegalStateException("TradeVolume requires KrakenNonceGenerator to return an unsigned 64-bit integer in canonical decimal form"); + } + params.put("nonce", numericNonce); + try { + return OBJECT_MAPPER.writeValueAsString(params); + } + catch (JsonProcessingException e) { + throw new IllegalStateException("Cannot encode TradeVolume parameters", e); + } + } + /** * A class-qualified trading pair for {@code TradeVolume}. * diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/priv/PostParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/priv/PostParams.java index 057a327..ffe885f 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/priv/PostParams.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/priv/PostParams.java @@ -2,6 +2,7 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; import java.util.Map; import java.util.function.Function; @@ -9,7 +10,7 @@ import lombok.Setter; /** - * The form-encoded body parameters of a {@link PrivateEndpoint}. The nonce is set by the library just before the request is signed. + * The body parameters of a {@link PrivateEndpoint}, form-encoded by default. The nonce is set by the library just before the request is signed. */ @Setter(AccessLevel.PACKAGE) public abstract class PostParams { @@ -19,25 +20,28 @@ public abstract class PostParams { * * @return the POST parameters */ - protected abstract Map params(); + protected abstract Map params(); private String nonce; /** - * Returns the parameters and the nonce as a URL encoded request body. + * Returns the parameters and the nonce as a request body using the parameter type's encoding. * * @return the encoded request body */ public String encoded() { - Map params = params(); + Map params = new LinkedHashMap<>(params()); params.put("nonce", nonce); + return encode(params); + } + protected String encode(Map params) { return params.keySet().stream() .reduce( new StringBuilder(), (postData, key) -> postData.append(key) .append("=") - .append(URLEncoder.encode(params.get(key), StandardCharsets.UTF_8)) + .append(URLEncoder.encode(params.get(key).toString(), StandardCharsets.UTF_8)) .append("&"), StringBuilder::append) .toString() diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/TradeVolumeEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/TradeVolumeEndpointTest.java index 5b7baae..ff6ce1d 100644 --- a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/TradeVolumeEndpointTest.java +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/TradeVolumeEndpointTest.java @@ -4,11 +4,15 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.io.InputStream; +import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.util.List; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.junit.jupiter.MockitoExtension; import com.fasterxml.jackson.databind.DeserializationFeature; @@ -18,7 +22,6 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import dev.andstuff.kraken.api.endpoint.KrakenResponse; -import dev.andstuff.kraken.api.endpoint.account.params.AssetClass; import dev.andstuff.kraken.api.endpoint.account.params.TradeVolumeParams; import dev.andstuff.kraken.api.endpoint.account.response.TradeVolume; import dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; @@ -103,10 +106,35 @@ void should_parse_fee_schedules_when_requested() throws Exception { assertThat(result.schedules().getFirst().tiers().getFirst().active()).isTrue(); assertThat(result.schedules().getFirst().tiers().getFirst().makerFee()).isEqualByComparingTo("0.25"); } + + @ParameterizedTest + @NullSource + @ValueSource(strings = {"", "not-a-number", "-1", "+1", "001", "1.0", "18446744073709551616"}) + void should_explain_invalid_nonce_when_encoding_json(String nonce) { + TradeVolumeEndpoint unit = new TradeVolumeEndpoint(); + + assertThatThrownBy(() -> unit.encodedParamsWith(nonce)).isInstanceOf(IllegalStateException.class) + .hasMessage("TradeVolume requires KrakenNonceGenerator to return an unsigned 64-bit integer in canonical decimal form"); + } + @Test - void should_reject_nonnumeric_nonce_when_encoding_json() { + void should_preserve_unsigned_nonce_when_encoding_its_maximum_value() throws Exception { TradeVolumeEndpoint unit = new TradeVolumeEndpoint(); + JsonMapper mapper = JsonMapper.builder().build(); + + String result = unit.encodedParamsWith("18446744073709551615"); + + assertThat(mapper.readTree(result).get("nonce").bigIntegerValue()).isEqualTo(new BigInteger("18446744073709551615")); + } + + @Test + void should_reuse_json_encoding_when_parameters_are_encoded_directly() throws Exception { + TradeVolumeParams params = TradeVolumeParams.builder().pairs(List.of("XBTUSD")).feeSchedule(true).build(); + TradeVolumeEndpoint unit = new TradeVolumeEndpoint(params); + + String result = unit.encodedParamsWith("123"); - assertThatThrownBy(() -> unit.encodedParamsWith("not-a-number")).isInstanceOf(NumberFormatException.class); + assertThat(params.encoded()).isEqualTo(result); + assertThat(result).isEqualTo("{\"pair\":\"XBTUSD\",\"fee_schedule\":true,\"nonce\":123}"); } } diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/params/TradeVolumeParamsTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/params/TradeVolumeParamsTest.java index 40306e1..df19ffb 100644 --- a/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/params/TradeVolumeParamsTest.java +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/params/TradeVolumeParamsTest.java @@ -14,26 +14,26 @@ class TradeVolumeParamsTest { @Test - void should_retain_structured_pairs_when_building_form_parameters() { + void should_retain_structured_pairs_when_building_request_parameters() { TradeVolumeParams unit = TradeVolumeParams.builder() .pairsWithClass(List.of(new TradeVolumeParams.Pair("TSLAx/USD", "equity_pair"))).build(); - Map result = unit.params(); + Map result = unit.params(); - assertThat(result).containsOnlyKeys("pair").containsEntry("pair", "[{\"asset\":\"TSLAx/USD\",\"aclass\":\"equity_pair\"}]"); + assertThat(result).containsOnlyKeys("pair").containsEntry("pair", List.of(new TradeVolumeParams.Pair("TSLAx/USD", "equity_pair"))); } @Test - void should_join_pair_names_when_building_form_parameters() { + void should_join_pair_names_when_building_request_parameters() { TradeVolumeParams unit = TradeVolumeParams.builder().pairs(List.of("XBTUSD", "ETHUSD")).build(); - Map result = unit.params(); + Map result = unit.params(); assertThat(result).containsOnlyKeys("pair").containsEntry("pair", "XBTUSD,ETHUSD"); } @Test - void should_reject_ambiguous_pairs_when_building_form_parameters() { + void should_reject_ambiguous_pairs_when_building_request_parameters() { TradeVolumeParams unit = TradeVolumeParams.builder().pairs(List.of("XBTUSD")) .pairsWithClass(List.of(new TradeVolumeParams.Pair("TSLAx/USD", "equity_pair"))).build(); diff --git a/library/src/test/java/dev/andstuff/kraken/api/rest/DefaultKrakenRestRequesterAccountTest.java b/library/src/test/java/dev/andstuff/kraken/api/rest/DefaultKrakenRestRequesterAccountTest.java index f2131c2..8c154a8 100644 --- a/library/src/test/java/dev/andstuff/kraken/api/rest/DefaultKrakenRestRequesterAccountTest.java +++ b/library/src/test/java/dev/andstuff/kraken/api/rest/DefaultKrakenRestRequesterAccountTest.java @@ -8,9 +8,11 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.math.BigDecimal; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.List; +import java.util.Map; import java.util.Optional; import javax.net.ssl.HttpsURLConnection; @@ -20,8 +22,10 @@ import org.mockito.junit.jupiter.MockitoExtension; import dev.andstuff.kraken.api.endpoint.KrakenException; +import dev.andstuff.kraken.api.endpoint.account.AccountBalanceEndpoint; import dev.andstuff.kraken.api.endpoint.account.CreditLinesEndpoint; import dev.andstuff.kraken.api.endpoint.account.TradeVolumeEndpoint; +import dev.andstuff.kraken.api.endpoint.account.params.AccountBalanceParams; import dev.andstuff.kraken.api.endpoint.account.params.TradeVolumeParams; import dev.andstuff.kraken.api.endpoint.account.response.CreditLines; import dev.andstuff.kraken.api.endpoint.account.response.TradeVolume; @@ -41,7 +45,7 @@ void should_sign_and_send_identical_json_when_requesting_trade_volume() throws E .pairsWithClass(List.of(new TradeVolumeParams.Pair("TSLAx/USD", "equity_pair"))).build()); URL url = endpoint.buildURL(); ByteArrayOutputStream output = new ByteArrayOutputStream(); - String body = "{\"nonce\":123,\"pair\":[{\"asset\":\"TSLAx/USD\",\"aclass\":\"equity_pair\"}]}"; + String body = "{\"pair\":[{\"asset\":\"TSLAx/USD\",\"aclass\":\"equity_pair\"}],\"nonce\":123}"; when(nonceGenerator.generate()).thenReturn("123"); when(connectionFactory.open(any(URL.class))).thenReturn(connection); when(connection.getURL()).thenReturn(url); @@ -90,4 +94,26 @@ void should_propagate_errors_when_kraken_rejects_trade_volume() throws Exception assertThatThrownBy(() -> unit.execute(endpoint, credentials, nonceGenerator)).isInstanceOf(KrakenException.class); } + + @Test + void should_sign_body_and_path_when_balance_selects_a_wallet() throws Exception { + DefaultKrakenRestRequester unit = new DefaultKrakenRestRequester(connectionFactory); + AccountBalanceEndpoint endpoint = new AccountBalanceEndpoint(AccountBalanceParams.builder().accountId("wallet +/&=").build()); + KrakenCredentials signingCredentials = new KrakenCredentials("public-example", "kQH5HW/8p1uGOVjbgWA7FunAmGO8lsSUXNsu3eow76sz84Q18fWxnyRzBHCd3pd5nE9qa99HAZtuZuj6F1huXg=="); + URL url = endpoint.buildURL(); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + when(nonceGenerator.generate()).thenReturn("123"); + when(connectionFactory.open(any(URL.class))).thenReturn(connection); + when(connection.getURL()).thenReturn(url); + when(connection.getOutputStream()).thenReturn(output); + when(connection.getHeaderField("Content-Type")).thenReturn("application/json"); + when(connection.getInputStream()).thenReturn(new ByteArrayInputStream("{\"error\":[],\"result\":{\"ZUSD\":\"1.25\"}}".getBytes(StandardCharsets.UTF_8))); + + Map result = unit.execute(endpoint, signingCredentials, nonceGenerator); + + assertThat(result.get("ZUSD")).isEqualByComparingTo("1.25"); + assertThat(url.getQuery()).isEqualTo("account_id=wallet+%2B%2F%26%3D"); + assertThat(output.toString(StandardCharsets.UTF_8)).isEqualTo("nonce=123"); + verify(connection).addRequestProperty("API-Sign", "WRKuiZ89EQ2MHYfSRx7pHztnIMr+ivo8S8E5olq+GcpBVw97M1jKx1ElzpGgC36H/tn8Po28EwNryLvK+rl4ig=="); + } } diff --git a/library/src/test/java/dev/andstuff/kraken/api/rest/KrakenCredentialsTest.java b/library/src/test/java/dev/andstuff/kraken/api/rest/KrakenCredentialsTest.java new file mode 100644 index 0000000..ecaa056 --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/rest/KrakenCredentialsTest.java @@ -0,0 +1,39 @@ +package dev.andstuff.kraken.api.rest; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URI; +import java.net.URL; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class KrakenCredentialsTest { + + @ParameterizedTest + @ValueSource(strings = {"https://api.kraken.com/0/private/AddOrder", "https://api.kraken.com/0/private/AddOrder?account_id=wallet"}) + void should_match_documented_signature_when_signing_the_uri_path(String endpointUrl) throws Exception { + KrakenCredentials unit = new KrakenCredentials("public-example", "kQH5HW/8p1uGOVjbgWA7FunAmGO8lsSUXNsu3eow76sz84Q18fWxnyRzBHCd3pd5nE9qa99HAZtuZuj6F1huXg=="); + URL url = URI.create(endpointUrl).toURL(); + String body = "nonce=1616492376594&ordertype=limit&pair=XBTUSD&price=37500&type=buy&volume=1.25"; + + String result = unit.sign(url, "1616492376594", body); + + assertThat(result).isEqualTo("4/dpxb3iT4tp/ZCVEwSnEsLxx0bqyhLpdfOpc6fn7OR8+UClSV5n9E6aSS8MPtnRfp32bAb0nmbRn6H8ndwLUQ=="); + } + + @Test + void should_match_reference_digest_when_signing_a_json_body() throws Exception { + KrakenCredentials unit = new KrakenCredentials("public-example", "kQH5HW/8p1uGOVjbgWA7FunAmGO8lsSUXNsu3eow76sz84Q18fWxnyRzBHCd3pd5nE9qa99HAZtuZuj6F1huXg=="); + URL url = URI.create("https://api.kraken.com/0/private/TradeVolume").toURL(); + String body = "{\"pair\":[{\"asset\":\"TSLAx/USD\",\"aclass\":\"equity_pair\"}],\"nonce\":123}"; + + String result = unit.sign(url, "123", body); + + assertThat(result).isEqualTo("JRJDryVhouSrIMbOftfDi5V0ILl6Mm6LMoX/LfS28me0PEos74LxkJ18uW9lfw1Y7osOoM78imN0uJjti1DJRw=="); + } +} diff --git a/library/src/test/resources/account/README.md b/library/src/test/resources/account/README.md index b132135..68e41c4 100644 --- a/library/src/test/resources/account/README.md +++ b/library/src/test/resources/account/README.md @@ -1 +1,3 @@ Fixtures are response examples from [Kraken’s Spot REST OpenAPI specification](https://docs.kraken.com/openapi/spot-rest.yaml), retrieved on 2026-09-08. The redundant `response` wrapper in the OrderAmends documentation example is removed to match the declared response envelope. The API key example is replaced with a test placeholder. Supplemental inline cases cover nullable credit results, fee schedules, unknown enum values, and margin order flags. Tests make no network requests. + +The signing tests use the public example secret and AddOrder vector in [Kraken authentication documentation](https://docs.kraken.com/exchange/guides/rest/authentication), never account credentials. The Balance and TradeVolume reference digests were computed independently with Python hashlib and hmac from that public example secret, the endpoint path and the exact nonce/body bytes. These tests are offline and do not place orders or access an account.