Check balances
Decrypt and read confidential token balances using the SDK and React hooks.
Confidential balances are stored on-chain as encrypted values. To display a human-readable number, the SDK decrypts them using FHE permits tied to the user's wallet. This guide walks through reading balances, understanding the caching layer, and working with multiple tokens.
Steps
1. Read your own balance
Call balanceOf() on a Token instance. The SDK fetches the encrypted value from the chain, decrypts it, and returns a bigint. In React, useConfidentialBalance wraps the same call with polling and caching.
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 token = sdk.createToken("0xEncryptedERC20");
const [address] = await walletClient.getAddresses();
const balance = await token.balanceOf(address);
console.log(`Confidential balance: ${balance}`);import { useConfidentialBalance, useHasPermit } from "@zama-fhe/react-sdk";
import { useAccount } from "wagmi";
const { address } = useAccount();
// Gate the read behind an existing permit so it never triggers a wallet signature on
// render (see step 2). Flip `enabled` on from an explicit "Decrypt" action once the
// user opts in.
const { data: hasPermit } = useHasPermit({ contractAddresses: ["0xEncryptedERC20"] });
const {
data: balance,
isLoading,
error,
} = useConfidentialBalance(
{ address: "0xEncryptedERC20", account: address },
{ enabled: hasPermit },
);2. Understand the first-time wallet signature
The first balanceOf(address) call for a token prompts the user's wallet for an EIP-712 signature. This creates FHE decrypt permits that are cached in your storage backend. Subsequent reads are silent -- no wallet popup.
If the user rejects the signature, the SDK throws a SigningRejectedError. See Handle Errors for recovery patterns.
You can pre-authorize multiple tokens up front with grantPermit. It signs in batches of up to 10 contracts — so a set of ≤10 is a single signature (larger sets prompt once per batch) — after which balanceOf() calls for those tokens are silent:
3. Balance caching
Decrypted balances are automatically cached in your storage backend (IndexedDB, async local storage, etc.). This means:
No spinner on page reload -- if a balance was previously decrypted, it is returned instantly from cache instead of re-running the 2-5 second FHE decryption.
Automatic invalidation -- the cache key includes the on-chain encrypted value, so when a transfer, shield, or unshield changes the balance, the old cache entry is naturally bypassed.
Best-effort -- cache reads and writes never throw. If storage is unavailable, the SDK falls back to a fresh decryption silently.
The cache is keyed by token address + owner address + encrypted value.
A single balanceOf() call reads the on-chain encrypted value and decrypts it in one pass — the relayer is only hit when the encrypted value has changed since the last decryption:
4. Work with raw encrypted values
Sometimes you need the encrypted value itself, for example to check whether a balance exists before attempting decryption. This is a core-SDK concern — reach for confidentialBalanceOf and decryptValues directly:
5. Distinguish "no balance" from "zero balance"
These are different situations that your UI should handle separately:
NoCiphertextError-- the account has never shielded tokens. There is no encrypted balance to decrypt. Show something like "No confidential balance" in your UI.Balance of
0n-- the account has shielded before but currently holds zero. Show "Balance: 0".
6. Batch decrypt across multiple tokens
When your app manages a portfolio of confidential tokens, use batch operations to minimize wallet prompts and parallelize decryption.
7. Read token metadata
Before displaying balances, you typically want the token's name, symbol, and decimals.
See the useMetadata reference for full options.
8. (React) Poll for balance updates
useConfidentialBalance (step 1) and useConfidentialBalances (step 6) integrate with React Query out of the box. Pass refetchInterval to either to poll on a timer:
Under the hood, useConfidentialBalance calls token.balanceOf(owner) — reading the on-chain encrypted value and decrypting via the SDK. Cached clear values are served instantly; the relayer is only hit when the encrypted value changes, and clear values are persisted in storage so page reloads show the balance without a spinner.
9. (React) Force a manual refresh
Mutations automatically invalidate balance caches, so the balance refreshes on its own after a shield, transfer, or unshield:
But if you need manual control (for example, after an external contract interaction), use zamaQueryKeys:
See the query keys reference for the raw key factories when you need finer targeting.
Next steps
See Avoid blind-sign wallet popups to gate balance queries behind explicit user action.
See Token Operations for the full
TokenAPI.See Hooks for
useConfidentialBalance,useConfidentialBalances, and query key details.To handle
NoCiphertextErrorand other failures, see Handle Errors.
Last updated