> For the complete documentation index, see [llms.txt](https://hinkal-team.gitbook.io/hinkal/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://hinkal-team.gitbook.io/hinkal/hinkal-api/api-integration/dfns.md).

# DFNS

Dfns gives your users a secure wallet they unlock with a passkey and a familiar social login - no seed phrase to manage. Because Hinkal only needs a standard signer to operate, a Dfns wallet plugs in directly: the same wallet a user signs in with can hold a private, shielded balance and run confidential deposits, withdrawals, transfers, swaps, and private sends.

Dfns's role is unchanged. It authenticates the user, manages the wallet and its keys, and produces signatures. Hinkal uses those signatures to authorize private operations and never takes custody of the wallet.

#### Compatibility

| Environment | Supported | Notes                                                        |
| ----------- | --------- | ------------------------------------------------------------ |
| Browser     | ✅         | React, Next.js                                               |
| Libraries   | ✅         | `@dfns/sdk`, `@dfns/sdk-browser`, `@dfns/lib-ethersjs6` v0.8 |

#### How the integration works

Three parties are involved:

* **The user's Dfns wallet** - produces every signature, unlocked with the user's passkey; the raw key never leaves Dfns.
* **The Hinkal API** - an HTTP service running inside a secure enclave. It decrypts the user's shielded balance, generates the zero-knowledge proofs, and builds the on-chain transactions. The wallet's private key never reaches it.
* **The relayer** - broadcasts private transactions, so the user's public wallet is never the on-chain sender of a confidential operation.

#### Installation

```sh
npm install @dfns/sdk @dfns/sdk-browser @dfns/lib-ethersjs6 ethers
```

```sh
yarn add @dfns/sdk @dfns/sdk-browser @dfns/lib-ethersjs6 ethers
```

#### 1. Authenticate and get a signer

Dfns authenticates the user through a social login (Google OIDC) backed by a passkey, then exposes their Ethereum wallet as an ethers signer.

The flow:

1. Obtain a Google OIDC `idToken` via Google Sign-In.
2. Exchange it for a Dfns auth token with `socialLogin`. If the user is new (the call returns 401/404), register them - create a passkey credential and an Ethereum wallet - then use the returned token.
3. List the user's wallets and select the active Ethereum one.
4. Initialize a `DfnsWallet` for that wallet.

```tsx
import { DfnsApiClient, DfnsError } from "@dfns/sdk";
import { WebAuthnSigner } from "@dfns/sdk-browser";
import { DfnsWallet } from "@dfns/lib-ethersjs6";
import { ethers } from "ethers";

const { orgId, apiUrl, relyingParty } = dfnsConfig;
const signer = new WebAuthnSigner({ relyingParty }); // passkey signer

const dfnsApi = (authToken) =>
  new DfnsApiClient({ orgId, authToken, baseUrl: apiUrl, signer });

// 1–2. Social login, registering a passkey + Ethereum wallet for new users
const socialLoginOrRegister = async (idToken) => {
  const body = { orgId, socialLoginProviderKind: "Oidc", idToken };
  try {
    return (await dfnsApi().auth.socialLogin({ body })).token;
  } catch (err) {
    const status = err instanceof DfnsError ? err.httpStatus : undefined;
    if (status !== 401 && status !== 404) throw err;
  }
  const challenge = await dfnsApi().auth.createSocialRegistrationChallenge({ body });
  const { authentication } = await dfnsApi(
    challenge.temporaryAuthenticationToken,
  ).auth.registerEndUser({
    body: {
      firstFactorCredential: await signer.create(challenge),
      wallets: [{ network: "Ethereum" }],
    },
  });
  return authentication.token;
};

// 3–4. Resolve the wallet and initialize a DfnsWallet
const client = dfnsApi(await socialLoginOrRegister(idToken));
const { items } = await client.wallets.listWallets();
const walletInfo = items.find(
  (w) => w.address?.match(/^0x[0-9a-fA-F]{40}$/) && w.status === "Active",
);
if (!walletInfo) throw new Error("No active DFNS Ethereum wallet found");

const dfnsWallet = await DfnsWallet.init({ walletId: walletInfo.id, dfnsClient: client });
const account = ethers.getAddress(walletInfo.address);
```

`DfnsWallet` becomes a standard `ethers.Signer` once connected to a provider for the target chain:

```tsx
const provider = new ethers.JsonRpcProvider(rpcUrl);
const signer = dfnsWallet.connect(provider); // ethers.Signer
```

From here, every Hinkal call is identical to any other wallet. Dfns produces each signature through the user's passkey, so each signing operation prompts a passkey approval - the user's confirmation of the action, equivalent to confirming a transaction in a browser wallet.

> **EIP-712 note.** `DfnsWallet.signTypedData` JSON-serializes the typed-data message, which throws on `BigInt` values (`chainId`, amounts). Convert any `BigInt` in the message to a string before signing.

#### 2. How requests are authorized

Hinkal authorizes requests with two kinds of signature, both produced by the Dfns wallet. The enclave reconstructs the same data and verifies the signature server-side before doing anything.

**Read signature (sessions and getters)**

Read-only endpoints - fetching the shielded balance, getting fees, quoting a swap - use a personal-message signature. The signed message is exactly:

```
Authorize Hinkal session
Session ID: <nonce>
```

**Transaction signature (EIP-712 typed data)**

Each transaction is authorized with an EIP-712 typed-data signature that commits to the exact operation. The enclave rebuilds the identical structure and verifies it, so the user approves precisely what will execute.

The domain is:

```ts
const domain = { name: "Hinkal Enclave", chainId };
```

Each operation has its own primary type. The full set:

```ts
const types = {
  TokenAmount: [
    { name: "token",  type: "address" },
    { name: "amount", type: "int256"  },
  ],
  Deposit: [
    { name: "nonce",        type: "string"        },
    { name: "chainId",      type: "uint256"       },
    { name: "tokenAmounts", type: "TokenAmount[]" },
  ],
  Withdraw: [
    { name: "nonce",        type: "string"        },
    { name: "chainId",      type: "uint256"       },
    { name: "tokenAmounts", type: "TokenAmount[]" },
    { name: "recipient",    type: "string"        },
  ],
  Transfer: [
    { name: "nonce",        type: "string"        },
    { name: "chainId",      type: "uint256"       },
    { name: "tokenAmounts", type: "TokenAmount[]" },
    { name: "recipient",    type: "string"        },
  ],
  Swap: [
    { name: "nonce",        type: "string"        },
    { name: "chainId",      type: "uint256"       },
    { name: "tokenAmounts", type: "TokenAmount[]" },
  ],
};
```

When signing, include only the primary type you are using plus the types it references. A deposit, for example:

```tsx
const nonce = crypto.randomUUID();
const signature = await signer.signTypedData(
  { name: "Hinkal Enclave", chainId },
  { Deposit: types.Deposit, TokenAmount: types.TokenAmount },
  { nonce, chainId, tokenAmounts: [{ token, amount }] },
);
```

Two rules the enclave enforces - the signature must match exactly:

* **Sort `tokenAmounts` by token address** (checksummed, ascending) before signing. The enclave re-sorts before verifying; an unsorted array produces a non-matching signature.
* **The `nonce` is single-use and expires after 60 seconds.** Generate a fresh UUID for every signed request; a reused or expired nonce is rejected server-side.

**Write session (sign once for 24 hours)**

If you open a write session, the user signs the session message once with an extra third line:

```
Authorize Hinkal session
Session ID: <nonce>
This signature can also be used to submit transactions.
```

That single signature authorizes every transaction for 24 hours - the per-transaction typed-data signature is skipped. Without a write session, each transaction is signed individually as shown above.

#### 3. Read the shielded balance

The shielded balance is the user's private, encrypted holdings. Reading it uses a read signature passed as query parameters:

```tsx
const params = new URLSearchParams({
  address:   account,
  chainId:   String(chainId),
  signature,
  nonce,
});
const res = await fetch(`https://api.hinkal.io/balance?${params}`);
const { balances } = await res.json();
```

The enclave decrypts the user's shielded outputs internally and returns the balances.

#### 4. Shield - deposit (public → private)

A deposit moves funds from the user's public Dfns wallet into their shielded balance.

1. The wallet signs the `Deposit` typed data (or a write session is reused).
2. `POST /deposit` with the signature and details.
3. The enclave returns an unsigned transaction (`txData`).
4. The user's wallet signs and broadcasts it - a deposit is the public on-ramp into privacy, sent from the user's own address.

```tsx
const res = await fetch("https://api.hinkal.io/deposit", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    signature,
    nonce,
    address: account,
    chainId,
    tokenAddresses,
    amounts,
  }),
});
const { txData } = await res.json();

