diff --git a/AGENTS.md b/AGENTS.md index 4963f39..b253fa2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,7 +11,7 @@ mvn -pl library package # build only the library module mvn -pl examples package # build only the examples module ``` -There are no tests in this project. CI runs `mvn clean package` on PRs targeting `master`. +The library's JUnit 5 tests use JSON fixtures and Mockito mocks of `KrakenRestRequester`, without network access or API keys. Run them with `mvn -pl library test`. CI runs `mvn clean package` on PRs targeting `master`. Java 25 with Temurin is required (configured via `maven-compiler-plugin` with `25`). diff --git a/README.md b/README.md index 1127e88..8193e26 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,26 @@ JsonNode order = api.query(KrakenAPI.Private.ADD_ORDER, Map.of( // Exception in thread "main" KrakenException(errors=[EGeneral:Permission denied]) ``` +### Account data + +All Account Data operations have typed methods, including balances, credit lines, orders, amendments, trades, positions, fee tiers, API key information, wallets, ledgers, and report exports. Optional settings use the corresponding `...Params.builder()`; omitted values keep Kraken's defaults. + +```java +Map balances = api.accountBalance(); +OpenOrders orders = api.openOrders(OpenOrdersParams.builder().trades(true).build()); +ClosedOrders page = api.closedOrders(ClosedOrdersParams.builder().offset(50).withoutCount(true).build()); +Map trades = api.queryTrades(QueryTradesParams.builder().transactionIds(List.of("THVRQM-33VKH-UCI7BS")).build()); +TradeVolume fees = api.tradeVolume(TradeVolumeParams.builder() + .pairsWithClass(List.of(new TradeVolumeParams.Pair("TSLAx/USD", "equity_pair"))) + .feeSchedule(true).build()); +``` + +`closedOrders` and `tradesHistory` expose the returned count, which is null when omitted by Kraken. Their `start` and `end` filters accept timestamp strings or transaction IDs. Monetary values use `BigDecimal`, timestamps use `Instant`, including fractional trade times and amendment times decoded from epoch nanoseconds. `creditLines` returns `Optional.empty()` when Kraken reports no credit lines. `accountBalance` can select a wallet using `AccountBalanceParams.accountId`, while `walletAccounts` lists the available wallets. + +`TradeVolumeParams` encodes requests as JSON to support class-qualified pairs. Custom REST requesters should send `endpoint.encodedParamsWith(nonce)` unchanged with `endpoint.getContentType()` and use `endpoint.unwrapResponse(response)` to handle endpoint-specific nullable results. + +Custom `KrakenNonceGenerator` implementations must produce increasing unsigned 64-bit integers as canonical decimal strings. `TradeVolume` encodes the nonce as a JSON number and rejects malformed, out-of-range or noncanonical values with an `IllegalStateException` that names the generator contract. Canonical formatting keeps the nonce used for signing identical to the number in the JSON body. + ### Custom endpoints An endpoint the library doesn't implement can also be given a proper type, instead of falling back to `JsonNode`. Extend `PublicEndpoint`, or `PrivateEndpoint` for a private one, and pass your endpoint to `query`: diff --git a/library/pom.xml b/library/pom.xml index f44f4cd..5700858 100644 --- a/library/pom.xml +++ b/library/pom.xml @@ -12,4 +12,22 @@ kraken-api Java Kraken API Client + + + org.junit.jupiter + junit-jupiter + test + + + org.assertj + assertj-core + test + + + org.mockito + mockito-junit-jupiter + test + + + diff --git a/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java b/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java index 67cf598..0f0d12d 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java +++ b/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java @@ -1,29 +1,72 @@ package dev.andstuff.kraken.api; +import java.math.BigDecimal; import java.util.List; import java.util.Map; +import java.util.Optional; import com.fasterxml.jackson.databind.JsonNode; import dev.andstuff.kraken.api.endpoint.KrakenException; +import dev.andstuff.kraken.api.endpoint.account.AccountBalanceEndpoint; +import dev.andstuff.kraken.api.endpoint.account.ApiKeyInfoEndpoint; +import dev.andstuff.kraken.api.endpoint.account.ClosedOrdersEndpoint; +import dev.andstuff.kraken.api.endpoint.account.CreditLinesEndpoint; +import dev.andstuff.kraken.api.endpoint.account.ExtendedBalanceEndpoint; import dev.andstuff.kraken.api.endpoint.account.LedgerEntriesEndpoint; import dev.andstuff.kraken.api.endpoint.account.LedgerInfoEndpoint; +import dev.andstuff.kraken.api.endpoint.account.OpenOrdersEndpoint; +import dev.andstuff.kraken.api.endpoint.account.OpenPositionsEndpoint; +import dev.andstuff.kraken.api.endpoint.account.OrderAmendsEndpoint; +import dev.andstuff.kraken.api.endpoint.account.QueryOrdersEndpoint; +import dev.andstuff.kraken.api.endpoint.account.QueryTradesEndpoint; import dev.andstuff.kraken.api.endpoint.account.RemoveReportEndpoint; import dev.andstuff.kraken.api.endpoint.account.ReportDataEndpoint; import dev.andstuff.kraken.api.endpoint.account.ReportsStatusesEndpoint; import dev.andstuff.kraken.api.endpoint.account.RequestReportEndpoint; +import dev.andstuff.kraken.api.endpoint.account.TradeBalanceEndpoint; +import dev.andstuff.kraken.api.endpoint.account.TradeVolumeEndpoint; +import dev.andstuff.kraken.api.endpoint.account.TradesHistoryEndpoint; +import dev.andstuff.kraken.api.endpoint.account.WalletAccountsEndpoint; +import dev.andstuff.kraken.api.endpoint.account.params.AccountBalanceParams; +import dev.andstuff.kraken.api.endpoint.account.params.ApiKeyInfoParams; +import dev.andstuff.kraken.api.endpoint.account.params.ClosedOrdersParams; +import dev.andstuff.kraken.api.endpoint.account.params.CreditLinesParams; +import dev.andstuff.kraken.api.endpoint.account.params.ExtendedBalanceParams; import dev.andstuff.kraken.api.endpoint.account.params.LedgerEntriesParams; import dev.andstuff.kraken.api.endpoint.account.params.LedgerInfoParams; +import dev.andstuff.kraken.api.endpoint.account.params.OpenOrdersParams; +import dev.andstuff.kraken.api.endpoint.account.params.OpenPositionsParams; +import dev.andstuff.kraken.api.endpoint.account.params.OrderAmendsParams; +import dev.andstuff.kraken.api.endpoint.account.params.QueryOrdersParams; +import dev.andstuff.kraken.api.endpoint.account.params.QueryTradesParams; import dev.andstuff.kraken.api.endpoint.account.params.RemovalType; import dev.andstuff.kraken.api.endpoint.account.params.RemoveReportParams; import dev.andstuff.kraken.api.endpoint.account.params.ReportDataParams; import dev.andstuff.kraken.api.endpoint.account.params.ReportType; import dev.andstuff.kraken.api.endpoint.account.params.ReportsStatusesParams; import dev.andstuff.kraken.api.endpoint.account.params.RequestReportParams; +import dev.andstuff.kraken.api.endpoint.account.params.TradeBalanceParams; +import dev.andstuff.kraken.api.endpoint.account.params.TradeVolumeParams; +import dev.andstuff.kraken.api.endpoint.account.params.TradesHistoryParams; +import dev.andstuff.kraken.api.endpoint.account.params.WalletAccountsParams; +import dev.andstuff.kraken.api.endpoint.account.response.AccountTrade; +import dev.andstuff.kraken.api.endpoint.account.response.ApiKeyInfo; +import dev.andstuff.kraken.api.endpoint.account.response.ClosedOrders; +import dev.andstuff.kraken.api.endpoint.account.response.CreditLines; +import dev.andstuff.kraken.api.endpoint.account.response.ExtendedBalance; import dev.andstuff.kraken.api.endpoint.account.response.LedgerEntry; import dev.andstuff.kraken.api.endpoint.account.response.LedgerInfo; +import dev.andstuff.kraken.api.endpoint.account.response.OpenOrders; +import dev.andstuff.kraken.api.endpoint.account.response.OpenPosition; +import dev.andstuff.kraken.api.endpoint.account.response.Order; +import dev.andstuff.kraken.api.endpoint.account.response.OrderAmends; import dev.andstuff.kraken.api.endpoint.account.response.Report; import dev.andstuff.kraken.api.endpoint.account.response.ReportRequest; +import dev.andstuff.kraken.api.endpoint.account.response.TradeBalance; +import dev.andstuff.kraken.api.endpoint.account.response.TradeVolume; +import dev.andstuff.kraken.api.endpoint.account.response.TradesHistory; +import dev.andstuff.kraken.api.endpoint.account.response.WalletAccounts; import dev.andstuff.kraken.api.endpoint.earn.EarnAllocateEndpoint; import dev.andstuff.kraken.api.endpoint.earn.EarnAllocateStatusEndpoint; import dev.andstuff.kraken.api.endpoint.earn.EarnAllocationsEndpoint; @@ -299,6 +342,306 @@ public Map ledgerEntries(LedgerEntriesParams params) { return query(new LedgerEntriesEndpoint(params)); } + /** + * Queries the {@code Balance} endpoint using default options. + * + * @return the account balance returned by Kraken + * @throws KrakenException if Kraken rejects the request + * @throws IllegalStateException if credentials are missing + */ + public Map accountBalance() { + return query(new AccountBalanceEndpoint()); + } + + /** + * Queries the {@code Balance} endpoint. + * + * @param params the request options + * @return the account balance returned by Kraken + * @throws KrakenException if Kraken rejects the request + * @throws IllegalStateException if credentials are missing + */ + public Map accountBalance(AccountBalanceParams params) { + return query(new AccountBalanceEndpoint(params)); + } + + /** + * Queries the {@code BalanceEx} endpoint using default options. + * + * @return the extended balance returned by Kraken + * @throws KrakenException if Kraken rejects the request + * @throws IllegalStateException if credentials are missing + */ + public Map extendedBalance() { + return query(new ExtendedBalanceEndpoint()); + } + + /** + * Queries the {@code BalanceEx} endpoint. + * + * @param params the request options + * @return the extended balance returned by Kraken + * @throws KrakenException if Kraken rejects the request + * @throws IllegalStateException if credentials are missing + */ + public Map extendedBalance(ExtendedBalanceParams params) { + return query(new ExtendedBalanceEndpoint(params)); + } + + /** + * Queries the {@code CreditLines} endpoint using default options. + * + * @return the credit lines returned by Kraken, or an empty optional when no credit lines exist + * @throws KrakenException if Kraken rejects the request + * @throws IllegalStateException if credentials are missing + */ + public Optional creditLines() { + return query(new CreditLinesEndpoint()); + } + + /** + * Queries the {@code CreditLines} endpoint. + * + * @param params the request options + * @return the credit lines returned by Kraken, or an empty optional when no credit lines exist + * @throws KrakenException if Kraken rejects the request + * @throws IllegalStateException if credentials are missing + */ + public Optional creditLines(CreditLinesParams params) { + return query(new CreditLinesEndpoint(params)); + } + + /** + * Queries the {@code TradeBalance} endpoint using default options. + * + * @return the trade balance returned by Kraken + * @throws KrakenException if Kraken rejects the request + * @throws IllegalStateException if credentials are missing + */ + public TradeBalance tradeBalance() { + return query(new TradeBalanceEndpoint()); + } + + /** + * Queries the {@code TradeBalance} endpoint. + * + * @param params the request options + * @return the trade balance returned by Kraken + * @throws KrakenException if Kraken rejects the request + * @throws IllegalStateException if credentials are missing + */ + public TradeBalance tradeBalance(TradeBalanceParams params) { + return query(new TradeBalanceEndpoint(params)); + } + + /** + * Queries the {@code OpenOrders} endpoint using default options. + * + * @return the open orders returned by Kraken + * @throws KrakenException if Kraken rejects the request + * @throws IllegalStateException if credentials are missing + */ + public OpenOrders openOrders() { + return query(new OpenOrdersEndpoint()); + } + + /** + * Queries the {@code OpenOrders} endpoint. + * + * @param params the request options + * @return the open orders returned by Kraken + * @throws KrakenException if Kraken rejects the request + * @throws IllegalStateException if credentials are missing + */ + public OpenOrders openOrders(OpenOrdersParams params) { + return query(new OpenOrdersEndpoint(params)); + } + + /** + * Queries the {@code ClosedOrders} endpoint using default options. + * + * @return the closed orders returned by Kraken + * @throws KrakenException if Kraken rejects the request + * @throws IllegalStateException if credentials are missing + */ + public ClosedOrders closedOrders() { + return query(new ClosedOrdersEndpoint()); + } + + /** + * Queries the {@code ClosedOrders} endpoint. + * + * @param params the request options + * @return the closed orders returned by Kraken + * @throws KrakenException if Kraken rejects the request + * @throws IllegalStateException if credentials are missing + */ + public ClosedOrders closedOrders(ClosedOrdersParams params) { + return query(new ClosedOrdersEndpoint(params)); + } + + /** + * Queries the {@code QueryOrders} endpoint. + * + * @param params the request options + * @return the query orders returned by Kraken + * @throws KrakenException if Kraken rejects the request + * @throws IllegalStateException if credentials are missing + */ + public Map queryOrders(QueryOrdersParams params) { + return query(new QueryOrdersEndpoint(params)); + } + + /** + * Queries the {@code OrderAmends} endpoint using default options. + * + * @return the order amends returned by Kraken + * @throws KrakenException if Kraken rejects the request + * @throws IllegalStateException if credentials are missing + */ + public OrderAmends orderAmends() { + return query(new OrderAmendsEndpoint()); + } + + /** + * Queries the {@code OrderAmends} endpoint. + * + * @param params the request options + * @return the order amends returned by Kraken + * @throws KrakenException if Kraken rejects the request + * @throws IllegalStateException if credentials are missing + */ + public OrderAmends orderAmends(OrderAmendsParams params) { + return query(new OrderAmendsEndpoint(params)); + } + + /** + * Queries the {@code TradesHistory} endpoint using default options. + * + * @return the trades history returned by Kraken + * @throws KrakenException if Kraken rejects the request + * @throws IllegalStateException if credentials are missing + */ + public TradesHistory tradesHistory() { + return query(new TradesHistoryEndpoint()); + } + + /** + * Queries the {@code TradesHistory} endpoint. + * + * @param params the request options + * @return the trades history returned by Kraken + * @throws KrakenException if Kraken rejects the request + * @throws IllegalStateException if credentials are missing + */ + public TradesHistory tradesHistory(TradesHistoryParams params) { + return query(new TradesHistoryEndpoint(params)); + } + + /** + * Queries the {@code QueryTrades} endpoint. + * + * @param params the request options + * @return the query trades returned by Kraken + * @throws KrakenException if Kraken rejects the request + * @throws IllegalStateException if credentials are missing + */ + public Map queryTrades(QueryTradesParams params) { + return query(new QueryTradesEndpoint(params)); + } + + /** + * Queries the {@code OpenPositions} endpoint using default options. + * + * @return the open positions returned by Kraken + * @throws KrakenException if Kraken rejects the request + * @throws IllegalStateException if credentials are missing + */ + public Map openPositions() { + return query(new OpenPositionsEndpoint()); + } + + /** + * Queries the {@code OpenPositions} endpoint. + * + * @param params the request options + * @return the open positions returned by Kraken + * @throws KrakenException if Kraken rejects the request + * @throws IllegalStateException if credentials are missing + */ + public Map openPositions(OpenPositionsParams params) { + return query(new OpenPositionsEndpoint(params)); + } + + /** + * Queries the {@code TradeVolume} endpoint using default options. + * + * @return the trade volume returned by Kraken + * @throws KrakenException if Kraken rejects the request + * @throws IllegalStateException if credentials are missing + */ + public TradeVolume tradeVolume() { + return query(new TradeVolumeEndpoint()); + } + + /** + * Queries the {@code TradeVolume} endpoint. + * + * @param params the request options + * @return the trade volume returned by Kraken + * @throws KrakenException if Kraken rejects the request + * @throws IllegalStateException if credentials are missing + */ + public TradeVolume tradeVolume(TradeVolumeParams params) { + return query(new TradeVolumeEndpoint(params)); + } + + /** + * Queries the {@code GetApiKeyInfo} endpoint using default options. + * + * @return the api key info returned by Kraken + * @throws KrakenException if Kraken rejects the request + * @throws IllegalStateException if credentials are missing + */ + public ApiKeyInfo apiKeyInfo() { + return query(new ApiKeyInfoEndpoint()); + } + + /** + * Queries the {@code GetApiKeyInfo} endpoint. + * + * @param params the request options + * @return the api key info returned by Kraken + * @throws KrakenException if Kraken rejects the request + * @throws IllegalStateException if credentials are missing + */ + public ApiKeyInfo apiKeyInfo(ApiKeyInfoParams params) { + return query(new ApiKeyInfoEndpoint(params)); + } + + /** + * Queries the {@code ListWalletAccounts} endpoint using default options. + * + * @return the wallet accounts returned by Kraken + * @throws KrakenException if Kraken rejects the request + * @throws IllegalStateException if credentials are missing + */ + public WalletAccounts walletAccounts() { + return query(new WalletAccountsEndpoint()); + } + + /** + * Queries the {@code ListWalletAccounts} endpoint. + * + * @param params the request options + * @return the wallet accounts returned by Kraken + * @throws KrakenException if Kraken rejects the request + * @throws IllegalStateException if credentials are missing + */ + public WalletAccounts walletAccounts(WalletAccountsParams params) { + return query(new WalletAccountsEndpoint(params)); + } + /** * Queries the private {@code AddExport} endpoint, asking Kraken to generate a report. The report is generated asynchronously: use {@link #reportsStatuses(ReportType)} to know when it is ready and {@link #reportData(String)} to download it. * @@ -644,6 +987,7 @@ public enum Private { GET_WEBSOCKETS_TOKEN("GetWebSocketsToken"), LEDGERS("Ledgers"), LEVEL3("Level3"), + LIST_WALLET_ACCOUNTS("ListWalletAccounts"), OPEN_ORDERS("OpenOrders"), OPEN_POSITIONS("OpenPositions"), ORDER_AMENDS("OrderAmends"), diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/Endpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/Endpoint.java index 3b109f4..c500667 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/Endpoint.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/Endpoint.java @@ -55,6 +55,17 @@ public JavaType wrappedResponseType(TypeFactory typeFactory) { KrakenResponse.class, typeFactory.constructType(responseType.getType())); } + /** + * Unwraps a JSON response or raises the Kraken error associated with an absent result. + * + * @param response the deserialized response envelope + * @return the endpoint result + * @throws KrakenException if the response has no result + */ + public T unwrapResponse(KrakenResponse response) { + return response.result().orElseThrow(() -> new KrakenException(response.error())); + } + /** * Reads the response of endpoints answering with a ZIP archive instead of JSON, e.g. report exports. Endpoints that can return such a response override this method. * diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/KrakenResponse.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/KrakenResponse.java index fce3c4e..e79c74a 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/KrakenResponse.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/KrakenResponse.java @@ -10,7 +10,7 @@ * * @param the type the {@code result} field is deserialized into * @param error the errors returned by Kraken, empty when the request succeeded - * @param result the payload of the response, empty when the request failed + * @param result the payload of the response, empty when the request failed or the endpoint returns a nullable result */ public record KrakenResponse(List error, Optional result) { @@ -18,7 +18,7 @@ public record KrakenResponse(List error, /** * Returns the payload of the response. * - * @return the payload, or an empty optional if the request failed + * @return the payload, or an empty optional if the request failed or its result is null */ public Optional result() { // TODO looks like an issue with jackson which returns Optional.of(NullNode.instance) instead of Optional.empty diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/AccountBalanceEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/AccountBalanceEndpoint.java new file mode 100644 index 0000000..65178f0 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/AccountBalanceEndpoint.java @@ -0,0 +1,55 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import java.math.BigDecimal; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +import com.fasterxml.jackson.core.type.TypeReference; + +import dev.andstuff.kraken.api.endpoint.account.params.AccountBalanceParams; +import dev.andstuff.kraken.api.endpoint.priv.PrivateEndpoint; + +/** + * The private {@code Balance} endpoint for account balance. + */ +public class AccountBalanceEndpoint extends PrivateEndpoint> { + + /** + * 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<>() {}); + } + + /** + * 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() { + AccountBalanceParams params = (AccountBalanceParams) getPostParams(); + 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..4e42d9e --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/CreditLinesEndpoint.java @@ -0,0 +1,48 @@ +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; +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 an empty optional when Kraken returns no credit lines + * @throws KrakenException if Kraken reports an error + */ + @Override + public Optional unwrapResponse(KrakenResponse> response) { + if (!response.error().isEmpty()) { + throw new KrakenException(response.error()); + } + return response.result().orElseGet(Optional::empty); + } +} 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..d4ff05c --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/TradeVolumeEndpoint.java @@ -0,0 +1,39 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import com.fasterxml.jackson.core.type.TypeReference; + +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 { + + /** + * 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<>() {}); + } + + /** + * 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..6a281d9 --- /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 dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; +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..33dbd66 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/ApiKeyInfoParams.java @@ -0,0 +1,28 @@ +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..8eefcd0 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/AssetClass.java @@ -0,0 +1,24 @@ +package dev.andstuff.kraken.api.endpoint.account.params; + +import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; +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 { + FOREX("forex"), + EQUITY_PAIR("equity_pair"), + FUTURES_CONTRACT("futures_contract"), + SYNTHETIC_PAIR("synthetic_pair"), + EXTERNAL_PAIR("external_pair"), + @JsonEnumDefaultValue UNKNOWN("unknown"); + + @JsonValue + 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 new file mode 100644 index 0000000..e8f1410 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/CloseTime.java @@ -0,0 +1,22 @@ +package dev.andstuff.kraken.api.endpoint.account.params; + +import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; +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 { + OPEN("open"), + CLOSE("close"), + BOTH("both"), + @JsonEnumDefaultValue UNKNOWN("unknown"); + + @JsonValue + 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 new file mode 100644 index 0000000..34f65af --- /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 dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; +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..b39eed6 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/Consolidation.java @@ -0,0 +1,20 @@ +package dev.andstuff.kraken.api.endpoint.account.params; + +import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; +import com.fasterxml.jackson.annotation.JsonValue; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * The consolidation values accepted by {@code OpenPositions}. + */ +@Getter +@RequiredArgsConstructor +public enum Consolidation { + MARKET("market"), + @JsonEnumDefaultValue UNKNOWN("unknown"); + + @JsonValue + 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 new file mode 100644 index 0000000..cd888a9 --- /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 dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; +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..81bf328 --- /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 dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; +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..f89d346 --- /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 dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; +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..e31926f --- /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 dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; +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..210ce46 --- /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 dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; +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..664cf3a --- /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 dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; +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..b62f901 --- /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 dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; +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/TradeBalanceParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradeBalanceParams.java new file mode 100644 index 0000000..fb8a0df --- /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 dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; +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..f626d56 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradeType.java @@ -0,0 +1,24 @@ +package dev.andstuff.kraken.api.endpoint.account.params; + +import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; +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 { + ALL("all"), + ANY_POSITION("any position"), + CLOSED_POSITION("closed position"), + CLOSING_POSITION("closing position"), + NO_POSITION("no position"), + @JsonEnumDefaultValue UNKNOWN("unknown"); + + @JsonValue + 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 new file mode 100644 index 0000000..9778fa9 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/TradeVolumeParams.java @@ -0,0 +1,99 @@ +package dev.andstuff.kraken.api.endpoint.account.params; + +import java.math.BigInteger; +import java.util.LinkedHashMap; +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 dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; +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 { + + private static final JsonMapper OBJECT_MAPPER = JsonMapper.builder().build(); + + /** + * 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 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()); + } + 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}. + * + * @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..9f5afb4 --- /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 dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; +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..15718bd --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/params/WalletAccountsParams.java @@ -0,0 +1,22 @@ +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..7992c79 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/AccountTrade.java @@ -0,0 +1,64 @@ +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; + +/** + * 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 an instant + * @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, + 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 new file mode 100644 index 0000000..670c57f --- /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..e97f8a1 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/ClosedOrders.java @@ -0,0 +1,12 @@ +package dev.andstuff.kraken.api.endpoint.account.response; + +import java.util.Map; + +/** + * 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..6f6b695 --- /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..cbb1a7b --- /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..c2186ac --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/OpenOrders.java @@ -0,0 +1,10 @@ +package dev.andstuff.kraken.api.endpoint.account.response; + +import java.util.Map; + +/** + * 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..90804de --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/OpenPosition.java @@ -0,0 +1,57 @@ +package dev.andstuff.kraken.api.endpoint.account.response; + +import java.math.BigDecimal; +import java.time.Instant; + +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 an instant + * @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 an instant, 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, + Instant 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..91891e2 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/Order.java @@ -0,0 +1,143 @@ +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.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 an instant + * @param startTime the start time as an instant + * @param expireTime the expire time as an instant + * @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 an instant + * @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") 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. + */ + 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..12e00b0 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/OrderAmends.java @@ -0,0 +1,66 @@ +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}. + * + * @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 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, + @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 new file mode 100644 index 0000000..2bbb980 --- /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..b10b072 --- /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..84fc0cc --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/account/response/TradesHistory.java @@ -0,0 +1,12 @@ +package dev.andstuff.kraken.api.endpoint.account.response; + +import java.util.Map; + +/** + * 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..32b51db --- /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/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/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..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 @@ -36,13 +36,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/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 efc4f55..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; @@ -15,7 +15,6 @@ 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; @@ -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} * @@ -72,6 +86,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())) { @@ -85,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; @@ -97,7 +112,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/KrakenAPIAccountTest.java b/library/src/test/java/dev/andstuff/kraken/api/KrakenAPIAccountTest.java new file mode 100644 index 0000000..81257bb --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/KrakenAPIAccountTest.java @@ -0,0 +1,511 @@ +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; +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; + +@ExtendWith(MockitoExtension.class) +class KrakenAPIAccountTest { + + @Mock private KrakenCredentials credentials; + @Mock private KrakenNonceGenerator nonceGenerator; + @Mock private KrakenRestRequester requester; + @Mock private Map accountBalanceResponse; + @Mock private Map extendedBalanceResponse; + @Mock private Map queryOrdersResponse; + @Mock private Map queryTradesResponse; + @Mock private Map openPositionsResponse; + + @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() { + 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); + + Optional 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() { + 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); + + Optional 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() { + 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); + + 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() { + 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); + + 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() { + 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); + + 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() { + 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); + + 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() { + 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); + + 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() { + 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); + + 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() { + 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); + + 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() { + 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); + + 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() { + 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); + + 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() { + 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); + + 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() { + 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); + + 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() { + 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); + + 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() { + 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); + + 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() { + 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); + + 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() { + 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); + + 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() { + 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); + + 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..ec97d0e --- /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 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.util.Arrays; +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.KrakenResponse; +import dev.andstuff.kraken.api.endpoint.account.params.AccountBalanceParams; +import dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; + +@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) + .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); + + 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..ab95da5 --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/ApiKeyInfoEndpointTest.java @@ -0,0 +1,71 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.InputStream; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +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.KrakenResponse; +import dev.andstuff.kraken.api.endpoint.account.params.ApiKeyInfoParams; +import dev.andstuff.kraken.api.endpoint.account.response.ApiKeyInfo; + +@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) + .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); + + 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..171d7b5 --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/ClosedOrdersEndpointTest.java @@ -0,0 +1,84 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.InputStream; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Arrays; +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.KrakenResponse; +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 { + + @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) + .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()).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 new file mode 100644 index 0000000..4f32687 --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/CreditLinesEndpointTest.java @@ -0,0 +1,92 @@ +package dev.andstuff.kraken.api.endpoint.account; + +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.util.Arrays; +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.CreditLinesParams; +import dev.andstuff.kraken.api.endpoint.account.response.CreditLines; +import dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier; + +@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) + .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).orElseThrow(); + + 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().addModules(new JavaTimeModule(), new Jdk8Module()).build(); + KrakenResponse> response = mapper.readValue("{\"error\":[],\"result\":null}", unit.wrappedResponseType(mapper.getTypeFactory())); + + 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().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 new file mode 100644 index 0000000..3de582b --- /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 static org.assertj.core.api.Assertions.assertThat; + +import java.io.InputStream; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +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.KrakenResponse; +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 { + + @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) + .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); + + 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..9ed221b --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/OpenOrdersEndpointTest.java @@ -0,0 +1,95 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.InputStream; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Arrays; +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.KrakenResponse; +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 { + + @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) + .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()).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().addModules(new JavaTimeModule(), 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..5dab5cc --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/OpenPositionsEndpointTest.java @@ -0,0 +1,77 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.InputStream; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +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.KrakenResponse; +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 { + + @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) + .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); + + 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..f0d46c2 --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/OrderAmendsEndpointTest.java @@ -0,0 +1,75 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.InputStream; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Arrays; +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.KrakenResponse; +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 { + + @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) + .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(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 new file mode 100644 index 0000000..b00931f --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/QueryOrdersEndpointTest.java @@ -0,0 +1,76 @@ +package dev.andstuff.kraken.api.endpoint.account; + +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.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.KrakenResponse; +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 { + + @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) + .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()).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 new file mode 100644 index 0000000..bea070a --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/QueryTradesEndpointTest.java @@ -0,0 +1,73 @@ +package dev.andstuff.kraken.api.endpoint.account; + +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.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.KrakenResponse; +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 { + + @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) + .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); + + 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..61db587 --- /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 static org.assertj.core.api.Assertions.assertThat; + +import java.io.InputStream; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +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.KrakenResponse; +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 { + + @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) + .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); + + 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..ff6ce1d --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/TradeVolumeEndpointTest.java @@ -0,0 +1,140 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import static org.assertj.core.api.Assertions.assertThat; +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; +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.KrakenResponse; +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 { + + @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) + .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); + + 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().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}]}]}} + """; + + 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"); + } + + @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_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"); + + 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/TradesHistoryEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/TradesHistoryEndpointTest.java new file mode 100644 index 0000000..0f438dd --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/TradesHistoryEndpointTest.java @@ -0,0 +1,86 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.InputStream; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Arrays; +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.KrakenResponse; +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 { + + @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) + .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()).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 new file mode 100644 index 0000000..a1b8538 --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/account/WalletAccountsEndpointTest.java @@ -0,0 +1,70 @@ +package dev.andstuff.kraken.api.endpoint.account; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.InputStream; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +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.KrakenResponse; +import dev.andstuff.kraken.api.endpoint.account.params.WalletAccountsParams; +import dev.andstuff.kraken.api.endpoint.account.response.WalletAccounts; + +@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) + .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); + + 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/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..df19ffb --- /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_request_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", List.of(new TradeVolumeParams.Pair("TSLAx/USD", "equity_pair"))); + } + + @Test + void should_join_pair_names_when_building_request_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_request_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/DefaultKrakenRestRequesterAccountTest.java b/library/src/test/java/dev/andstuff/kraken/api/rest/DefaultKrakenRestRequesterAccountTest.java new file mode 100644 index 0000000..8c154a8 --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/rest/DefaultKrakenRestRequesterAccountTest.java @@ -0,0 +1,119 @@ +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.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; + +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.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; + +@ExtendWith(MockitoExtension.class) +class DefaultKrakenRestRequesterAccountTest { + + @Mock private DefaultKrakenRestRequester.ConnectionFactory connectionFactory; + @Mock private HttpsURLConnection connection; + @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 = "{\"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); + 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))); + + TradeVolume result = unit.execute(endpoint, 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_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(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))); + + Optional result = unit.execute(endpoint, credentials, nonceGenerator); + + 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(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))); + + 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/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..68e41c4 --- /dev/null +++ b/library/src/test/resources/account/README.md @@ -0,0 +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. 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..4b6a361 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 @@ -94,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} + @@ -127,6 +148,11 @@ + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + org.apache.maven.plugins maven-compiler-plugin