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;

.onPoll(callback) — optional

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

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.

For the full meaning of submit retries versus request-status polling, see Decryption Lifecycle.

const result = await client
  .decryptForTx(ctHash)
  .onPoll(({ attemptIndex, elapsedMs }) => { 
    console.log('decrypt polling', { attemptIndex, elapsedMs }); 
  }) 
  .withPermit()
  .execute();
 
result.signature;

.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 result = await client
  .decryptForTx(ctHash)
  .set404RetryTimeout(15_000) 
  .withPermit()
  .execute();
 
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(...)).