Decrypting to View
Use decryptForView to reveal a confidential (encrypted) value locally (in your app) so you can display it in the UI.
Unlike decryptForTx, this flow does not return an on-chain-verifiable signature, and it is not meant to be published on-chain.
The flow is as follows:
- Read the handle from your contract (often named
ctHashin code) (see Reading from a contract). - Ensure you have a permit that authorizes decryption of that value (see Permit quickstart).
- Call
decryptForView(ctHash, utype)and then run.execute()to get the plaintext (see Decrypt for UI).
Prerequisites
-
Create and connect a client (see the client page).
-
Know the handle (often named
ctHashin code) and the encrypted type (utype).
-
Have a permit available for the connected
chainId + account(see permits).If you don’t have one yet, the quickest way is to create a self permit once per account + chain.
Reading from a contract (getting a handle + utype)
Typically, the value you want to decrypt comes from a view call.
First, read the raw encrypted return value and convert it into { ctHash, utype } using @cofhe/abi. Here ctHash is the handle.
Below are two equivalent ways to read an encrypted return value from a predeployed Sepolia contract and derive a handle + utype.
// Viem: public client + contract call
const publicClient = createPublicClient({
chain: sepolia,
transport: http(rpcUrl),
});
const abi = [
{
type: 'function',
name: 'getValue',
stateMutability: 'view',
inputs: [],
outputs: [{ name: '', type: 'bytes32', internalType: 'euint32' }],
},
] as const;
// Read the raw encrypted return value.
const raw = await publicClient.readContract({
address: contractAddress,
abi,
functionName: 'getValue',
args: [],
});
// Convert raw ABI values into `{ ctHash, utype }`.
const encrypted = transformEncryptedReturnTypes(abi, 'getValue', raw);
encrypted.ctHash; // ^?
encrypted.utype; // ^?Permit quickstart
If you want a “just make it work” setup for UI decryption, do this once after connecting:
const publicClient = createPublicClient({
chain: sepolia,
transport: http(),
});
const walletClient = createWalletClient({
chain: sepolia,
transport: http(),
account,
});
await client.connect(publicClient, walletClient);
// Creates a permit if needed, stores it, and selects it as the active permit.
await client.permits.getOrCreateSelfPermit();After this, decryptForView(...) can automatically use the active permit when you run .execute().
Decrypt for UI
Choose the pattern that matches how your app manages permits:
const publicClient = createPublicClient({
chain: sepolia,
transport: http(),
});
const walletClient = createWalletClient({
chain: sepolia,
transport: http(),
account,
});
await client.connect(publicClient, walletClient);
await client.permits.getOrCreateSelfPermit();
const plaintext = await client
.decryptForView(ctHash, FheTypes.Uint32)
.execute();
plaintext; // ^?What decryptForView returns
Running .execute() on decryptForView(...) resolves to a scalar JS value (typed in TypeScript as UnsealedItem<U>):
- For integer utypes (
FheTypes.Uint8 | FheTypes.Uint16 | FheTypes.Uint32 | FheTypes.Uint64 | FheTypes.Uint128): abigint - For
FheTypes.Bool: aboolean - For
FheTypes.Uint160(address): a checksummed0x...address string
If you decrypt an integer type, you’ll usually want to:
- keep it as a
bigintand format it for display, or - convert to a JS
numberonly if you’re sure it’s within safe range.
After decrypting: common UI patterns
Pick the pattern that matches what you’re displaying:
const decimals = 6;
const display = formatUnits(amount, decimals);
display;Builder API
decryptForView(ctHash, utype) returns a builder. Unlike decryptForTx, this flow always requires a permit (there is no .withoutPermit() mode).
.execute() — required, call last
Runs the decryption and returns a UI-friendly scalar value (see What decryptForView returns).
const plaintext = await client
.decryptForView(ctHash, FheTypes.Uint32)
.execute();
plaintext;.withPermit(...) — optional
Select which permit to use:
.withPermit()uses the active permit for the resolvedchainId + account..withPermit(permitHash)fetches a stored permit by hash..withPermit(permit)uses the provided permit object.
If you don’t call .withPermit(...), decryptForView also uses the active permit for the resolved chainId + account.
const plaintext = await client
.decryptForView(ctHash, FheTypes.Uint8)
.withPermit()
.execute();
plaintext;.setAccount(address) — optional
Overrides the account used to resolve the active permit / stored permit.
const plaintext = await client
.decryptForView(ctHash, FheTypes.Uint32)
.setAccount('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045')
.withPermit()
.execute();
plaintext;.setChainId(chainId) — optional
Overrides the chain used to resolve the Threshold Network URL and permits.
const plaintext = await client
.decryptForView(ctHash, FheTypes.Uint16)
.setChainId(11155111)
.withPermit()
.execute();
plaintext;Common pitfalls
- Missing permit:
decryptForViewwill fail if there is no active permit for the currentchainId + account(or if the permit you pass doesn’t authorize decrypting that handle). - Wrong
utype: you must pass the correct FHE type for the handle.Boolandaddressare converted intoboolean/ checksummed0x...strings; integer types stay asbigint. - Wrong chain/account: permits are scoped to
chainId + account. If the user switches wallets or networks, create/select the correct permit (or set them explicitly via.setChainId(...)/.setAccount(...)).