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:
onUnwrapSubmitted-- fires when the first transaction is mined.onFinalizing-- fires while the SDK polls for the decryption proof (this can take several seconds).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:
getPendingUnshield-- returns the unwrap transaction hash of an interrupted unshield, ornullif 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 andnullcomes back, so a stale entry never shows a resume prompt.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.
A resume can lose the race. Another tab, a double click, or a third party can finalize the same request first. resumeUnshield then clears the persisted state and throws UnshieldAlreadyFinalizedError instead of broadcasting a transaction that would revert. Treat the error as completion: the funds already arrived, so dismiss the prompt and refresh balances. useResumeUnshield refreshes the affected queries automatically. See UnshieldAlreadyFinalizedError.
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)(orunwrapAll()) submits phase 1 and returns anUnwrapResult: thetxHash, minedreceipt, and theunwrapRequestIdneeded 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.
When you drive the two phases yourself, you own the state between them. getPendingUnshield() and resumeUnshield() only track unshields started through unshield() -- they will not pick up an unwrap you submitted directly. Persist the unwrapRequestId yourself if the session might end before you finalize.
Next steps
See WrappedToken for the full
WrappedToken.unshieldandWrappedToken.unshieldAllAPI.See Hooks for
useUnshield,useUnshieldAll,useResumeUnshield, and the low-leveluseUnwrap/useUnwrapAll/useFinalizeUnwrapdetails.If your unshield fails, see Handle Errors for troubleshooting
TransactionRevertedErrorand related issues.
Last updated