For the complete documentation index, see llms.txt. This page is also available as Markdown.

Unshield tokens

Convert confidential tokens back to public ERC-20 by unshielding them.

Unshielding converts encrypted tokens back into standard ERC-20 tokens that are visible on-chain. The process involves two on-chain steps (unwrap and finalize), but the SDK handles both in a single call — wrappedToken.unshield() in the core SDK, or the useUnshield hook in React.

Steps

1. Unshield a specific amount

Call unshield() with the amount you want to convert back to public tokens. The SDK submits the unwrap transaction, waits for the decryption proof, and then submits the finalize transaction.

By default, the SDK validates the confidential balance before submitting. If the balance is insufficient, it throws InsufficientConfidentialBalanceError before any transaction is sent. Pass skipBalanceCheck: true to bypass (e.g. for smart wallets that cannot produce EIP-712 signatures).

import { createConfig } from "@zama-fhe/sdk/viem";
import { ZamaSDK } from "@zama-fhe/sdk";
import { web } from "@zama-fhe/sdk/web";
import { sepolia } from "@zama-fhe/sdk/chains";

const config = createConfig({
  chains: [sepolia],
  publicClient,
  walletClient,
  storage,
  relayers: { [sepolia.id]: web() },
});
const sdk = new ZamaSDK(config);
const wrappedToken = sdk.createWrappedToken("0xWrappedEncryptedERC20");

const { txHash, receipt } = await wrappedToken.unshield(500n);
import { useUnshield } from "@zama-fhe/react-sdk";

const { mutateAsync: unshield, isPending } = useUnshield("0xWrappedEncryptedERC20");

const { txHash, receipt } = await unshield({ amount: 500n });

The returned txHash is the finalize transaction hash. The receipt confirms on-chain completion.

2. Track progress with callbacks

Because unshielding involves two transactions with a waiting period in between, you can provide callbacks to keep your UI in sync with each phase.

Callbacks are safe to use -- if one throws, the unshield still completes. The typical timeline is:

  1. onUnwrapSubmitted -- fires when the first transaction is mined.

  2. onFinalizing -- fires while the SDK polls for the decryption proof (this can take several seconds).

  3. onFinalizeSubmitted -- fires when the second transaction is mined and the tokens are public again.

3. Unshield your entire balance

If you want to convert all confidential tokens back to public, use unshieldAll(). It reads the current encrypted balance and unshields the full amount directly, without decrypting it first. It accepts the same lifecycle callbacks as unshield() — but not the skipBalanceCheck option from step 1, which is specific to unshield().

4. Handle interrupted unshields

If the user closes their browser between the unwrap and finalize steps, the unwrap is on-chain but the finalize has not happened yet. The SDK persists the unwrap transaction hash automatically when phase 1 is submitted and clears it once finalization confirms, so you only need to detect and resume the pending state on the next page load.

Resuming is intentionally caller-driven: surface a "resume" prompt rather than finalizing on load, so you never trigger a wallet transaction the user did not initiate.

The flow is:

  1. getPendingUnshield -- returns the unwrap transaction hash of an interrupted unshield, or null if none is pending. The SDK saved it automatically during phase 1. Before reporting it, the SDK verifies it on-chain: an already-finalized request is cleared and null comes back, so a stale entry never shows a resume prompt.

  2. resumeUnshield -- picks up where the SDK left off by polling for the proof and submitting the finalize transaction. On success the SDK clears the persisted state for you.

The SDK persists and clears the pending-unshield state for you — there are no storage helpers to call by hand. getPendingUnshield() (read) and unshield() / resumeUnshield() (orchestrated write) are the full surface. If you orchestrate unwrap + finalizeUnwrap yourself, manage your own persistence between the two phases. React mutation hooks automatically invalidate balance queries on success, so your UI stays in sync without manual cache management.

5. Decompose the unshield into explicit phases

unshield() orchestrates both on-chain steps for you and should be your default. Drop down to the low-level primitives only when you genuinely need per-phase control -- for example, to submit the unwrap now and let the user finalize later from a different screen.

  • unwrap(amount) (or unwrapAll()) submits phase 1 and returns an UnwrapResult: the txHash, mined receipt, and the unwrapRequestId needed for phase 2.

  • finalizeUnwrap(unwrapRequestId) fetches the public decryption proof and submits phase 2.

Use unwrapAll() / useUnwrapAll in place of unwrap / useUnwrap to request the full confidential balance instead of a fixed amount.

Next steps

  • See WrappedToken for the full WrappedToken.unshield and WrappedToken.unshieldAll API.

  • See Hooks for useUnshield, useUnshieldAll, useResumeUnshield, and the low-level useUnwrap / useUnwrapAll / useFinalizeUnwrap details.

  • If your unshield fails, see Handle Errors for troubleshooting TransactionRevertedError and related issues.

Last updated