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 an ACP that authorizes decryption of that value (see ACP 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 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: 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:
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): 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 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 resolvedchainId + 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.
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:
decryptForViewwill fail if there is no active ACP for the currentchainId + 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.Boolandaddressare converted intoboolean/ checksummed0x...strings; integer types stay asbigint. - 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(...)).