> ## Documentation Index
> Fetch the complete documentation index at: https://docs.yield.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Zama widget

> Offer Zama confidential vaults in @stakekit/widget 0.0.294 or later: no Zama props, one optional wallet method if your app manages the wallet, worker and CSP setup, testing and troubleshooting.

This guide shows how to offer Zama confidential vaults in an existing `@stakekit/widget` integration. [Zama confidential vaults](https://docs.zama.org/protocol/confidential-vault) keep each user's deposit and balance encrypted on-chain. The widget handles the encryption, the wrap into the confidential token, reveal and hide, the daily batches and both withdraw modes.

There is no Zama prop to set. You update the widget to 0.0.294 or later, enable Zama yields on your project and, if your app manages the wallet, add one optional method to your wallet adapter. The vaults then appear in Earn next to your other yields for wallets on Ethereum, and on Ethereum Sepolia for the test vault. To build your own interface instead, see the [Zama API](https://docs.yield.xyz/guides/zama/index.md) guide.

## At a glance

| Item | Value |
| - | - |
| Widget version | `@stakekit/widget` 0.0.294 or later |
| API key | The same project key you use for Earn. No Zama key. |
| Access | Zama yields are enabled per project in the Yield.xyz dashboard. |
| Networks | Ethereum, plus Ethereum Sepolia for testing |
| Vaults | Steakhouse Confidential Prime USDC, Flowdesk Confidential High Yield USDT, RockawayX Confidential RWA USDC, Steakhouse Prime USDT, Wintermute Confidential WBTC and Bitwise Premium RWA AUSD |
| Wallet | Works with the built-in wallet. An app-managed wallet adds one optional method, `signZamaPermit`. |

## Prerequisites

* **Zama yields enabled on your project.** Enable them in the Yield.xyz dashboard, from your project's [enabled-yields list](https://docs.yield.xyz/documentation/advanced-setup/dashboard/yield-blocking.md). If you do not have a project yet, [set one up](https://docs.yield.xyz/documentation/get-started/onboarding.md) first.
* **A funded test wallet** with ETH for gas on Ethereum, or Sepolia ETH for the test vault. Deposits use a high gas limit, so keep more ETH than a usual vault deposit needs.

## Update the widget

```bash theme={null}
npm install @stakekit/widget@0.0.294
```

Any later version also works. Zama traffic goes through the Yield.xyz API with your API key, so the widget needs no Zama credentials.

## The method: `signZamaPermit`

If the widget connects the wallet, skip this section. Its built-in EVM wallets sign the Zama permit themselves.

If you pass your own wallet through `externalProviders`, add `signZamaPermit` to the provider. It signs the EIP-712 permit that lets the user decrypt their own confidential balances. Only the wallet owner can decrypt their amounts, so a reveal needs their signature. Zama's guide to [reading confidential balances](https://docs.zama.org/protocol/confidential-vault/guides/read-balances) explains the permit. Your Earn methods (`signMessage`, `switchChain` and `sendTransaction`) stay as they are, and every Zama transaction goes through `sendTransaction` like any other Earn transaction.

The method is optional, so the widget mounts without it, but Zama deposits are blocked. See [Without `signZamaPermit`](#without-signzamapermit). Include Ethereum (`1`) in `supportedChainIds`, and Ethereum Sepolia (`11155111`) if you test on the Sepolia vault.

```ts theme={null}
readonly signZamaPermit?: (
  typedData: SKEip712TypedData,
  meta: SKZamaPermitMeta
) => Promise<SKZamaPermitResult>;
```

`SKWallet`, `SKEip712TypedData`, `SKZamaPermitMeta` and `SKZamaPermitResult` are exported from both `@stakekit/widget` and `@stakekit/widget/bundle`.

### What the widget passes

`typedData` is a standard EIP-712 payload. Sign it as it is.

| Field | Type | Notes |
| - | - | - |
| `domain` | `{ chainId?, name?, salt?, verifyingContract?, version? }` | `chainId` is a number, not a bigint or hex string. |
| `types` | `Record<string, { name: string; type: string }[]>` | The EIP-712 type definitions. |
| `primaryType` | `string` | The type to sign. |
| `message` | `Record<string, unknown>` | The permit itself, including the contracts it covers. |

`meta` describes the permit for your signing prompt:

| Field | Type | Notes |
| - | - | - |
| `address` | `string` | The wallet whose balances the permit reveals. |
| `chainId` | `number` | `1` for Ethereum, `11155111` for Ethereum Sepolia. |
| `contracts` | `string[]` | The confidential token and vault contracts the permit can decrypt. |

Use `meta` to label the request your users or approvers see, for example "Reveal your confidential balances". The permit is a signature only. It moves no funds and sends no transaction.

### Results

```ts theme={null}
// Signed.
{ type: "success", signature: "0x..." }

// The user declined in the wallet.
{ type: "cancelled" }

// Signing failed.
{ type: "error", error: "Signing rejected" }
```

A declined or failed permit stops the reveal, and the user can try again.

### When the widget calls it

* **Reveal.** When the user taps Reveal on a confidential token balance, or "Reveal balance" or "Decrypt" on a position.
* **Deposit from the confidential token.** Before the deposit, the widget reveals the user's confidential token balance to check the amount against it.

A deposit from the plain token does not ask for a permit, but the widget still checks that your provider has `signZamaPermit` before it starts. See [Without `signZamaPermit`](#without-signzamapermit).

### Example

This extends the provider from the [Yield widget](https://docs.yield.xyz/guides/widgets-dashboard/yield-widget.md#wallet-and-signing) guide.

```tsx viem theme={null}
import { SKApp, type SKWallet, type SupportedSKChainIds } from "@stakekit/widget";
import { walletClient } from "./wallet-client"; // your viem WalletClient

type Props = { address: string | undefined; chainId: SupportedSKChainIds };

const provider: SKWallet = {
  signMessage: async (message) => yourWallet.signMessage(message),
  switchChain: async (chainId) => yourWallet.switchChain(chainId),
  sendTransaction: async (tx, txMeta) => {
    // Inspect tx before you send it. Never modify it.
    const txHash = await yourWallet.sendTransaction(tx);
    return { type: "success", txHash };
  },

  // Sign the permit that reveals Zama confidential balances.
  signZamaPermit: async (typedData, meta) => {
    try {
      const signature = await walletClient.signTypedData({
        account: meta.address as `0x${string}`,
        domain: typedData.domain,
        types: typedData.types,
        primaryType: typedData.primaryType,
        message: typedData.message,
      });
      return { type: "success", signature };
    } catch (error) {
      if (isUserRejection(error)) return { type: "cancelled" };
      return { type: "error", error: String(error) };
    }
  },
};

export const Widget = ({ address, chainId }: Props) => (
  <SKApp
    apiKey={YIELD_API_KEY}
    externalProviders={{
      type: "generic",
      currentAddress: address ?? "", // "" while disconnected, never null
      currentChain: chainId,
      supportedChainIds: [1, 11155111],
      provider,
    }}
  />
);
```

```ts eth_signTypedData_v4 theme={null}
import type { SKWallet } from "@stakekit/widget";

// Any EIP-1193 provider, for example window.ethereum.
declare const ethereum: { request: (args: { method: string; params: unknown[] }) => Promise<unknown> };

export const signZamaPermit: SKWallet["signZamaPermit"] = async (typedData, meta) => {
  try {
    const signature = (await ethereum.request({
      method: "eth_signTypedData_v4",
      params: [
        meta.address,
        JSON.stringify(typedData, (_, value) =>
          typeof value === "bigint" ? value.toString() : value
        ),
      ],
    })) as string;
    return { type: "success", signature };
  } catch (error) {
    // EIP-1193 code 4001: the user rejected the request.
    if ((error as { code?: number })?.code === 4001) return { type: "cancelled" };
    return { type: "error", error: String(error) };
  }
};
```

`yourWallet` and `isUserRejection` are placeholders for your wallet SDK or custody provider. The signature must come from `meta.address`, the connected account.

> [!WARNING]
>
> Keep `signZamaPermit` on every provider you pass. The widget reads the provider you passed most recently, so a rerender without the method blocks the next Zama deposit.

### Without `signZamaPermit`

If your provider has no `signZamaPermit`:

* **Deposits are blocked.** The "Encrypting your amount" step fails with "This request can't be completed. Go back and change it.", so the deposit transaction is never sent. The underlying error is `This wallet cannot reveal confidential balances.`
* **Reveal is unavailable.** Balances stay hidden, and the withdraw form, which needs the revealed share balance, stays behind "Decrypt".
* **Pending requests still work.** The user can still cancel a pending deposit or withdrawal, and finish an instant withdrawal with "Complete withdrawal". These need no permit.

## Content Security Policy and bundlers

Zama encryption runs in a Web Worker so that it does not block the page.

* Hosts built with webpack, Turbopack or Vite copy the worker script from the widget's `dist` automatically. The widget starts the worker from that copy after a same-origin `HEAD` request finds it, which needs `connect-src 'self'`.
* Otherwise, for example in Angular, with a CDN-hosted bundle or a cross-origin Next.js `assetPrefix`, the widget starts a copy of the worker inlined into a lazy chunk, from a `blob:` URL. This needs `worker-src blob:` (or `data:`). The inlined copy is about 5 MB and is downloaded only by hosts that do not serve the worker script.
* If neither works, encryption runs on the page's main thread. It still completes, but the page can be less responsive while it runs.

If you set a Content Security Policy, add `'self'` to `connect-src` for the worker check, and `blob:` to `worker-src` if your host uses the inlined copy. Keep the entries your widget integration already needs.

## What the user sees

### Discover a vault

Zama vaults appear in Earn with their APY. The vault details say "When you deposit ... you'll receive .... Your balance stays encrypted on-chain", with the vault's own tokens, and show the daily batch time and the withdraw modes: "Withdraw privately when the next batch settles, or instantly with a public amount".

The APY is the vault's base rate. Zama runs its own incentives, which need a separate opt-in in the Zama app. The vault details link to it: "See incentives and delegation options for this vault on Zama. Learn more". The widget does not opt users in.

![Widget Earn view of the RockawayX Confidential RWA USDC vault via Zama, with a masked cUSDC balance, a Reveal button, the batch and withdraw notes, and the Deposit button](https://mintcdn.com/yieldxyz/6k1gPzjv-roZjrxm/images/guides/zama/zama-widget-deposit.png?fit=max\&auto=format\&n=6k1gPzjv-roZjrxm\&q=85\&s=3a1218fab3d138dab774b9fa779ee4ce)

A Zama vault in Earn, with cUSDC as the input token and its balance masked

### Deposit from the plain token

The user picks a vault and enters an amount of USDC, USDT, WBTC or AUSD. The widget then runs these steps, each shown as its own row:

**1. Encrypting your amount**

The widget encrypts the amount in the browser first. This usually takes 10 to 30 seconds, and the user keeps the page open. No transaction is sent until it finishes.

**2. RESET APPROVAL and APPROVAL (when needed)**

The widget reads the wallet's allowance for the confidential token:

* The allowance already covers the amount: no approval.
* The allowance is zero: "APPROVAL".
* The allowance is above zero but too small, or cannot be read: "RESET APPROVAL" to zero first, then "APPROVAL". Tokens such as USDT revert when one non-zero allowance changes to another.

**3. WRAP**

The plain token is wrapped into its [confidential token](https://docs.zama.org/protocol/protocol-apps/confidential-tokens/confidential-wrapper), an ERC-7984 token (for example USDC into cUSDC).

**4. DEPOSIT**

One deposit transaction into the vault. The amount stays encrypted on-chain.

**5. Deposit submitted**

The final screen shows "Deposit of ... submitted" and the next batch time: "Joins the next batch in ..." or "Joins the next batch, due any moment".

The deposit joins the vault's next daily batch. Earning starts once that batch settles: "You'll start earning once the batcher settles your deposit in the next batch".

### Deposit from the confidential token

If the user already holds the confidential token (for example cUSDC), they can pick it as the input token. Its balance shows masked, with a Reveal button. Reveal asks for the permit signature and shows the amount, with Hide to mask it again.

The deposit skips the approve and wrap. When the user confirms, the widget reveals the confidential balance and checks the amount against it, then encrypts the amount and sends the deposit.

### Manage

* **Hidden amounts.** Confidential positions show "Hidden" in the positions list, even after a reveal. The Manage totals leave them out: "Totals exclude confidential positions, whose amounts stay private."
* **Reveal and Hide.** In position details, "Reveal balance" asks for the permit signature and shows the amounts. "Hide balance" masks them again.
* **Pending deposit or withdrawal.** A request waiting for its batch shows "In progress" in the positions list, and "Pending deposit" or "Pending withdrawal" in the position details, with "... remaining" next to the entering or exiting balance when the batch time is known.
* **Cancel.** A pending request has "Cancel deposit" or "Cancel withdrawal". The widget explains: "Cancelling returns everything you have in this batch as cUSDC, right away" (or the vault share token for a withdrawal). The cancel returns the whole amount that wallet has in that batch.
* **In flight.** While a cancel is broadcast and not yet confirmed, the row shows "Cancelling…" in place of the cancel action.

![Widget Manage tab listing Zama positions with Hidden amounts next to other positions, and the note that totals exclude confidential positions](https://mintcdn.com/yieldxyz/6k1gPzjv-roZjrxm/images/guides/zama/zama-widget-positions.png?fit=max\&auto=format\&n=6k1gPzjv-roZjrxm\&q=85\&s=6dab9e62855322ae82cdfeedb82f5fd1)

Zama positions show Hidden amounts, and a request waiting for its batch shows In progress

![Widget position details for Steakhouse Confidential Prime USDC showing a pending deposit with a Cancel deposit button, a Decrypt button, and Hidden active and entering balances with Reveal balance](https://mintcdn.com/yieldxyz/6k1gPzjv-roZjrxm/images/guides/zama/zama-widget-pending-deposit.png?fit=max\&auto=format\&n=6k1gPzjv-roZjrxm\&q=85\&s=3c11fd0419dd541289693229dd1083e3)

A pending deposit with Cancel deposit, and the encrypted balance waiting to be decrypted

### Withdraw

The withdraw form shows "Your balance is encrypted. Decrypt it to withdraw." until the user taps "Decrypt" and signs the permit. The user then enters an amount in vault shares (for example csteakcUSDC), with the approximate underlying value shown below, and picks the receive token:

| Mode | Receive token | What happens |
| - | - | - |
| Private | The confidential token, for example cUSDC. The default. | "Private withdrawal: cUSDC stays private and is paid out when the next batch settles." The widget encrypts the amount, then sends the redeem. The first private withdrawal adds one-time "SET OPERATOR" steps that let the vault router move the share tokens. The flow ends on "Withdrawal of ... submitted" with the next batch time. |
| Instant | The plain token, for example USDC. | "Instant withdrawal: USDC arrives right away, but the amount is public on-chain." The widget runs these steps: "Encrypting your amount", unwrap, "Decrypting the withdrawal amount", withdraw and redeem. The amount shows as approximate (≈). |

If an instant withdrawal stops partway, for example because the user closes the page, the position shows "Instant withdrawal in progress" with "Complete withdrawal" to finish it.

### Positions in other vaults

To keep the chosen vault private, every deposit and every private withdrawal also sends an encrypted zero to each other Zama vault for the same asset. Those vaults can then show a position marked "Hidden", like any other encrypted position. Once the user reveals their balances, a position that holds zero drops out of the list. This is expected and needs no action. Zama's [confidentiality](https://docs.zama.org/protocol/confidential-vault/concepts/confidentiality) page lists what stays private and what is public on-chain.

### Where revealed amounts live

Revealed amounts are held in memory only, never in browser storage. They clear when the user taps Hide, when the connected account changes, after a transaction, and when the page reloads.

## Testing

1. Update to 0.0.294 or later and enable Zama yields on your dev project.
2. If you pass your own wallet, add `signZamaPermit`, and add `11155111` to `supportedChainIds`.
3. Connect a test wallet on Ethereum Sepolia with Sepolia ETH and test USDC, and open the test vault (`ethereum-sepolia-usdc-zama-steakhouse-usdc-confidential-vault`).
4. Deposit a small amount from USDC. Check "Encrypting your amount", then the approval and "WRAP" rows and the deposit, then the "Deposit ... submitted" screen with the next batch time.
5. Open the position. Check that it shows "Hidden", that "Reveal balance" asks for one signature and shows the amount, and that "Hide balance" masks it.
6. Cancel the pending deposit. Check that "Cancelling…" shows while the cancel is in flight and that the cUSDC returns to the wallet.
7. Deposit again, wait for the batch to settle, then make one private and one instant withdrawal.
8. Repeat a small deposit and withdrawal on an Ethereum mainnet vault before you go live.

If you pass your own wallet, also check that declining the permit returns `{ type: "cancelled" }` and leaves the balance hidden, that `meta.contracts` and `meta.chainId` look right in your signing prompt, and that a provider without `signZamaPermit` blocks the deposit as described above.

## Troubleshooting

Each entry is titled with the message the user sees, or with the symptom when there is no message.

### Configuration

> **Zama vaults do not appear**
>
> Check in the dashboard that Zama yields are enabled on the project of the API key you use, and that the wallet is on Ethereum (or Ethereum Sepolia for the test vault). If you pass your own wallet, check that the chain is in `supportedChainIds`. Check that `npm ls @stakekit/widget` shows 0.0.294 or later.

### Signing

> **This request can't be completed. Go back and change it.**
>
> On the "Encrypting your amount" step of a deposit, this usually means the provider has no `signZamaPermit` (`This wallet cannot reveal confidential balances.`). Add the method and keep it on every rerender. For a deposit from the confidential token, it can also mean the amount is larger than the confidential balance (`Amount exceeds the confidential balance.`). Go back and lower the amount.

> **Could not reveal the balance. Try again.**
>
> The permit was declined or failed. Check that `signZamaPermit` signs with `meta.address`, the connected account, and that it returns `{ type: "success", signature }`.

### Encryption and the worker

> **Encryption failed. Retry to try again.**
>
> Encryption could not finish. Tap Retry. If it keeps failing, check the console for a blocked worker or network request and review your [Content Security Policy](#content-security-policy-and-bundlers).

> **The page freezes during Encrypting your amount**
>
> Encryption fell back to the main thread. Serve the worker script from your origin, or allow `worker-src blob:`, so it runs in a worker.

> **The deposit fails for gas**
>
> Deposits use a high gas limit. Add ETH to the wallet and try again.

### Balances

> **The position still shows right after a cancel**
>
> Balances update once the cancel confirms on-chain. While it is in flight, the row shows "Cancelling…". Refresh the position after a short wait.

> **A pending request has no next batch time**
>
> The widget shows "... remaining" next to a pending balance only when the API returns the batch time for that request. The request still joins the next batch.

> **Withdraw shows Decrypt instead of an amount**
>
> The withdraw form needs the revealed share balance. Tap "Decrypt" and sign the permit. If your app manages the wallet, check that the provider has `signZamaPermit`.

## FAQ

> **Why are amounts hidden?**
>
> Zama vaults keep deposits and balances encrypted on-chain, so the API returns them encrypted too. Only the wallet owner can reveal them, by signing a permit. The widget shows them until the user hides them or leaves the page.

> **When does a deposit start earning?**
>
> Once the vault's next daily batch settles. The deposit screen and the pending row show the time to the next batch.

> **Can a user cancel part of a pending request?**
>
> No. A cancel returns everything that wallet has in the batch, in one transaction.

> **Why does a pending deposit include money deposited in the Zama app?**
>
> Deposits from the same wallet into the same batch are combined, wherever they were made. If the wallet also deposited into that batch in the Zama app, the pending deposit in the widget shows the combined amount, and a cancel returns both.

> **Do we need a Zama API key?**
>
> No. The widget uses your Yield.xyz API key for Zama traffic.

## Next steps

> **[Zama API](https://docs.yield.xyz/guides/zama/index.md)**
>
> The full lifecycle behind the widget, for teams that build their own UI.

> **[Zama protocol](https://docs.yield.xyz/documentation/earn/defi/vaults/zama.md)**
>
> How confidential vaults, batches and the two exit modes work.

> **[Yield widget](https://docs.yield.xyz/guides/widgets-dashboard/yield-widget.md)**
>
> Install, configure and connect a wallet to the widget.

> **[Zama documentation](https://docs.zama.org/protocol/confidential-vault)**
>
> Zama's own guide to confidential vaults, batches and balances.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.