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;.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.
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;.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(...)).