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
143 changes: 137 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,74 @@
# Dfns Java SDK

Java 17 SDK for the Dfns API.
Welcome, builders. This repo holds the Dfns Java SDK. Useful links:

- [Dfns Website](https://www.dfns.co)
- [Dfns API Docs](https://docs.dfns.co)

## Installation

Requires **Java 17+**. The SDK is built with Gradle and will be published to Maven Central
under group `co.dfns`, artifact `dfns-sdk-java`:

**Gradle**

```groovy
implementation("co.dfns:dfns-sdk-java:$version")
```

**Maven**

```xml
<dependency>
<groupId>co.dfns</groupId>
<artifactId>dfns-sdk-java</artifactId>
<version>${version}</version>
</dependency>
```

> Maven Central publishing is being finalized; until it lands, build the SDK from source.

## Quick Start

```java
import co.dfns.sdk.*;
import co.dfns.sdk.auth.*;
import co.dfns.sdk.DfnsClient;
import co.dfns.sdk.DfnsClientConfig;

// Create the client (read-only operations)
DfnsClientConfig config = DfnsClientConfig.builder()
.authToken("your-auth-token")
// .baseUrl("https://api.dfns.io") // Optional, this is the default
.build();

// Create a signer with your credential ID and private key
DfnsClient client = new DfnsClient(config);

// List wallets
var wallets = client.wallets.listWallets(null);
System.out.println(wallets);
```

An async client with the same API but `CompletableFuture` return types is also available:

```java
import co.dfns.sdk.DfnsAsyncClient;

DfnsAsyncClient asyncClient = new DfnsAsyncClient(config);
```

## User Action Signing

Some operations (like creating wallets or signing transactions) require user action signing.
Configure a signer to enable these operations:

```java
import co.dfns.sdk.DfnsClient;
import co.dfns.sdk.DfnsClientConfig;
import co.dfns.sdk.auth.KeySigner;
import co.dfns.sdk.auth.Signer;
import co.dfns.sdk.wallets.model.CreateWalletRequest;
import co.dfns.sdk.wallets.model.Network;

// Create a signer from your credential ID and private key bytes
Signer signer = KeySigner.fromEd25519PrivateKey("cr-xxx-xxx", privateKeyBytes);

DfnsClientConfig config = DfnsClientConfig.builder()
Expand All @@ -17,9 +77,57 @@ DfnsClientConfig config = DfnsClientConfig.builder()
.build();

DfnsClient client = new DfnsClient(config);
DfnsAsyncClient asyncClient = new DfnsAsyncClient(config);

// Operations requiring signatures are signed automatically
var wallet = client.wallets.createWallet(new CreateWalletRequest(Network.EthereumSepolia));
System.out.println(wallet);
```

## Delegated Signing

In some setups you want your **server** to talk to Dfns on behalf of a user, while the user
keeps signing every request themselves (e.g. with a WebAuthn credential in a web app). The
`DfnsDelegatedClient` supports this: it needs **no `Signer`**, and every operation that needs
a user action signature is split into an `...Init` / `...Complete` pair.

- `...Init` takes the request payload and returns a `UserActionChallenge` to be signed
out-of-band (typically by the end user in the browser).
- `...Complete` takes the same payload, the challenge identifier, and the signed
`CredentialAssertion`, and performs the request.

A typical flow: the server calls `...Init` and sends the challenge to the user; the user
signs it with their credential and returns the assertion; the server calls `...Complete`.

```java
import co.dfns.sdk.DfnsClientConfig;
import co.dfns.sdk.DfnsDelegatedClient;
import co.dfns.sdk.auth.CredentialAssertion;
import co.dfns.sdk.auth.UserActionChallenge;
import co.dfns.sdk.wallets.model.CreateWalletRequest;
import co.dfns.sdk.wallets.model.Network;

// No signer needed — challenges are signed out-of-band (e.g. by the end user).
DfnsClientConfig config = DfnsClientConfig.builder()
.authToken("user-auth-token")
.build();

DfnsDelegatedClient client = new DfnsDelegatedClient(config);

CreateWalletRequest body = new CreateWalletRequest(Network.EthereumSepolia);

// Step 1 (server): start the action, get a challenge.
UserActionChallenge challenge = client.wallets.createWalletInit(body);

// Step 2 (client): the user signs `challenge` with their credential and returns the
// signed assertion (a CredentialAssertion) to the server.
CredentialAssertion assertion = signChallengeOutOfBand(challenge);

// Step 3 (server): complete the action with the signed challenge.
var wallet = client.wallets.createWalletComplete(body, challenge.challengeIdentifier(), assertion);
```

A `DfnsDelegatedAsyncClient` with `CompletableFuture` return types is also available.

## Available Domains

- `client.agreements` — AgreementsClient
Expand All @@ -38,12 +146,35 @@ DfnsAsyncClient asyncClient = new DfnsAsyncClient(config);
- `client.wallets` — WalletsClient
- `client.webhooks` — WebhooksClient

Each domain provides typed methods for all available API endpoints. The same domains are
exposed (as `Delegated*Client`) on `DfnsDelegatedClient`.

## Error Handling

```java
import co.dfns.sdk.DfnsException;

try {
var result = client.wallets.listWallets();
var result = client.wallets.listWallets(null);
} catch (DfnsException e) {
System.err.println("HTTP status: " + e.getHttpStatus());
System.err.println("Dfns error code: " + e.getDfnsErrorCode());
System.err.println("Message: " + e.getErrorMessage());
}
```

## Supported Key Types

The `KeySigner` supports the following private key types, each via a factory method that
takes the credential ID and the private key bytes:

| Factory method | Key type |
|---|---|
| `KeySigner.fromEd25519PrivateKey` | Ed25519 (EdDSA) |
| `KeySigner.fromEcdsaP256PrivateKey` | ECDSA (P-256) |
| `KeySigner.fromSecp256k1PrivateKey` | ECDSA (secp256k1) |
| `KeySigner.fromRsaPrivateKey` | RSA (PKCS#1 v1.5, SHA-256) |

## License

MIT License - See LICENSE file for details.
66 changes: 66 additions & 0 deletions src/main/java/co/dfns/sdk/DfnsDelegatedAsyncClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package co.dfns.sdk;

import co.dfns.sdk.internal.DfnsHttpClient;
import co.dfns.sdk.agreements.DelegatedAgreementsAsyncClient;
import co.dfns.sdk.allocations.DelegatedAllocationsAsyncClient;
import co.dfns.sdk.auth.DelegatedAuthAsyncClient;
import co.dfns.sdk.exchanges.DelegatedExchangesAsyncClient;
import co.dfns.sdk.feesponsors.DelegatedFeeSponsorsAsyncClient;
import co.dfns.sdk.keys.DelegatedKeysAsyncClient;
import co.dfns.sdk.networks.DelegatedNetworksAsyncClient;
import co.dfns.sdk.payouts.DelegatedPayoutsAsyncClient;
import co.dfns.sdk.permissions.DelegatedPermissionsAsyncClient;
import co.dfns.sdk.policies.DelegatedPoliciesAsyncClient;
import co.dfns.sdk.signers.DelegatedSignersAsyncClient;
import co.dfns.sdk.staking.DelegatedStakingAsyncClient;
import co.dfns.sdk.swaps.DelegatedSwapsAsyncClient;
import co.dfns.sdk.wallets.DelegatedWalletsAsyncClient;
import co.dfns.sdk.webhooks.DelegatedWebhooksAsyncClient;

/**
* Dfns async SDK client for delegated user action signing. Its domain clients split signed
* operations into Init/Complete calls so challenges can be signed out-of-band (e.g. by an
* end user) rather than by a Signer held in this process.
*/
public class DfnsDelegatedAsyncClient implements AutoCloseable {
private final DfnsHttpClient httpClient;
public final DelegatedAgreementsAsyncClient agreements;
public final DelegatedAllocationsAsyncClient allocations;
public final DelegatedAuthAsyncClient auth;
public final DelegatedExchangesAsyncClient exchanges;
public final DelegatedFeeSponsorsAsyncClient feeSponsors;
public final DelegatedKeysAsyncClient keys;
public final DelegatedNetworksAsyncClient networks;
public final DelegatedPayoutsAsyncClient payouts;
public final DelegatedPermissionsAsyncClient permissions;
public final DelegatedPoliciesAsyncClient policies;
public final DelegatedSignersAsyncClient signers;
public final DelegatedStakingAsyncClient staking;
public final DelegatedSwapsAsyncClient swaps;
public final DelegatedWalletsAsyncClient wallets;
public final DelegatedWebhooksAsyncClient webhooks;

public DfnsDelegatedAsyncClient(DfnsClientConfig config) {
this.httpClient = new DfnsHttpClient(config);
this.agreements = new DelegatedAgreementsAsyncClient(httpClient);
this.allocations = new DelegatedAllocationsAsyncClient(httpClient);
this.auth = new DelegatedAuthAsyncClient(httpClient);
this.exchanges = new DelegatedExchangesAsyncClient(httpClient);
this.feeSponsors = new DelegatedFeeSponsorsAsyncClient(httpClient);
this.keys = new DelegatedKeysAsyncClient(httpClient);
this.networks = new DelegatedNetworksAsyncClient(httpClient);
this.payouts = new DelegatedPayoutsAsyncClient(httpClient);
this.permissions = new DelegatedPermissionsAsyncClient(httpClient);
this.policies = new DelegatedPoliciesAsyncClient(httpClient);
this.signers = new DelegatedSignersAsyncClient(httpClient);
this.staking = new DelegatedStakingAsyncClient(httpClient);
this.swaps = new DelegatedSwapsAsyncClient(httpClient);
this.wallets = new DelegatedWalletsAsyncClient(httpClient);
this.webhooks = new DelegatedWebhooksAsyncClient(httpClient);
}

@Override
public void close() {
httpClient.close();
}
}
66 changes: 66 additions & 0 deletions src/main/java/co/dfns/sdk/DfnsDelegatedClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package co.dfns.sdk;

import co.dfns.sdk.internal.DfnsHttpClient;
import co.dfns.sdk.agreements.DelegatedAgreementsClient;
import co.dfns.sdk.allocations.DelegatedAllocationsClient;
import co.dfns.sdk.auth.DelegatedAuthClient;
import co.dfns.sdk.exchanges.DelegatedExchangesClient;
import co.dfns.sdk.feesponsors.DelegatedFeeSponsorsClient;
import co.dfns.sdk.keys.DelegatedKeysClient;
import co.dfns.sdk.networks.DelegatedNetworksClient;
import co.dfns.sdk.payouts.DelegatedPayoutsClient;
import co.dfns.sdk.permissions.DelegatedPermissionsClient;
import co.dfns.sdk.policies.DelegatedPoliciesClient;
import co.dfns.sdk.signers.DelegatedSignersClient;
import co.dfns.sdk.staking.DelegatedStakingClient;
import co.dfns.sdk.swaps.DelegatedSwapsClient;
import co.dfns.sdk.wallets.DelegatedWalletsClient;
import co.dfns.sdk.webhooks.DelegatedWebhooksClient;

/**
* Dfns SDK client for delegated user action signing. Its domain clients split signed
* operations into Init/Complete calls so challenges can be signed out-of-band (e.g. by an
* end user) rather than by a Signer held in this process.
*/
public class DfnsDelegatedClient implements AutoCloseable {
private final DfnsHttpClient httpClient;
public final DelegatedAgreementsClient agreements;
public final DelegatedAllocationsClient allocations;
public final DelegatedAuthClient auth;
public final DelegatedExchangesClient exchanges;
public final DelegatedFeeSponsorsClient feeSponsors;
public final DelegatedKeysClient keys;
public final DelegatedNetworksClient networks;
public final DelegatedPayoutsClient payouts;
public final DelegatedPermissionsClient permissions;
public final DelegatedPoliciesClient policies;
public final DelegatedSignersClient signers;
public final DelegatedStakingClient staking;
public final DelegatedSwapsClient swaps;
public final DelegatedWalletsClient wallets;
public final DelegatedWebhooksClient webhooks;

public DfnsDelegatedClient(DfnsClientConfig config) {
this.httpClient = new DfnsHttpClient(config);
this.agreements = new DelegatedAgreementsClient(httpClient);
this.allocations = new DelegatedAllocationsClient(httpClient);
this.auth = new DelegatedAuthClient(httpClient);
this.exchanges = new DelegatedExchangesClient(httpClient);
this.feeSponsors = new DelegatedFeeSponsorsClient(httpClient);
this.keys = new DelegatedKeysClient(httpClient);
this.networks = new DelegatedNetworksClient(httpClient);
this.payouts = new DelegatedPayoutsClient(httpClient);
this.permissions = new DelegatedPermissionsClient(httpClient);
this.policies = new DelegatedPoliciesClient(httpClient);
this.signers = new DelegatedSignersClient(httpClient);
this.staking = new DelegatedStakingClient(httpClient);
this.swaps = new DelegatedSwapsClient(httpClient);
this.wallets = new DelegatedWalletsClient(httpClient);
this.webhooks = new DelegatedWebhooksClient(httpClient);
}

@Override
public void close() {
httpClient.close();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package co.dfns.sdk.agreements;

import co.dfns.sdk.internal.DfnsHttpClient;
import co.dfns.sdk.agreements.model.*;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import co.dfns.sdk.auth.UserActionChallenge;
import co.dfns.sdk.auth.CredentialAssertion;

public class DelegatedAgreementsAsyncClient {
private final DfnsHttpClient httpClient;

public DelegatedAgreementsAsyncClient(DfnsHttpClient httpClient) {
this.httpClient = httpClient;
}

/** Get Latest Unaccepted Agreement */
public CompletableFuture<GetLatestUnacceptedAgreementResponse> getLatestUnacceptedAgreement(GetLatestUnacceptedAgreementQuery query) {
return httpClient.getAsync("/agreements/latest-unaccepted", query.toMap(), GetLatestUnacceptedAgreementResponse.class);
}

/** Delegated signing step 1 for Record Agreement Acceptance: returns the challenge to sign out-of-band. */
public CompletableFuture<UserActionChallenge> recordAgreementAcceptanceInit(String agreementId) {
return httpClient.createUserActionChallengeAsync("POST", "/agreements/" + agreementId + "/accept", null);
}

/** Delegated signing step 2 for Record Agreement Acceptance: submits the signed challenge and issues the request. */
public CompletableFuture<RecordAgreementAcceptanceResponse> recordAgreementAcceptanceComplete(String agreementId, String challengeIdentifier, CredentialAssertion assertion) {
return httpClient.completeUserActionSigningAsync(challengeIdentifier, assertion)
.thenCompose(userAction -> httpClient.executeWithUserActionAsync("POST", "/agreements/" + agreementId + "/accept", java.util.Map.of(), null, RecordAgreementAcceptanceResponse.class, userAction));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package co.dfns.sdk.agreements;

import co.dfns.sdk.internal.DfnsHttpClient;
import co.dfns.sdk.agreements.model.*;
import java.util.Map;
import co.dfns.sdk.auth.UserActionChallenge;
import co.dfns.sdk.auth.CredentialAssertion;

public class DelegatedAgreementsClient {
private final DfnsHttpClient httpClient;

public DelegatedAgreementsClient(DfnsHttpClient httpClient) {
this.httpClient = httpClient;
}

/** Get Latest Unaccepted Agreement */
public GetLatestUnacceptedAgreementResponse getLatestUnacceptedAgreement(GetLatestUnacceptedAgreementQuery query) {
return httpClient.get("/agreements/latest-unaccepted", query.toMap(), GetLatestUnacceptedAgreementResponse.class);
}

/** Delegated signing step 1 for Record Agreement Acceptance: returns the challenge to sign out-of-band. */
public UserActionChallenge recordAgreementAcceptanceInit(String agreementId) {
return httpClient.createUserActionChallenge("POST", "/agreements/" + agreementId + "/accept", null);
}

/** Delegated signing step 2 for Record Agreement Acceptance: submits the signed challenge and issues the request. */
public RecordAgreementAcceptanceResponse recordAgreementAcceptanceComplete(String agreementId, String challengeIdentifier, CredentialAssertion assertion) {
String userAction = httpClient.completeUserActionSigning(challengeIdentifier, assertion);
return httpClient.executeWithUserAction("POST", "/agreements/" + agreementId + "/accept", java.util.Map.of(), null, RecordAgreementAcceptanceResponse.class, userAction);
}
}
Loading
Loading