Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ Every endpoint extends `Endpoint<T>`, either `PublicEndpoint<T>` (GET on `/0/pub

`funding/` contains the private Funding (Legacy) endpoints for deposits, withdrawals and wallet transfers, using the same endpoint, parameter and response layout. `trading/` contains the private Trading endpoints for placing, amending, editing and cancelling orders, the dead man's switch and WebSocket tokens.

`fundingbeta/` contains the Funding (Beta) endpoints, which extend `FundingBetaEndpoint<T>` instead: any HTTP method on `/funding/{path}`, e.g. `GET /funding/v1/methods/withdraw` or `DELETE /funding/v1/addresses/{id}`, with path parameters encoded by the endpoint, query parameters and an optional JSON body from `FundingBetaParams`, and nested query objects in bracket notation, e.g. `asset[class]=currency`. The nonce goes in the `API-Nonce` header, the signed path includes the query string, and responses are not wrapped in the `{error, result}` envelope: HTTP error statuses become a `KrakenException`. Shared value types such as `Asset`, `AssetAmount` and `Scope` live in `fundingbeta/params/` and are used by both parameters and responses.

Parameters are form-encoded by default. Parameters with arrays or nested objects extend `JsonPostParams`, which sends a JSON body with a numeric nonce, and their endpoint overrides `getContentType()` to return `application/json`, e.g. `TradeVolume`, `AddOrderBatch` and `CancelOrderBatch`.

`KrakenRestRequester` performs the HTTP calls and can be swapped for another HTTP client. Responses are unwrapped from the Kraken `{error, result}` envelope by `KrakenResponse<T>`; ZIP responses (report exports) go through `Endpoint.processZipResponse()`.
`KrakenRestRequester` performs the HTTP calls and can be swapped for another HTTP client; its Funding (Beta) `execute` method is a default method throwing `UnsupportedOperationException`, so requesters written before it keep compiling. Responses are unwrapped from the Kraken `{error, result}` envelope by `KrakenResponse<T>`; ZIP responses (report exports) go through `Endpoint.processZipResponse()`.

## Conventions

Expand Down
48 changes: 46 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ KrakenAPI api = new KrakenAPI();
Map<String, OrderBook> books = api.query(new MyOrderBookEndpoint("XBTUSD"));
```

The endpoint is run through the same `KrakenRestRequester` as the built-in ones, and a `PrivateEndpoint` is signed with the credentials and nonce generator the `KrakenAPI` instance was built with. Querying one on an instance built without credentials throws an `IllegalStateException`.
The endpoint is run through the same `KrakenRestRequester` as the built-in ones, and a `PrivateEndpoint` is signed with the credentials and nonce generator the `KrakenAPI` instance was built with. Querying one on an instance built without credentials throws an `IllegalStateException`. Funding (Beta) endpoints extend `FundingBetaEndpoint<T>` instead, taking the HTTP method and the path under `/funding`, e.g. `new FundingBetaEndpoint<>("GET", "v1/networks", new TypeReference<JsonNode>() {})`.

Pull requests adding such an endpoint to the library are welcome, see the [architecture documentation](docs/ARCHITECTURE.md).

Expand All @@ -246,6 +246,49 @@ Status methods return the same response record for paginated objects and unpagin

`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.

### Funding (Beta)

All 15 [Funding (Beta)](https://docs.kraken.com/api-reference/funding-beta/list-funding-methods) operations have typed methods. They use stable method, network and address identifiers, withdrawal addresses saved for a method, a network or a network group, and fee quotes that pin the fee rate of a withdrawal.

| Operation | Typed method | Response |
|---|---|---|
| `GET /funding/v1/methods/{direction}` | `fundingMethods(direction)` / `fundingMethods(params)` | `FundingMethods` |
| `GET /funding/v1/assets/{direction}` | `fundingAssets(direction)` / `fundingAssets(params)` | `FundingAssets` |
| `GET /funding/v1/networks` | `fundingNetworks()` / `fundingNetworks(params)` | `FundingNetworks` |
| `GET /funding/v1/fees/{method_id}` | `fundingFees(params)` | `FundingFees` |
| `GET /funding/v1/limits/deposit/{asset_class}/{asset}` | `fundingDepositLimits(params)` | `FundingDepositLimits` |
| `GET /funding/v1/limits/withdrawal/{asset_class}/{asset}` | `fundingWithdrawalLimits(params)` | `FundingWithdrawalLimits` |
| `PUT /funding/v1/deposit/address` | `claimFundingDepositAddress(params)` | `ClaimedFundingDepositAddress` |
| `GET /funding/v2/deposit/addresses` | `fundingDepositAddresses()` / `fundingDepositAddresses(params)` | `FundingDepositAddresses` |
| `GET /funding/v1/deposits` | `fundingDeposits()` / `fundingDeposits(params)` | `FundingDeposits` |
| `GET /funding/v1/addresses` | `fundingAddresses()` / `fundingAddresses(params)` | `FundingAddresses` |
| `POST /funding/v1/addresses` | `createFundingAddress(params)` | `FundingAddressCreated` |
| `PUT /funding/v1/addresses/{id}` | `updateFundingAddress(params)` | `FundingAddressUpdated` |
| `DELETE /funding/v1/addresses/{id}` | `deleteFundingAddress(id)` / `deleteFundingAddress(params)` | `boolean` |
| `GET /funding/v1/withdrawals` | `fundingWithdrawals()` / `fundingWithdrawals(params)` | `FundingWithdrawals` |
| `POST /funding/v1/withdrawals` | `createFundingWithdrawal(params)` | `FundingWithdrawalCreated` |

```java
FundingMethods methods = api.fundingMethods(FundingMethodsParams.builder()
.direction(Direction.WITHDRAW).asset(new Asset(AssetClass.CURRENCY, "USDC")).build());
String methodId = methods.methods().getFirst().methodId();

FundingAddressCreated address = api.createFundingAddress(CreateFundingAddressParams.builder()
.scope(Scope.network(methods.methods().getFirst().network().networkId()))
.address("0xBef7B36845cA31045E86D0B46DBCac4e6752").name("Hardware wallet").build());

FundingFees quote = api.fundingFees(FundingFeesParams.builder()
.methodId(methodId).amount(new BigDecimal("5")).feeIncluded(true).build());
FundingWithdrawalCreated withdrawal = api.createFundingWithdrawal(CreateFundingWithdrawalParams.builder()
.scope(Scope.method(methodId)).addressId(address.addressId())
.amount(new AssetAmount(new Asset(AssetClass.CURRENCY, "USDC"), new BigDecimal("5")))
.withdrawalFeeToken(quote.withdrawalFeeToken()).feeIncluded(true).build());
```

Pass a `withdrawalFeeToken` to pin the quoted fee rate for 5 minutes, or a `maxFee` to cap the current fee; `feeIncluded` must then be set and match the quote. List operations return a `nextCursor()`, null on the last page, to pass back as `cursor` without the other filters. Amounts use `BigDecimal`, times use `Instant`, and limit time windows use `Duration`; a limit value is a `count()` for attempt and success limits and `amounts()` otherwise. Every parameter builder accepts an `accountId`.

These endpoints live under `/funding` instead of `/0/private`: the nonce is sent in the `API-Nonce` header, the signed path includes the query string, nested query objects use bracket notation, e.g. `asset[class]=currency`, and bodies are JSON. Kraken answers errors with an HTTP error status, thrown as a `KrakenException` whose only error is the status code followed by the response body. The deposit `status` filter is not supported yet, as the specification doesn't define how its list and range forms are encoded.

### 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:
Expand All @@ -254,12 +297,13 @@ The current implementation of the library uses the JDK's HttpsURLConnection to m
public class MyRestTemplateRestRequester implements KrakenRestRequester {
public <T> T execute(PublicEndpoint<T> endpoint) { /* your implementation */ }
public <T> T execute(PrivateEndpoint<T> endpoint, KrakenCredentials credentials, KrakenNonceGenerator nonceGenerator) { /* your implementation */ }
public <T> T execute(FundingBetaEndpoint<T> endpoint, KrakenCredentials credentials, KrakenNonceGenerator nonceGenerator) { /* optional */ }
}

KrakenAPI api = new KrakenAPI(new KrakenCredentials(key, secret), new MyRestTemplateRestRequester());
```

See `DefaultKrakenRestRequester` for the default implementation.
The Funding (Beta) `execute` method has a default implementation throwing an `UnsupportedOperationException`, so existing requesters keep compiling; implement it to query Funding (Beta) endpoints: send `endpoint.encodedBody()` unchanged, sign it with `credentials.sign(url.getFile(), nonce, body)`, send the nonce in the `API-Nonce` header, and deserialize the whole response body into `endpoint.getResponseType()`. See `DefaultKrakenRestRequester` for the default implementation.

### Custom nonce generator

Expand Down
60 changes: 56 additions & 4 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
The library is a Java client for the [Kraken REST API](https://docs.kraken.com/rest/). It is organized around four core concepts:

- **`KrakenAPI`** — The main entry point. A facade that exposes typed methods for implemented endpoints and generic methods for any endpoint.
- **`Endpoint<T>`** — Represents a single API call. Knows its HTTP method, URL path, parameters, and response type. Splits into `PublicEndpoint<T>` (GET) and `PrivateEndpoint<T>` (POST with HMAC signing).
- **`Endpoint<T>`** — Represents a single API call. Knows its HTTP method, URL path, parameters, and response type. Splits into `PublicEndpoint<T>` (GET), `PrivateEndpoint<T>` (POST with HMAC signing) and `FundingBetaEndpoint<T>` (any HTTP method on `/funding`, with the nonce in a header).
- **`KrakenRestRequester`** — Interface that performs the actual HTTP request and response parsing. `DefaultKrakenRestRequester` is the built-in implementation using `HttpsURLConnection`.
- **Params / Response types** — Each endpoint has dedicated parameter objects (`QueryParams` for public, `PostParams` for private) and response records deserialized via Jackson.

Expand Down Expand Up @@ -84,6 +84,48 @@ sequenceDiagram
KrakenAPI-->>User: LedgerInfo
```

### Funding (Beta) Endpoint

Funding (Beta) endpoints live under `/funding/v1` and `/funding/v2` and follow a different protocol: path parameters, query parameters in bracket notation for nested objects, `GET`, `POST`, `PUT` and `DELETE` requests, an optional JSON body, the nonce in the `API-Nonce` header, and responses that are not wrapped in the `{error, result}` envelope.

```mermaid
sequenceDiagram
participant User
participant KrakenAPI
participant FundingBetaEndpoint
participant FundingBetaParams
participant NonceGen as KrakenNonceGenerator
participant Credentials as KrakenCredentials
participant RestRequester as DefaultKrakenRestRequester
participant Kraken as Kraken API

User->>KrakenAPI: fundingFees(params)
KrakenAPI->>FundingBetaEndpoint: new FundingFeesEndpoint(params)
KrakenAPI->>RestRequester: execute(endpoint, credentials, nonceGenerator)

RestRequester->>NonceGen: generate()
NonceGen-->>RestRequester: "1712750400000"

RestRequester->>FundingBetaEndpoint: encodedBody()
FundingBetaEndpoint->>FundingBetaParams: encodedBody()
FundingBetaParams-->>RestRequester: "" (GET) or a JSON object

RestRequester->>FundingBetaEndpoint: buildURL()
FundingBetaEndpoint->>FundingBetaParams: toMap()
FundingBetaEndpoint-->>RestRequester: https://api.kraken.com/funding/v1/fees/{method_id}?amount=5

RestRequester->>Credentials: sign(path + "?" + query, nonce, body)
Note over Credentials: SHA-256(nonce + body)<br/>HMAC-SHA512(base64(secret), signed path + sha256)
Credentials-->>RestRequester: Base64 signature

RestRequester->>Kraken: GET with API-Key + API-Sign + API-Nonce headers
Kraken-->>RestRequester: {"fee": {…}, "withdrawal_fee_token": "…"}

RestRequester->>RestRequester: deserialize into FundingFees, or throw KrakenException on an HTTP error status
RestRequester-->>KrakenAPI: FundingFees
KrakenAPI-->>User: FundingFees
```

## Component Diagram

```mermaid
Expand All @@ -98,6 +140,7 @@ classDiagram
+ledgerInfo(params) LedgerInfo
+query(PublicEndpoint~T~) T
+query(PrivateEndpoint~T~) T
+query(FundingBetaEndpoint~T~) T
+query(endpoint) JsonNode
+queryPublic(path) JsonNode
+queryPrivate(path) JsonNode
Expand All @@ -123,10 +166,17 @@ classDiagram
+buildURL() URL
}

class FundingBetaEndpoint~T~ {
-FundingBetaParams params
+encodedBody() String
+buildURL() URL
}

class KrakenRestRequester {
<<interface>>
+execute(PublicEndpoint~T~) T
+execute(PrivateEndpoint~T~, credentials, nonceGenerator) T
+execute(FundingBetaEndpoint~T~, credentials, nonceGenerator) T
}

class DefaultKrakenRestRequester {
Expand All @@ -145,6 +195,7 @@ classDiagram

class KrakenCredentials {
+sign(url, nonce, params) String
+sign(signedPath, nonce, body) String
}

class KrakenNonceGenerator {
Expand All @@ -154,6 +205,7 @@ classDiagram

Endpoint <|-- PublicEndpoint
Endpoint <|-- PrivateEndpoint
Endpoint <|-- FundingBetaEndpoint
KrakenRestRequester <|.. DefaultKrakenRestRequester
KrakenAPI --> KrakenRestRequester
KrakenAPI --> KrakenCredentials
Expand All @@ -168,7 +220,7 @@ classDiagram

| Tier | Methods | Return type | When to use |
|------|---------|-------------|-------------|
| **Custom endpoint** | `query(myEndpoint)` | Whatever the endpoint declares | You wrote your own `PublicEndpoint`/`PrivateEndpoint` for an endpoint the library doesn't implement |
| **Custom endpoint** | `query(myEndpoint)` | Whatever the endpoint declares | You wrote your own `PublicEndpoint`/`PrivateEndpoint`/`FundingBetaEndpoint` for an endpoint the library doesn't implement |
| **Typed** | `assetInfo()`, `ledgerInfo()`, etc. | Domain records | Endpoint has a dedicated implementation |
| **Enum-based** | `query(Public.TICKER, params)` | `JsonNode` | Endpoint is in the `Public`/`Private` enum but not yet typed |
| **Raw path** | `queryPublic("Trades", params)` | `JsonNode` | Endpoint isn't in the enum yet (e.g., newly added by Kraken) |
Expand All @@ -178,8 +230,8 @@ classDiagram
To add a new typed endpoint:

1. Create a response record in the appropriate `response/` package
2. Create a params class implementing `QueryParams` (public) or extending `PostParams` (private); extend `JsonPostParams` instead when the body contains arrays or nested objects, and override `getContentType()` on the endpoint to return `application/json`
3. Create an endpoint class extending `PublicEndpoint<T>` or `PrivateEndpoint<T>`
2. Create a params class implementing `QueryParams` (public) or extending `PostParams` (private); extend `JsonPostParams` instead when the body contains arrays or nested objects, and override `getContentType()` on the endpoint to return `application/json`. Funding (Beta) params extend `FundingBetaParams`, returning query parameters from `toMap()` and a JSON body from `body()`
3. Create an endpoint class extending `PublicEndpoint<T>`, `PrivateEndpoint<T>` or `FundingBetaEndpoint<T>`
4. Add a convenience method to `KrakenAPI`

Steps 1 to 3 work just as well from outside the library, for an endpoint you need before it is implemented here. Step 4 is then replaced by handing the endpoint to `KrakenAPI.query(...)`, which runs it through the configured `KrakenRestRequester` and signs private requests with the credentials and nonce generator the instance was built with:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
import com.fasterxml.jackson.databind.JsonNode;

import dev.andstuff.kraken.api.KrakenAPI;
import dev.andstuff.kraken.api.endpoint.fundingbeta.params.Asset;
import dev.andstuff.kraken.api.endpoint.fundingbeta.params.AssetClass;
import dev.andstuff.kraken.api.endpoint.fundingbeta.params.Direction;
import dev.andstuff.kraken.api.endpoint.fundingbeta.params.FundingMethodsParams;
import dev.andstuff.kraken.api.endpoint.fundingbeta.response.FundingMethods;
import dev.andstuff.kraken.api.endpoint.market.params.AssetPairParams;
import dev.andstuff.kraken.api.endpoint.market.response.AssetInfo;
import dev.andstuff.kraken.api.endpoint.market.response.AssetPairs;
Expand Down Expand Up @@ -88,5 +93,11 @@ static void main() {
.validate(true)
.build());
log.info("{}", typedOrder);

FundingMethods withdrawalMethods = api.fundingMethods(FundingMethodsParams.builder()
.direction(Direction.WITHDRAW)
.asset(new Asset(AssetClass.CURRENCY, "USDC"))
.build());
log.info("{}", withdrawalMethods);
}
}
Loading
Loading