const tx = await signer.sendTransaction(txData);
await tx.wait();
```

After confirmation, the funds are in the user's shielded balance.

#### 5. Unshield - withdraw (private → public)

A withdrawal sends funds from the shielded balance to any public address. The user does not broadcast it: the enclave builds and proves the transaction and the relayer broadcasts it, so the user's wallet is never the on-chain sender.

1. The wallet signs the `Withdraw` typed data (or a write session is reused).
2. `POST /withdraw`.
3. The response is the resulting `txHash`.

```tsx
const res = await fetch("https://api.hinkal.io/withdraw", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    signature,
    nonce,
    address: account,
    chainId,
    tokenAddresses,
    amounts,
    recipientAddress,
    feeToken,
    feeAmount,
  }),
});
const { txHash } = await res.json();
```

#### 6. Transfer (private → private)

A transfer moves funds from the user's shielded balance to another user's shielded balance. Both sides stay private - there is no public on-chain link between sender and recipient, and the amount is not visible. The user does not broadcast it: the enclave proves the transaction and the relayer broadcasts it.

The recipient is identified by their **recipient info** - a private identifier the recipient obtains from `GET /recipient-info`(authorized by their own session signature) and shares with the sender. It is not a public address.

```tsx
// Recipient fetches their info and shares it with the sender
const params = new URLSearchParams({
  address: recipientAccount, chainId: String(chainId), signature, nonce,
});
const { recipientInfo } = await (
  await fetch(`https://api.hinkal.io/recipient-info?${params}`)
).json();
```

The sender then signs the `Transfer` typed data and submits:

```tsx
const res = await fetch("https://api.hinkal.io/transfer", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    signature,
    nonce,
    address: account,
    chainId,
    tokenAddresses,
    amounts,
    recipientAddress: recipientInfo,
    feeToken,
    feeAmount,
  }),
});
const { txHash } = await res.json();
```

The enclave proves and the relayer broadcasts.

#### 7. Swap (within the shielded balance)

A swap exchanges one token for another inside the shielded balance. It is a two-call flow: first fetch a quote, then execute.

```tsx
// 1. Quote — read signature
const quoteParams = new URLSearchParams({
  signature, nonce, address: account, chainId: String(chainId),
  inputTokenAddress, outputTokenAddress, amount,
});
const quote = await (
  await fetch(`https://api.hinkal.io/get-swap-data?${quoteParams}`)
).json();

