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..4c31aea 100644
--- a/README.md
+++ b/README.md
@@ -127,6 +127,24 @@ The endpoint is run through the same `KrakenRestRequester` as the built-in ones,
Pull requests adding such an endpoint to the library are welcome, see the [architecture documentation](docs/ARCHITECTURE.md).
+### Funding
+
+The ten Funding operations listed in issue #82 have typed methods: `depositMethods`, `depositAddresses`, `depositStatus`, `withdrawalMethods`, `withdrawalAddresses`, `withdrawalInfo`, `withdraw`, `withdrawalStatus`, `cancelWithdrawal`, and `walletTransfer`. These wrap Kraken's `/0/private` Funding endpoints, now grouped as [Funding (Legacy)](https://docs.kraken.com/api-reference/funding/get-deposit-methods); Funding (Beta) is a separate API group.
+
+```java
+List methods = api.depositMethods(DepositMethodsParams.builder().asset("XBT").build());
+DepositStatus page = api.depositStatus(DepositStatusParams.builder().asset("XBT").cursor(true).limit(25).build());
+if (page.nextCursor() != null && !page.nextCursor().isEmpty()) {
+ DepositStatus nextPage = api.depositStatus(DepositStatusParams.builder().cursor(page.nextCursor()).limit(25).build());
+}
+WithdrawalInfo estimate = api.withdrawalInfo(WithdrawalInfoParams.builder()
+ .asset("XBT").key("my-saved-withdrawal-key").amount(new BigDecimal("0.01")).build());
+```
+
+Status methods return the same response record for paginated objects and unpaginated arrays. Pass `cursor(true)` to start pagination and then pass each non-empty `nextCursor()` token to retrieve the next page. An unlimited deposit limit is represented by `DepositLimit.unlimited() == true`, with a null amount; a missing limit remains null. Amounts and fees use `BigDecimal`.
+
+`withdraw` submits a withdrawal to a saved key, `cancelWithdrawal` requests cancellation, and `walletTransfer` moves assets from the Spot Wallet to the Futures Wallet. Withdrawal parameters support address confirmation and `maxFee`. A false cancellation result means Kraken did not accept the cancellation. Deposit address parameters support generating a new address and specifying the amount for Lightning invoices; responses preserve destination tags and memos.
+
### Custom REST requester
The current implementation of the library uses the JDK's HttpsURLConnection to make HTTP request. If that doesn't suit your needs and wish to use something else (e.g. Spring RestTemplate, Apache HttpComponents, OkHttp), you can implement the KrakenRestRequester interface and pass it to the KrakenAPI constructor:
diff --git a/library/pom.xml b/library/pom.xml
index f44f4cd..f977d31 100644
--- a/library/pom.xml
+++ b/library/pom.xml
@@ -12,4 +12,25 @@
kraken-api
Java Kraken API Client
+
+
+ org.junit.jupiter
+ junit-jupiter
+ ${junit.version}
+ test
+
+
+ org.assertj
+ assertj-core
+ ${assertj.version}
+ test
+
+
+ org.mockito
+ mockito-junit-jupiter
+ ${mockito.version}
+ test
+
+
+
diff --git a/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java b/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java
index 67cf598..d1b2a9a 100644
--- a/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java
+++ b/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java
@@ -37,6 +37,34 @@
import dev.andstuff.kraken.api.endpoint.earn.response.AllocationStatus;
import dev.andstuff.kraken.api.endpoint.earn.response.EarnAllocations;
import dev.andstuff.kraken.api.endpoint.earn.response.EarnStrategies;
+import dev.andstuff.kraken.api.endpoint.funding.CancelWithdrawalEndpoint;
+import dev.andstuff.kraken.api.endpoint.funding.DepositAddressesEndpoint;
+import dev.andstuff.kraken.api.endpoint.funding.DepositMethodsEndpoint;
+import dev.andstuff.kraken.api.endpoint.funding.DepositStatusEndpoint;
+import dev.andstuff.kraken.api.endpoint.funding.WalletTransferEndpoint;
+import dev.andstuff.kraken.api.endpoint.funding.WithdrawEndpoint;
+import dev.andstuff.kraken.api.endpoint.funding.WithdrawalAddressesEndpoint;
+import dev.andstuff.kraken.api.endpoint.funding.WithdrawalInfoEndpoint;
+import dev.andstuff.kraken.api.endpoint.funding.WithdrawalMethodsEndpoint;
+import dev.andstuff.kraken.api.endpoint.funding.WithdrawalStatusEndpoint;
+import dev.andstuff.kraken.api.endpoint.funding.params.CancelWithdrawalParams;
+import dev.andstuff.kraken.api.endpoint.funding.params.DepositAddressesParams;
+import dev.andstuff.kraken.api.endpoint.funding.params.DepositMethodsParams;
+import dev.andstuff.kraken.api.endpoint.funding.params.DepositStatusParams;
+import dev.andstuff.kraken.api.endpoint.funding.params.WalletTransferParams;
+import dev.andstuff.kraken.api.endpoint.funding.params.WithdrawParams;
+import dev.andstuff.kraken.api.endpoint.funding.params.WithdrawalAddressesParams;
+import dev.andstuff.kraken.api.endpoint.funding.params.WithdrawalInfoParams;
+import dev.andstuff.kraken.api.endpoint.funding.params.WithdrawalMethodsParams;
+import dev.andstuff.kraken.api.endpoint.funding.params.WithdrawalStatusParams;
+import dev.andstuff.kraken.api.endpoint.funding.response.DepositAddress;
+import dev.andstuff.kraken.api.endpoint.funding.response.DepositMethod;
+import dev.andstuff.kraken.api.endpoint.funding.response.DepositStatus;
+import dev.andstuff.kraken.api.endpoint.funding.response.FundingReference;
+import dev.andstuff.kraken.api.endpoint.funding.response.WithdrawalAddress;
+import dev.andstuff.kraken.api.endpoint.funding.response.WithdrawalInfo;
+import dev.andstuff.kraken.api.endpoint.funding.response.WithdrawalMethod;
+import dev.andstuff.kraken.api.endpoint.funding.response.WithdrawalStatus;
import dev.andstuff.kraken.api.endpoint.market.AssetInfoEndpoint;
import dev.andstuff.kraken.api.endpoint.market.AssetPairEndpoint;
import dev.andstuff.kraken.api.endpoint.market.ServerTimeEndpoint;
@@ -68,6 +96,7 @@
import dev.andstuff.kraken.api.rest.KrakenCredentials;
import dev.andstuff.kraken.api.rest.KrakenNonceGenerator;
import dev.andstuff.kraken.api.rest.KrakenRestRequester;
+
import lombok.Builder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@@ -354,6 +383,170 @@ public boolean cancelReport(String id) {
return query(new RemoveReportEndpoint(RemoveReportParams.of(id, RemovalType.CANCEL))).wasCanceled();
}
+ /**
+ * Queries the {@code DepositMethods} endpoint.
+ *
+ * @param params the request options
+ * @return the deposit methods returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public List depositMethods(DepositMethodsParams params) {
+ return query(new DepositMethodsEndpoint(params));
+ }
+
+ /**
+ * Queries the {@code DepositAddresses} endpoint.
+ *
+ * @param params the request options
+ * @return the deposit addresses returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public List depositAddresses(DepositAddressesParams params) {
+ return query(new DepositAddressesEndpoint(params));
+ }
+
+ /**
+ * Queries the {@code DepositStatus} endpoint using default options.
+ *
+ * @return the deposit status returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public DepositStatus depositStatus() {
+ return query(new DepositStatusEndpoint());
+ }
+
+ /**
+ * Queries the {@code DepositStatus} endpoint.
+ *
+ * @param params the request options
+ * @return the deposit status returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public DepositStatus depositStatus(DepositStatusParams params) {
+ return query(new DepositStatusEndpoint(params));
+ }
+
+ /**
+ * Queries the {@code WithdrawMethods} endpoint using default options.
+ *
+ * @return the withdrawal methods returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public List withdrawalMethods() {
+ return query(new WithdrawalMethodsEndpoint());
+ }
+
+ /**
+ * Queries the {@code WithdrawMethods} endpoint.
+ *
+ * @param params the request options
+ * @return the withdrawal methods returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public List withdrawalMethods(WithdrawalMethodsParams params) {
+ return query(new WithdrawalMethodsEndpoint(params));
+ }
+
+ /**
+ * Queries the {@code WithdrawAddresses} endpoint using default options.
+ *
+ * @return the withdrawal addresses returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public List withdrawalAddresses() {
+ return query(new WithdrawalAddressesEndpoint());
+ }
+
+ /**
+ * Queries the {@code WithdrawAddresses} endpoint.
+ *
+ * @param params the request options
+ * @return the withdrawal addresses returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public List withdrawalAddresses(WithdrawalAddressesParams params) {
+ return query(new WithdrawalAddressesEndpoint(params));
+ }
+
+ /**
+ * Queries the {@code WithdrawInfo} endpoint.
+ *
+ * @param params the request options
+ * @return the withdrawal info returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public WithdrawalInfo withdrawalInfo(WithdrawalInfoParams params) {
+ return query(new WithdrawalInfoEndpoint(params));
+ }
+
+ /**
+ * Requests a withdrawal using {@code Withdraw}.
+ *
+ * @param params the request options
+ * @return the withdraw returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public FundingReference withdraw(WithdrawParams params) {
+ return query(new WithdrawEndpoint(params));
+ }
+
+ /**
+ * Queries the {@code WithdrawStatus} endpoint using default options.
+ *
+ * @return the withdrawal status returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public WithdrawalStatus withdrawalStatus() {
+ return query(new WithdrawalStatusEndpoint());
+ }
+
+ /**
+ * Queries the {@code WithdrawStatus} endpoint.
+ *
+ * @param params the request options
+ * @return the withdrawal status returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public WithdrawalStatus withdrawalStatus(WithdrawalStatusParams params) {
+ return query(new WithdrawalStatusEndpoint(params));
+ }
+
+ /**
+ * Requests cancellation of a withdrawal using {@code WithdrawCancel}.
+ *
+ * @param params the request options
+ * @return whether Kraken accepted the cancellation; false is a valid unsuccessful result
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public boolean cancelWithdrawal(CancelWithdrawalParams params) {
+ return query(new CancelWithdrawalEndpoint(params));
+ }
+
+ /**
+ * Transfers funds between wallets using {@code WalletTransfer}.
+ *
+ * @param params the request options
+ * @return the wallet transfer returned by Kraken
+ * @throws KrakenException if Kraken rejects the request
+ * @throws IllegalStateException if credentials are missing
+ */
+ public FundingReference walletTransfer(WalletTransferParams params) {
+ return query(new WalletTransferEndpoint(params));
+ }
+
/**
* Queries the private {@code CreateSubaccount} endpoint, creating a trading subaccount. It must be called with an API key of the master account, having the withdraw funds permission.
*
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/CancelWithdrawalEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/CancelWithdrawalEndpoint.java
new file mode 100644
index 0000000..11c0a7f
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/CancelWithdrawalEndpoint.java
@@ -0,0 +1,31 @@
+package dev.andstuff.kraken.api.endpoint.funding;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+
+import dev.andstuff.kraken.api.endpoint.funding.params.CancelWithdrawalParams;
+import dev.andstuff.kraken.api.endpoint.priv.PrivateEndpoint;
+
+/**
+ * The private {@code WithdrawCancel} endpoint for cancel withdrawal.
+ */
+public class CancelWithdrawalEndpoint extends PrivateEndpoint {
+
+ /**
+ * Creates the {@code WithdrawCancel} endpoint using the required parameters.
+ *
+ * @param asset the asset sent to Kraken
+ * @param referenceId the referenceId sent to Kraken
+ */
+ public CancelWithdrawalEndpoint(String asset, String referenceId) {
+ this(CancelWithdrawalParams.builder().asset(asset).referenceId(referenceId).build());
+ }
+
+ /**
+ * Creates the {@code WithdrawCancel} endpoint.
+ *
+ * @param params the request parameters
+ */
+ public CancelWithdrawalEndpoint(CancelWithdrawalParams params) {
+ super("WithdrawCancel", params, new TypeReference<>() {});
+ }
+}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/DepositAddressesEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/DepositAddressesEndpoint.java
new file mode 100644
index 0000000..da578c3
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/DepositAddressesEndpoint.java
@@ -0,0 +1,34 @@
+package dev.andstuff.kraken.api.endpoint.funding;
+
+import java.util.List;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+
+import dev.andstuff.kraken.api.endpoint.funding.params.DepositAddressesParams;
+import dev.andstuff.kraken.api.endpoint.funding.response.DepositAddress;
+import dev.andstuff.kraken.api.endpoint.priv.PrivateEndpoint;
+
+/**
+ * The private {@code DepositAddresses} endpoint for deposit addresses.
+ */
+public class DepositAddressesEndpoint extends PrivateEndpoint> {
+
+ /**
+ * Creates the {@code DepositAddresses} endpoint using the required parameters.
+ *
+ * @param asset the asset sent to Kraken
+ * @param method the method sent to Kraken
+ */
+ public DepositAddressesEndpoint(String asset, String method) {
+ this(DepositAddressesParams.builder().asset(asset).method(method).build());
+ }
+
+ /**
+ * Creates the {@code DepositAddresses} endpoint.
+ *
+ * @param params the request parameters
+ */
+ public DepositAddressesEndpoint(DepositAddressesParams params) {
+ super("DepositAddresses", params, new TypeReference<>() {});
+ }
+}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/DepositMethodsEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/DepositMethodsEndpoint.java
new file mode 100644
index 0000000..9e13e3c
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/DepositMethodsEndpoint.java
@@ -0,0 +1,33 @@
+package dev.andstuff.kraken.api.endpoint.funding;
+
+import java.util.List;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+
+import dev.andstuff.kraken.api.endpoint.funding.params.DepositMethodsParams;
+import dev.andstuff.kraken.api.endpoint.funding.response.DepositMethod;
+import dev.andstuff.kraken.api.endpoint.priv.PrivateEndpoint;
+
+/**
+ * The private {@code DepositMethods} endpoint for deposit methods.
+ */
+public class DepositMethodsEndpoint extends PrivateEndpoint> {
+
+ /**
+ * Creates the {@code DepositMethods} endpoint using the required parameters.
+ *
+ * @param asset the asset sent to Kraken
+ */
+ public DepositMethodsEndpoint(String asset) {
+ this(DepositMethodsParams.builder().asset(asset).build());
+ }
+
+ /**
+ * Creates the {@code DepositMethods} endpoint.
+ *
+ * @param params the request parameters
+ */
+ public DepositMethodsEndpoint(DepositMethodsParams params) {
+ super("DepositMethods", params, new TypeReference<>() {});
+ }
+}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/DepositStatusEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/DepositStatusEndpoint.java
new file mode 100644
index 0000000..03a81c4
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/DepositStatusEndpoint.java
@@ -0,0 +1,29 @@
+package dev.andstuff.kraken.api.endpoint.funding;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+
+import dev.andstuff.kraken.api.endpoint.funding.params.DepositStatusParams;
+import dev.andstuff.kraken.api.endpoint.funding.response.DepositStatus;
+import dev.andstuff.kraken.api.endpoint.priv.PrivateEndpoint;
+
+/**
+ * The private {@code DepositStatus} endpoint for deposit status.
+ */
+public class DepositStatusEndpoint extends PrivateEndpoint {
+
+ /**
+ * Creates the {@code DepositStatus} endpoint with default options.
+ */
+ public DepositStatusEndpoint() {
+ this(DepositStatusParams.builder().build());
+ }
+
+ /**
+ * Creates the {@code DepositStatus} endpoint.
+ *
+ * @param params the request parameters
+ */
+ public DepositStatusEndpoint(DepositStatusParams params) {
+ super("DepositStatus", params, new TypeReference<>() {});
+ }
+}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/WalletTransferEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/WalletTransferEndpoint.java
new file mode 100644
index 0000000..84f2317
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/WalletTransferEndpoint.java
@@ -0,0 +1,38 @@
+package dev.andstuff.kraken.api.endpoint.funding;
+
+import java.math.BigDecimal;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+
+import dev.andstuff.kraken.api.endpoint.funding.params.DestinationWallet;
+import dev.andstuff.kraken.api.endpoint.funding.params.SourceWallet;
+import dev.andstuff.kraken.api.endpoint.funding.params.WalletTransferParams;
+import dev.andstuff.kraken.api.endpoint.funding.response.FundingReference;
+import dev.andstuff.kraken.api.endpoint.priv.PrivateEndpoint;
+
+/**
+ * The private {@code WalletTransfer} endpoint for wallet transfer.
+ */
+public class WalletTransferEndpoint extends PrivateEndpoint {
+
+ /**
+ * Creates the {@code WalletTransfer} endpoint using the required parameters.
+ *
+ * @param asset the asset sent to Kraken
+ * @param sourceWallet the sourceWallet sent to Kraken
+ * @param destinationWallet the destinationWallet sent to Kraken
+ * @param amount the amount sent to Kraken
+ */
+ public WalletTransferEndpoint(String asset, SourceWallet sourceWallet, DestinationWallet destinationWallet, BigDecimal amount) {
+ this(WalletTransferParams.builder().asset(asset).sourceWallet(sourceWallet).destinationWallet(destinationWallet).amount(amount).build());
+ }
+
+ /**
+ * Creates the {@code WalletTransfer} endpoint.
+ *
+ * @param params the request parameters
+ */
+ public WalletTransferEndpoint(WalletTransferParams params) {
+ super("WalletTransfer", params, new TypeReference<>() {});
+ }
+}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/WithdrawEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/WithdrawEndpoint.java
new file mode 100644
index 0000000..027d25d
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/WithdrawEndpoint.java
@@ -0,0 +1,35 @@
+package dev.andstuff.kraken.api.endpoint.funding;
+
+import java.math.BigDecimal;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+
+import dev.andstuff.kraken.api.endpoint.funding.params.WithdrawParams;
+import dev.andstuff.kraken.api.endpoint.funding.response.FundingReference;
+import dev.andstuff.kraken.api.endpoint.priv.PrivateEndpoint;
+
+/**
+ * The private {@code Withdraw} endpoint for withdraw.
+ */
+public class WithdrawEndpoint extends PrivateEndpoint {
+
+ /**
+ * Creates the {@code Withdraw} endpoint using the required parameters.
+ *
+ * @param asset the asset sent to Kraken
+ * @param key the key sent to Kraken
+ * @param amount the amount sent to Kraken
+ */
+ public WithdrawEndpoint(String asset, String key, BigDecimal amount) {
+ this(WithdrawParams.builder().asset(asset).key(key).amount(amount).build());
+ }
+
+ /**
+ * Creates the {@code Withdraw} endpoint.
+ *
+ * @param params the request parameters
+ */
+ public WithdrawEndpoint(WithdrawParams params) {
+ super("Withdraw", params, new TypeReference<>() {});
+ }
+}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/WithdrawalAddressesEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/WithdrawalAddressesEndpoint.java
new file mode 100644
index 0000000..4534346
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/WithdrawalAddressesEndpoint.java
@@ -0,0 +1,31 @@
+package dev.andstuff.kraken.api.endpoint.funding;
+
+import java.util.List;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+
+import dev.andstuff.kraken.api.endpoint.funding.params.WithdrawalAddressesParams;
+import dev.andstuff.kraken.api.endpoint.funding.response.WithdrawalAddress;
+import dev.andstuff.kraken.api.endpoint.priv.PrivateEndpoint;
+
+/**
+ * The private {@code WithdrawAddresses} endpoint for withdrawal addresses.
+ */
+public class WithdrawalAddressesEndpoint extends PrivateEndpoint> {
+
+ /**
+ * Creates the {@code WithdrawAddresses} endpoint with default options.
+ */
+ public WithdrawalAddressesEndpoint() {
+ this(WithdrawalAddressesParams.builder().build());
+ }
+
+ /**
+ * Creates the {@code WithdrawAddresses} endpoint.
+ *
+ * @param params the request parameters
+ */
+ public WithdrawalAddressesEndpoint(WithdrawalAddressesParams params) {
+ super("WithdrawAddresses", params, new TypeReference<>() {});
+ }
+}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/WithdrawalInfoEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/WithdrawalInfoEndpoint.java
new file mode 100644
index 0000000..79b4d1a
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/WithdrawalInfoEndpoint.java
@@ -0,0 +1,35 @@
+package dev.andstuff.kraken.api.endpoint.funding;
+
+import java.math.BigDecimal;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+
+import dev.andstuff.kraken.api.endpoint.funding.params.WithdrawalInfoParams;
+import dev.andstuff.kraken.api.endpoint.funding.response.WithdrawalInfo;
+import dev.andstuff.kraken.api.endpoint.priv.PrivateEndpoint;
+
+/**
+ * The private {@code WithdrawInfo} endpoint for withdrawal info.
+ */
+public class WithdrawalInfoEndpoint extends PrivateEndpoint {
+
+ /**
+ * Creates the {@code WithdrawInfo} endpoint using the required parameters.
+ *
+ * @param asset the asset sent to Kraken
+ * @param key the key sent to Kraken
+ * @param amount the amount sent to Kraken
+ */
+ public WithdrawalInfoEndpoint(String asset, String key, BigDecimal amount) {
+ this(WithdrawalInfoParams.builder().asset(asset).key(key).amount(amount).build());
+ }
+
+ /**
+ * Creates the {@code WithdrawInfo} endpoint.
+ *
+ * @param params the request parameters
+ */
+ public WithdrawalInfoEndpoint(WithdrawalInfoParams params) {
+ super("WithdrawInfo", params, new TypeReference<>() {});
+ }
+}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/WithdrawalMethodsEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/WithdrawalMethodsEndpoint.java
new file mode 100644
index 0000000..e052c59
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/WithdrawalMethodsEndpoint.java
@@ -0,0 +1,31 @@
+package dev.andstuff.kraken.api.endpoint.funding;
+
+import java.util.List;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+
+import dev.andstuff.kraken.api.endpoint.funding.params.WithdrawalMethodsParams;
+import dev.andstuff.kraken.api.endpoint.funding.response.WithdrawalMethod;
+import dev.andstuff.kraken.api.endpoint.priv.PrivateEndpoint;
+
+/**
+ * The private {@code WithdrawMethods} endpoint for withdrawal methods.
+ */
+public class WithdrawalMethodsEndpoint extends PrivateEndpoint> {
+
+ /**
+ * Creates the {@code WithdrawMethods} endpoint with default options.
+ */
+ public WithdrawalMethodsEndpoint() {
+ this(WithdrawalMethodsParams.builder().build());
+ }
+
+ /**
+ * Creates the {@code WithdrawMethods} endpoint.
+ *
+ * @param params the request parameters
+ */
+ public WithdrawalMethodsEndpoint(WithdrawalMethodsParams params) {
+ super("WithdrawMethods", params, new TypeReference<>() {});
+ }
+}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/WithdrawalStatusEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/WithdrawalStatusEndpoint.java
new file mode 100644
index 0000000..6de3af7
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/WithdrawalStatusEndpoint.java
@@ -0,0 +1,29 @@
+package dev.andstuff.kraken.api.endpoint.funding;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+
+import dev.andstuff.kraken.api.endpoint.funding.params.WithdrawalStatusParams;
+import dev.andstuff.kraken.api.endpoint.funding.response.WithdrawalStatus;
+import dev.andstuff.kraken.api.endpoint.priv.PrivateEndpoint;
+
+/**
+ * The private {@code WithdrawStatus} endpoint for withdrawal status.
+ */
+public class WithdrawalStatusEndpoint extends PrivateEndpoint {
+
+ /**
+ * Creates the {@code WithdrawStatus} endpoint with default options.
+ */
+ public WithdrawalStatusEndpoint() {
+ this(WithdrawalStatusParams.builder().build());
+ }
+
+ /**
+ * Creates the {@code WithdrawStatus} endpoint.
+ *
+ * @param params the request parameters
+ */
+ public WithdrawalStatusEndpoint(WithdrawalStatusParams params) {
+ super("WithdrawStatus", params, new TypeReference<>() {});
+ }
+}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/AssetClass.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/AssetClass.java
new file mode 100644
index 0000000..83457f7
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/AssetClass.java
@@ -0,0 +1,30 @@
+package dev.andstuff.kraken.api.endpoint.funding.params;
+
+import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+/**
+ * The asset class values accepted by {@code DepositMethods}.
+ */
+public enum AssetClass {
+ @JsonProperty("currency") CURRENCY("currency"),
+ @JsonProperty("tokenized_asset") TOKENIZED_ASSET("tokenized_asset"),
+ @JsonEnumDefaultValue UNKNOWN("unknown");
+
+ private final String value;
+
+ AssetClass(String value) {
+ this.value = value;
+ }
+
+ /**
+ * Returns the value sent to Kraken.
+ *
+ * @return the API value
+ */
+ @JsonValue
+ public String getValue() {
+ return value;
+ }
+}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/CancelWithdrawalParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/CancelWithdrawalParams.java
new file mode 100644
index 0000000..9259306
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/CancelWithdrawalParams.java
@@ -0,0 +1,38 @@
+package dev.andstuff.kraken.api.endpoint.funding.params;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import dev.andstuff.kraken.api.endpoint.priv.PostParams;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NonNull;
+
+/**
+ * The parameters of {@code WithdrawCancel}; omitted options use Kraken's defaults.
+ */
+@Getter
+@Builder(toBuilder = true)
+public class CancelWithdrawalParams extends PostParams {
+
+ /**
+ * Asset being withdrawn.
+ */
+ @NonNull
+ private final String asset;
+
+ /**
+ * Withdrawal reference ID.
+ */
+ @NonNull
+ private final String referenceId;
+
+ @Override
+ protected Map params() {
+ Map params = new HashMap<>();
+ putIfNonNull(params, "asset", asset);
+ putIfNonNull(params, "refid", referenceId);
+ return params;
+ }
+}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/DepositAddressesParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/DepositAddressesParams.java
new file mode 100644
index 0000000..8ad5d4c
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/DepositAddressesParams.java
@@ -0,0 +1,57 @@
+package dev.andstuff.kraken.api.endpoint.funding.params;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.Map;
+
+import dev.andstuff.kraken.api.endpoint.priv.PostParams;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NonNull;
+
+/**
+ * The parameters of {@code DepositAddresses}; omitted options use Kraken's defaults.
+ */
+@Getter
+@Builder(toBuilder = true)
+public class DepositAddressesParams extends PostParams {
+
+ /**
+ * Asset being deposited.
+ */
+ @NonNull
+ private final String asset;
+
+ /**
+ * Asset class being deposited.
+ */
+ private final AssetClass assetClass;
+
+ /**
+ * Name of the deposit method.
+ */
+ @NonNull
+ private final String method;
+
+ /**
+ * Whether or not to generate a new address.
+ */
+ private final Boolean generateNew;
+
+ /**
+ * Amount you wish to deposit (only required for {@code method=Bitcoin Lightning}).
+ */
+ private final BigDecimal amount;
+
+ @Override
+ protected Map params() {
+ Map params = new HashMap<>();
+ putIfNonNull(params, "asset", asset);
+ putIfNonNull(params, "aclass", assetClass, AssetClass::getValue);
+ putIfNonNull(params, "method", method);
+ putIfNonNull(params, "new", generateNew);
+ putIfNonNull(params, "amount", amount, BigDecimal::toPlainString);
+ return params;
+ }
+}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/DepositMethodsParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/DepositMethodsParams.java
new file mode 100644
index 0000000..1c17baa
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/DepositMethodsParams.java
@@ -0,0 +1,43 @@
+package dev.andstuff.kraken.api.endpoint.funding.params;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import dev.andstuff.kraken.api.endpoint.priv.PostParams;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NonNull;
+
+/**
+ * The parameters of {@code DepositMethods}; omitted options use Kraken's defaults.
+ */
+@Getter
+@Builder(toBuilder = true)
+public class DepositMethodsParams extends PostParams {
+
+ /**
+ * Asset being deposited.
+ */
+ @NonNull
+ private final String asset;
+
+ /**
+ * Asset class being deposited (optional).
+ */
+ private final AssetClass assetClass;
+
+ /**
+ * The rebase multiplier for {@code DepositMethods}. 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, "aclass", assetClass, AssetClass::getValue);
+ putIfNonNull(params, "rebase_multiplier", rebaseMultiplier, RebaseMultiplier::getValue);
+ return params;
+ }
+}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/DepositStatusParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/DepositStatusParams.java
new file mode 100644
index 0000000..c876f8b
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/DepositStatusParams.java
@@ -0,0 +1,99 @@
+package dev.andstuff.kraken.api.endpoint.funding.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 DepositStatus}; omitted options use Kraken's defaults.
+ */
+@Getter
+@Builder(toBuilder = true)
+public class DepositStatusParams extends PostParams {
+
+ /**
+ * Filter for specific asset being deposited.
+ */
+ private final String asset;
+
+ /**
+ * Filter for specific asset class being deposited.
+ */
+ private final AssetClass assetClass;
+
+ /**
+ * Filter for specific name of deposit method.
+ */
+ private final String method;
+
+ /**
+ * The earliest request timestamp in Unix seconds, inclusive.
+ */
+ private final String start;
+
+ /**
+ * The latest request timestamp in Unix seconds, inclusive.
+ */
+ private final String end;
+
+ /**
+ * The next-page cursor, or {@code true}/{@code false} to enable or disable pagination. Kraken defaults to {@code false} when omitted.
+ */
+ private final String cursor;
+
+ /**
+ * Number of results to include per page.
+ */
+ private final Integer limit;
+
+ /**
+ * The rebase multiplier for {@code DepositStatus}. 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, "aclass", assetClass, AssetClass::getValue);
+ putIfNonNull(params, "method", method);
+ putIfNonNull(params, "start", start);
+ putIfNonNull(params, "end", end);
+ putIfNonNull(params, "cursor", cursor);
+ putIfNonNull(params, "limit", limit);
+ putIfNonNull(params, "rebase_multiplier", rebaseMultiplier, RebaseMultiplier::getValue);
+ return params;
+ }
+
+ /**
+ * Builds status filters, accepting either a cursor token or a pagination flag.
+ */
+ public static class DepositStatusParamsBuilder {
+
+ /**
+ * Selects a page using Kraken's opaque cursor token.
+ *
+ * @param cursor the token, or null to omit pagination
+ * @return this builder
+ */
+ public DepositStatusParamsBuilder cursor(String cursor) {
+ this.cursor = cursor;
+ return this;
+ }
+
+ /**
+ * Enables or disables cursor pagination for the first request.
+ *
+ * @param enabled whether Kraken should return a paginated response
+ * @return this builder
+ */
+ public DepositStatusParamsBuilder cursor(boolean enabled) {
+ this.cursor = Boolean.toString(enabled);
+ return this;
+ }
+ }
+}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/DestinationWallet.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/DestinationWallet.java
new file mode 100644
index 0000000..1c2c1e3
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/DestinationWallet.java
@@ -0,0 +1,29 @@
+package dev.andstuff.kraken.api.endpoint.funding.params;
+
+import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+/**
+ * The destination wallet values accepted by {@code WalletTransfer}.
+ */
+public enum DestinationWallet {
+ @JsonProperty("Futures Wallet") FUTURES_WALLET("Futures Wallet"),
+ @JsonEnumDefaultValue UNKNOWN("unknown");
+
+ private final String value;
+
+ DestinationWallet(String value) {
+ this.value = value;
+ }
+
+ /**
+ * Returns the value sent to Kraken.
+ *
+ * @return the API value
+ */
+ @JsonValue
+ public String getValue() {
+ return value;
+ }
+}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/RebaseMultiplier.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/RebaseMultiplier.java
new file mode 100644
index 0000000..37cab8f
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/RebaseMultiplier.java
@@ -0,0 +1,30 @@
+package dev.andstuff.kraken.api.endpoint.funding.params;
+
+import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+/**
+ * The rebase multiplier values accepted by {@code DepositMethods}.
+ */
+public enum RebaseMultiplier {
+ @JsonProperty("rebased") REBASED("rebased"),
+ @JsonProperty("base") BASE("base"),
+ @JsonEnumDefaultValue UNKNOWN("unknown");
+
+ private final String value;
+
+ RebaseMultiplier(String value) {
+ this.value = value;
+ }
+
+ /**
+ * Returns the value sent to Kraken.
+ *
+ * @return the API value
+ */
+ @JsonValue
+ public String getValue() {
+ return value;
+ }
+}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/SourceWallet.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/SourceWallet.java
new file mode 100644
index 0000000..5c6442c
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/SourceWallet.java
@@ -0,0 +1,29 @@
+package dev.andstuff.kraken.api.endpoint.funding.params;
+
+import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+/**
+ * The source wallet values accepted by {@code WalletTransfer}.
+ */
+public enum SourceWallet {
+ @JsonProperty("Spot Wallet") SPOT_WALLET("Spot Wallet"),
+ @JsonEnumDefaultValue UNKNOWN("unknown");
+
+ private final String value;
+
+ SourceWallet(String value) {
+ this.value = value;
+ }
+
+ /**
+ * Returns the value sent to Kraken.
+ *
+ * @return the API value
+ */
+ @JsonValue
+ public String getValue() {
+ return value;
+ }
+}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/WalletTransferParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/WalletTransferParams.java
new file mode 100644
index 0000000..23b8c4f
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/WalletTransferParams.java
@@ -0,0 +1,53 @@
+package dev.andstuff.kraken.api.endpoint.funding.params;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.Map;
+
+import dev.andstuff.kraken.api.endpoint.priv.PostParams;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NonNull;
+
+/**
+ * The parameters of {@code WalletTransfer}; omitted options use Kraken's defaults.
+ */
+@Getter
+@Builder(toBuilder = true)
+public class WalletTransferParams extends PostParams {
+
+ /**
+ * Asset to transfer (asset ID or {@code altname}).
+ */
+ @NonNull
+ private final String asset;
+
+ /**
+ * Source wallet.
+ */
+ @NonNull
+ private final SourceWallet sourceWallet;
+
+ /**
+ * Destination wallet.
+ */
+ @NonNull
+ private final DestinationWallet destinationWallet;
+
+ /**
+ * Amount to transfer.
+ */
+ @NonNull
+ private final BigDecimal amount;
+
+ @Override
+ protected Map params() {
+ Map params = new HashMap<>();
+ putIfNonNull(params, "asset", asset);
+ putIfNonNull(params, "from", sourceWallet, SourceWallet::getValue);
+ putIfNonNull(params, "to", destinationWallet, DestinationWallet::getValue);
+ putIfNonNull(params, "amount", amount, BigDecimal::toPlainString);
+ return params;
+ }
+}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/WithdrawParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/WithdrawParams.java
new file mode 100644
index 0000000..c4577f6
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/WithdrawParams.java
@@ -0,0 +1,70 @@
+package dev.andstuff.kraken.api.endpoint.funding.params;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.Map;
+
+import dev.andstuff.kraken.api.endpoint.priv.PostParams;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NonNull;
+
+/**
+ * The parameters of {@code Withdraw}; omitted options use Kraken's defaults.
+ */
+@Getter
+@Builder(toBuilder = true)
+public class WithdrawParams extends PostParams {
+
+ /**
+ * Asset being withdrawn.
+ */
+ @NonNull
+ private final String asset;
+
+ /**
+ * Specify the asset class of the asset being withdrawn.
+ */
+ private final AssetClass assetClass;
+
+ /**
+ * The withdrawal key name configured in the Kraken account.
+ */
+ @NonNull
+ private final String key;
+
+ /**
+ * The optional address confirmation; Kraken rejects the withdrawal if it does not match the configured key.
+ */
+ private final String address;
+
+ /**
+ * Amount to be withdrawn.
+ */
+ @NonNull
+ private final BigDecimal amount;
+
+ /**
+ * The maximum acceptable fee; Kraken rejects the withdrawal when the processed fee exceeds this amount.
+ */
+ private final BigDecimal maxFee;
+
+ /**
+ * The rebase multiplier for {@code Withdraw}. 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, "aclass", assetClass, AssetClass::getValue);
+ putIfNonNull(params, "key", key);
+ putIfNonNull(params, "address", address);
+ putIfNonNull(params, "amount", amount, BigDecimal::toPlainString);
+ putIfNonNull(params, "max_fee", maxFee, BigDecimal::toPlainString);
+ putIfNonNull(params, "rebase_multiplier", rebaseMultiplier, RebaseMultiplier::getValue);
+ return params;
+ }
+}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/WithdrawalAddressesParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/WithdrawalAddressesParams.java
new file mode 100644
index 0000000..16ee351
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/WithdrawalAddressesParams.java
@@ -0,0 +1,53 @@
+package dev.andstuff.kraken.api.endpoint.funding.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 WithdrawAddresses}; omitted options use Kraken's defaults.
+ */
+@Getter
+@Builder(toBuilder = true)
+public class WithdrawalAddressesParams extends PostParams {
+
+ /**
+ * Filter addresses for specific asset.
+ */
+ private final String asset;
+
+ /**
+ * Filter addresses for specific asset class.
+ */
+ private final AssetClass assetClass;
+
+ /**
+ * Filter addresses for specific method.
+ */
+ private final String method;
+
+ /**
+ * The withdrawal key name configured in the Kraken account.
+ */
+ private final String key;
+
+ /**
+ * Filter by verification status of the withdrawal address. Withdrawal addresses successfully completing email confirmation will have a verification status of true.
+ */
+ private final Boolean verified;
+
+ @Override
+ protected Map params() {
+ Map params = new HashMap<>();
+ putIfNonNull(params, "asset", asset);
+ putIfNonNull(params, "aclass", assetClass, AssetClass::getValue);
+ putIfNonNull(params, "method", method);
+ putIfNonNull(params, "key", key);
+ putIfNonNull(params, "verified", verified);
+ return params;
+ }
+}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/WithdrawalInfoParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/WithdrawalInfoParams.java
new file mode 100644
index 0000000..33a4493
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/WithdrawalInfoParams.java
@@ -0,0 +1,46 @@
+package dev.andstuff.kraken.api.endpoint.funding.params;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.Map;
+
+import dev.andstuff.kraken.api.endpoint.priv.PostParams;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NonNull;
+
+/**
+ * The parameters of {@code WithdrawInfo}; omitted options use Kraken's defaults.
+ */
+@Getter
+@Builder(toBuilder = true)
+public class WithdrawalInfoParams extends PostParams {
+
+ /**
+ * Asset being withdrawn.
+ */
+ @NonNull
+ private final String asset;
+
+ /**
+ * The withdrawal key name configured in the Kraken account.
+ */
+ @NonNull
+ private final String key;
+
+ /**
+ * Amount to be withdrawn.
+ */
+ @NonNull
+ private final BigDecimal amount;
+
+ @Override
+ protected Map params() {
+ Map params = new HashMap<>();
+ putIfNonNull(params, "asset", asset);
+ putIfNonNull(params, "key", key);
+ putIfNonNull(params, "amount", amount, BigDecimal::toPlainString);
+ return params;
+ }
+}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/WithdrawalMethodsParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/WithdrawalMethodsParams.java
new file mode 100644
index 0000000..f1cff34
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/WithdrawalMethodsParams.java
@@ -0,0 +1,47 @@
+package dev.andstuff.kraken.api.endpoint.funding.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 WithdrawMethods}; omitted options use Kraken's defaults.
+ */
+@Getter
+@Builder(toBuilder = true)
+public class WithdrawalMethodsParams extends PostParams {
+
+ /**
+ * Filter methods for specific asset.
+ */
+ private final String asset;
+
+ /**
+ * Filter methods for specific asset class.
+ */
+ private final AssetClass assetClass;
+
+ /**
+ * Filter methods for specific network.
+ */
+ private final String network;
+
+ /**
+ * The rebase multiplier for {@code WithdrawMethods}. 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, "aclass", assetClass, AssetClass::getValue);
+ putIfNonNull(params, "network", network);
+ putIfNonNull(params, "rebase_multiplier", rebaseMultiplier, RebaseMultiplier::getValue);
+ return params;
+ }
+}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/WithdrawalStatusParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/WithdrawalStatusParams.java
new file mode 100644
index 0000000..6af0857
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/params/WithdrawalStatusParams.java
@@ -0,0 +1,99 @@
+package dev.andstuff.kraken.api.endpoint.funding.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 WithdrawStatus}; omitted options use Kraken's defaults.
+ */
+@Getter
+@Builder(toBuilder = true)
+public class WithdrawalStatusParams extends PostParams {
+
+ /**
+ * Filter for specific asset being withdrawn.
+ */
+ private final String asset;
+
+ /**
+ * Filter for specific asset class being withdrawn.
+ */
+ private final AssetClass assetClass;
+
+ /**
+ * Filter for specific name of withdrawal method.
+ */
+ private final String method;
+
+ /**
+ * The earliest request timestamp in Unix seconds, inclusive.
+ */
+ private final String start;
+
+ /**
+ * The latest request timestamp in Unix seconds, inclusive.
+ */
+ private final String end;
+
+ /**
+ * The next-page cursor, or {@code true}/{@code false} to enable or disable pagination.
+ */
+ private final String cursor;
+
+ /**
+ * Number of results to include per page.
+ */
+ private final Integer limit;
+
+ /**
+ * The rebase multiplier for {@code WithdrawStatus}. 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, "aclass", assetClass, AssetClass::getValue);
+ putIfNonNull(params, "method", method);
+ putIfNonNull(params, "start", start);
+ putIfNonNull(params, "end", end);
+ putIfNonNull(params, "cursor", cursor);
+ putIfNonNull(params, "limit", limit);
+ putIfNonNull(params, "rebase_multiplier", rebaseMultiplier, RebaseMultiplier::getValue);
+ return params;
+ }
+
+ /**
+ * Builds status filters, accepting either a cursor token or a pagination flag.
+ */
+ public static class WithdrawalStatusParamsBuilder {
+
+ /**
+ * Selects a page using Kraken's opaque cursor token.
+ *
+ * @param cursor the token, or null to omit pagination
+ * @return this builder
+ */
+ public WithdrawalStatusParamsBuilder cursor(String cursor) {
+ this.cursor = cursor;
+ return this;
+ }
+
+ /**
+ * Enables or disables cursor pagination for the first request.
+ *
+ * @param enabled whether Kraken should return a paginated response
+ * @return this builder
+ */
+ public WithdrawalStatusParamsBuilder cursor(boolean enabled) {
+ this.cursor = Boolean.toString(enabled);
+ return this;
+ }
+ }
+}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/Deposit.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/Deposit.java
new file mode 100644
index 0000000..09f6778
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/Deposit.java
@@ -0,0 +1,59 @@
+package dev.andstuff.kraken.api.endpoint.funding.response;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * The deposit returned by {@code DepositStatus}.
+ *
+ * @param method the method
+ * @param assetClass the asset class
+ * @param asset the asset
+ * @param referenceId the reference id
+ * @param transactionId the transaction id
+ * @param info the info
+ * @param amount the amount
+ * @param fee the fee
+ * @param time the time as Unix seconds
+ * @param status the status
+ * @param statusProp the status prop
+ * @param originators the sending transaction IDs for swept deposits
+ */
+public record Deposit(String method,
+ @JsonProperty("aclass") String assetClass,
+ String asset,
+ @JsonProperty("refid") String referenceId,
+ @JsonProperty("txid") String transactionId,
+ String info,
+ BigDecimal amount,
+ BigDecimal fee,
+ Long time,
+ Status status,
+ @JsonProperty("status-prop") StatusProp statusProp,
+ List originators) {
+
+ /**
+ * The status values used by the {@code DepositStatus} endpoint.
+ */
+ public enum Status {
+ @JsonProperty("Initial") INITIAL,
+ @JsonProperty("Pending") PENDING,
+ @JsonProperty("EarlyConfirmed") EARLY_CONFIRMED,
+ @JsonProperty("Settled") SETTLED,
+ @JsonProperty("Success") SUCCESS,
+ @JsonProperty("Failure") FAILURE,
+ @JsonEnumDefaultValue UNKNOWN
+ }
+
+ /**
+ * The status prop values used by the {@code DepositStatus} endpoint.
+ */
+ public enum StatusProp {
+ @JsonProperty("return") RETURN,
+ @JsonProperty("onhold") ONHOLD,
+ @JsonEnumDefaultValue UNKNOWN
+ }
+}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/DepositAddress.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/DepositAddress.java
new file mode 100644
index 0000000..fdb8ad0
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/DepositAddress.java
@@ -0,0 +1,18 @@
+package dev.andstuff.kraken.api.endpoint.funding.response;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * The deposit address returned by {@code DepositAddresses}.
+ *
+ * @param address the address
+ * @param expireTime the expiration time as Unix seconds, or "0" for a non-expiring address
+ * @param unused whether the address has never been used; null when omitted
+ * @param tag the destination tag
+ * @param memo the destination memo
+ */
+public record DepositAddress(String address,
+ @JsonProperty("expiretm") String expireTime,
+ @JsonProperty("new") Boolean unused,
+ String tag,
+ String memo) {}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/DepositLimit.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/DepositLimit.java
new file mode 100644
index 0000000..a454b45
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/DepositLimit.java
@@ -0,0 +1,28 @@
+package dev.andstuff.kraken.api.endpoint.funding.response;
+
+import java.math.BigDecimal;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+
+/**
+ * The {@code DepositMethods} limit, which Kraken returns as an amount or {@code false} for no limit.
+ *
+ * @param amount the maximum net amount, or null when unlimited
+ * @param unlimited whether Kraken returned the unlimited marker
+ */
+@JsonDeserialize(using = DepositLimitDeserializer.class)
+public record DepositLimit(BigDecimal amount, boolean unlimited) {
+
+ /**
+ * Creates either a finite limit or an unlimited marker.
+ *
+ * @param amount the maximum net amount, present only for a finite limit
+ * @param unlimited whether the limit is unlimited
+ * @throws IllegalArgumentException if amount and unlimited contradict each other
+ */
+ public DepositLimit {
+ if (unlimited == (amount != null)) {
+ throw new IllegalArgumentException("Finite limits require an amount; unlimited limits must not have one");
+ }
+ }
+}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/DepositLimitDeserializer.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/DepositLimitDeserializer.java
new file mode 100644
index 0000000..beab8c9
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/DepositLimitDeserializer.java
@@ -0,0 +1,35 @@
+package dev.andstuff.kraken.api.endpoint.funding.response;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
+
+final class DepositLimitDeserializer extends StdDeserializer {
+
+ DepositLimitDeserializer() {
+ super(DepositLimit.class);
+ }
+
+ @Override
+ public DepositLimit deserialize(JsonParser parser, DeserializationContext context) throws IOException {
+ if (parser.hasToken(JsonToken.VALUE_FALSE)) {
+ return new DepositLimit(null, true);
+ }
+ if (parser.currentToken().isNumeric()) {
+ return new DepositLimit(parser.getDecimalValue(), false);
+ }
+ if (parser.hasToken(JsonToken.VALUE_STRING)) {
+ try {
+ return new DepositLimit(new BigDecimal(parser.getText()), false);
+ }
+ catch (NumberFormatException e) {
+ return context.reportInputMismatch(DepositLimit.class, "Expected a decimal deposit limit or false");
+ }
+ }
+ return (DepositLimit) context.handleUnexpectedToken(DepositLimit.class, parser);
+ }
+}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/DepositMethod.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/DepositMethod.java
new file mode 100644
index 0000000..3a824cc
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/DepositMethod.java
@@ -0,0 +1,24 @@
+package dev.andstuff.kraken.api.endpoint.funding.response;
+
+import java.math.BigDecimal;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * The deposit method returned by {@code DepositMethods}.
+ *
+ * @param method the method
+ * @param limit the current maximum deposit, an unlimited marker, or null if omitted
+ * @param fee the fee
+ * @param feePercentage the fee percentage
+ * @param addressSetupFee the address setup fee
+ * @param genAddress the gen address
+ * @param minimum the minimum
+ */
+public record DepositMethod(String method,
+ DepositLimit limit,
+ BigDecimal fee,
+ @JsonProperty("fee-percentage") BigDecimal feePercentage,
+ @JsonProperty("address-setup-fee") String addressSetupFee,
+ @JsonProperty("gen-address") Boolean genAddress,
+ BigDecimal minimum) {}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/DepositStatus.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/DepositStatus.java
new file mode 100644
index 0000000..b8c3f41
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/DepositStatus.java
@@ -0,0 +1,16 @@
+package dev.andstuff.kraken.api.endpoint.funding.response;
+
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+
+/**
+ * The transactions returned by {@code DepositStatus} in either paginated or unpaginated form.
+ *
+ * @param deposits the transactions, ordered from newest to oldest
+ * @param nextCursor the next-page token; null or empty when there is no next page
+ */
+@JsonDeserialize(using = DepositStatusDeserializer.class)
+public record DepositStatus(List deposits,
+ @JsonProperty("next_cursor") String nextCursor) {}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/DepositStatusDeserializer.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/DepositStatusDeserializer.java
new file mode 100644
index 0000000..295c212
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/DepositStatusDeserializer.java
@@ -0,0 +1,50 @@
+package dev.andstuff.kraken.api.endpoint.funding.response;
+
+import java.io.IOException;
+import java.util.List;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
+
+final class DepositStatusDeserializer extends StdDeserializer {
+
+ DepositStatusDeserializer() {
+ super(DepositStatus.class);
+ }
+
+ @Override
+ public DepositStatus deserialize(JsonParser parser, DeserializationContext context) throws IOException {
+ JavaType listType = context.getTypeFactory().constructCollectionType(List.class, Deposit.class);
+ if (parser.isExpectedStartArrayToken()) {
+ return new DepositStatus(context.readValue(parser, listType), null);
+ }
+ if (!parser.isExpectedStartObjectToken()) {
+ return (DepositStatus) context.handleUnexpectedToken(DepositStatus.class, parser);
+ }
+ List entries = null;
+ String nextCursor = null;
+ while (parser.nextToken() != JsonToken.END_OBJECT) {
+ String field = parser.currentName();
+ parser.nextToken();
+ switch (field) {
+ case "deposits", "deposit" -> {
+ if (parser.isExpectedStartObjectToken()) {
+ entries = List.of(context.readValue(parser, Deposit.class));
+ }
+ else {
+ entries = context.readValue(parser, listType);
+ }
+ }
+ case "next_cursor" -> nextCursor = context.readValue(parser, String.class);
+ default -> parser.skipChildren();
+ }
+ }
+ if (entries == null) {
+ return context.reportInputMismatch(DepositStatus.class, "Expected a deposits list in the paginated response");
+ }
+ return new DepositStatus(entries, nextCursor);
+ }
+}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/FundingReference.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/FundingReference.java
new file mode 100644
index 0000000..d7cd0d0
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/FundingReference.java
@@ -0,0 +1,10 @@
+package dev.andstuff.kraken.api.endpoint.funding.response;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * The funding reference returned by {@code Withdraw and WalletTransfer}.
+ *
+ * @param referenceId the reference id
+ */
+public record FundingReference(@JsonProperty("refid") String referenceId) {}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/Withdrawal.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/Withdrawal.java
new file mode 100644
index 0000000..e27b64e
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/Withdrawal.java
@@ -0,0 +1,62 @@
+package dev.andstuff.kraken.api.endpoint.funding.response;
+
+import java.math.BigDecimal;
+
+import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * The withdrawal returned by {@code WithdrawStatus}.
+ *
+ * @param method the method
+ * @param network the network
+ * @param assetClass the asset class
+ * @param asset the asset
+ * @param referenceId the reference id
+ * @param transactionId the transaction id
+ * @param info the info
+ * @param amount the amount
+ * @param fee the fee
+ * @param time the time as Unix seconds
+ * @param status the status
+ * @param statusProp the status prop
+ * @param key the key
+ */
+public record Withdrawal(String method,
+ String network,
+ @JsonProperty("aclass") String assetClass,
+ String asset,
+ @JsonProperty("refid") String referenceId,
+ @JsonProperty("txid") String transactionId,
+ String info,
+ BigDecimal amount,
+ BigDecimal fee,
+ Long time,
+ Status status,
+ @JsonProperty("status-prop") StatusProp statusProp,
+ String key) {
+
+ /**
+ * The status values used by the {@code WithdrawStatus} endpoint.
+ */
+ public enum Status {
+ @JsonProperty("Initial") INITIAL,
+ @JsonProperty("Pending") PENDING,
+ @JsonProperty("Settled") SETTLED,
+ @JsonProperty("Success") SUCCESS,
+ @JsonProperty("Failure") FAILURE,
+ @JsonEnumDefaultValue UNKNOWN
+ }
+
+ /**
+ * The status prop values used by the {@code WithdrawStatus} endpoint.
+ */
+ public enum StatusProp {
+ @JsonProperty("cancel-pending") CANCEL_PENDING,
+ @JsonProperty("canceled") CANCELED,
+ @JsonProperty("cancel-denied") CANCEL_DENIED,
+ @JsonProperty("return") RETURN,
+ @JsonProperty("onhold") ONHOLD,
+ @JsonEnumDefaultValue UNKNOWN
+ }
+}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/WithdrawalAddress.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/WithdrawalAddress.java
new file mode 100644
index 0000000..d865c8e
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/WithdrawalAddress.java
@@ -0,0 +1,22 @@
+package dev.andstuff.kraken.api.endpoint.funding.response;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * The withdrawal address returned by {@code WithdrawAddresses}.
+ *
+ * @param address the address
+ * @param asset the asset
+ * @param method the method
+ * @param key the key
+ * @param tag the destination tag
+ * @param memo the destination memo
+ * @param verified the verified
+ */
+public record WithdrawalAddress(String address,
+ String asset,
+ String method,
+ String key,
+ String tag,
+ String memo,
+ Boolean verified) {}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/WithdrawalInfo.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/WithdrawalInfo.java
new file mode 100644
index 0000000..4dbd4a2
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/WithdrawalInfo.java
@@ -0,0 +1,18 @@
+package dev.andstuff.kraken.api.endpoint.funding.response;
+
+import java.math.BigDecimal;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * The withdrawal info returned by {@code WithdrawInfo}.
+ *
+ * @param method the method
+ * @param limit the limit
+ * @param amount the amount
+ * @param fee the fee
+ */
+public record WithdrawalInfo(String method,
+ BigDecimal limit,
+ BigDecimal amount,
+ BigDecimal fee) {}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/WithdrawalMethod.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/WithdrawalMethod.java
new file mode 100644
index 0000000..e62f4b0
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/WithdrawalMethod.java
@@ -0,0 +1,62 @@
+package dev.andstuff.kraken.api.endpoint.funding.response;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * The withdrawal method returned by {@code WithdrawMethods}.
+ *
+ * @param asset the asset
+ * @param method the method
+ * @param methodId the method id
+ * @param network the network
+ * @param networkId the network id
+ * @param minimum the minimum
+ * @param fee the fee
+ * @param limits the limits
+ */
+public record WithdrawalMethod(String asset,
+ String method,
+ @JsonProperty("method_id") String methodId,
+ String network,
+ @JsonProperty("network_id") String networkId,
+ BigDecimal minimum,
+ Fee fee,
+ List limits) {
+
+ /**
+ * The fee returned by {@code WithdrawMethods}.
+ *
+ * @param assetClass the asset class
+ * @param asset the asset
+ * @param fee the fee
+ * @param feePercentage the fee percentage
+ */
+ public record Fee(@JsonProperty("aclass") String assetClass,
+ String asset,
+ BigDecimal fee,
+ @JsonProperty("fee_percentage") BigDecimal feePercentage) {}
+
+ /**
+ * The limits returned by {@code WithdrawMethods}.
+ *
+ * @param description the description
+ * @param limitType the limit type
+ * @param limits the limits
+ */
+ public record Limits(String description,
+ @JsonProperty("limit_type") String limitType,
+ Map limits) {}
+
+ /**
+ * Amounts within one {@code WithdrawMethods} limit window.
+ *
+ * @param maximum the maximum amount during the window
+ * @param remaining the remaining amount during the window
+ * @param used the amount already used during the window
+ */
+ public record LimitWindow(BigDecimal maximum, BigDecimal remaining, BigDecimal used) {}
+}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/WithdrawalStatus.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/WithdrawalStatus.java
new file mode 100644
index 0000000..3ed3b0d
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/WithdrawalStatus.java
@@ -0,0 +1,16 @@
+package dev.andstuff.kraken.api.endpoint.funding.response;
+
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+
+/**
+ * The transactions returned by {@code WithdrawStatus} in either paginated or unpaginated form.
+ *
+ * @param withdrawals the transactions, ordered from newest to oldest
+ * @param nextCursor the next-page token; null or empty when there is no next page
+ */
+@JsonDeserialize(using = WithdrawalStatusDeserializer.class)
+public record WithdrawalStatus(List withdrawals,
+ @JsonProperty("next_cursor") String nextCursor) {}
diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/WithdrawalStatusDeserializer.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/WithdrawalStatusDeserializer.java
new file mode 100644
index 0000000..5f22e84
--- /dev/null
+++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/funding/response/WithdrawalStatusDeserializer.java
@@ -0,0 +1,50 @@
+package dev.andstuff.kraken.api.endpoint.funding.response;
+
+import java.io.IOException;
+import java.util.List;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
+
+final class WithdrawalStatusDeserializer extends StdDeserializer {
+
+ WithdrawalStatusDeserializer() {
+ super(WithdrawalStatus.class);
+ }
+
+ @Override
+ public WithdrawalStatus deserialize(JsonParser parser, DeserializationContext context) throws IOException {
+ JavaType listType = context.getTypeFactory().constructCollectionType(List.class, Withdrawal.class);
+ if (parser.isExpectedStartArrayToken()) {
+ return new WithdrawalStatus(context.readValue(parser, listType), null);
+ }
+ if (!parser.isExpectedStartObjectToken()) {
+ return (WithdrawalStatus) context.handleUnexpectedToken(WithdrawalStatus.class, parser);
+ }
+ List entries = null;
+ String nextCursor = null;
+ while (parser.nextToken() != JsonToken.END_OBJECT) {
+ String field = parser.currentName();
+ parser.nextToken();
+ switch (field) {
+ case "withdrawals", "withdrawal" -> {
+ if (parser.isExpectedStartObjectToken()) {
+ entries = List.of(context.readValue(parser, Withdrawal.class));
+ }
+ else {
+ entries = context.readValue(parser, listType);
+ }
+ }
+ case "next_cursor" -> nextCursor = context.readValue(parser, String.class);
+ default -> parser.skipChildren();
+ }
+ }
+ if (entries == null) {
+ return context.reportInputMismatch(WithdrawalStatus.class, "Expected a withdrawals list in the paginated response");
+ }
+ return new WithdrawalStatus(entries, nextCursor);
+ }
+}
diff --git a/library/src/test/java/dev/andstuff/kraken/api/FundingKrakenAPITest.java b/library/src/test/java/dev/andstuff/kraken/api/FundingKrakenAPITest.java
new file mode 100644
index 0000000..56b9e22
--- /dev/null
+++ b/library/src/test/java/dev/andstuff/kraken/api/FundingKrakenAPITest.java
@@ -0,0 +1,317 @@
+package dev.andstuff.kraken.api;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+
+import dev.andstuff.kraken.api.endpoint.funding.CancelWithdrawalEndpoint;
+import dev.andstuff.kraken.api.endpoint.funding.DepositAddressesEndpoint;
+import dev.andstuff.kraken.api.endpoint.funding.DepositMethodsEndpoint;
+import dev.andstuff.kraken.api.endpoint.funding.DepositStatusEndpoint;
+import dev.andstuff.kraken.api.endpoint.funding.WalletTransferEndpoint;
+import dev.andstuff.kraken.api.endpoint.funding.WithdrawEndpoint;
+import dev.andstuff.kraken.api.endpoint.funding.WithdrawalAddressesEndpoint;
+import dev.andstuff.kraken.api.endpoint.funding.WithdrawalInfoEndpoint;
+import dev.andstuff.kraken.api.endpoint.funding.WithdrawalMethodsEndpoint;
+import dev.andstuff.kraken.api.endpoint.funding.WithdrawalStatusEndpoint;
+import dev.andstuff.kraken.api.endpoint.funding.params.CancelWithdrawalParams;
+import dev.andstuff.kraken.api.endpoint.funding.params.DepositAddressesParams;
+import dev.andstuff.kraken.api.endpoint.funding.params.DepositMethodsParams;
+import dev.andstuff.kraken.api.endpoint.funding.params.DepositStatusParams;
+import dev.andstuff.kraken.api.endpoint.funding.params.DestinationWallet;
+import dev.andstuff.kraken.api.endpoint.funding.params.SourceWallet;
+import dev.andstuff.kraken.api.endpoint.funding.params.WalletTransferParams;
+import dev.andstuff.kraken.api.endpoint.funding.params.WithdrawParams;
+import dev.andstuff.kraken.api.endpoint.funding.params.WithdrawalAddressesParams;
+import dev.andstuff.kraken.api.endpoint.funding.params.WithdrawalInfoParams;
+import dev.andstuff.kraken.api.endpoint.funding.params.WithdrawalMethodsParams;
+import dev.andstuff.kraken.api.endpoint.funding.params.WithdrawalStatusParams;
+import dev.andstuff.kraken.api.endpoint.funding.response.DepositAddress;
+import dev.andstuff.kraken.api.endpoint.funding.response.DepositMethod;
+import dev.andstuff.kraken.api.endpoint.funding.response.DepositStatus;
+import dev.andstuff.kraken.api.endpoint.funding.response.FundingReference;
+import dev.andstuff.kraken.api.endpoint.funding.response.WithdrawalAddress;
+import dev.andstuff.kraken.api.endpoint.funding.response.WithdrawalInfo;
+import dev.andstuff.kraken.api.endpoint.funding.response.WithdrawalMethod;
+import dev.andstuff.kraken.api.endpoint.funding.response.WithdrawalStatus;
+import dev.andstuff.kraken.api.rest.KrakenCredentials;
+import dev.andstuff.kraken.api.rest.KrakenNonceGenerator;
+import dev.andstuff.kraken.api.rest.KrakenRestRequester;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+class FundingKrakenAPITest {
+
+ @Mock private KrakenCredentials credentials;
+ @Mock private KrakenNonceGenerator nonceGenerator;
+ @Mock private KrakenRestRequester requester;
+ @Mock private List depositMethodsResponse;
+ @Mock private List depositAddressesResponse;
+ @Mock private DepositStatus depositStatusResponse;
+ @Mock private List withdrawalMethodsResponse;
+ @Mock private List withdrawalAddressesResponse;
+ @Mock private WithdrawalInfo withdrawalInfoResponse;
+ @Mock private FundingReference withdrawResponse;
+ @Mock private WithdrawalStatus withdrawalStatusResponse;
+ @Mock private FundingReference walletTransferResponse;
+
+ @Test
+ void should_route_depositMethods_options_when_called() {
+ KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester);
+ DepositMethodsParams params = DepositMethodsParams.builder().asset("id +/&=").build();
+ when(requester.execute(any(DepositMethodsEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(depositMethodsResponse);
+
+ List result = unit.depositMethods(params);
+
+ assertThat(result).isSameAs(depositMethodsResponse);
+ verify(requester).execute(argThat((DepositMethodsEndpoint endpoint) -> endpoint.getPostParams() == params), same(credentials), same(nonceGenerator));
+ }
+
+ @Test
+ void should_reject_depositMethods_when_credentials_are_missing() {
+ KrakenAPI unit = new KrakenAPI(null, nonceGenerator, requester);
+ DepositMethodsParams params = DepositMethodsParams.builder().asset("id +/&=").build();
+
+ assertThatThrownBy(() -> unit.depositMethods(params)).isInstanceOf(IllegalStateException.class);
+ verifyNoInteractions(requester);
+ }
+
+ @Test
+ void should_route_depositAddresses_options_when_called() {
+ KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester);
+ DepositAddressesParams params = DepositAddressesParams.builder().asset("id +/&=").method("id +/&=").build();
+ when(requester.execute(any(DepositAddressesEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(depositAddressesResponse);
+
+ List result = unit.depositAddresses(params);
+
+ assertThat(result).isSameAs(depositAddressesResponse);
+ verify(requester).execute(argThat((DepositAddressesEndpoint endpoint) -> endpoint.getPostParams() == params), same(credentials), same(nonceGenerator));
+ }
+
+ @Test
+ void should_reject_depositAddresses_when_credentials_are_missing() {
+ KrakenAPI unit = new KrakenAPI(null, nonceGenerator, requester);
+ DepositAddressesParams params = DepositAddressesParams.builder().asset("id +/&=").method("id +/&=").build();
+
+ assertThatThrownBy(() -> unit.depositAddresses(params)).isInstanceOf(IllegalStateException.class);
+ verifyNoInteractions(requester);
+ }
+
+ @Test
+ void should_route_depositStatus_options_when_called() {
+ KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester);
+ DepositStatusParams params = DepositStatusParams.builder().build();
+ when(requester.execute(any(DepositStatusEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(depositStatusResponse);
+
+ DepositStatus result = unit.depositStatus(params);
+
+ assertThat(result).isSameAs(depositStatusResponse);
+ verify(requester).execute(argThat((DepositStatusEndpoint endpoint) -> endpoint.getPostParams() == params), same(credentials), same(nonceGenerator));
+ }
+
+ @Test
+ void should_route_depositStatus_defaults_when_called() {
+ KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester);
+ when(requester.execute(any(DepositStatusEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(depositStatusResponse);
+
+ DepositStatus result = unit.depositStatus();
+
+ assertThat(result).isSameAs(depositStatusResponse);
+ verify(requester).execute(any(DepositStatusEndpoint.class), same(credentials), same(nonceGenerator));
+ }
+
+ @Test
+ void should_reject_depositStatus_when_credentials_are_missing() {
+ KrakenAPI unit = new KrakenAPI(null, nonceGenerator, requester);
+
+ assertThatThrownBy(() -> unit.depositStatus()).isInstanceOf(IllegalStateException.class);
+ verifyNoInteractions(requester);
+ }
+
+ @Test
+ void should_route_withdrawalMethods_options_when_called() {
+ KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester);
+ WithdrawalMethodsParams params = WithdrawalMethodsParams.builder().build();
+ when(requester.execute(any(WithdrawalMethodsEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(withdrawalMethodsResponse);
+
+ List result = unit.withdrawalMethods(params);
+
+ assertThat(result).isSameAs(withdrawalMethodsResponse);
+ verify(requester).execute(argThat((WithdrawalMethodsEndpoint endpoint) -> endpoint.getPostParams() == params), same(credentials), same(nonceGenerator));
+ }
+
+ @Test
+ void should_route_withdrawalMethods_defaults_when_called() {
+ KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester);
+ when(requester.execute(any(WithdrawalMethodsEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(withdrawalMethodsResponse);
+
+ List result = unit.withdrawalMethods();
+
+ assertThat(result).isSameAs(withdrawalMethodsResponse);
+ verify(requester).execute(any(WithdrawalMethodsEndpoint.class), same(credentials), same(nonceGenerator));
+ }
+
+ @Test
+ void should_reject_withdrawalMethods_when_credentials_are_missing() {
+ KrakenAPI unit = new KrakenAPI(null, nonceGenerator, requester);
+
+ assertThatThrownBy(() -> unit.withdrawalMethods()).isInstanceOf(IllegalStateException.class);
+ verifyNoInteractions(requester);
+ }
+
+ @Test
+ void should_route_withdrawalAddresses_options_when_called() {
+ KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester);
+ WithdrawalAddressesParams params = WithdrawalAddressesParams.builder().build();
+ when(requester.execute(any(WithdrawalAddressesEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(withdrawalAddressesResponse);
+
+ List result = unit.withdrawalAddresses(params);
+
+ assertThat(result).isSameAs(withdrawalAddressesResponse);
+ verify(requester).execute(argThat((WithdrawalAddressesEndpoint endpoint) -> endpoint.getPostParams() == params), same(credentials), same(nonceGenerator));
+ }
+
+ @Test
+ void should_route_withdrawalAddresses_defaults_when_called() {
+ KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester);
+ when(requester.execute(any(WithdrawalAddressesEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(withdrawalAddressesResponse);
+
+ List result = unit.withdrawalAddresses();
+
+ assertThat(result).isSameAs(withdrawalAddressesResponse);
+ verify(requester).execute(any(WithdrawalAddressesEndpoint.class), same(credentials), same(nonceGenerator));
+ }
+
+ @Test
+ void should_reject_withdrawalAddresses_when_credentials_are_missing() {
+ KrakenAPI unit = new KrakenAPI(null, nonceGenerator, requester);
+
+ assertThatThrownBy(() -> unit.withdrawalAddresses()).isInstanceOf(IllegalStateException.class);
+ verifyNoInteractions(requester);
+ }
+
+ @Test
+ void should_route_withdrawalInfo_options_when_called() {
+ KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester);
+ WithdrawalInfoParams params = WithdrawalInfoParams.builder().asset("id +/&=").key("id +/&=").amount(new BigDecimal("0.0000000012300")).build();
+ when(requester.execute(any(WithdrawalInfoEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(withdrawalInfoResponse);
+
+ WithdrawalInfo result = unit.withdrawalInfo(params);
+
+ assertThat(result).isSameAs(withdrawalInfoResponse);
+ verify(requester).execute(argThat((WithdrawalInfoEndpoint endpoint) -> endpoint.getPostParams() == params), same(credentials), same(nonceGenerator));
+ }
+
+ @Test
+ void should_reject_withdrawalInfo_when_credentials_are_missing() {
+ KrakenAPI unit = new KrakenAPI(null, nonceGenerator, requester);
+ WithdrawalInfoParams params = WithdrawalInfoParams.builder().asset("id +/&=").key("id +/&=").amount(new BigDecimal("0.0000000012300")).build();
+
+ assertThatThrownBy(() -> unit.withdrawalInfo(params)).isInstanceOf(IllegalStateException.class);
+ verifyNoInteractions(requester);
+ }
+
+ @Test
+ void should_route_withdraw_options_when_called() {
+ KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester);
+ WithdrawParams params = WithdrawParams.builder().asset("id +/&=").key("id +/&=").amount(new BigDecimal("0.0000000012300")).build();
+ when(requester.execute(any(WithdrawEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(withdrawResponse);
+
+ FundingReference result = unit.withdraw(params);
+
+ assertThat(result).isSameAs(withdrawResponse);
+ verify(requester).execute(argThat((WithdrawEndpoint endpoint) -> endpoint.getPostParams() == params), same(credentials), same(nonceGenerator));
+ }
+
+ @Test
+ void should_reject_withdraw_when_credentials_are_missing() {
+ KrakenAPI unit = new KrakenAPI(null, nonceGenerator, requester);
+ WithdrawParams params = WithdrawParams.builder().asset("id +/&=").key("id +/&=").amount(new BigDecimal("0.0000000012300")).build();
+
+ assertThatThrownBy(() -> unit.withdraw(params)).isInstanceOf(IllegalStateException.class);
+ verifyNoInteractions(requester);
+ }
+
+ @Test
+ void should_route_withdrawalStatus_options_when_called() {
+ KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester);
+ WithdrawalStatusParams params = WithdrawalStatusParams.builder().build();
+ when(requester.execute(any(WithdrawalStatusEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(withdrawalStatusResponse);
+
+ WithdrawalStatus result = unit.withdrawalStatus(params);
+
+ assertThat(result).isSameAs(withdrawalStatusResponse);
+ verify(requester).execute(argThat((WithdrawalStatusEndpoint endpoint) -> endpoint.getPostParams() == params), same(credentials), same(nonceGenerator));
+ }
+
+ @Test
+ void should_route_withdrawalStatus_defaults_when_called() {
+ KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester);
+ when(requester.execute(any(WithdrawalStatusEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(withdrawalStatusResponse);
+
+ WithdrawalStatus result = unit.withdrawalStatus();
+
+ assertThat(result).isSameAs(withdrawalStatusResponse);
+ verify(requester).execute(any(WithdrawalStatusEndpoint.class), same(credentials), same(nonceGenerator));
+ }
+
+ @Test
+ void should_reject_withdrawalStatus_when_credentials_are_missing() {
+ KrakenAPI unit = new KrakenAPI(null, nonceGenerator, requester);
+
+ assertThatThrownBy(() -> unit.withdrawalStatus()).isInstanceOf(IllegalStateException.class);
+ verifyNoInteractions(requester);
+ }
+
+ @Test
+ void should_route_cancelWithdrawal_options_when_called() {
+ KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester);
+ CancelWithdrawalParams params = CancelWithdrawalParams.builder().asset("id +/&=").referenceId("id +/&=").build();
+ when(requester.execute(any(CancelWithdrawalEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(Boolean.FALSE);
+
+ Boolean result = unit.cancelWithdrawal(params);
+
+ assertThat(result).isSameAs(Boolean.FALSE);
+ verify(requester).execute(argThat((CancelWithdrawalEndpoint endpoint) -> endpoint.getPostParams() == params), same(credentials), same(nonceGenerator));
+ }
+
+ @Test
+ void should_reject_cancelWithdrawal_when_credentials_are_missing() {
+ KrakenAPI unit = new KrakenAPI(null, nonceGenerator, requester);
+ CancelWithdrawalParams params = CancelWithdrawalParams.builder().asset("id +/&=").referenceId("id +/&=").build();
+
+ assertThatThrownBy(() -> unit.cancelWithdrawal(params)).isInstanceOf(IllegalStateException.class);
+ verifyNoInteractions(requester);
+ }
+
+ @Test
+ void should_route_walletTransfer_options_when_called() {
+ KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester);
+ WalletTransferParams params = WalletTransferParams.builder().asset("id +/&=").sourceWallet(SourceWallet.SPOT_WALLET).destinationWallet(DestinationWallet.FUTURES_WALLET).amount(new BigDecimal("0.0000000012300")).build();
+ when(requester.execute(any(WalletTransferEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(walletTransferResponse);
+
+ FundingReference result = unit.walletTransfer(params);
+
+ assertThat(result).isSameAs(walletTransferResponse);
+ verify(requester).execute(argThat((WalletTransferEndpoint endpoint) -> endpoint.getPostParams() == params), same(credentials), same(nonceGenerator));
+ }
+
+ @Test
+ void should_reject_walletTransfer_when_credentials_are_missing() {
+ KrakenAPI unit = new KrakenAPI(null, nonceGenerator, requester);
+ WalletTransferParams params = WalletTransferParams.builder().asset("id +/&=").sourceWallet(SourceWallet.SPOT_WALLET).destinationWallet(DestinationWallet.FUTURES_WALLET).amount(new BigDecimal("0.0000000012300")).build();
+
+ assertThatThrownBy(() -> unit.walletTransfer(params)).isInstanceOf(IllegalStateException.class);
+ verifyNoInteractions(requester);
+ }
+
+}
diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/funding/CancelWithdrawalEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/funding/CancelWithdrawalEndpointTest.java
new file mode 100644
index 0000000..a07790e
--- /dev/null
+++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/funding/CancelWithdrawalEndpointTest.java
@@ -0,0 +1,84 @@
+package dev.andstuff.kraken.api.endpoint.funding;
+
+import java.math.BigDecimal;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.MapperFeature;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+
+import dev.andstuff.kraken.api.endpoint.KrakenException;
+import dev.andstuff.kraken.api.endpoint.KrakenResponse;
+import dev.andstuff.kraken.api.endpoint.funding.params.*;
+import dev.andstuff.kraken.api.endpoint.funding.response.*;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@ExtendWith(MockitoExtension.class)
+class CancelWithdrawalEndpointTest {
+
+ @Test
+ void should_encode_all_options_when_supplied() {
+ CancelWithdrawalEndpoint unit = new CancelWithdrawalEndpoint(CancelWithdrawalParams.builder().asset("id +/&=").referenceId("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("asset", "id +/&="),
+ Map.entry("refid", "id +/&=")));
+ assertThat(unit.buildURL().getPath()).isEqualTo("/0/private/WithdrawCancel");
+ assertThat(unit.getHttpMethod()).isEqualTo("POST");
+ }
+
+ @Test
+ void should_reject_missing_asset_when_building_parameters() {
+ assertThatThrownBy(() -> CancelWithdrawalParams.builder().referenceId("id +/&=").build()).isInstanceOf(NullPointerException.class).hasMessageContaining("asset");
+ }
+
+ @Test
+ void should_reject_missing_referenceId_when_building_parameters() {
+ assertThatThrownBy(() -> CancelWithdrawalParams.builder().asset("id +/&=").build()).isInstanceOf(NullPointerException.class).hasMessageContaining("referenceId");
+ }
+
+ @Test
+ void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exception {
+ CancelWithdrawalEndpoint unit = new CancelWithdrawalEndpoint(CancelWithdrawalParams.builder().asset("id +/&=").referenceId("id +/&=").build());
+ JsonMapper mapper = JsonMapper.builder()
+ .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
+ .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)
+ .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
+ .addModule(new Jdk8Module()).build();
+ String json = Files.readString(Path.of("src/test/resources/funding/WithdrawCancel.json"));
+
+ KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory()));
+ Boolean result = response.result().orElseThrow();
+
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ void should_preserve_false_when_cancellation_did_not_succeed() throws Exception {
+ CancelWithdrawalEndpoint unit = new CancelWithdrawalEndpoint("XBT", "reference");
+ JsonMapper mapper = JsonMapper.builder().addModule(new Jdk8Module()).build();
+
+ KrakenResponse response = mapper.readValue("{\"error\":[],\"result\":false}", unit.wrappedResponseType(mapper.getTypeFactory()));
+
+ assertThat(response.result()).contains(false);
+ }
+}
diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/funding/DepositAddressesEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/funding/DepositAddressesEndpointTest.java
new file mode 100644
index 0000000..2f4aa1f
--- /dev/null
+++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/funding/DepositAddressesEndpointTest.java
@@ -0,0 +1,82 @@
+package dev.andstuff.kraken.api.endpoint.funding;
+
+import java.math.BigDecimal;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.MapperFeature;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+
+import dev.andstuff.kraken.api.endpoint.KrakenException;
+import dev.andstuff.kraken.api.endpoint.KrakenResponse;
+import dev.andstuff.kraken.api.endpoint.funding.params.*;
+import dev.andstuff.kraken.api.endpoint.funding.response.*;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@ExtendWith(MockitoExtension.class)
+class DepositAddressesEndpointTest {
+
+ @Test
+ void should_encode_all_options_when_supplied() {
+ DepositAddressesEndpoint unit = new DepositAddressesEndpoint(DepositAddressesParams.builder().asset("id +/&=").assetClass(AssetClass.TOKENIZED_ASSET).method("id +/&=").generateNew(false).amount(new BigDecimal("0.0000000012300")).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("aclass", "tokenized_asset"),
+ Map.entry("method", "id +/&="),
+ Map.entry("new", "false"),
+ Map.entry("amount", "0.0000000012300")));
+ assertThat(unit.buildURL().getPath()).isEqualTo("/0/private/DepositAddresses");
+ assertThat(unit.getHttpMethod()).isEqualTo("POST");
+ }
+
+ @Test
+ void should_reject_missing_asset_when_building_parameters() {
+ assertThatThrownBy(() -> DepositAddressesParams.builder().method("id +/&=").build()).isInstanceOf(NullPointerException.class).hasMessageContaining("asset");
+ }
+
+ @Test
+ void should_reject_missing_method_when_building_parameters() {
+ assertThatThrownBy(() -> DepositAddressesParams.builder().asset("id +/&=").build()).isInstanceOf(NullPointerException.class).hasMessageContaining("method");
+ }
+
+ @Test
+ void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exception {
+ DepositAddressesEndpoint unit = new DepositAddressesEndpoint(DepositAddressesParams.builder().asset("id +/&=").method("id +/&=").build());
+ JsonMapper mapper = JsonMapper.builder()
+ .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
+ .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)
+ .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
+ .addModule(new Jdk8Module()).build();
+ String json = Files.readString(Path.of("src/test/resources/funding/DepositAddresses.json"));
+
+ KrakenResponse> response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory()));
+ List result = response.result().orElseThrow();
+
+ assertThat(result).hasSize(5);
+ assertThat(result.getFirst().unused()).isTrue();
+ assertThat(result.get(2).unused()).isNull();
+ assertThat(result.get(3).tag()).isEqualTo("1361101127");
+ assertThat(result.getLast().memo()).isEqualTo("4150096490");
+ assertThat(result.getFirst().expireTime()).isEqualTo("0");
+ }
+}
diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/funding/DepositMethodsEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/funding/DepositMethodsEndpointTest.java
new file mode 100644
index 0000000..fde2c55
--- /dev/null
+++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/funding/DepositMethodsEndpointTest.java
@@ -0,0 +1,104 @@
+package dev.andstuff.kraken.api.endpoint.funding;
+
+import java.math.BigDecimal;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.MapperFeature;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+
+import dev.andstuff.kraken.api.endpoint.KrakenException;
+import dev.andstuff.kraken.api.endpoint.KrakenResponse;
+import dev.andstuff.kraken.api.endpoint.funding.params.*;
+import dev.andstuff.kraken.api.endpoint.funding.response.*;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@ExtendWith(MockitoExtension.class)
+class DepositMethodsEndpointTest {
+
+ @Test
+ void should_encode_all_options_when_supplied() {
+ DepositMethodsEndpoint unit = new DepositMethodsEndpoint(DepositMethodsParams.builder().asset("id +/&=").assetClass(AssetClass.TOKENIZED_ASSET).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("aclass", "tokenized_asset"),
+ Map.entry("rebase_multiplier", "base")));
+ assertThat(unit.buildURL().getPath()).isEqualTo("/0/private/DepositMethods");
+ assertThat(unit.getHttpMethod()).isEqualTo("POST");
+ }
+
+ @Test
+ void should_reject_missing_asset_when_building_parameters() {
+ assertThatThrownBy(() -> DepositMethodsParams.builder().build()).isInstanceOf(NullPointerException.class).hasMessageContaining("asset");
+ }
+
+ @Test
+ void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exception {
+ DepositMethodsEndpoint unit = new DepositMethodsEndpoint(DepositMethodsParams.builder().asset("id +/&=").build());
+ JsonMapper mapper = JsonMapper.builder()
+ .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
+ .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)
+ .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
+ .addModule(new Jdk8Module()).build();
+ String json = Files.readString(Path.of("src/test/resources/funding/DepositMethods.json"));
+
+ KrakenResponse> response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory()));
+ List result = response.result().orElseThrow();
+
+ assertThat(result).hasSize(2);
+ assertThat(result.getFirst().limit().unlimited()).isTrue();
+ assertThat(result.getFirst().limit().amount()).isNull();
+ assertThat(result.getFirst().fee()).isEqualByComparingTo("0.0000000000");
+ assertThat(result.getFirst().minimum()).isEqualByComparingTo("0.00010000");
+ }
+
+ @Test
+ void should_preserve_finite_limits_when_strings_or_numbers_are_returned() throws Exception {
+ DepositMethodsEndpoint unit = new DepositMethodsEndpoint("XBT");
+ JsonMapper mapper = JsonMapper.builder().addModule(new Jdk8Module()).build();
+ String json = """
+ {"error":[],"result":[{"limit":"0.0000000000123456789","fee-percentage":"0.1"},{"limit":1234567890.1234567890123456789},{"limit":0},{"limit":null},{}]}
+ """;
+
+ KrakenResponse> response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory()));
+ List result = response.result().orElseThrow();
+
+ assertThat(result.getFirst().limit().amount()).isEqualByComparingTo("0.0000000000123456789");
+ assertThat(result.getFirst().limit().unlimited()).isFalse();
+ assertThat(result.getFirst().feePercentage()).isEqualByComparingTo("0.1");
+ assertThat(result.get(1).limit().amount()).isEqualByComparingTo("1234567890.1234567890123456789");
+ assertThat(result.get(2).limit().amount()).isZero();
+ assertThat(result.get(2).limit().unlimited()).isFalse();
+ assertThat(result.get(3).limit()).isNull();
+ assertThat(result.getLast().limit()).isNull();
+ }
+
+ @Test
+ void should_reject_invalid_limit_when_true_is_returned() {
+ DepositMethodsEndpoint unit = new DepositMethodsEndpoint("XBT");
+ JsonMapper mapper = JsonMapper.builder().addModule(new Jdk8Module()).build();
+
+ assertThatThrownBy(() -> mapper.readValue("{\"error\":[],\"result\":[{\"limit\":true}]}", unit.wrappedResponseType(mapper.getTypeFactory())))
+ .isInstanceOf(com.fasterxml.jackson.databind.JsonMappingException.class);
+ }
+}
diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/funding/DepositStatusEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/funding/DepositStatusEndpointTest.java
new file mode 100644
index 0000000..eaeb9ac
--- /dev/null
+++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/funding/DepositStatusEndpointTest.java
@@ -0,0 +1,155 @@
+package dev.andstuff.kraken.api.endpoint.funding;
+
+import java.math.BigDecimal;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.MapperFeature;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+
+import dev.andstuff.kraken.api.endpoint.KrakenException;
+import dev.andstuff.kraken.api.endpoint.KrakenResponse;
+import dev.andstuff.kraken.api.endpoint.funding.params.*;
+import dev.andstuff.kraken.api.endpoint.funding.response.*;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@ExtendWith(MockitoExtension.class)
+class DepositStatusEndpointTest {
+
+ @Test
+ void should_encode_all_options_when_supplied() {
+ DepositStatusEndpoint unit = new DepositStatusEndpoint(DepositStatusParams.builder().asset("id +/&=").assetClass(AssetClass.TOKENIZED_ASSET).method("id +/&=").start("1700000000").end("1700000000").cursor("id +/&=").limit(0).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("aclass", "tokenized_asset"),
+ Map.entry("method", "id +/&="),
+ Map.entry("start", "1700000000"),
+ Map.entry("end", "1700000000"),
+ Map.entry("cursor", "id +/&="),
+ Map.entry("limit", "0"),
+ Map.entry("rebase_multiplier", "base")));
+ assertThat(unit.buildURL().getPath()).isEqualTo("/0/private/DepositStatus");
+ assertThat(unit.getHttpMethod()).isEqualTo("POST");
+ }
+
+ @Test
+ void should_omit_optional_values_when_not_supplied() {
+ DepositStatusEndpoint unit = new DepositStatusEndpoint();
+
+ assertThat(unit.encodedParamsWith("123")).isEqualTo("nonce=123");
+ }
+
+ @Test
+ void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exception {
+ DepositStatusEndpoint unit = new DepositStatusEndpoint(DepositStatusParams.builder().build());
+ JsonMapper mapper = JsonMapper.builder()
+ .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
+ .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)
+ .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
+ .addModule(new Jdk8Module()).build();
+ String json = Files.readString(Path.of("src/test/resources/funding/DepositStatus.json"));
+
+ KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory()));
+ DepositStatus result = response.result().orElseThrow();
+
+ assertThat(result.deposits()).hasSize(2);
+ assertThat(result.nextCursor()).isNull();
+ assertThat(result.deposits().getFirst().amount()).isEqualByComparingTo("0.78125000");
+ assertThat(result.deposits().getFirst().status()).isEqualTo(Deposit.Status.SUCCESS);
+ assertThat(result.deposits().getLast().statusProp()).isEqualTo(Deposit.StatusProp.ONHOLD);
+ assertThat(result.deposits().getLast().originators()).hasSize(2);
+ }
+
+ @Test
+ void should_request_first_page_when_pagination_is_enabled() {
+ DepositStatusEndpoint unit = new DepositStatusEndpoint(DepositStatusParams.builder().cursor(true).limit(2).build());
+
+ assertThat(unit.encodedParamsWith("123")).contains("cursor=true", "limit=2", "nonce=123");
+ }
+
+ @Test
+ void should_disable_pagination_when_explicitly_requested() {
+ DepositStatusEndpoint unit = new DepositStatusEndpoint(DepositStatusParams.builder().cursor(false).build());
+
+ assertThat(unit.encodedParamsWith("123")).contains("cursor=false");
+ }
+
+ @Test
+ void should_preserve_cursor_and_precision_when_reading_a_page() throws Exception {
+ DepositStatusEndpoint unit = new DepositStatusEndpoint();
+ JsonMapper mapper = JsonMapper.builder().addModule(new Jdk8Module())
+ .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)
+ .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES).build();
+ String json = """
+ {"error":[],"result":{"deposits":[{"amount":"0.0000000000123456789","time":4102444800,"status":"future-status","status-prop":"future-property","future_field":{"nested":true}}],"next_cursor":"opaque +/=cursor","future_page_field":[]}}
+ """;
+
+ KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory()));
+ DepositStatus result = response.result().orElseThrow();
+
+ assertThat(result.nextCursor()).isEqualTo("opaque +/=cursor");
+ assertThat(result.deposits().getFirst().amount()).isEqualByComparingTo("0.0000000000123456789");
+ assertThat(result.deposits().getFirst().time()).isEqualTo(4102444800L);
+ assertThat(result.deposits().getFirst().status()).isEqualTo(Deposit.Status.UNKNOWN);
+ assertThat(result.deposits().getFirst().statusProp()).isEqualTo(Deposit.StatusProp.UNKNOWN);
+ }
+
+ @Test
+ void should_accept_singular_field_when_returned_in_a_page() throws Exception {
+ DepositStatusEndpoint unit = new DepositStatusEndpoint();
+ JsonMapper mapper = JsonMapper.builder().addModule(new Jdk8Module()).build();
+ String json = """
+ {"error":[],"result":{"deposit":[{"refid":"reference"}],"next_cursor":""}}
+ """;
+
+ KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory()));
+ DepositStatus result = response.result().orElseThrow();
+
+ assertThat(result.nextCursor()).isEmpty();
+ assertThat(result.deposits().getFirst().referenceId()).isEqualTo("reference");
+ }
+
+ @Test
+ void should_return_empty_transactions_when_last_page_is_empty() throws Exception {
+ DepositStatusEndpoint unit = new DepositStatusEndpoint();
+ JsonMapper mapper = JsonMapper.builder().addModule(new Jdk8Module()).build();
+ String json = """
+ {"error":[],"result":{"deposits":[],"next_cursor":null}}
+ """;
+
+ KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory()));
+ DepositStatus result = response.result().orElseThrow();
+
+ assertThat(result.deposits()).isEmpty();
+ assertThat(result.nextCursor()).isNull();
+ }
+
+ @Test
+ void should_reject_malformed_page_when_transactions_are_missing() {
+ DepositStatusEndpoint unit = new DepositStatusEndpoint();
+ JsonMapper mapper = JsonMapper.builder().addModule(new Jdk8Module()).build();
+
+ assertThatThrownBy(() -> mapper.readValue("{\"error\":[],\"result\":{\"next_cursor\":\"next\"}}", unit.wrappedResponseType(mapper.getTypeFactory())))
+ .isInstanceOf(com.fasterxml.jackson.databind.JsonMappingException.class);
+ }
+}
diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/funding/WalletTransferEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/funding/WalletTransferEndpointTest.java
new file mode 100644
index 0000000..9aac5ad
--- /dev/null
+++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/funding/WalletTransferEndpointTest.java
@@ -0,0 +1,86 @@
+package dev.andstuff.kraken.api.endpoint.funding;
+
+import java.math.BigDecimal;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.MapperFeature;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+
+import dev.andstuff.kraken.api.endpoint.KrakenException;
+import dev.andstuff.kraken.api.endpoint.KrakenResponse;
+import dev.andstuff.kraken.api.endpoint.funding.params.*;
+import dev.andstuff.kraken.api.endpoint.funding.response.*;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@ExtendWith(MockitoExtension.class)
+class WalletTransferEndpointTest {
+
+ @Test
+ void should_encode_all_options_when_supplied() {
+ WalletTransferEndpoint unit = new WalletTransferEndpoint(WalletTransferParams.builder().asset("id +/&=").sourceWallet(SourceWallet.SPOT_WALLET).destinationWallet(DestinationWallet.FUTURES_WALLET).amount(new BigDecimal("0.0000000012300")).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("from", "Spot Wallet"),
+ Map.entry("to", "Futures Wallet"),
+ Map.entry("amount", "0.0000000012300")));
+ assertThat(unit.buildURL().getPath()).isEqualTo("/0/private/WalletTransfer");
+ assertThat(unit.getHttpMethod()).isEqualTo("POST");
+ }
+
+ @Test
+ void should_reject_missing_asset_when_building_parameters() {
+ assertThatThrownBy(() -> WalletTransferParams.builder().sourceWallet(SourceWallet.SPOT_WALLET).destinationWallet(DestinationWallet.FUTURES_WALLET).amount(new BigDecimal("0.0000000012300")).build()).isInstanceOf(NullPointerException.class).hasMessageContaining("asset");
+ }
+
+ @Test
+ void should_reject_missing_sourceWallet_when_building_parameters() {
+ assertThatThrownBy(() -> WalletTransferParams.builder().asset("id +/&=").destinationWallet(DestinationWallet.FUTURES_WALLET).amount(new BigDecimal("0.0000000012300")).build()).isInstanceOf(NullPointerException.class).hasMessageContaining("sourceWallet");
+ }
+
+ @Test
+ void should_reject_missing_destinationWallet_when_building_parameters() {
+ assertThatThrownBy(() -> WalletTransferParams.builder().asset("id +/&=").sourceWallet(SourceWallet.SPOT_WALLET).amount(new BigDecimal("0.0000000012300")).build()).isInstanceOf(NullPointerException.class).hasMessageContaining("destinationWallet");
+ }
+
+ @Test
+ void should_reject_missing_amount_when_building_parameters() {
+ assertThatThrownBy(() -> WalletTransferParams.builder().asset("id +/&=").sourceWallet(SourceWallet.SPOT_WALLET).destinationWallet(DestinationWallet.FUTURES_WALLET).build()).isInstanceOf(NullPointerException.class).hasMessageContaining("amount");
+ }
+
+ @Test
+ void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exception {
+ WalletTransferEndpoint unit = new WalletTransferEndpoint(WalletTransferParams.builder().asset("id +/&=").sourceWallet(SourceWallet.SPOT_WALLET).destinationWallet(DestinationWallet.FUTURES_WALLET).amount(new BigDecimal("0.0000000012300")).build());
+ JsonMapper mapper = JsonMapper.builder()
+ .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
+ .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)
+ .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
+ .addModule(new Jdk8Module()).build();
+ String json = Files.readString(Path.of("src/test/resources/funding/WalletTransfer.json"));
+
+ KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory()));
+ FundingReference result = response.result().orElseThrow();
+
+ assertThat(result.referenceId()).isEqualTo("FTQcuak-V6Za8qrWnhzTx67yYHz8Tg");
+ }
+}
diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/funding/WithdrawEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/funding/WithdrawEndpointTest.java
new file mode 100644
index 0000000..8346ab5
--- /dev/null
+++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/funding/WithdrawEndpointTest.java
@@ -0,0 +1,84 @@
+package dev.andstuff.kraken.api.endpoint.funding;
+
+import java.math.BigDecimal;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.MapperFeature;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+
+import dev.andstuff.kraken.api.endpoint.KrakenException;
+import dev.andstuff.kraken.api.endpoint.KrakenResponse;
+import dev.andstuff.kraken.api.endpoint.funding.params.*;
+import dev.andstuff.kraken.api.endpoint.funding.response.*;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@ExtendWith(MockitoExtension.class)
+class WithdrawEndpointTest {
+
+ @Test
+ void should_encode_all_options_when_supplied() {
+ WithdrawEndpoint unit = new WithdrawEndpoint(WithdrawParams.builder().asset("id +/&=").assetClass(AssetClass.TOKENIZED_ASSET).key("id +/&=").address("id +/&=").amount(new BigDecimal("0.0000000012300")).maxFee(new BigDecimal("0.0000000012300")).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("aclass", "tokenized_asset"),
+ Map.entry("key", "id +/&="),
+ Map.entry("address", "id +/&="),
+ Map.entry("amount", "0.0000000012300"),
+ Map.entry("max_fee", "0.0000000012300"),
+ Map.entry("rebase_multiplier", "base")));
+ assertThat(unit.buildURL().getPath()).isEqualTo("/0/private/Withdraw");
+ assertThat(unit.getHttpMethod()).isEqualTo("POST");
+ }
+
+ @Test
+ void should_reject_missing_asset_when_building_parameters() {
+ assertThatThrownBy(() -> WithdrawParams.builder().key("id +/&=").amount(new BigDecimal("0.0000000012300")).build()).isInstanceOf(NullPointerException.class).hasMessageContaining("asset");
+ }
+
+ @Test
+ void should_reject_missing_key_when_building_parameters() {
+ assertThatThrownBy(() -> WithdrawParams.builder().asset("id +/&=").amount(new BigDecimal("0.0000000012300")).build()).isInstanceOf(NullPointerException.class).hasMessageContaining("key");
+ }
+
+ @Test
+ void should_reject_missing_amount_when_building_parameters() {
+ assertThatThrownBy(() -> WithdrawParams.builder().asset("id +/&=").key("id +/&=").build()).isInstanceOf(NullPointerException.class).hasMessageContaining("amount");
+ }
+
+ @Test
+ void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exception {
+ WithdrawEndpoint unit = new WithdrawEndpoint(WithdrawParams.builder().asset("id +/&=").key("id +/&=").amount(new BigDecimal("0.0000000012300")).build());
+ JsonMapper mapper = JsonMapper.builder()
+ .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
+ .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)
+ .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
+ .addModule(new Jdk8Module()).build();
+ String json = Files.readString(Path.of("src/test/resources/funding/Withdraw.json"));
+
+ KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory()));
+ FundingReference result = response.result().orElseThrow();
+
+ assertThat(result.referenceId()).isEqualTo("FTQcuak-V6Za8qrWnhzTx67yYHz8Tg");
+ }
+}
diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/funding/WithdrawalAddressesEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/funding/WithdrawalAddressesEndpointTest.java
new file mode 100644
index 0000000..b4e9946
--- /dev/null
+++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/funding/WithdrawalAddressesEndpointTest.java
@@ -0,0 +1,75 @@
+package dev.andstuff.kraken.api.endpoint.funding;
+
+import java.math.BigDecimal;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.MapperFeature;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+
+import dev.andstuff.kraken.api.endpoint.KrakenException;
+import dev.andstuff.kraken.api.endpoint.KrakenResponse;
+import dev.andstuff.kraken.api.endpoint.funding.params.*;
+import dev.andstuff.kraken.api.endpoint.funding.response.*;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@ExtendWith(MockitoExtension.class)
+class WithdrawalAddressesEndpointTest {
+
+ @Test
+ void should_encode_all_options_when_supplied() {
+ WithdrawalAddressesEndpoint unit = new WithdrawalAddressesEndpoint(WithdrawalAddressesParams.builder().asset("id +/&=").assetClass(AssetClass.TOKENIZED_ASSET).method("id +/&=").key("id +/&=").verified(false).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("aclass", "tokenized_asset"),
+ Map.entry("method", "id +/&="),
+ Map.entry("key", "id +/&="),
+ Map.entry("verified", "false")));
+ assertThat(unit.buildURL().getPath()).isEqualTo("/0/private/WithdrawAddresses");
+ assertThat(unit.getHttpMethod()).isEqualTo("POST");
+ }
+
+ @Test
+ void should_omit_optional_values_when_not_supplied() {
+ WithdrawalAddressesEndpoint unit = new WithdrawalAddressesEndpoint();
+
+ assertThat(unit.encodedParamsWith("123")).isEqualTo("nonce=123");
+ }
+
+ @Test
+ void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exception {
+ WithdrawalAddressesEndpoint unit = new WithdrawalAddressesEndpoint(WithdrawalAddressesParams.builder().build());
+ JsonMapper mapper = JsonMapper.builder()
+ .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
+ .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)
+ .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
+ .addModule(new Jdk8Module()).build();
+ String json = Files.readString(Path.of("src/test/resources/funding/WithdrawAddresses.json"));
+
+ KrakenResponse> response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory()));
+ List result = response.result().orElseThrow();
+
+ assertThat(result.getFirst().key()).isEqualTo("btc-wallet-1");
+ assertThat(result.getFirst().verified()).isTrue();
+ }
+}
diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/funding/WithdrawalInfoEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/funding/WithdrawalInfoEndpointTest.java
new file mode 100644
index 0000000..194d3e7
--- /dev/null
+++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/funding/WithdrawalInfoEndpointTest.java
@@ -0,0 +1,82 @@
+package dev.andstuff.kraken.api.endpoint.funding;
+
+import java.math.BigDecimal;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.MapperFeature;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+
+import dev.andstuff.kraken.api.endpoint.KrakenException;
+import dev.andstuff.kraken.api.endpoint.KrakenResponse;
+import dev.andstuff.kraken.api.endpoint.funding.params.*;
+import dev.andstuff.kraken.api.endpoint.funding.response.*;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@ExtendWith(MockitoExtension.class)
+class WithdrawalInfoEndpointTest {
+
+ @Test
+ void should_encode_all_options_when_supplied() {
+ WithdrawalInfoEndpoint unit = new WithdrawalInfoEndpoint(WithdrawalInfoParams.builder().asset("id +/&=").key("id +/&=").amount(new BigDecimal("0.0000000012300")).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("key", "id +/&="),
+ Map.entry("amount", "0.0000000012300")));
+ assertThat(unit.buildURL().getPath()).isEqualTo("/0/private/WithdrawInfo");
+ assertThat(unit.getHttpMethod()).isEqualTo("POST");
+ }
+
+ @Test
+ void should_reject_missing_asset_when_building_parameters() {
+ assertThatThrownBy(() -> WithdrawalInfoParams.builder().key("id +/&=").amount(new BigDecimal("0.0000000012300")).build()).isInstanceOf(NullPointerException.class).hasMessageContaining("asset");
+ }
+
+ @Test
+ void should_reject_missing_key_when_building_parameters() {
+ assertThatThrownBy(() -> WithdrawalInfoParams.builder().asset("id +/&=").amount(new BigDecimal("0.0000000012300")).build()).isInstanceOf(NullPointerException.class).hasMessageContaining("key");
+ }
+
+ @Test
+ void should_reject_missing_amount_when_building_parameters() {
+ assertThatThrownBy(() -> WithdrawalInfoParams.builder().asset("id +/&=").key("id +/&=").build()).isInstanceOf(NullPointerException.class).hasMessageContaining("amount");
+ }
+
+ @Test
+ void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exception {
+ WithdrawalInfoEndpoint unit = new WithdrawalInfoEndpoint(WithdrawalInfoParams.builder().asset("id +/&=").key("id +/&=").amount(new BigDecimal("0.0000000012300")).build());
+ JsonMapper mapper = JsonMapper.builder()
+ .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
+ .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)
+ .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
+ .addModule(new Jdk8Module()).build();
+ String json = Files.readString(Path.of("src/test/resources/funding/WithdrawInfo.json"));
+
+ KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory()));
+ WithdrawalInfo result = response.result().orElseThrow();
+
+ assertThat(result.limit()).isEqualByComparingTo("332.00956139");
+ assertThat(result.amount()).isEqualByComparingTo("0.72485000");
+ assertThat(result.fee()).isEqualByComparingTo("0.00020000");
+ }
+}
diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/funding/WithdrawalMethodsEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/funding/WithdrawalMethodsEndpointTest.java
new file mode 100644
index 0000000..e938e53
--- /dev/null
+++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/funding/WithdrawalMethodsEndpointTest.java
@@ -0,0 +1,91 @@
+package dev.andstuff.kraken.api.endpoint.funding;
+
+import java.math.BigDecimal;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.MapperFeature;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+
+import dev.andstuff.kraken.api.endpoint.KrakenException;
+import dev.andstuff.kraken.api.endpoint.KrakenResponse;
+import dev.andstuff.kraken.api.endpoint.funding.params.*;
+import dev.andstuff.kraken.api.endpoint.funding.response.*;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@ExtendWith(MockitoExtension.class)
+class WithdrawalMethodsEndpointTest {
+
+ @Test
+ void should_encode_all_options_when_supplied() {
+ WithdrawalMethodsEndpoint unit = new WithdrawalMethodsEndpoint(WithdrawalMethodsParams.builder().asset("id +/&=").assetClass(AssetClass.TOKENIZED_ASSET).network("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("aclass", "tokenized_asset"),
+ Map.entry("network", "id +/&="),
+ Map.entry("rebase_multiplier", "base")));
+ assertThat(unit.buildURL().getPath()).isEqualTo("/0/private/WithdrawMethods");
+ assertThat(unit.getHttpMethod()).isEqualTo("POST");
+ }
+
+ @Test
+ void should_omit_optional_values_when_not_supplied() {
+ WithdrawalMethodsEndpoint unit = new WithdrawalMethodsEndpoint();
+
+ assertThat(unit.encodedParamsWith("123")).isEqualTo("nonce=123");
+ }
+
+ @Test
+ void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exception {
+ WithdrawalMethodsEndpoint unit = new WithdrawalMethodsEndpoint(WithdrawalMethodsParams.builder().build());
+ JsonMapper mapper = JsonMapper.builder()
+ .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
+ .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)
+ .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
+ .addModule(new Jdk8Module()).build();
+ String json = Files.readString(Path.of("src/test/resources/funding/WithdrawMethods.json"));
+
+ KrakenResponse> response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory()));
+ List result = response.result().orElseThrow();
+
+ assertThat(result.getFirst().fee().fee()).isEqualByComparingTo("0.00001500");
+ assertThat(result.getLast().fee().feePercentage()).isEqualByComparingTo("0.1");
+ assertThat(result.getFirst().networkId()).isEqualTo("ee9d686d-aeb6-4e61-9d83-448e3a7511f3");
+ }
+
+ @Test
+ void should_parse_limits_by_window_when_method_has_rate_limits() throws Exception {
+ WithdrawalMethodsEndpoint unit = new WithdrawalMethodsEndpoint();
+ JsonMapper mapper = JsonMapper.builder().addModule(new Jdk8Module()).build();
+ String json = """
+ {"error":[],"result":[{"asset":"XXBT","limits":[{"description":"Daily","limit_type":"amount","limits":{"86400":{"maximum":"2.00000000","remaining":"1.90000000","used":"0.10000000"}}}]}]}
+ """;
+
+ KrakenResponse> response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory()));
+ WithdrawalMethod.LimitWindow result = response.result().orElseThrow().getFirst().limits().getFirst().limits().get("86400");
+
+ assertThat(result.maximum()).isEqualByComparingTo("2.00000000");
+ assertThat(result.remaining()).isEqualByComparingTo("1.90000000");
+ assertThat(result.used()).isEqualByComparingTo("0.10000000");
+ }
+}
diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/funding/WithdrawalStatusEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/funding/WithdrawalStatusEndpointTest.java
new file mode 100644
index 0000000..78b1b1b
--- /dev/null
+++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/funding/WithdrawalStatusEndpointTest.java
@@ -0,0 +1,154 @@
+package dev.andstuff.kraken.api.endpoint.funding;
+
+import java.math.BigDecimal;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.MapperFeature;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+
+import dev.andstuff.kraken.api.endpoint.KrakenException;
+import dev.andstuff.kraken.api.endpoint.KrakenResponse;
+import dev.andstuff.kraken.api.endpoint.funding.params.*;
+import dev.andstuff.kraken.api.endpoint.funding.response.*;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@ExtendWith(MockitoExtension.class)
+class WithdrawalStatusEndpointTest {
+
+ @Test
+ void should_encode_all_options_when_supplied() {
+ WithdrawalStatusEndpoint unit = new WithdrawalStatusEndpoint(WithdrawalStatusParams.builder().asset("id +/&=").assetClass(AssetClass.TOKENIZED_ASSET).method("id +/&=").start("1700000000").end("1700000000").cursor("id +/&=").limit(0).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("aclass", "tokenized_asset"),
+ Map.entry("method", "id +/&="),
+ Map.entry("start", "1700000000"),
+ Map.entry("end", "1700000000"),
+ Map.entry("cursor", "id +/&="),
+ Map.entry("limit", "0"),
+ Map.entry("rebase_multiplier", "base")));
+ assertThat(unit.buildURL().getPath()).isEqualTo("/0/private/WithdrawStatus");
+ assertThat(unit.getHttpMethod()).isEqualTo("POST");
+ }
+
+ @Test
+ void should_omit_optional_values_when_not_supplied() {
+ WithdrawalStatusEndpoint unit = new WithdrawalStatusEndpoint();
+
+ assertThat(unit.encodedParamsWith("123")).isEqualTo("nonce=123");
+ }
+
+ @Test
+ void should_deserialize_typed_response_when_reading_kraken_fixture() throws Exception {
+ WithdrawalStatusEndpoint unit = new WithdrawalStatusEndpoint(WithdrawalStatusParams.builder().build());
+ JsonMapper mapper = JsonMapper.builder()
+ .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
+ .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)
+ .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
+ .addModule(new Jdk8Module()).build();
+ String json = Files.readString(Path.of("src/test/resources/funding/WithdrawStatus.json"));
+
+ KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory()));
+ WithdrawalStatus result = response.result().orElseThrow();
+
+ assertThat(result.withdrawals()).hasSize(2);
+ assertThat(result.nextCursor()).isNull();
+ assertThat(result.withdrawals().getFirst().amount()).isEqualByComparingTo("0.72485000");
+ assertThat(result.withdrawals().getLast().statusProp()).isEqualTo(Withdrawal.StatusProp.CANCELED);
+ assertThat(result.withdrawals().getFirst().time()).isEqualTo(1688014586L);
+ }
+
+ @Test
+ void should_request_first_page_when_pagination_is_enabled() {
+ WithdrawalStatusEndpoint unit = new WithdrawalStatusEndpoint(WithdrawalStatusParams.builder().cursor(true).limit(2).build());
+
+ assertThat(unit.encodedParamsWith("123")).contains("cursor=true", "limit=2", "nonce=123");
+ }
+
+ @Test
+ void should_disable_pagination_when_explicitly_requested() {
+ WithdrawalStatusEndpoint unit = new WithdrawalStatusEndpoint(WithdrawalStatusParams.builder().cursor(false).build());
+
+ assertThat(unit.encodedParamsWith("123")).contains("cursor=false");
+ }
+
+ @Test
+ void should_preserve_cursor_and_precision_when_reading_a_page() throws Exception {
+ WithdrawalStatusEndpoint unit = new WithdrawalStatusEndpoint();
+ JsonMapper mapper = JsonMapper.builder().addModule(new Jdk8Module())
+ .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)
+ .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES).build();
+ String json = """
+ {"error":[],"result":{"withdrawals":[{"amount":"0.0000000000123456789","time":4102444800,"status":"future-status","status-prop":"future-property","future_field":{"nested":true}}],"next_cursor":"opaque +/=cursor","future_page_field":[]}}
+ """;
+
+ KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory()));
+ WithdrawalStatus result = response.result().orElseThrow();
+
+ assertThat(result.nextCursor()).isEqualTo("opaque +/=cursor");
+ assertThat(result.withdrawals().getFirst().amount()).isEqualByComparingTo("0.0000000000123456789");
+ assertThat(result.withdrawals().getFirst().time()).isEqualTo(4102444800L);
+ assertThat(result.withdrawals().getFirst().status()).isEqualTo(Withdrawal.Status.UNKNOWN);
+ assertThat(result.withdrawals().getFirst().statusProp()).isEqualTo(Withdrawal.StatusProp.UNKNOWN);
+ }
+
+ @Test
+ void should_accept_singular_field_when_returned_in_a_page() throws Exception {
+ WithdrawalStatusEndpoint unit = new WithdrawalStatusEndpoint();
+ JsonMapper mapper = JsonMapper.builder().addModule(new Jdk8Module()).build();
+ String json = """
+ {"error":[],"result":{"withdrawal":[{"refid":"reference"}],"next_cursor":""}}
+ """;
+
+ KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory()));
+ WithdrawalStatus result = response.result().orElseThrow();
+
+ assertThat(result.nextCursor()).isEmpty();
+ assertThat(result.withdrawals().getFirst().referenceId()).isEqualTo("reference");
+ }
+
+ @Test
+ void should_return_empty_transactions_when_last_page_is_empty() throws Exception {
+ WithdrawalStatusEndpoint unit = new WithdrawalStatusEndpoint();
+ JsonMapper mapper = JsonMapper.builder().addModule(new Jdk8Module()).build();
+ String json = """
+ {"error":[],"result":{"withdrawals":[],"next_cursor":null}}
+ """;
+
+ KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory()));
+ WithdrawalStatus result = response.result().orElseThrow();
+
+ assertThat(result.withdrawals()).isEmpty();
+ assertThat(result.nextCursor()).isNull();
+ }
+
+ @Test
+ void should_reject_malformed_page_when_transactions_are_missing() {
+ WithdrawalStatusEndpoint unit = new WithdrawalStatusEndpoint();
+ JsonMapper mapper = JsonMapper.builder().addModule(new Jdk8Module()).build();
+
+ assertThatThrownBy(() -> mapper.readValue("{\"error\":[],\"result\":{\"next_cursor\":\"next\"}}", unit.wrappedResponseType(mapper.getTypeFactory())))
+ .isInstanceOf(com.fasterxml.jackson.databind.JsonMappingException.class);
+ }
+}
diff --git a/library/src/test/java/dev/andstuff/kraken/api/rest/FundingRestRequesterTest.java b/library/src/test/java/dev/andstuff/kraken/api/rest/FundingRestRequesterTest.java
new file mode 100644
index 0000000..bc5513d
--- /dev/null
+++ b/library/src/test/java/dev/andstuff/kraken/api/rest/FundingRestRequesterTest.java
@@ -0,0 +1,84 @@
+package dev.andstuff.kraken.api.rest;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+
+import javax.net.ssl.HttpsURLConnection;
+
+import com.fasterxml.jackson.databind.type.TypeFactory;
+
+import dev.andstuff.kraken.api.endpoint.KrakenException;
+import dev.andstuff.kraken.api.endpoint.funding.CancelWithdrawalEndpoint;
+import dev.andstuff.kraken.api.endpoint.funding.WithdrawEndpoint;
+import dev.andstuff.kraken.api.endpoint.funding.params.WithdrawParams;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Answers;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class FundingRestRequesterTest {
+
+ @InjectMocks private DefaultKrakenRestRequester unit;
+ @Mock(answer = Answers.CALLS_REAL_METHODS) private CancelWithdrawalEndpoint cancellation;
+ @Mock(answer = Answers.CALLS_REAL_METHODS) private WithdrawEndpoint withdrawal;
+ @Mock private WithdrawParams withdrawalParams;
+ @Mock private HttpsURLConnection connection;
+ @Mock private URL url;
+ @Mock private KrakenCredentials credentials;
+ @Mock private KrakenNonceGenerator nonceGenerator;
+
+ @Test
+ void should_return_false_when_kraken_cannot_cancel_withdrawal() throws Exception {
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ String body = "asset=XBT&refid=reference&nonce=123";
+ when(nonceGenerator.generate()).thenReturn("123");
+ doReturn(body).when(cancellation).encodedParamsWith("123");
+ doReturn(url).when(cancellation).buildURL();
+ when(url.openConnection()).thenReturn(connection);
+ when(connection.getURL()).thenReturn(url);
+ doReturn("POST").when(cancellation).getHttpMethod();
+ 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\":false}".getBytes(StandardCharsets.UTF_8)));
+ doReturn(new CancelWithdrawalEndpoint("XBT", "reference").wrappedResponseType(TypeFactory.createDefaultInstance())).when(cancellation).wrappedResponseType(any());
+
+ Boolean result = unit.execute(cancellation, credentials, nonceGenerator);
+
+ assertThat(result).isFalse();
+ assertThat(output.toString(StandardCharsets.UTF_8)).isEqualTo(body);
+ verify(credentials).sign(url, "123", body);
+ verify(connection).addRequestProperty("API-Key", "test-key");
+ verify(connection).addRequestProperty("API-Sign", "test-signature");
+ }
+
+ @Test
+ void should_propagate_errors_when_kraken_rejects_withdrawal() throws Exception {
+ when(nonceGenerator.generate()).thenReturn("123");
+ doReturn("asset=XBT&key=saved-key&amount=0.01&nonce=123").when(withdrawal).encodedParamsWith("123");
+ doReturn(url).when(withdrawal).buildURL();
+ when(url.openConnection()).thenReturn(connection);
+ when(connection.getURL()).thenReturn(url);
+ doReturn("POST").when(withdrawal).getHttpMethod();
+ when(connection.getOutputStream()).thenReturn(new ByteArrayOutputStream());
+ when(connection.getHeaderField("Content-Type")).thenReturn("application/json");
+ when(connection.getInputStream()).thenReturn(new ByteArrayInputStream("{\"error\":[\"EFunding:Max fee exceeded\"]}".getBytes(StandardCharsets.UTF_8)));
+ doReturn(new WithdrawEndpoint(withdrawalParams).wrappedResponseType(TypeFactory.createDefaultInstance())).when(withdrawal).wrappedResponseType(any());
+
+ assertThatThrownBy(() -> unit.execute(withdrawal, credentials, nonceGenerator)).isInstanceOf(KrakenException.class);
+ }
+}
diff --git a/library/src/test/resources/funding/DepositAddresses.json b/library/src/test/resources/funding/DepositAddresses.json
new file mode 100644
index 0000000..a306642
--- /dev/null
+++ b/library/src/test/resources/funding/DepositAddresses.json
@@ -0,0 +1,30 @@
+{
+ "error": [],
+ "result": [
+ {
+ "address": "2N9fRkx5JTWXWHmXzZtvhQsufvoYRMq9ExV",
+ "expiretm": "0",
+ "new": true
+ },
+ {
+ "address": "2NCpXUCEYr8ur9WXM1tAjZSem2w3aQeTcAo",
+ "expiretm": "0",
+ "new": true
+ },
+ {
+ "address": "2Myd4eaAW96ojk38A2uDK4FbioCayvkEgVq",
+ "expiretm": "0"
+ },
+ {
+ "address": "rLHzPsX3oXdzU2qP17kHCH2G4csZv1rAJh",
+ "expiretm": "0",
+ "new": true,
+ "tag": "1361101127"
+ },
+ {
+ "address": "krakenkraken",
+ "expiretm": "0",
+ "memo": "4150096490"
+ }
+ ]
+}
diff --git a/library/src/test/resources/funding/DepositMethods.json b/library/src/test/resources/funding/DepositMethods.json
new file mode 100644
index 0000000..9140058
--- /dev/null
+++ b/library/src/test/resources/funding/DepositMethods.json
@@ -0,0 +1,18 @@
+{
+ "error": [],
+ "result": [
+ {
+ "method": "Bitcoin",
+ "limit": false,
+ "fee": "0.0000000000",
+ "gen-address": true,
+ "minimum": "0.00010000"
+ },
+ {
+ "method": "Bitcoin Lightning",
+ "limit": false,
+ "fee": "0.00000000",
+ "minimum": "0.00010000"
+ }
+ ]
+}
diff --git a/library/src/test/resources/funding/DepositStatus.json b/library/src/test/resources/funding/DepositStatus.json
new file mode 100644
index 0000000..55270cd
--- /dev/null
+++ b/library/src/test/resources/funding/DepositStatus.json
@@ -0,0 +1,34 @@
+{
+ "error": [],
+ "result": [
+ {
+ "method": "Bitcoin",
+ "aclass": "currency",
+ "asset": "XXBT",
+ "refid": "FTQcuak-V6Za8qrWnhzTx67yYHz8Tg",
+ "txid": "6544b41b607d8b2512baf801755a3a87b6890eacdb451be8a94059fb11f0a8d9",
+ "info": "2Myd4eaAW96ojk38A2uDK4FbioCayvkEgVq",
+ "amount": "0.78125000",
+ "fee": "0.0000000000",
+ "time": 1688992722,
+ "status": "Success",
+ "status-prop": "return"
+ },
+ {
+ "method": "Ether (Hex)",
+ "aclass": "currency",
+ "asset": "XETH",
+ "refid": "FTQcuak-V6Za8qrPnhsTx47yYLz8Tg",
+ "txid": "0x339c505eba389bf2c6bebb982cc30c6d82d0bd6a37521fa292890b6b180affc0",
+ "info": "0xca210f4121dc891c9154026c3ae3d1832a005048",
+ "amount": "0.1383862742",
+ "time": 1688992722,
+ "status": "Settled",
+ "status-prop": "onhold",
+ "originators": [
+ "0x70b6343b104785574db2c1474b3acb3937ab5de7346a5b857a78ee26954e0e2d",
+ "0x5b32f6f792904a446226b17f607850d0f2f7533cdc35845bfe432b5b99f55b66"
+ ]
+ }
+ ]
+}
diff --git a/library/src/test/resources/funding/README.md b/library/src/test/resources/funding/README.md
new file mode 100644
index 0000000..7130a08
--- /dev/null
+++ b/library/src/test/resources/funding/README.md
@@ -0,0 +1 @@
+Fixtures are response examples from [Kraken’s Spot REST OpenAPI specification](https://docs.kraken.com/openapi/spot-rest.yaml), retrieved on 2026-09-08. Supplemental inline cases cover paginated status responses, missing and unlimited deposit limits, precise amounts, rate limit windows, enum fallback, and unsuccessful cancellation. The DepositStatus schema uses a singular `deposit` field while the unpaginated example is an array; status parsers accept singular and plural collection fields. All tests run locally without API keys, network calls, or funding actions.
diff --git a/library/src/test/resources/funding/WalletTransfer.json b/library/src/test/resources/funding/WalletTransfer.json
new file mode 100644
index 0000000..4f76bdf
--- /dev/null
+++ b/library/src/test/resources/funding/WalletTransfer.json
@@ -0,0 +1,6 @@
+{
+ "error": [],
+ "result": {
+ "refid": "FTQcuak-V6Za8qrWnhzTx67yYHz8Tg"
+ }
+}
diff --git a/library/src/test/resources/funding/Withdraw.json b/library/src/test/resources/funding/Withdraw.json
new file mode 100644
index 0000000..4f76bdf
--- /dev/null
+++ b/library/src/test/resources/funding/Withdraw.json
@@ -0,0 +1,6 @@
+{
+ "error": [],
+ "result": {
+ "refid": "FTQcuak-V6Za8qrWnhzTx67yYHz8Tg"
+ }
+}
diff --git a/library/src/test/resources/funding/WithdrawAddresses.json b/library/src/test/resources/funding/WithdrawAddresses.json
new file mode 100644
index 0000000..b269075
--- /dev/null
+++ b/library/src/test/resources/funding/WithdrawAddresses.json
@@ -0,0 +1,12 @@
+{
+ "error": [],
+ "result": [
+ {
+ "address": "bc1qxdsh4sdd29h6ldehz0se5c61asq8cgwyjf2y3z",
+ "asset": "XBT",
+ "method": "Bitcoin",
+ "key": "btc-wallet-1",
+ "verified": true
+ }
+ ]
+}
diff --git a/library/src/test/resources/funding/WithdrawCancel.json b/library/src/test/resources/funding/WithdrawCancel.json
new file mode 100644
index 0000000..5b7a954
--- /dev/null
+++ b/library/src/test/resources/funding/WithdrawCancel.json
@@ -0,0 +1,4 @@
+{
+ "error": [],
+ "result": true
+}
diff --git a/library/src/test/resources/funding/WithdrawInfo.json b/library/src/test/resources/funding/WithdrawInfo.json
new file mode 100644
index 0000000..dcb151a
--- /dev/null
+++ b/library/src/test/resources/funding/WithdrawInfo.json
@@ -0,0 +1,9 @@
+{
+ "error": [],
+ "result": {
+ "method": "Bitcoin",
+ "limit": "332.00956139",
+ "amount": "0.72485000",
+ "fee": "0.00020000"
+ }
+}
diff --git a/library/src/test/resources/funding/WithdrawMethods.json b/library/src/test/resources/funding/WithdrawMethods.json
new file mode 100644
index 0000000..07c9746
--- /dev/null
+++ b/library/src/test/resources/funding/WithdrawMethods.json
@@ -0,0 +1,32 @@
+{
+ "error": [],
+ "result": [
+ {
+ "asset": "XXBT",
+ "method": "Bitcoin",
+ "method_id": "12fca2ad-edae-4d8c-acbb-4a424c1fbdeb",
+ "network": "Bitcoin",
+ "network_id": "ee9d686d-aeb6-4e61-9d83-448e3a7511f3",
+ "minimum": "0.0004",
+ "fee": {
+ "aclass": "currency",
+ "asset": "XXBT",
+ "fee": "0.00001500"
+ }
+ },
+ {
+ "asset": "XXBT",
+ "method": "Bitcoin Lightning",
+ "method_id": "1ac8da36-8eec-4ed0-977a-82e4b50f2e49",
+ "network": "Lightning",
+ "network_id": "03ed9b12-29f1-4dc3-8d05-bd58e3798518",
+ "minimum": "0.00001",
+ "fee": {
+ "aclass": "currency",
+ "asset": "XXBT",
+ "fee": "0",
+ "fee_percentage": "0.1"
+ }
+ }
+ ]
+}
diff --git a/library/src/test/resources/funding/WithdrawStatus.json b/library/src/test/resources/funding/WithdrawStatus.json
new file mode 100644
index 0000000..1ad4b78
--- /dev/null
+++ b/library/src/test/resources/funding/WithdrawStatus.json
@@ -0,0 +1,32 @@
+{
+ "error": [],
+ "result": [
+ {
+ "method": "Bitcoin",
+ "aclass": "currency",
+ "asset": "XXBT",
+ "refid": "FTQcuak-V6Za8qrWnhzTx67yYHz8Tg",
+ "txid": "29323ce235cee8dae22503caba7....8ad3a506879a03b1e87992923d80428",
+ "info": "bc1qm32pq....3ewt0j37s2g",
+ "amount": "0.72485000",
+ "fee": "0.00020000",
+ "time": 1688014586,
+ "status": "Pending",
+ "key": "btc-wallet-1"
+ },
+ {
+ "method": "Bitcoin",
+ "aclass": "currency",
+ "asset": "XXBT",
+ "refid": "FTQcuak-V6Za8qrPnhsTx47yYLz8Tg",
+ "txid": "29323ce212ceb2daf81255cbea8a5...ad7a626471e05e1f82929501e82934",
+ "info": "bc1qa35ls....3egf0872h3w",
+ "amount": "0.72485000",
+ "fee": "0.00020000",
+ "time": 1688015423,
+ "status": "Failure",
+ "status-prop": "canceled",
+ "key": "btc-wallet-2"
+ }
+ ]
+}
diff --git a/pom.xml b/pom.xml
index 5976be8..b7d6779 100644
--- a/pom.xml
+++ b/pom.xml
@@ -54,6 +54,9 @@
2.0.18
2.23.0
5.12.0
+ 5.14.0
+ 3.27.7
+ 5.23.0
3.15.0
@@ -62,6 +65,7 @@
3.12.0
3.3.1
3.4.0
+ 3.5.4
0.11.0
@@ -127,6 +131,11 @@
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ ${maven-surefire-plugin.version}
+
org.apache.maven.plugins
maven-compiler-plugin