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:
- Call
decryptForTx(ctHash)with the encrypted handle you want to reveal (see Decrypt for a transaction). - Receive the plaintext value and a Threshold Network signature that binds that plaintext to the handle (see What
decryptForTxreturns). - 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
-
Create and connect a client (see the client page).
-
Know the on-chain encrypted handle you want to decrypt (often named
ctHashin 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).
-
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.
- If the policy allows anyone to decrypt it (a common setup), a permit is not required and
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: 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 abigintor a0x...hex string)decryptedValue: bigint— the plaintext value (always abigint, even if the underlying type is e.g.uint32)signature:0x${string}— the Threshold Network signature as a hex string with0x
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:
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 resolvedchainId + account..withPermit(permitHash)fetches a stored permit by hash..withPermit(permit)uses the provided permit object.
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(...)).