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 an ACP that authorizes decryption of that value (see ACP 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 an ACP available for the connected chainId + account (see ACPs).

    If you don’t have one yet, the quickest way is to create a self ACP 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; // ^?

ACP 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 an ACP if needed, stores it, and selects it as the active acp.
await client.acp.getOrCreateSelfACP();

After this, decryptForView(...) can automatically use the active ACP when you run .execute().

Decrypt for UI

Choose the pattern that matches how your app manages ACPs:

Active acp
 
const publicClient = createPublicClient({
  chain: sepolia,
  transport: http(),
});
const walletClient = createWalletClient({
  chain: sepolia,
  transport: http(),
  account,
});
 
await client.connect(publicClient, walletClient);
 
await client.acp.getOrCreateSelfACP();
 
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 an ACP (there is no .withoutACP() 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;

.onPoll(callback) — optional

Registers a callback that runs once per poll attempt while waiting for the threshold network to complete the request.

This is useful for showing “decrypting…” UI, measuring latency, or logging.

During submit retries, requestId is an empty string because the backend has not created a request yet. This can happen while the SDK is retrying transient 204 or short-lived 404 submit responses.

const plaintext = await client
  .decryptForView(ctHash, FheTypes.Uint32)
  .onPoll(({ attemptIndex, elapsedMs }) => { 
    console.log('sealoutput polling', { attemptIndex, elapsedMs }); 
  }) 
  .execute();
 
plaintext;

.set404RetryTimeout(timeoutMs) — optional

Controls how long the SDK should keep retrying submit-time 404 Not Found responses before failing. The default is 10000 ms. This retry only applies while the backend reports the ciphertext isn't indexed yet (ct_not_found); a 404 with any other error code fails immediately.

Use this when your backend is eventually consistent and may need a few seconds to index a freshly-created ciphertext handle.

const plaintext = await client
  .decryptForView(ctHash, FheTypes.Uint32)
  .set404RetryTimeout(15_000) 
  .execute();
 
plaintext;

.withACP(...) — optional

Select which ACP to use:

  • .withACP() uses the active ACP for the resolved chainId + account.
  • .withACP(acpHash) fetches a stored ACP by hash.
  • .withACP(acp) uses the provided ACP object.

If you don’t call .withACP(...), decryptForView also uses the active ACP for the resolved chainId + account.

Active acp
const plaintext = await client
  .decryptForView(ctHash, FheTypes.Uint8)
  .withACP() 
  .execute();
 
plaintext;

.setAccount(address) — optional

Overrides the account used to resolve the active ACP / stored ACP.

const plaintext = await client
  .decryptForView(ctHash, FheTypes.Uint32)
  .setAccount('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045') 
  .withACP()
  .execute();
 
plaintext;

.setChainId(chainId) — optional

Overrides the chain used to resolve the Threshold Network URL and ACPs.

const plaintext = await client
  .decryptForView(ctHash, FheTypes.Uint16)
  .setChainId(11155111) 
  .withACP()
  .execute();
 
plaintext;

Common pitfalls

  • Missing ACP: decryptForView will fail if there is no active ACP for the current chainId + account (or if the ACP 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: ACPs are scoped to chainId + account. If the user switches wallets or networks, create/select the correct ACP (or set them explicitly via .setChainId(...) / .setAccount(...)).