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;.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 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(...)).