View vs Publish in the new @cofhe/sdk

17 April 2026

CoFHE makes encrypted computation practical for on-chain applications — but the sharp edge, especially for new builders, has always been decryption: when you can decrypt, who can decrypt, and what it means to “reveal” a value in a public system.

With the new @cofhe/sdk, we’re shipping a clearer, safer decryption model that matches how real applications behave:

  • sometimes you only need plaintext locally (to render a UI)
  • sometimes you need plaintext on-chain (to drive protocol logic), and you need a verifiable proof for that reveal

This post explains what changed, why we changed it, and how to integrate it cleanly.

The problem: “decrypt” isn’t one thing

On public chains, decryption is never just “get me the number.” It’s a security boundary:

  • If a value is decrypted locally, it can remain confidential on-chain.
  • If a value is decrypted for a transaction, it becomes part of public state (or at least public calldata/events), and other contracts/users can now reason about it.

Historically, it’s easy for developers to conflate these intents — and that leads to footguns:

  • accidentally revealing values when you only needed them for display
  • wiring in heavy on-chain flows for simple UI reads
  • unclear “who is allowed to decrypt?” behavior

What we shipped: two explicit decryption flows

1) Decrypt to View (UI-only)

Use this when you want to show plaintext in your app while keeping the value confidential on-chain.

  • API: client.decryptForView(ctHash, utype).execute()
  • Returns: a typed plaintext value (e.g. bigint, boolean, address string)
  • Permit: always required

On-chain, these encrypted handles are stored as bytes32. In your client code you’ll usually treat ctHash as a 0x... string or a bigint — both forms are accepted by the SDK.

Conceptually, the dapp is saying:

“I’m authorized to see this, but I’m not publishing it.”

You’ll usually pair this with contract reads that return encrypted handles (e.g. euint64), transforming them to { ctHash, utype } before decrypting.

2) Decrypt to Transact (on-chain publish)

Use this when the plaintext must be revealed verifiably on-chain (examples: unshielding, auction reveals, game outcomes, any “reveal step”).

  • API: client.decryptForTx(ctHash).withPermit().execute() or .withoutPermit().execute()
  • Returns: { ctHash, decryptedValue, signature }
  • Permit: depends on contract ACL
    • use .withoutPermit() only if the contract made the handle publicly decryptable (e.g. FHE.allowPublic / FHE.allowGlobal)
    • otherwise use .withPermit(...) so the Threshold Network can enforce per-address ACL

That output is designed to plug directly into contract-side verification/publishing patterns:

  • FHE.publishDecryptResult(...)
  • FHE.verifyDecryptResult(...)

Conceptually, the dapp is saying:

“I’m revealing this publicly, and here is a proof this plaintext matches the ciphertext.”

Why this is better (the receipts)

Clear security boundary (less accidental leakage)

By separating view vs publish intent at the API level, the SDK forces a conscious choice.

If you need a UI value → stay in the view flow If you need a protocol reveal → opt into on-chain + signature verification

Better developer ergonomics (fewer ambiguous states)

Permits are explicit and first-class:

  • you create or import them
  • you select them
  • decrypt operations reflect whether a permit is required

This makes wallet prompts and auth flows easier to reason about.

More composable protocol design

decryptForTx returns a payload that directly matches on-chain verification flows, enabling reusable reveal patterns across protocols.

Integration walkthrough: choosing the right flow

Decision guide

  • Plaintext only for UI? → decryptForView
  • Plaintext must be public on-chain? → decryptForTx + publish/verify

Common patterns

Pattern A: Private balances / per-user views

  • Read encrypted handle from contract
  • Ensure user has permit
  • Use decryptForView

Pattern B: Reveal step (unshield, auction, claim)

  • Ensure contract ACL is correct (FHE.allow, FHE.allowSender, FHE.allowPublic)
  • Use decryptForTx (with or without permit depending on ACL)
  • Pass { ctHash, decryptedValue, signature } into contract

Failure modes and how to debug

Decrypt-for-view fails

Usually:

  • missing/incorrect permit (chainId + account scoped)
  • wrong utype
  • ACL doesn’t grant access

Decrypt-for-tx fails

Usually:

  • .withoutPermit() but no public decrypt ACL
  • .withPermit() but permit doesn’t match handle or chain/account
  • contract-side verification is wrong (missing verifyDecryptResult)

Closing: shipping the safer default

The theme of this release is explicitness:

  • explicit intent (view vs publish)
  • explicit authorization (permits)
  • explicit verification (signatures)

If you’re migrating, audit every decrypt call:

→ label it UI-only or protocol reveal

Quick migration guide: legacy FHE.decrypt → new flow

1) Replace decrypt trigger with explicit permission

Old:

FHE.decrypt(encryptedValue)

New:

  • FHE.allowPublic(encryptedValue)
  • or FHE.allow(encryptedValue, address)

2) Move decryption to the client

  • Public:
    client.decryptForTx(ctHash).withoutPermit().execute()
  • Restricted:
    client.decryptForTx(ctHash).withPermit(...).execute()

Returns:

  • decryptedValue
  • signature
  • ctHash

3) Update contract finalize logic

  • Use FHE.publishDecryptResult(...) for stored results
  • Use FHE.verifyDecryptResult(...) for one-time validation

4) Keep getDecryptResultSafe only if needed

  • Still valid for reading published results
  • Not needed if using verifyDecryptResult

Docs + migration links

Confidential Computing for

the Next Wave of DeFi

Join developers and protocols building the next generation of

onchain applications — powered by encrypted execution.