Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

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:

  1. Read the handle from your contract (often named ctHash in code) (see Reading from a contract).
  2. Ensure you have a permit that authorizes decryption of that value (see Permit quickstart).
  3. Call decryptForView(ctHash, utype) and then run .execute() to get the plaintext (see Decrypt for UI).

Prerequisites

  1. Create and connect a client (see the client page).

  2. Know the handle (often named ctHash in code) and the encrypted type (utype).

  1. 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
 
// 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:

Active permit
 
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): a bigint
  • For FheTypes.Bool: a boolean
  • For FheTypes.Uint160 (address): a checksummed 0x... address string

If you decrypt an integer type, you’ll usually want to:

  • keep it as a bigint and format it for display, or
  • convert to a JS number only if you’re sure it’s within safe range.

After decrypting: common UI patterns

Pick the pattern that matches what you’re displaying:

Format bigint for display
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 resolved chainId + 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.

Active permit
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: decryptForView will fail if there is no active permit for the current chainId + 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. Bool and address are converted into boolean / checksummed 0x... strings; integer types stay as bigint.
  • 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(...)).