// 2. Execute — Swap typed-data signature (or write session)
const res = await fetch("https://api.hinkal.io/swap", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    signature,
    nonce,
    address:          account,
    chainId,
    tokenAddresses,
    amounts,
    externalActionId: quote.externalActionId,
    swapData:         quote.swapData,
    feeToken,
    feeAmount,
  }),
});
const { txHash } = await res.json();
```

As with withdrawals and transfers, the enclave proves and the relayer broadcasts.

#### 8. Private send (one deposit → many private payouts)

A private send deposits public funds once and has the enclave pay out privately to one or more recipients- the on-chain link between sender and recipients is never visible.

1. `POST /private-send` with the token, recipients, and amounts → returns an order (`orderId`, `approvalAddress`, `serializedTx`, `amountIn`, `amountOut`, `fee`).
2. Approve the ERC20 spend if `approvalAddress` is set.
3. Sign and broadcast the deposit with the user's Dfns wallet.
4. Poll `GET /private-send/{orderId}` until every scheduled payout completes.

```tsx
const res = await fetch("https://api.hinkal.io/private-send", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    signature,
    nonce,
    address: account,
    chainId,
    tokenAddress,
    recipients,   // [{ address, amount }, ...]
    feeToken,
  }),
});
const order = await res.json();
// { orderId, approvalAddress, serializedTx, amountIn, amountOut, fee }

if (order.approvalAddress) {
  const token = new ethers.Contract(tokenAddress, ERC20_ABI, signer);
  await (await token.approve(order.approvalAddress, BigInt(order.amountIn))).wait();
}

const tx = ethers.Transaction.from(
  "0x" + Buffer.from(order.serializedTx, "base64").toString("hex"),
);
tx.nonce = await signer.provider.getTransactionCount(account, "pending");
const depositHash = await signer.provider.send(
  "eth_sendRawTransaction",
  [await signer.signTransaction(tx)],
);

const result = await pollOrder(order.orderId);
```

`amountIn` is what leaves the wallet; `amountOut` is what recipients collectively receive; the difference is the protocol fee. Amounts are in the token's smallest unit. The user broadcasts the single deposit; the enclave then proves and the relayer broadcasts each private payout.

#### Summary

| Operation                    | Endpoint                                 | Signature             | Who broadcasts                     |
| ---------------------------- | ---------------------------------------- | --------------------- | ---------------------------------- |
| Read balance                 | `GET /balance`                           | Read (message)        | —                                  |
| Deposit (shield)             | `POST /deposit`                          | `Deposit` typed data  | The user's wallet                  |
| Withdraw (unshield)          | `POST /withdraw`                         | `Withdraw` typed data | Relayer                            |
| Transfer (private → private) | `GET /recipient-info` + `POST /transfer` | `Transfer` typed data | Relayer                            |
| Swap                         | `GET /get-swap-data` + `POST /swap`      | `Swap` typed data     | Relayer                            |
| Private send                 | `POST /private-send` + poll              | session or per-tx     | User (deposit) + relayer (payouts) |
