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 Transact

Use decryptForTx to reveal a confidential (encrypted) value on-chain: it returns the plaintext together with a Threshold Network signature, so a contract can verify the reveal when you publish it in a transaction.

The flow is as follows:

  1. Call decryptForTx(ctHash) with the encrypted handle you want to reveal (see Decrypt for a transaction).
  2. Receive the plaintext value and a Threshold Network signature that binds that plaintext to the handle (see What decryptForTx returns).
  3. Submit an on-chain transaction that publishes or verifies the result (see Writing Decrypt Result to Contract).

Common examples:

  • Unshield a confidential token: reveal the encrypted amount you’re unshielding so the contract can finalize the public transfer.
  • Finalize a private auction / game move: bids or moves are submitted encrypted, and the winner is revealed later in a verifiable way.

Prerequisites

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

  2. Know the on-chain encrypted handle you want to decrypt (often named ctHash in code).

In most apps, you get the handle by reading it from your contract (e.g. a stored encrypted value, an event arg, or a return value from a view call).

  1. Determine whether the contract-level ACL policy for this specific handle requires a permit (see permits).

    • If the policy allows anyone to decrypt it (a common setup), a permit is not required and .withoutPermit() will work.
    • If the policy restricts decryption, you must use .withPermit(...) or the decryption will fail.

Reading from a contract (getting a handle)

Typically, the value you want to reveal comes from a view call (or from an event arg / stored encrypted value).

If your contract ABI uses encrypted internalTypes (e.g. internalType: "euint32" while the ABI type is bytes32), you can convert the raw return value into a { ctHash, utype } pair using @cofhe/abi. Here ctHash is the handle.

For decryptForTx, you only need the handle.

Below are two equivalent ways to read an encrypted return value from a predeployed Sepolia contract and derive a handle.

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; // ^?

Preparations: Permit (only if required)

Often, decryptForTx is used to reveal a value at a point where your protocol already considers it OK for that value to become public on-chain. In those cases, there’s usually no need to restrict who is allowed to perform the reveal, so the contract’s ACL policy can allow anyone to decrypt.

For example:

  • Unshielding: once a user chooses to unshield (i.e. convert a portion of their confidential balance into a public amount), the amount being unshielded is no longer meant to stay secret.
  • Auction / game reveal: when it’s time to reveal the outcome, it typically doesn’t matter who submits the reveal transaction — only that the result is revealed verifiably.

In these cases, you can skip permits and use .withoutPermit().

If the ACL policy restricts decryption for this handle, obtain a permit before calling decryptForTx:

  • If decryption is restricted to your address, generate a self-permit.
  • If decryption is restricted to a different address, import a permit from the address that is allowed to decrypt.

For details, see permits.

Decision guide:

ACL policy for this ctHash

├─ Anyone can decrypt (no restriction)
│  └─ No permit needed
│     └─ decryptForTx(ctHash)
│           .withoutPermit()
│           .execute()

└─ Restricted to a specific address

   ├─ Your address is allowed
   │  └─ Generate a self-permit
   │     └─ decryptForTx(ctHash)
   │           .withPermit()
   │           .execute()

   └─ A different address is allowed
      └─ Import a permit from them
         └─ decryptForTx(ctHash)
               .withPermit(importedPermit)
               .execute()

Decrypt for a transaction

What decryptForTx returns

decryptForTx(...).execute() resolves to an object with:

  • ctHash: bigint | string — the handle you decrypted (either a bigint or a 0x... hex string)
  • decryptedValue: bigint — the plaintext value (always a bigint, even if the underlying type is e.g. uint32)
  • signature: 0x${string} — the Threshold Network signature as a hex string with 0x

You’ll typically pass decryptedValue and signature directly into your transaction.

Decrypt (choose permit mode)

Choose the mode that matches the contract’s ACL policy for this handle:

No permit
const decryptResult = await client
  .decryptForTx(ctHash)
  .withoutPermit()
  .execute();
 
decryptResult; // ^?
decryptResult.decryptedValue;
decryptResult.signature;

Next step: write the transaction

Once you have { ctHash, decryptedValue, signature }, you’ll typically submit a transaction that either:

  • publishes the result via FHE.publishDecryptResult(...), or
  • verifies it inside your contract via FHE.verifyDecryptResult(...).

See Writing Decrypt Result to Contract for examples.

Builder API

decryptForTx(ctHash) returns a builder. Before calling .execute(), you must select exactly one permit mode: .withPermit(...) or .withoutPermit().

.execute() — required, call last

Runs the decryption and returns { ctHash, decryptedValue, signature }.

const result = await client.decryptForTx(ctHash).withPermit().execute(); 
 
result.decryptedValue;
result.signature;

.withPermit(...) — required unless using .withoutPermit()

Decrypt using a permit:

  • .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.
Active permit
const result = await client
  .decryptForTx(ctHash)
  .withPermit() 
  .execute();
 
result.signature;

.withoutPermit() — required unless using .withPermit(...)

Decrypt via global allowance (no permit). This only works if your contract’s ACL policy allows anyone to decrypt that handle.

const result = await client
  .decryptForTx(ctHash)
  .withoutPermit() 
  .execute();
 
result.decryptedValue;

.setAccount(address) — optional

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

const result = await client
  .decryptForTx(ctHash)
  .setAccount('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045') 
  .withPermit()
  .execute();
 
const handleFromResult = result.ctHash;
handleFromResult;

.setChainId(chainId) — optional

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

const result = await client
  .decryptForTx(ctHash)
  .setChainId(11155111) 
  .withPermit()
  .execute();
 
result.signature;

Common pitfalls

  • Permit mode must be selected: you must call exactly one of .withPermit(...) or .withoutPermit() before .execute().
  • Wrong chain/account: permits are scoped to chainId + account. If you use .withPermit() and get an ACL/permit error, double-check you’re connected to the expected chain and account (or explicitly set them via .setChainId(...) / .setAccount(...)).