# Cofhe SDK Docs > Documentation for the Cofhe SDK ## Example This is an example page. The `@cofhe/foundry` package is currently in development. ## Get started Hello world! ## Getting Started `@cofhe/hardhat-plugin` extends Hardhat with everything you need to build and test FHE-enabled contracts locally — mock contracts, a pre-configured CoFHE client, and test utilities. ### What the plugin provides * **Mock contracts** deployed automatically on every `npx hardhat test` / `npx hardhat node` run, simulating the full CoFHE coprocessor stack on the Hardhat network. * **`hre.cofhe`** — a namespaced API for creating and connecting CoFHE clients with Hardhat signers. * **`hre.cofhe.mocks`** — utilities for reading raw plaintext values and interacting with mock contracts directly in tests. * **Pre-configured networks** — `localcofhe`, `eth-sepolia`, and `arb-sepolia` are injected automatically. ### Installation ::::steps #### Install the package :::code-group ```bash [npm] npm install @cofhe/hardhat-plugin ``` ```bash [pnpm] pnpm add @cofhe/hardhat-plugin ``` ```bash [yarn] yarn add @cofhe/hardhat-plugin ``` ::: #### Import in your Hardhat config ```ts [hardhat.config.ts] import '@cofhe/hardhat-plugin'; // [!code focus] export default { solidity: '0.8.28', }; ``` That's it. The plugin automatically deploys the mock contracts before every test run. :::: ### Configuration The plugin adds an optional `cofhe` key to your Hardhat config: ```ts [hardhat.config.ts] import '@cofhe/hardhat-plugin'; export default { solidity: '0.8.28', cofhe: { logMocks: true, // log FHE ops to the console (default: true) gasWarning: true, // warn when mock gas usage is high (default: true) }, }; ``` ### Pre-configured networks The following networks are injected automatically. You can override any of them by defining the same key under `networks` in your config. | Network | URL | Chain ID | | ------------- | --------------------------- | ---------- | | `localcofhe` | `http://127.0.0.1:42069` | — | | `eth-sepolia` | Ethereum Sepolia public RPC | `11155111` | | `arb-sepolia` | Arbitrum Sepolia public RPC | `421614` | For testnets, set `PRIVATE_KEY` (and optionally `SEPOLIA_RPC_URL` / `ARBITRUM_SEPOLIA_RPC_URL`) in your environment. ### Auto-deployment Mock contracts are deployed automatically before: * `npx hardhat test` * `npx hardhat node` To skip auto-deployment (e.g., when running tests only against an external RPC): ```bash COFHE_SKIP_MOCKS_DEPLOY=1 npx hardhat test ``` ### Next steps * [Client](/hardhat/client) — Create and connect a CoFHE client in tests * [Mock Contracts](/hardhat/mock-contracts) — Read plaintext values and interact with mock contracts * [Logging](/hardhat/logging) — Inspect FHE operations in test output * [Testing](/hardhat/testing) — End-to-end test patterns ## Migrating from `cofhejs` `@cofhe/sdk` is the successor to `cofhejs`, redesigned around an explicit, builder-pattern API that gives you full control over encryption, decryption, and permit management. Migration should be strightforward with a 1-to-1 mapping of functions and features. #### Why migrate? * **Explicit API** — no more implicit initialization or auto-generated permits. Every action is opt-in. * **Builder pattern** — `encryptInputs`, `decryptForView`, and `decryptForTx` use a chainable builder so you can set overrides (account, chain, callbacks) before calling `.execute()`. * **`decryptForTx` feature** — `cofhejs` does not provide an api for generating decryption signatures for on-chain usage. * **Deferred key loading** — FHE keys and TFHE WASM are fetched lazily on the first `encryptInputs` call, not during initialization. * **Better multichain support** — configure multiple chains up front and override per-call. * **Structured errors** — typed `CofheError` objects with error codes replace the `Result` wrapper. #### Requirements * Node.js 18+ * TypeScript 5+ * Viem 2+ #### Installation Remove `cofhejs` and install `@cofhe/sdk`: ```bash npm uninstall cofhejs && npm install @cofhe/sdk ``` ### 1. Initialization The single `cofhejs.initializeWithEthers(...)` / `cofhejs.initializeWithViem(...)` call is replaced by a three-step flow: create a config, create a client, then connect. FHE keys and WASM are no longer fetched eagerly during init — they are deferred until the first `encryptInputs` call. ::::note :::details[Changes] | | `cofhejs` | `@cofhe/sdk` | | ------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------ | | **Entry** | `cofhejs.initializeWithEthers(...)` / `cofhejs.initializeWithViem(...)` | `createCofheConfig(...)` → `createCofheClient(config)` → `client.connect(...)` | | **Key fetching** | Immediate (during init) | Deferred (first `encryptInputs` call) | | **WASM init** | Immediate (during init) | Deferred (first `encryptInputs` call) | | **Environment** | `"LOCAL"` / `"TESTNET"` / `"MAINNET"` string | Chain objects via `supportedChains: [chains.sepolia]` | | **Provider format** | Ethers provider/signer or viem clients | Always viem clients (use adapters for ethers) | ::: :::: #### Before (`cofhejs`) `cofhejs` uses a global singleton that is initialized eagerly — keys are fetched, the WASM module is loaded, and the provider/signer are stored all in one call: :::code-group ```ts [Ethers] import { cofhejs } from 'cofhejs/node'; await cofhejs.initializeWithEthers({ ethersProvider: provider, ethersSigner: signer, environment: 'TESTNET', }); ``` ```ts [Viem] import { cofhejs } from 'cofhejs/web'; await cofhejs.initializeWithViem({ viemClient: publicClient, viemWalletClient: walletClient, environment: 'TESTNET', }); ``` ::: #### After (`@cofhe/sdk`) `@cofhe/sdk` splits initialization into three steps: **config → client → connect**. No keys are fetched until you actually encrypt. :::code-group ```ts twoslash [Web (viem)] // @noErrors import { type PublicClient, type WalletClient } from 'viem'; declare const publicClient: PublicClient; declare const walletClient: WalletClient; // ---cut--- import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; const config = createCofheConfig({ supportedChains: [chains.sepolia], }); const client = createCofheClient(config); await client.connect(publicClient, walletClient); ``` ```ts twoslash [Node (viem)] // @noErrors import { type PublicClient, type WalletClient } from 'viem'; declare const publicClient: PublicClient; declare const walletClient: WalletClient; // ---cut--- import { createCofheConfig, createCofheClient } from '@cofhe/sdk/node'; import { chains } from '@cofhe/sdk/chains'; const config = createCofheConfig({ supportedChains: [chains.sepolia], }); const client = createCofheClient(config); await client.connect(publicClient, walletClient); ``` ```ts twoslash [Ethers v6 (via adapter)] // @noErrors import { type Provider, type Signer } from 'ethers'; declare const provider: Provider; declare const signer: Signer; // ---cut--- import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { Ethers6Adapter } from '@cofhe/sdk/adapters'; import { chains } from '@cofhe/sdk/chains'; const config = createCofheConfig({ supportedChains: [chains.sepolia], }); const client = createCofheClient(config); const { publicClient, walletClient } = await Ethers6Adapter( provider, signer ); await client.connect(publicClient, walletClient); ``` ::: ### 2. Encrypting inputs `cofhejs.encrypt(...)` is replaced by a builder: `client.encryptInputs([...]).execute()`. The builder lets you chain optional overrides (account, chain, worker, progress callback) before executing. ::::note :::details[Changes] | | `cofhejs` | `@cofhe/sdk` | | --------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------- | | **Function** | `cofhejs.encrypt([...], callback)` | `client.encryptInputs([...]).execute()` | | **Return value** | `Result` with `.success` / `.data` / `.error` | Direct value (throws `CofheError` on failure) | | **Progress callback** | Second argument to `encrypt` | `.onStep(callback)` on the builder | | **Callback states** | `Extract`, `Pack`, `Prove`, `Verify`, `Replace`, `Done` | `InitTfhe`, `FetchKeys`, `Pack`, `Prove`, `Verify` | | **Result types** | `CoFheInUint8`, `CoFheInBool`, etc. | `EncryptedUint8Input`, `EncryptedBoolInput`, etc. (all extend `EncryptedItemInput`) | | **Overrides** | Not available | `.setAccount(...)`, `.setChainId(...)`, `.setUseWorker(...)` | | **Mixed-type arrays** | `cofhejs.encrypt([{ a: Encryptable.uint8(10) }])` | Not supported — pass a flat array of `Encryptable` items | ::: :::: #### Before (`cofhejs`) ```ts import { cofhejs, Encryptable } from 'cofhejs/node'; const result = await cofhejs.encrypt( [Encryptable.uint64(42n), Encryptable.bool(true)], (state) => console.log(state) ); if (!result.success) { console.error(result.error); return; } const [eAmount, eFlag] = result.data; ``` #### After (`@cofhe/sdk`) ```ts twoslash // @noErrors import { type CofheClient, type CofheConfig } from '@cofhe/sdk'; declare const client: CofheClient; // ---cut--- import { Encryptable, EncryptStep } from '@cofhe/sdk'; const [eAmount, eFlag] = await client .encryptInputs([Encryptable.uint64(42n), Encryptable.bool(true)]) .onStep((step, ctx) => { if (ctx?.isStart) console.log(`Starting: ${step}`); }) .execute(); ``` :::note The `Encryptable` factory functions (`Encryptable.uint32(...)`, `Encryptable.bool(...)`, etc.) work the same way in both libraries. ::: ### 3. Decrypting / Unsealing `cofhejs` has a single `unseal` function. `@cofhe/sdk` splits decryption into two purpose-built methods: * **`decryptForView`** — returns the plaintext for UI display (no on-chain signature). * **`decryptForTx`** — returns the plaintext **and** a Threshold Network signature for on-chain verification. ::::note :::details[Changes] | | `cofhejs` | `@cofhe/sdk` | | ------------------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------- | | **Function** | `cofhejs.unseal(sealed, type)` | `client.decryptForView(ctHash, type)` or `client.decryptForTx(ctHash)` | | **Permit handling** | Automatic (uses most recent permit) | Explicit — `.withPermit()` / `.withPermit(permit)` / `.withPermit(hash)` / `.withoutPermit()` | | **Return value** | `Result` | Direct value: `bigint`, `boolean`, or `string` for view; `{ ctHash, decryptedValue, signature }` for tx | | **On-chain verification** | Not built in | `decryptForTx` returns a signature for `FHE.publishDecryptResult(...)` or `FHE.verifyDecryptResult(...)` | | **`utype`** | Required for `unseal` | Required for `decryptForView`, not needed for `decryptForTx` | ::: :::: #### Before (`cofhejs`) ```ts import { cofhejs, FheTypes } from 'cofhejs/node'; const sealedBalance = await contract.getBalance(); const result = await cofhejs.unseal(sealedBalance, FheTypes.Uint64); if (!result.success) { console.error(result.error); return; } console.log(result.data); // bigint ``` #### After (`@cofhe/sdk`) — viewing in UI ```ts twoslash // @noErrors import { type CofheClient, type CofheConfig } from '@cofhe/sdk'; declare const client: CofheClient; declare const contract: { getBalance: () => Promise }; // ---cut--- import { FheTypes } from '@cofhe/sdk'; const ctHash = await contract.getBalance(); const balance = await client .decryptForView(ctHash, FheTypes.Uint64) .execute(); ``` #### After (`@cofhe/sdk`) — publishing on-chain :::code-group ```ts twoslash [TypeScript] // @noErrors import { type CofheClient, type CofheConfig } from '@cofhe/sdk'; declare const client: CofheClient; declare const myContract: { getEncryptedAmount: () => Promise; publishDecryptResult: ( ctHash: string, decryptedValue: bigint, signature: string ) => Promise; }; // ---cut--- const ctHash = await myContract.getEncryptedAmount(); const { decryptedValue, signature } = await client .decryptForTx(ctHash) .withoutPermit() .execute(); await myContract.publishDecryptResult(ctHash, decryptedValue, signature); ``` ```solidity [MyContract.sol] import '@fhenixprotocol/cofhe-contracts/FHE.sol'; contract MyContract { euint64 private _encryptedAmount; function getEncryptedAmount() public view returns (euint64) { return _encryptedAmount; } function publishDecryptResult( euint64 ctHash, uint64 plaintext, bytes calldata signature ) external { FHE.publishDecryptResult(ctHash, plaintext, signature); } } ``` ::: ### 4. Permits Permits are no longer auto-generated during initialization. All permit operations are now explicit through `client.permits`, with separate methods for self permits, sharing permits, and importing shared permits. ::::note :::details[Changes] | | `cofhejs` | `@cofhe/sdk` | | ------------------- | ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | **Auto-generation** | `generatePermit: true` (default) | Never — always explicit | | **Creation** | `cofhejs.createPermit({ type, issuer })` | `client.permits.createSelf(...)`, `client.permits.createSharing(...)`, `client.permits.importShared(...)` | | **Return type** | `Result` | Direct `Permit` object | | **Active permit** | Implicitly used by `unseal` | `getOrCreateSelfPermit()` sets the active permit; `decryptForView/Tx` uses it automatically | | **Sharing** | `createPermit({ type: "sharing", recipient })` → `createPermit({ type: "recipient", ...permit })` | `createSharing(...)` → `importShared(...)` | | **Selection** | Implicit | `client.permits.selectActivePermit(hash)` | | **Retrieval** | `cofhejs.getPermit(...)` | `client.permits.getPermit(hash)`, `client.permits.getPermits()`, `client.permits.getActivePermit()` | ::: :::: #### Before (`cofhejs`) Permits could be auto-generated during initialization (`generatePermit: true`, the default), or created manually: ```ts // Auto-generated during init (default) await cofhejs.initializeWithEthers({ ethersProvider: provider, ethersSigner: signer, environment: 'TESTNET', // generatePermit: true ← default }); // Manual creation const result = await cofhejs.createPermit({ type: 'self', issuer: wallet.address, }); if (!result.success) { console.error(result.error); return; } ``` #### After (`@cofhe/sdk`) Permits are never auto-generated. You explicitly create, retrieve, and select them through `client.permits`: ```ts twoslash // @noErrors import { type CofheClient, type CofheConfig, FheTypes } from '@cofhe/sdk'; declare const client: CofheClient; declare const account: `0x${string}`; declare const ctHash: `0x${string}`; // ---cut--- // Create a self permit (prompts for wallet signature) const permit = await client.permits.createSelf({ issuer: account, name: 'My dApp permit', }); // Or use the convenience method that creates one only if needed const permit2 = await client.permits.getOrCreateSelfPermit(); // Use with decryptForView (active permit is used automatically) const value = await client .decryptForView(ctHash, FheTypes.Uint32) .execute(); // Or pass a specific permit const value2 = await client .decryptForView(ctHash, FheTypes.Uint32) .withPermit(permit) .execute(); ``` ### 5. Error handling #### Before (`cofhejs`) All async operations return a `Result` wrapper: ```ts const result = await cofhejs.encrypt([Encryptable.uint32(42n)]); if (!result.success) { console.error('Failed:', result.error); // string return; } const encrypted = result.data; ``` #### After (`@cofhe/sdk`) Operations return values directly and throw typed `CofheError` objects on failure: ```ts twoslash // @noErrors import { type CofheClient, type CofheConfig, Encryptable } from '@cofhe/sdk'; declare const client: CofheClient; // ---cut--- import { isCofheError, CofheErrorCode } from '@cofhe/sdk'; try { const encrypted = await client .encryptInputs([Encryptable.uint32(42n)]) .execute(); } catch (err) { if (isCofheError(err)) { console.error(err.code); // CofheErrorCode enum console.error(err.message); // human-readable message } } ``` ### 6. Import path changes | `cofhejs` | `@cofhe/sdk` | | -------------- | ---------------------------------------------------- | | `cofhejs/node` | `@cofhe/sdk/node` | | `cofhejs/web` | `@cofhe/sdk/web` | | N/A | `@cofhe/sdk` (core types, `Encryptable`, `FheTypes`) | | N/A | `@cofhe/sdk/permits` | | N/A | `@cofhe/sdk/adapters` | | N/A | `@cofhe/sdk/chains` | ### 7. Type renames | `cofhejs` | `@cofhe/sdk` | | ---------------- | ----------------------- | | `CoFheInItem` | `EncryptedItemInput` | | `CoFheInBool` | `EncryptedBoolInput` | | `CoFheInUint8` | `EncryptedUint8Input` | | `CoFheInUint16` | `EncryptedUint16Input` | | `CoFheInUint32` | `EncryptedUint32Input` | | `CoFheInUint64` | `EncryptedUint64Input` | | `CoFheInUint128` | `EncryptedUint128Input` | | `CoFheInAddress` | `EncryptedAddressInput` | ### Quick-reference: full before & after :::code-group ```ts [cofhejs (before)] import { cofhejs, Encryptable, FheTypes } from 'cofhejs/node'; import { ethers } from 'ethers'; // 1. Initialize const provider = new ethers.JsonRpcProvider('https://rpc.sepolia.org'); const wallet = new ethers.Wallet(PRIVATE_KEY, provider); await cofhejs.initializeWithEthers({ ethersProvider: provider, ethersSigner: wallet, environment: 'TESTNET', }); // 2. Encrypt const encrypted = await cofhejs.encrypt( [Encryptable.uint64(100n)], (state) => console.log(state) ); if (!encrypted.success) throw new Error(encrypted.error); // 3. Send transaction await contract.deposit(encrypted.data[0]); // 4. Create permit & unseal const permit = await cofhejs.createPermit({ type: 'self', issuer: wallet.address, }); if (!permit.success) throw new Error(permit.error); const sealed = await contract.getBalance(); const balance = await cofhejs.unseal(sealed, FheTypes.Uint64); if (!balance.success) throw new Error(balance.error); console.log(balance.data); ``` ```ts twoslash [@cofhe/sdk (after)] // @noErrors import { createCofheConfig, createCofheClient } from '@cofhe/sdk/node'; import { Encryptable, FheTypes, EncryptStep } from '@cofhe/sdk'; import { Ethers6Adapter } from '@cofhe/sdk/adapters'; import { chains } from '@cofhe/sdk/chains'; import { ethers } from 'ethers'; declare const PRIVATE_KEY: string; declare const contract: any; // 1. Initialize const provider = new ethers.JsonRpcProvider('https://rpc.sepolia.org'); const wallet = new ethers.Wallet(PRIVATE_KEY, provider); const config = createCofheConfig({ supportedChains: [chains.sepolia], }); const client = createCofheClient(config); const { publicClient, walletClient } = await Ethers6Adapter( provider, wallet ); await client.connect(publicClient, walletClient); // 2. Encrypt const [eAmount] = await client .encryptInputs([Encryptable.uint64(100n)]) .onStep((step) => console.log(step)) .execute(); // 3. Send transaction await contract.deposit(eAmount); // 4. Create permit & decrypt await client.permits.getOrCreateSelfPermit(); const ctHash = await contract.getBalance(); const balance = await client .decryptForView(ctHash, FheTypes.Uint64) .execute(); console.log(balance); ``` ```solidity [Contract] import '@fhenixprotocol/cofhe-contracts/FHE.sol'; contract ConfidentialVault { mapping(address => euint64) private _balances; function deposit(InEuint64 calldata encryptedAmount) external { euint64 amount = FHE.asEuint64(encryptedAmount); _balances[msg.sender] = FHE.add(_balances[msg.sender], amount); } function getBalance() public view returns (euint64) { return _balances[msg.sender]; } } ``` ::: ## Quick Start Set up a Hardhat project with `@cofhe/hardhat-plugin`, write a contract that stores an encrypted `uint32`, and test the encrypt → store → decrypt flow. ### Prerequisites * Node.js 18+ * An existing Hardhat project (or run `npx hardhat init` to create one) * `@nomicfoundation/hardhat-toolbox` or `@nomicfoundation/hardhat-ethers` installed ### 1) Install dependencies :::code-group ```bash [npm] npm install @cofhe/hardhat-plugin@^0.4.0 @cofhe/sdk@^0.4.0 @fhenixprotocol/cofhe-contracts@^0.1.0 ``` ```bash [pnpm] pnpm add @cofhe/hardhat-plugin@^0.4.0 @cofhe/sdk@^0.4.0 @fhenixprotocol/cofhe-contracts@^0.1.0 ``` ```bash [yarn] yarn add @cofhe/hardhat-plugin@^0.4.0 @cofhe/sdk@^0.4.0 @fhenixprotocol/cofhe-contracts@^0.1.0 ``` ::: | Package | Version | Purpose | | --------------------------------- | -------- | ---------------------------------------------------------------------- | | `@cofhe/hardhat-plugin` | `^0.4.0` | Extends Hardhat with `hre.cofhe`, deploys mock contracts automatically | | `@cofhe/sdk` | `^0.4.0` | Client-side encryption, decryption, and permit management | | `@fhenixprotocol/cofhe-contracts` | `^0.1.0` | `FHE.sol` — the Solidity library imported by your contracts | :::note `@fhenixprotocol/cofhe-contracts@0.1.0` requires `@cofhe/sdk` version `>= 0.4.0`. ::: ### 2) Configure Hardhat Import the plugin and set `evmVersion` to `cancun` (required for the transient storage opcodes used by FHE contracts). ```ts [hardhat.config.ts] import { HardhatUserConfig } from 'hardhat/config'; import '@nomicfoundation/hardhat-toolbox'; import '@cofhe/hardhat-plugin'; // [!code ++] const config: HardhatUserConfig = { solidity: { version: '0.8.28', settings: { evmVersion: 'cancun', // [!code ++] }, }, }; export default config; ``` :::warning Without `evmVersion: 'cancun'`, compilation will fail with errors from `@fhenixprotocol/cofhe-contracts`. ::: ### 3) Write a contract Create a minimal contract that accepts an encrypted input and stores it on-chain. ```solidity [contracts/MyContract.sol] // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.28; import '@fhenixprotocol/cofhe-contracts/FHE.sol'; contract MyContract { euint32 public storedValue; function setValue(InEuint32 memory inValue) external { storedValue = FHE.asEuint32(inValue); FHE.allowThis(storedValue); FHE.allowSender(storedValue); } } ``` * `euint32` — an encrypted `uint32` stored on-chain as a [ciphertext handle](/reference/dictionary#handle). * `InEuint32` — the encrypted input struct produced by the SDK. * `FHE.allowThis` / `FHE.allowSender` — grant the contract and caller permission to read the encrypted value (required by the ACL). ### 4) Write a test Use `hre.cofhe.createClientWithBatteries` to get a fully configured SDK client with a self-permit, then encrypt → send → decrypt. ```ts [test/MyContract.test.ts] import hre from 'hardhat'; import { CofheClient, Encryptable, FheTypes } from '@cofhe/sdk'; import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers'; import { expect } from 'chai'; describe('MyContract', () => { let cofheClient: CofheClient; let signer: HardhatEthersSigner; before(async () => { [signer] = await hre.ethers.getSigners(); cofheClient = await hre.cofhe.createClientWithBatteries(signer); }); it('stores and decrypts an encrypted value', async () => { const Factory = await hre.ethers.getContractFactory('MyContract'); const contract = await Factory.deploy(); // 1. Encrypt the input const [encrypted] = await cofheClient .encryptInputs([Encryptable.uint32(42n)]) .execute(); // 2. Send to contract await (await contract.setValue(encrypted)).wait(); // 3. Read the stored handle and decrypt const ctHash = await contract.storedValue(); const decrypted = await cofheClient .decryptForView(ctHash, FheTypes.Uint32) .execute(); expect(decrypted).to.equal(42n); }); }); ``` ### 5) Run ```bash npx hardhat test ``` The plugin deploys mock contracts automatically — no extra setup needed. ``` MyContract ✓ stores and decrypts an encrypted value 1 passing (1s) ``` ### What just happened? 1. The **Hardhat plugin** deployed mock versions of the CoFHE coprocessor contracts (TaskManager, ACL, ZK verifier, threshold network) before the test ran. 2. `createClientWithBatteries` created an SDK client connected to the Hardhat network, with a self-permit ready to go. 3. `encryptInputs` encrypted the plaintext `42` into an FHE ciphertext with a ZK proof (simulated by the mock verifier). 4. The contract stored the ciphertext handle on-chain and set ACL permissions. 5. `decryptForView` used the permit to decrypt the handle back to `42n` locally. ### Next steps Now that you have a working setup, explore the full SDK and tooling: * [Client](/sdk/client) — config options, adapters, and connection lifecycle. * [Encrypting Inputs](/sdk/encrypting-inputs) — the builder API, supported types, and progress callbacks. * [Decrypting to View](/sdk/decrypt-to-view) — reveal encrypted state in your UI with permits. * [Decrypting to Transact](/sdk/decrypt-to-tx) — reveal values on-chain with a verifiable signature. * [Permits](/sdk/permits) — create, share, and manage decryption authorization. * [Mock Contracts](/hardhat/mock-contracts) — read plaintext directly and assert encrypted state in tests. * [Logging](/hardhat/logging) — inspect every FHE operation your contracts perform. * [EncryptedCounter.sol](/reference/encrypted-counter-sol) — a more complete reference contract demonstrating all SDK features. The `@cofhe/react` package is currently in development. ## Cofhe Client This page covers the core SDK lifecycle: 1. Create config (`createCofheConfig`) 2. Create client (`createCofheClient`) 3. Connect to client (`client.connect`) 4. Managing connection (change account / disconnect) ### 1) Create config Import `createCofheConfig` from the entrypoint that matches your runtime: * Browser apps: `@cofhe/sdk/web` * Node.js scripts/backends: `@cofhe/sdk/node` :::warning **Browser bundlers (Vite)** `@cofhe/sdk/web` loads TFHE via a WASM module and may use Web Workers. If you use Vite, exclude `tfhe` from dependency prebundling so `new URL('tfhe_bg.wasm', import.meta.url)` resolves to an actual `.wasm` file (otherwise TFHE init can fail with a “magic word” error). ```ts // vite.config.ts import { defineConfig } from 'vite'; export default defineConfig({ optimizeDeps: { exclude: ['tfhe'] }, worker: { format: 'es' }, }); ``` ::: The only required field is `supportedChains`. ```ts twoslash import { createCofheConfig } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; const config = createCofheConfig({ supportedChains: [chains.sepolia], // Optional knobs // defaultPermitExpiration: 60 * 60 * 24 * 30, // useWorkers: true, }); ``` ### 2) Create cofhe client Import `createCofheClient` from the entrypoint that matches your runtime: * Browser apps: `@cofhe/sdk/web` * Node.js scripts/backends: `@cofhe/sdk/node` ```ts twoslash import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); // ---cut--- const cofheClient = createCofheClient(config); cofheClient.connected; // false cofheClient.connecting; // false ``` ### 3) Connect The SDK connects to CoFHE using viem clients: * `PublicClient`: read-only chain access * `WalletClient`: signing + sending transactions If you already have viem clients, you can pass them directly to `cofheClient.connect(publicClient, walletClient)`. If you're using another wallet/provider stack, see below for `@cofhe/sdk/adapters`. Connecting initializes the SDK's in-memory connection state (`account`, `chainId`, and the viem clients). ```ts twoslash // @noErrorValidation import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; import { createPublicClient, createWalletClient, http } from 'viem'; import type { PublicClient, WalletClient } from 'viem'; import { sepolia } from 'viem/chains'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const cofheClient = createCofheClient(config); // ---cut--- const publicClient: PublicClient = createPublicClient({ chain: sepolia, transport: http(), }); const walletClient: WalletClient = createWalletClient({ chain: sepolia, transport: http(), }); await cofheClient.connect(publicClient, walletClient); // Check connection cofheClient.connected; ``` #### Using adapters (optional) If you're using another wallet/provider stack, `@cofhe/sdk/adapters` exposes adapters that convert into viem-shaped clients. We currently support adapters for Ethers v5 and v6 clients and providers, wagmi clients, and smart wallet clients: :::code-group ```ts twoslash [Ethers v6] // @noErrorValidation // @noErrors import { Ethers6Adapter } from '@cofhe/sdk/adapters'; import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const cofheClient = createCofheClient(config); // ---cut--- import { ethers } from 'ethers'; // ethers v6 const provider = new ethers.JsonRpcProvider('https://rpc.sepolia.org'); const signer = new ethers.Wallet('0xYOUR_PRIVATE_KEY', provider); const { publicClient, walletClient } = await Ethers6Adapter( provider, signer ); await cofheClient.connect(publicClient, walletClient); ``` ```ts twoslash [Ethers v5] // @noErrorValidation // @noErrors import { Ethers5Adapter } from '@cofhe/sdk/adapters'; import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const cofheClient = createCofheClient(config); // ---cut--- import { ethers } from 'ethers5'; // ethers v5 const provider = new ethers.providers.JsonRpcProvider( 'https://rpc.sepolia.org' ); const signer = new ethers.Wallet('0xYOUR_PRIVATE_KEY').connect(provider); const { publicClient, walletClient } = await Ethers5Adapter( provider, signer ); await cofheClient.connect(publicClient, walletClient); ``` ```ts twoslash [Wagmi] // @noErrorValidation // @noErrors import { WagmiAdapter } from '@cofhe/sdk/adapters'; import { createPublicClient, createWalletClient, http } from 'viem'; import { sepolia } from 'viem/chains'; import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const cofheClient = createCofheClient(config); // ---cut--- const wagmiPublicClient = createPublicClient({ chain: sepolia, transport: http(), }); const wagmiWalletClient = createWalletClient({ chain: sepolia, transport: http(), }); const { publicClient, walletClient } = await WagmiAdapter( wagmiWalletClient, wagmiPublicClient ); await cofheClient.connect(publicClient, walletClient); ``` ::: ### 4) Managing connections #### Reconnect behavior Calling `connect` again with the same clients is a no-op. Calling it with new clients replaces the connection state. #### Changing connected account To switch the client's connected account call `cofheClient.connect()` with updated viem clients. ```ts twoslash import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; import { createPublicClient, createWalletClient, http } from 'viem'; import { sepolia } from 'viem/chains'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const cofheClient = createCofheClient(config); declare const bobAddress: `0x${string}`; declare const aliceAddress: `0x${string}`; // ---cut--- const publicClient = createPublicClient({ chain: sepolia, transport: http(), }); const bobWalletClient = createWalletClient({ chain: sepolia, transport: http(), account: bobAddress, }); const aliceWalletClient = createWalletClient({ chain: sepolia, transport: http(), account: aliceAddress, }); // connect to Bob's wallet await cofheClient.connect(publicClient, bobWalletClient); cofheClient.connection.account; // connect to Alice's wallet await cofheClient.connect(publicClient, aliceWalletClient); cofheClient.connection.account; ``` #### Disconnecting To manually disconnect call `cofheClient.disconnect()`. Disconnecting clears the in-memory connection state (clients/account/chainId) and marks the client as disconnected. It does **not** delete persisted permits or stored FHE keys. ```ts twoslash import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const cofheClient = createCofheClient(config); // ---cut--- cofheClient.disconnect(); cofheClient.connected; ``` ## 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](/reference/dictionary#handle) you want to reveal (see [Decrypt for a transaction](#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](#what-decryptfortx-returns)). 3. Submit an on-chain transaction that publishes or verifies the result (see [Writing Decrypt Result to Contract](./writing-decrypt-result-to-contract.mdx)). 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. :::note If you only need to show plaintext in your UI (and you do **not** need an on-chain-verifiable signature), use [`decryptForView`](./decrypt-to-view.mdx) instead. ::: ### Prerequisites 1. Create and connect a client (see the [client page](./client.mdx)). 2. Know the **on-chain** encrypted [handle](/reference/dictionary#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). :::tip * On-chain, a handle is represented as a `bytes32`. * In JavaScript/TypeScript, libraries like Viem will typically give you a hex string (`0x${string}`). * The SDK accepts either a hex string or a `bigint`. ::: :::note `decryptForTx` does not take a `utype`. It always returns the plaintext as a `bigint` because the result is intended to be passed into a transaction (your contract decides whether that `bigint` is interpreted as `uint32`, `uint64`, etc). If you need UI-friendly decoding (e.g. `boolean` or address formatting), use [`decryptForView`](./decrypt-to-view.mdx). ::: 3. Determine whether the contract-level ACL policy for this specific handle requires a [permit](/reference/dictionary#permit) (see [permits](./permits.mdx)). * 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 `internalType`s (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. :::code-group ```ts twoslash [Viem] // [!include ~/../../examples/docs-snippets/read-encrypted-return/viem.ts:docs-snippet] ``` ```ts twoslash [Ethers] // [!include ~/../../examples/docs-snippets/read-encrypted-return/ethers.ts:docs-snippet] ``` ::: ### 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](./permits.mdx). 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: :::code-group ```ts twoslash [No permit] import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const client = createCofheClient(config); declare const ctHash: `0x${string}`; // ---cut--- const decryptResult = await client .decryptForTx(ctHash) .withoutPermit() .execute(); decryptResult; // ^? decryptResult.decryptedValue; decryptResult.signature; ``` ```ts twoslash [Permit (active)] import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const client = createCofheClient(config); declare const ctHash: `0x${string}`; // ---cut--- const decryptResult = await client .decryptForTx(ctHash) .withPermit() .execute(); decryptResult; // ^? ``` ```ts twoslash [Permit (explicit)] import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const client = createCofheClient(config); declare const ctHash: `0x${string}`; // ---cut--- const permit = await client.permits.getOrCreateSelfPermit(); const decryptResult = await client .decryptForTx(ctHash) .withPermit(permit) .execute(); decryptResult; // ^? ``` ::: ### 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](./writing-decrypt-result-to-contract.mdx) 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 }`. ```ts twoslash import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const client = createCofheClient(config); declare const ctHash: `0x${string}`; // ---cut--- const result = await client.decryptForTx(ctHash).withPermit().execute(); // [!code focus] 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. :::code-group ```ts twoslash [Active permit] import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const client = createCofheClient(config); declare const ctHash: `0x${string}`; // ---cut--- const result = await client .decryptForTx(ctHash) .withPermit() // [!code focus] .execute(); result.signature; ``` ```ts twoslash [Permit object] import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const client = createCofheClient(config); declare const ctHash: `0x${string}`; // ---cut--- const permit = await client.permits.getOrCreateSelfPermit(); const result = await client .decryptForTx(ctHash) .withPermit(permit) // [!code focus] .execute(); result.decryptedValue; ``` ```ts twoslash [Permit hash] import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const client = createCofheClient(config); declare const ctHash: `0x${string}`; declare const permitHash: `0x${string}`; // ---cut--- const result = await client .decryptForTx(ctHash) .withPermit(permitHash) // [!code focus] .execute(); result; ``` ::: #### `.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. ```ts twoslash import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const client = createCofheClient(config); declare const ctHash: `0x${string}`; // ---cut--- const result = await client .decryptForTx(ctHash) .withoutPermit() // [!code focus] .execute(); result.decryptedValue; ``` #### `.setAccount(address)` — optional Overrides the account used to resolve the active permit / stored permit. ```ts twoslash import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const client = createCofheClient(config); declare const ctHash: `0x${string}`; // ---cut--- const result = await client .decryptForTx(ctHash) .setAccount('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045') // [!code focus] .withPermit() .execute(); const handleFromResult = result.ctHash; handleFromResult; ``` #### `.setChainId(chainId)` — optional Overrides the chain used to resolve the Threshold Network URL and permits. ```ts twoslash import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const client = createCofheClient(config); declare const ctHash: `0x${string}`; // ---cut--- const result = await client .decryptForTx(ctHash) .setChainId(11155111) // [!code focus] .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(...)`). ## Decrypting to View Use `decryptForView` to reveal a **confidential (encrypted)** value **locally** (in your app) so you can display it in the UI. Unlike [`decryptForTx`](./decrypt-to-tx.mdx), this flow does **not** return an on-chain-verifiable signature, and it is **not** meant to be published on-chain. The flow is as follows: 1. Read the [handle](/reference/dictionary#handle) from your contract (often named `ctHash` in code) (see [Reading from a contract](#reading-from-a-contract-getting-a-handle--utype)). 2. Ensure you have a [permit](/reference/dictionary#permit) that authorizes decryption of that value (see [Permit quickstart](#permit-quickstart)). 3. Call `decryptForView(ctHash, utype)` and then run `.execute()` to get the plaintext (see [Decrypt for UI](#decrypt-for-ui)). :::note `decryptForView` always decrypts using a permit (there is no `.withoutPermit()` mode). If your protocol intends for the plaintext to become publicly visible on-chain, use [`decryptForTx`](./decrypt-to-tx.mdx) instead. ::: ### Prerequisites 1. Create and connect a client (see the [client page](./client.mdx)). 2. Know the [handle](/reference/dictionary#handle) (often named `ctHash` in code) and the encrypted type ([`utype`](/reference/dictionary#utype)). :::tip **Getting a handle** In most apps, the handle comes from reading a stored encrypted value, an event arg, or a return value from a `view` call. ::: :::tip **Providing `utype` (required; not arbitrary)** `utype` must match the handle’s underlying FHE type (it’s not a “pick whatever you want” parameter). The SDK uses it to convert the decrypted `bigint` into a convenient JS type so you can display it. In many apps you can **derive `{ ctHash, utype }` from the contract ABI** using `@cofhe/abi` (see the snippets below). Here `ctHash` is the handle. If you only have a raw handle (e.g. from storage/event logs), you must still know which encrypted type it corresponds to. Supported `utype`s for `decryptForView`: * `FheTypes.Bool` → returns a `boolean` * `FheTypes.Uint160` (address) → returns a checksummed `0x...` string * `FheTypes.Uint8 | Uint16 | Uint32 | Uint64 | Uint128` → returns a `bigint` ::: :::note This is why `decryptForView` requires `utype`, but `decryptForTx` does not: tx decryption is meant for on-chain publishing and returns a raw `bigint` plaintext + signature. ::: 3. Have a permit available for the connected `chainId + account` (see [permits](./permits.mdx)). If you don’t have one yet, the quickest way is to create a self permit once per account + chain. ### Reading from a contract (getting a handle + `utype`) Typically, the value you want to decrypt comes from a `view` call. First, read the raw encrypted return value and convert it into `{ ctHash, utype }` using `@cofhe/abi`. Here `ctHash` is the handle. Below are two equivalent ways to read an encrypted return value from a predeployed Sepolia contract and derive a handle + `utype`. :::code-group ```ts twoslash [Viem] // [!include ~/../../examples/docs-snippets/read-encrypted-return/viem.ts:docs-snippet] ``` ```ts twoslash [Ethers] // [!include ~/../../examples/docs-snippets/read-encrypted-return/ethers.ts:docs-snippet] ``` ::: :::note Depending on your contract ABI, the handle may be surfaced as either: * `bytes32` → returned as a `0x...` hex string (both viem and ethers) * `uint256` → returned as a `bigint` (viem and ethers v6; ethers v5 returns a `BigNumber`) `decryptForView` accepts both `bigint` and `0x...` strings (it coerces via `BigInt(...)`). If you’re on ethers v5 and you get a `BigNumber`, convert it first (e.g. `BigInt(bn.toString())`). ::: ### Permit quickstart If you want a “just make it work” setup for UI decryption, do this once after connecting: ```ts twoslash import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; import { createPublicClient, createWalletClient, http } from 'viem'; import { sepolia } from 'viem/chains'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const client = createCofheClient(config); declare const account: `0x${string}`; // ---cut--- const publicClient = createPublicClient({ chain: sepolia, transport: http(), }); const walletClient = createWalletClient({ chain: sepolia, transport: http(), account, }); await client.connect(publicClient, walletClient); // Creates a permit if needed, stores it, and selects it as the active permit. await client.permits.getOrCreateSelfPermit(); ``` After this, `decryptForView(...)` can automatically use the active permit when you run `.execute()`. ### Decrypt for UI Choose the pattern that matches how your app manages permits: :::code-group ```ts twoslash [Active permit] import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; import { FheTypes } from '@cofhe/sdk'; import { createPublicClient, createWalletClient, http } from 'viem'; import { sepolia } from 'viem/chains'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const client = createCofheClient(config); declare const ctHash: `0x${string}`; declare const account: `0x${string}`; // ---cut--- const publicClient = createPublicClient({ chain: sepolia, transport: http(), }); const walletClient = createWalletClient({ chain: sepolia, transport: http(), account, }); await client.connect(publicClient, walletClient); await client.permits.getOrCreateSelfPermit(); const plaintext = await client .decryptForView(ctHash, FheTypes.Uint32) .execute(); plaintext; // ^? ``` ```ts twoslash [Permit object] import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; import { FheTypes } from '@cofhe/sdk'; import { createPublicClient, createWalletClient, http } from 'viem'; import { sepolia } from 'viem/chains'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const client = createCofheClient(config); declare const ctHash: `0x${string}`; declare const account: `0x${string}`; // ---cut--- const publicClient = createPublicClient({ chain: sepolia, transport: http(), }); const walletClient = createWalletClient({ chain: sepolia, transport: http(), account, }); await client.connect(publicClient, walletClient); const permit = await client.permits.getOrCreateSelfPermit(); const plaintext = await client .decryptForView(ctHash, FheTypes.Uint64) .withPermit(permit) .execute(); plaintext; // ^? ``` ```ts twoslash [Permit hash] import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; import { FheTypes } from '@cofhe/sdk'; import { createPublicClient, createWalletClient, http } from 'viem'; import { sepolia } from 'viem/chains'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const client = createCofheClient(config); declare const ctHash: `0x${string}`; declare const permitHash: `0x${string}`; declare const account: `0x${string}`; // ---cut--- const publicClient = createPublicClient({ chain: sepolia, transport: http(), }); const walletClient = createWalletClient({ chain: sepolia, transport: http(), account, }); await client.connect(publicClient, walletClient); const plaintext = await client .decryptForView(ctHash, FheTypes.Uint8) .withPermit(permitHash) .execute(); plaintext; // ^? ``` ::: ### What `decryptForView` returns Running `.execute()` on `decryptForView(...)` resolves to a scalar JS value (typed in TypeScript as `UnsealedItem`): * For integer utypes (`FheTypes.Uint8 | FheTypes.Uint16 | FheTypes.Uint32 | FheTypes.Uint64 | FheTypes.Uint128`): a `bigint` * For `FheTypes.Bool`: a `boolean` * For `FheTypes.Uint160` (address): a checksummed `0x...` address string If you decrypt an integer type, you’ll usually want to: * keep it as a `bigint` and format it for display, or * convert to a JS `number` only if you’re sure it’s within safe range. ### After decrypting: common UI patterns Pick the pattern that matches what you’re displaying: :::code-group ```ts twoslash [Format bigint for display] import { formatUnits } from 'viem'; declare const amount: bigint; // ---cut--- const decimals = 6; const display = formatUnits(amount, decimals); display; ``` ```ts twoslash [Bigint → number (range-check)] declare const amount: bigint; // ---cut--- const MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER); const asNumber = amount <= MAX_SAFE ? Number(amount) : undefined; asNumber; ``` ```ts twoslash [Booleans & addresses] declare const decryptedIsAllowed: boolean; declare const decryptedOwner: `0x${string}`; // ---cut--- const statusLabel = decryptedIsAllowed ? 'Allowed' : 'Not allowed'; const shortOwner = `${decryptedOwner.slice(0, 6)}…${decryptedOwner.slice(-4)}`; ({ statusLabel, shortOwner }); ``` ::: ### Builder API `decryptForView(ctHash, utype)` returns a builder. Unlike `decryptForTx`, this flow always requires a permit (there is no `.withoutPermit()` mode). #### `.execute()` — required, call last Runs the decryption and returns a UI-friendly scalar value (see [What `decryptForView` returns](#what-decryptforview-returns)). ```ts twoslash import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; import { FheTypes } from '@cofhe/sdk'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const client = createCofheClient(config); declare const ctHash: `0x${string}`; // ---cut--- const plaintext = await client .decryptForView(ctHash, FheTypes.Uint32) .execute(); // [!code focus] plaintext; ``` #### `.withPermit(...)` — optional Select which permit to use: * `.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. If you don’t call `.withPermit(...)`, `decryptForView` also uses the active permit for the resolved `chainId + account`. :::code-group ```ts twoslash [Active permit] import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; import { FheTypes } from '@cofhe/sdk'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const client = createCofheClient(config); declare const ctHash: `0x${string}`; // ---cut--- const plaintext = await client .decryptForView(ctHash, FheTypes.Uint8) .withPermit() // [!code focus] .execute(); plaintext; ``` ```ts twoslash [Permit object] import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; import { FheTypes } from '@cofhe/sdk'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const client = createCofheClient(config); declare const ctHash: `0x${string}`; // ---cut--- const permit = await client.permits.getOrCreateSelfPermit(); const plaintext = await client .decryptForView(ctHash, FheTypes.Uint64) .withPermit(permit) // [!code focus] .execute(); plaintext; ``` ```ts twoslash [Permit hash] import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; import { FheTypes } from '@cofhe/sdk'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const client = createCofheClient(config); declare const ctHash: `0x${string}`; declare const permitHash: `0x${string}`; // ---cut--- const plaintext = await client .decryptForView(ctHash, FheTypes.Bool) .withPermit(permitHash) // [!code focus] .execute(); plaintext; ``` ::: #### `.setAccount(address)` — optional Overrides the account used to resolve the active permit / stored permit. ```ts twoslash import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; import { FheTypes } from '@cofhe/sdk'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const client = createCofheClient(config); declare const ctHash: `0x${string}`; // ---cut--- const plaintext = await client .decryptForView(ctHash, FheTypes.Uint32) .setAccount('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045') // [!code focus] .withPermit() .execute(); plaintext; ``` #### `.setChainId(chainId)` — optional Overrides the chain used to resolve the Threshold Network URL and permits. ```ts twoslash import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; import { FheTypes } from '@cofhe/sdk'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const client = createCofheClient(config); declare const ctHash: `0x${string}`; // ---cut--- const plaintext = await client .decryptForView(ctHash, FheTypes.Uint16) .setChainId(11155111) // [!code focus] .withPermit() .execute(); plaintext; ``` ### Common pitfalls * **Missing permit**: `decryptForView` will fail if there is no active permit for the current `chainId + account` (or if the permit you pass doesn’t authorize decrypting that handle). * **Wrong `utype`**: you must pass the correct FHE type for the handle. `Bool` and `address` are converted into `boolean` / checksummed `0x...` strings; integer types stay as `bigint`. * **Wrong chain/account**: permits are scoped to `chainId + account`. If the user switches wallets or networks, create/select the correct permit (or set them explicitly via `.setChainId(...)` / `.setAccount(...)`). ## Encrypting Inputs `encryptInputs` encrypts plaintext values into FHE ciphertexts that can be passed as inputs to a confidential smart contract transaction. Values must be encrypted before being passed on-chain to preserve confidentiality. It returns an `EncryptInputsBuilder` which lets you configure the encryption and then call `.execute()` to run it. The flow is as follows: 1. Decide which plaintext value(s) you want to encrypt. 2. Wrap each value with a typing helper (e.g. `Encryptable.uint32(…)`) to construct typed encryptable inputs (see [`Encryptable` — typing inputs](#encryptable--typing-inputs)). 3. Call `client.encryptInputs([...]).execute()` to produce [`EncryptedItemInput` objects](#encryptediteminput--the-result-type) (see [Builder API](#builder-api)). 4. Submit a transaction to your contract and pass the encrypted inputs as `InE*` parameters (see [Writing encrypted data to a contract](./writing-encrypted-data-to-contract.mdx)). ### Prerequisites 1. Create and connect a client (see the [client page](./client.mdx)). `encryptInputs(...)` requires a connected client so the SDK can resolve `chainId`, `account`, and the underlying RPC clients. 2. Know which encrypted type you want to encode each value as. The type is chosen by which `Encryptable.*` factory you use (and must match the Solidity parameter type your contract expects, e.g. `InEuint32` vs `InEuint64`). ### Basic usage ```ts twoslash import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { Encryptable } from '@cofhe/sdk'; import { chains } from '@cofhe/sdk/chains'; import type { PublicClient, WalletClient } from 'viem'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const cofheClient = createCofheClient(config); declare const publicClient: PublicClient; declare const walletClient: WalletClient; // ---cut--- await cofheClient.connect(publicClient, walletClient); const encrypted = await cofheClient .encryptInputs([ Encryptable.uint32(42n), Encryptable.bool(true), Encryptable.address('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'), ]) .execute(); const [eUint32, eBool, eAddress] = encrypted; ``` The return type is a typed tuple that mirrors the array you pass in — each element is the corresponding `Encrypted*Input` type. For the exact shape of each item, see [`EncryptedItemInput` — the result type](#encryptediteminput--the-result-type). ### What `encryptInputs` returns Running `.execute()` returns a typed tuple of `EncryptedItemInput` items that mirrors the array you passed to `encryptInputs([...])`. Each item contains the [handle](/reference/dictionary#handle) (`ctHash` in the SDK type), the encrypted type ([`utype`](/reference/dictionary#utype)), the security zone, and a verifier signature authorizing that input. For the exact shape, see [`EncryptedItemInput` — the result type](#encryptediteminput--the-result-type). ### Builder API #### `.execute()` — required, call last Runs the encryption pipeline and returns the `EncryptedItemInput[]` tuple. ```ts twoslash import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { Encryptable } from '@cofhe/sdk'; import { chains } from '@cofhe/sdk/chains'; import type { PublicClient, WalletClient } from 'viem'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const cofheClient = createCofheClient(config); declare const publicClient: PublicClient; declare const walletClient: WalletClient; // ---cut--- await cofheClient.connect(publicClient, walletClient); const [encryptedAge, encryptedFlag] = await cofheClient .encryptInputs([Encryptable.uint8(25n), Encryptable.bool(true)]) .execute(); // [!code focus] ``` #### `.setAccount(address)` — optional Override the address that "owns" the encrypted input. Only that address will be allowed to use the encrypted inputs on-chain. Defaults to the account from the connected `WalletClient`. ```ts twoslash import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { Encryptable } from '@cofhe/sdk'; import { chains } from '@cofhe/sdk/chains'; import type { PublicClient, WalletClient } from 'viem'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const cofheClient = createCofheClient(config); declare const publicClient: PublicClient; declare const walletClient: WalletClient; // ---cut--- await cofheClient.connect(publicClient, walletClient); const encrypted = await cofheClient .encryptInputs([Encryptable.uint64(10n)]) .setAccount('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045') // [!code focus] .execute(); ``` #### `.setChainId(chainId)` — optional Override the chain the encrypted input will be used on. Defaults to the chain ID of the connected `PublicClient`. ```ts twoslash import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { Encryptable } from '@cofhe/sdk'; import { chains } from '@cofhe/sdk/chains'; import type { PublicClient, WalletClient } from 'viem'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const cofheClient = createCofheClient(config); declare const publicClient: PublicClient; declare const walletClient: WalletClient; // ---cut--- await cofheClient.connect(publicClient, walletClient); const encrypted = await cofheClient .encryptInputs([Encryptable.uint64(10n)]) .setChainId(11155111) // [!code focus] .execute(); ``` #### `.setUseWorker(boolean)` — optional Overrides the `useWorkers` flag from `CofheConfig` for this specific call. When `true` (the default), ZK proof generation runs in a Web Worker to avoid blocking the main thread. No-op in a node environment. ```ts twoslash import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { Encryptable } from '@cofhe/sdk'; import { chains } from '@cofhe/sdk/chains'; import type { PublicClient, WalletClient } from 'viem'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const cofheClient = createCofheClient(config); declare const publicClient: PublicClient; declare const walletClient: WalletClient; // ---cut--- await cofheClient.connect(publicClient, walletClient); const encrypted = await cofheClient .encryptInputs([Encryptable.uint32(7n)]) .setUseWorker(false) // [!code focus] .execute(); ``` #### `.onStep(callback)` — optional Registers a callback that fires at the start and end of each encryption step. Useful for building progress indicators. The callback receives the current `EncryptStep` enum value and a context object with `isStart`, `isEnd`, and `duration` (milliseconds, only meaningful on `isEnd`). ```ts twoslash import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { Encryptable, EncryptStep } from '@cofhe/sdk'; import { chains } from '@cofhe/sdk/chains'; import type { PublicClient, WalletClient } from 'viem'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const cofheClient = createCofheClient(config); declare const publicClient: PublicClient; declare const walletClient: WalletClient; // ---cut--- await cofheClient.connect(publicClient, walletClient); const encrypted = await cofheClient .encryptInputs([Encryptable.uint64(10n)]) .onStep((step, ctx) => { // [!code focus] if (ctx?.isStart) console.log(`Starting: ${step}`); // [!code focus] if (ctx?.isEnd) console.log(`Done: ${step} (${ctx.duration}ms)`); // [!code focus] }) // [!code focus] .execute(); ``` The `EncryptStep` enum values fired in order: ```ts twoslash import { EncryptStep } from '@cofhe/sdk'; // ---cut--- EncryptStep.InitTfhe; // 'initTfhe' EncryptStep.FetchKeys; // 'fetchKeys' EncryptStep.Pack; // 'pack' EncryptStep.Prove; // 'prove' EncryptStep.Verify; // 'verify' ``` ##### The encryption flow Calling `.execute()` runs five sequential steps. You can observe them via the `.onStep()` callback. | Step | Description | | ----------- | ----------------------------------------------------------------------------------- | | `InitTfhe` | Lazy-initializes the TFHE WASM module (browser/Node). A no-op after the first call. | | `FetchKeys` | Fetches (or loads from cache) the FHE public key and CRS for the target chain. | | `Pack` | Packs the plaintext values into a ZK list ready for proving. | | `Prove` | Generates the ZK proof of knowledge (ZKPoK). Uses a Web Worker when available. | | `Verify` | Sends the proof to the CoFHE verifier. Returns signed `EncryptedItemInput` objects. | ### `Encryptable` — typing inputs Use the `Encryptable` factory to create the items you want to encrypt. Each factory function accepts the plaintext value and an optional `securityZone`. | Factory | Data type | Solidity input param | | ---------------------------- | ------------------ | -------------------- | | `Encryptable.bool(value)` | `boolean` | `InEbool` | | `Encryptable.uint8(value)` | `bigint \| string` | `InEuint8` | | `Encryptable.uint16(value)` | `bigint \| string` | `InEuint16` | | `Encryptable.uint32(value)` | `bigint \| string` | `InEuint32` | | `Encryptable.uint64(value)` | `bigint \| string` | `InEuint64` | | `Encryptable.uint128(value)` | `bigint \| string` | `InEuint128` | | `Encryptable.address(value)` | `bigint \| string` | `InEaddress` | You can also use the generic `Encryptable.create(type, value)` form: ```ts twoslash import { Encryptable } from '@cofhe/sdk'; // ---cut--- Encryptable.create('uint32', 42n); Encryptable.create('bool', false); Encryptable.create('address', '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'); ``` **Bit limit:** A single `encryptInputs` call may encrypt at most **2048 bits** of plaintext in total. Exceeding this limit throws a `ZkPackFailed` error. ### `EncryptedItemInput` — the result type Each element of the returned array is an `EncryptedItemInput`: ```ts type EncryptedItemInput = { ctHash: bigint; // The handle registered with CoFHE securityZone: number; // The security zone the input was encrypted under utype: FheTypes; // The FHE type (Bool, Uint8, …, Uint160) signature: string; // CoFHE verifier signature authorizing this input }; ``` Pass these directly into a contract function that accepts `InEuint*` structs. The contract's CoFHE library validates the signature on-chain before operating on the ciphertext. ### Common pitfalls * **Wrong `Encryptable` type**: `Encryptable.uint32(...)` must match what your Solidity function expects (e.g. `InEuint32`). * **Wrong account / chain**: encrypted inputs are authorized for a specific `account + chainId`. If you override these (or connect to the wrong network/wallet), your inputs may not be usable for the intended transaction. * **Bit limit exceeded**: a single call can encrypt at most **2048 bits** of plaintext. Exceeding this throws `ZkPackFailed`. ## Permits Permits are EIP-712 signatures that authorize decryption of confidential data. The `issuer` field identifies who is accessing the data - the issuer must have been granted access on-chain via `FHE.allow(handle, address)`. When a permit is used, CoFHE validates it against the ACL contract to confirm that the issuer has access to the requested encrypted handle. Each permit includes a sealing keypair. The public key is sent to CoFHE so it can re-encrypt the data for the permit holder. The private key stays client-side and is used to unseal the returned data. Permits are stored locally and identified by a deterministic hash of their fields. Permits can be used to decrypt your own data (self permits), or to delegate your access to another party (sharing permits). :::info The examples below show two approaches for working with permits. The `client.permits` API is the recommended approach -- it automatically signs permits with the connected wallet and manages the permit store. The `PermitUtils` API is a lower-level alternative that gives you direct control over signing and storage. ::: ### When do you need a permit? * **`decryptForView`**: always requires a permit. * **`decryptForTx`**: depends on the contract's ACL policy for that [handle](/reference/dictionary#handle) (`ctHash` in code). * If the policy allows anyone to decrypt, you can use `.withoutPermit()`. * If the policy restricts decryption, you must use `.withPermit(...)`. ### Prerequisites 1. Create and connect a client (see the [client page](./client.mdx)). Permits are scoped to a **chainId + account**. The SDK uses the connected chain and account by default. ### Quickstart The recommended approach is to create (or reuse) a self permit. Once created, it is stored and set as the active permit for the connected chain and account. :::code-group ```ts twoslash [client.permits] // @noErrors import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; import type { PublicClient, WalletClient } from 'viem'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const client = createCofheClient(config); declare const publicClient: PublicClient; declare const walletClient: WalletClient; // ---cut--- await client.connect(publicClient, walletClient); // Returns the active self permit if one exists, otherwise creates and signs a new one. const permit = await client.permits.getOrCreateSelfPermit(); ``` ```ts twoslash [PermitUtils] // @noErrors import { PermitUtils, setPermit, setActivePermitHash, } from '@cofhe/sdk/permits'; import type { PublicClient, WalletClient } from 'viem'; declare const publicClient: PublicClient; declare const walletClient: WalletClient; // ---cut--- const permit = await PermitUtils.createSelfAndSign( { issuer: walletClient.account!.address }, publicClient, walletClient ); // Manually store and activate the permit const chainId = await publicClient.getChainId(); const account = walletClient.account!.address; setPermit(chainId, account, permit); setActivePermitHash(chainId, account, permit.hash); ``` ::: After this, the active permit is picked up automatically: * `decryptForView(...).execute()` uses the active permit. * `decryptForTx(...).withPermit().execute()` uses the active permit. ### Permit types The SDK supports three permit types: | Type | Who signs | Use case | | ----------- | ----------------------------------------- | --------------------------------------------------------- | | `self` | issuer only | Decrypt your own data (most common) | | `sharing` | issuer only | A shareable "offer" created by the issuer for a recipient | | `recipient` | recipient (and includes issuer signature) | The imported permit after the recipient signs it | Notes: * A permit includes a **sealing keypair**. The public key is sent to CoFHE for re-encryption. The private key stays client-side for unsealing. * Permit `expiration` is a unix timestamp in **seconds**. The default is **7 days from creation**. * When a permit is created via `client.permits.*`, it is automatically stored and set as the active permit for the current chain and account. ### Creating a self permit A self permit lets you decrypt data that was allowed to your address. Use `createSelf` to always create a new permit, or `getOrCreateSelfPermit` to reuse an existing active permit. #### createSelf :::code-group ```ts twoslash [client.permits] // @noErrors import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; import type { PublicClient, WalletClient } from 'viem'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const client = createCofheClient(config); declare const publicClient: PublicClient; declare const walletClient: WalletClient; // ---cut--- await client.connect(publicClient, walletClient); const permit = await client.permits.createSelf({ issuer: walletClient.account!.address, name: 'My self permit', }); permit.type; permit.hash; ``` ```ts twoslash [PermitUtils] // @noErrors import { PermitUtils } from '@cofhe/sdk/permits'; import type { PublicClient, WalletClient } from 'viem'; declare const publicClient: PublicClient; declare const walletClient: WalletClient; // ---cut--- const permit = await PermitUtils.createSelfAndSign( { issuer: walletClient.account!.address, name: 'My self permit', }, publicClient, walletClient ); permit.type; permit.hash; ``` ::: #### getOrCreateSelfPermit Only available via the `client.permits` API. Returns the active self permit if one exists. Otherwise creates and signs a new one. This is the recommended approach for most applications. ```ts twoslash [client.permits] // @noErrors import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; import type { PublicClient, WalletClient } from 'viem'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const client = createCofheClient(config); declare const publicClient: PublicClient; declare const walletClient: WalletClient; // ---cut--- await client.connect(publicClient, walletClient); const permit = await client.permits.getOrCreateSelfPermit(); permit.type; ``` ### Sharing permits Sharing permits let an issuer delegate their ACL access to a recipient. The recipient can then decrypt the issuer's data without needing their own `FHE.allow`. This flow may be useful for auditors or other parties that need to access data but do not have the ability to grant themselves access. ::::steps #### Issuer creates a sharing permit The issuer creates a sharing permit specifying the recipient's address. The issuer's signature does not include a sealing key. :::code-group ```ts twoslash [client.permits] // @noErrors import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; import type { PublicClient, WalletClient } from 'viem'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const client = createCofheClient(config); declare const publicClient: PublicClient; declare const walletClient: WalletClient; declare const recipient: `0x${string}`; // ---cut--- await client.connect(publicClient, walletClient); const sharingPermit = await client.permits.createSharing({ issuer: walletClient.account!.address, recipient, name: 'Share with recipient', }); ``` ```ts twoslash [PermitUtils] // @noErrors import { PermitUtils } from '@cofhe/sdk/permits'; import type { PublicClient, WalletClient } from 'viem'; declare const publicClient: PublicClient; declare const walletClient: WalletClient; declare const recipient: `0x${string}`; // ---cut--- const sharingPermit = await PermitUtils.createSharingAndSign( { issuer: walletClient.account!.address, recipient, name: 'Share with recipient', }, publicClient, walletClient ); ``` ::: #### Issuer exports the permit Export the permit as a JSON blob and share it with the recipient. ```ts twoslash [PermitUtils] // @noErrors import { PermitUtils } from '@cofhe/sdk/permits'; import type { Permit } from '@cofhe/sdk/permits'; declare const sharingPermit: Permit; // ---cut--- const exported = PermitUtils.export(sharingPermit); ``` :::info The exported JSON does not contain any sensitive data and can be shared via any channel. ::: :::warning Do not share `serialize(permit)` output -- serialization is meant for *local persistence* and includes the sealing private key. ::: #### Recipient imports and signs The recipient imports the exported JSON and signs it with their wallet. On import, a new sealing key is generated for the recipient. The recipient's sealing key is the one CoFHE uses for re-encryption. The resulting permit is stored and set as active. :::code-group ```ts twoslash [client.permits] // @noErrors import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; import type { PublicClient, WalletClient } from 'viem'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const client = createCofheClient(config); declare const publicClient: PublicClient; declare const walletClient: WalletClient; declare const exported: string; // ---cut--- await client.connect(publicClient, walletClient); const recipientPermit = await client.permits.importShared(exported); recipientPermit.type; recipientPermit.hash; ``` ```ts twoslash [PermitUtils] // @noErrors import { PermitUtils, setPermit, setActivePermitHash, } from '@cofhe/sdk/permits'; import type { PublicClient, WalletClient } from 'viem'; declare const publicClient: PublicClient; declare const walletClient: WalletClient; declare const exported: string; // ---cut--- const recipientPermit = await PermitUtils.importSharedAndSign( exported, publicClient, walletClient ); const chainId = await publicClient.getChainId(); const account = walletClient.account!.address; setPermit(chainId, account, recipientPermit); setActivePermitHash(chainId, account, recipientPermit.hash); ``` ::: :::: ### Active permit management The SDK tracks all stored permits and an **active permit hash** per `chainId + account`. Creating or importing a permit via `client.permits.*` automatically stores it and selects it as active. #### List stored permits ```ts twoslash // @noErrors import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; import type { PublicClient, WalletClient } from 'viem'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const client = createCofheClient(config); declare const publicClient: PublicClient; declare const walletClient: WalletClient; // ---cut--- await client.connect(publicClient, walletClient); const permits = client.permits.getPermits(); Object.keys(permits); ``` #### Read / select the active permit ```ts twoslash // @noErrors import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; import type { PublicClient, WalletClient } from 'viem'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const client = createCofheClient(config); declare const publicClient: PublicClient; declare const walletClient: WalletClient; declare const somePermitHash: string; // ---cut--- await client.connect(publicClient, walletClient); const active = client.permits.getActivePermit(); active?.hash; client.permits.selectActivePermit(somePermitHash); ``` #### Removing permits ```ts twoslash // @noErrors import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; import type { PublicClient, WalletClient } from 'viem'; const config = createCofheConfig({ supportedChains: [chains.sepolia] }); const client = createCofheClient(config); declare const publicClient: PublicClient; declare const walletClient: WalletClient; declare const permitHash: string; // ---cut--- await client.connect(publicClient, walletClient); client.permits.removePermit(permitHash); client.permits.removeActivePermit(); ``` ### Persistence and security * The SDK persists permits in a store keyed by `chainId + account`. * In web and React environments, this store uses `localStorage` under the key `cofhesdk-permits`. * A stored permit includes the **sealing private key**. Treat it like a secret. * Never share serialized permits with other users. * To share access, use `PermitUtils.export(...)` which strips sensitive fields. ## Writing Decrypt Result to Contract This page covers the “decrypt → write tx” flow after you run [`decryptForTx`](./decrypt-to-tx.mdx): you take `{ ctHash, decryptedValue, signature }` and submit a transaction that your contract can verify. There are two common patterns: * **Publish**: call `FHE.publishDecryptResult(ctHash, plaintext, signature)` so other contracts/users can reference the published result. * **Verify-only**: call `FHE.verifyDecryptResult(ctHash, plaintext, signature)` inside your contract without publishing globally. ### Prerequisites 1. Run [`decryptForTx`](./decrypt-to-tx.mdx) and get a result object with: * `ctHash` * `decryptedValue` * `signature` 2. Ensure the Solidity parameter type you pass matches your encrypted type. `decryptedValue` is a `bigint`. If your Solidity function expects a smaller integer type (e.g. `uint32`), make sure the value is within range. ### Publish the decrypt result on-chain The intended consumer of `decryptForTx` is an on-chain verifier such as `FHE.publishDecryptResult(...)` (or a wrapper function in your contract). In practice, you publish the result by calling a function on **your contract** that invokes `FHE.publishDecryptResult` internally. You submit an on-chain transaction that provides: * the handle you decrypted (`decryptResult.ctHash`), * the plaintext value, and * the Threshold Network signature returned by `decryptForTx`. :::code-group ```solidity [Solidity] import '@fhenixprotocol/cofhe-contracts/FHE.sol'; // Example wrapper (adjust plaintext/result type to match your encrypted type). function publishDecryptResult(bytes32 ctHash, uint32 plaintext, bytes calldata signature) external { FHE.publishDecryptResult(ctHash, plaintext, signature); } ``` ```ts [TypeScript] // `decryptResult` is returned by `decryptForTx(...).execute()` const tx = await myContract.publishDecryptResult( decryptResult.ctHash, decryptResult.decryptedValue, decryptResult.signature ); await tx.wait(); ``` ::: ### Verify a decrypt result signature (without publishing) Some protocols don’t need (or don’t want) to publish the decrypt result globally — they only need to verify that the provided plaintext and signature match a specific handle (`ctHash`). For example, an “unshield” flow can accept `(ctHash, plaintext, signature)` and only proceed if the signature is valid: ```solidity import '@fhenixprotocol/cofhe-contracts/FHE.sol'; function unshield(bytes32 ctHash, uint32 plaintext, bytes calldata signature) external { require(FHE.verifyDecryptResult(ctHash, plaintext, signature), 'Invalid decrypt signature'); // ...continue with protocol logic... } ``` :::note If you prefer to publish the result (so it can be reused elsewhere), use `FHE.publishDecryptResult(...)` instead. ::: ## Writing Encrypted Data to Contract This page covers the “encrypt → write tx” flow: encrypt plaintext values into `InE*` structs and pass them directly into a contract call. `encryptInputs` returns `EncryptedItemInput` objects that match the Solidity `InE*` input structs. The on-chain CoFHE library validates the verifier signature before the contract can use the ciphertext. The flow is as follows: 1. Ensure your contract function accepts encrypted `InE*` parameters. 2. Encrypt the plaintext values with [`encryptInputs`](./encrypting-inputs.mdx). 3. Send a transaction and pass the encrypted structs as the `InE*` arguments. ### Prerequisites 1. Create and connect a client (see the [client page](./client.mdx)). 2. Your contract function must accept encrypted `InE*` structs. The encrypted type you choose in JavaScript/TypeScript must match the Solidity parameter type: * `Encryptable.uint32(...)` → `InEuint32` * `Encryptable.bool(...)` → `InEbool` * `Encryptable.address(...)` → `InEaddress` ### Example: encrypt and call a contract The reference contract used throughout these docs is [`EncryptedCounter.sol`](/reference/encrypted-counter-sol). It exposes `setCount(InEuint32)` which is the canonical “encrypt input → send in the same transaction” flow. :::note In `EncryptedCounter.sol`, `setCount` is `onlyOwner`, so the connected `account` must be the contract owner. ::: :::code-group ```solidity [Solidity] // From EncryptedCounter.sol (see /reference/encrypted-counter-sol) function setCount(InEuint32 memory _inCount) external onlyOwner { count = FHE.asEuint32(_inCount); FHE.allowThis(count); FHE.allowSender(count); decrypted = false; decryptedCount = 0; } ``` ```ts twoslash [TypeScript] import { Encryptable, assertCorrectEncryptedItemInput } from '@cofhe/sdk'; import type { CofheClient } from '@cofhe/sdk'; import { parseAbi, type PublicClient, type WalletClient } from 'viem'; import { sepolia } from 'viem/chains'; declare const cofheClient: CofheClient; declare const publicClient: PublicClient; declare const walletClient: WalletClient; declare const account: `0x${string}`; declare const encryptedCounterAddress: `0x${string}`; const encryptedCounterAbi = parseAbi([ 'function setCount((uint256 ctHash,uint8 securityZone,uint8 utype,bytes signature) _inCount)', ]); // 1) Encrypt right before sending the transaction const [inCount] = await cofheClient .encryptInputs([Encryptable.uint32(42n)]) .execute(); assertCorrectEncryptedItemInput(inCount); // 2) Pass the encrypted struct as the `InE*` argument const hash = await walletClient.writeContract({ chain: sepolia, account, address: encryptedCounterAddress, abi: encryptedCounterAbi, functionName: 'setCount', args: [inCount], }); await publicClient.waitForTransactionReceipt({ hash }); ``` ::: ### Common pitfalls * **Wrong `Encryptable` type**: the `Encryptable.*` factory must match the Solidity parameter type (`InEuint32` vs `InEuint64`, etc). * **Wrong account / chain**: encrypted inputs are authorized for a specific `account + chainId`. If you encrypt under the wrong wallet/network, the contract call may revert. * **ABI struct shape mismatch**: if you hand-write an ABI, ensure the tuple fields are `(ctHash, securityZone, utype, signature)` in the same order your client library expects. ## Dictionary A quick glossary of common terms used throughout the Cofhe SDK and CoFHE docs. ### Handle A **handle** (sometimes called a “ciphertext handle”) is the on-chain identifier for an encrypted value. * In TypeScript examples you will often see this handle stored in a variable named `ctHash` (historical naming). * In Solidity ABIs it is typically represented as a `bytes32`. * Encrypted value types like `euint32` are stored on-chain as a handle. You pass the handle into decryption APIs like `decryptForView` / `decryptForTx`, and into access-control operations like `FHE.allow(handle, address)`. ### Ciphertext A **ciphertext** is the actual encrypted data. In many app flows you do not manipulate ciphertext bytes directly; instead you pass around a **handle** that refers to ciphertext registered with CoFHE. ### utype A **type tag** that describes the encrypted primitive type (e.g. `Uint32`, `Uint64`). * In the SDK it is commonly referred to as `utype`. * You’ll use it when decrypting for view (so the SDK can decode the returned plaintext correctly). ### Encrypted input The payload you submit in a transaction when calling a contract function that accepts encrypted inputs. * Off-chain: produced by `client.encryptInputs([...]).execute()` as `EncryptedItemInput` objects. * On-chain: passed into functions as `InEuint32`, `InEuint64`, etc. (structs validated by the CoFHE library before use). ### Permit A **permit** is an EIP-712 signature that authorizes decryption of one or more handles. * The permit’s **issuer** is the address doing the decryption. * Permits are checked against the on-chain ACL policy (e.g. via `FHE.allow(handle, address)`). See: [/sdk/permits](/sdk/permits). ### Sealing keypair Permits include a sealing keypair used for transport security: * The **public key** is sent to CoFHE so it can **re-encrypt** decrypted data for the permit holder. * The **private key** stays client-side and is used to **unseal** the returned data. ### Re-encryption A step where CoFHE/Threshold Network transforms decrypted data into a form that only the permit holder can unseal (using the permit’s sealing public key). ### Threshold Network The off-chain network that performs decryption operations (and, for tx flows, produces signatures that smart contracts can verify). ### decryptForView A decryption flow intended for UI / off-chain reads: * Returns plaintext only to the caller (not published on-chain). * **Always requires a permit.** ### decryptForTx A decryption flow intended for transactions: * Returns plaintext **plus a signature** that binds the plaintext to the handle. * The signature can be checked on-chain via `FHE.verifyDecryptResult(handle, plaintext, signature)`. * May or may not require a permit depending on the contract’s ACL policy for that handle. ### ACL **ACL** (access control list) rules determine who is allowed to decrypt a given handle. Contracts typically grant access by calling `FHE.allow(handle, address)`. ### Security zone A parameter that scopes where an encrypted value is valid/usable (for example, separating different application domains or environments). In the SDK it travels with encrypted inputs alongside the handle and type tag. import { Button } from 'vocs/components'; ## EncryptedCounter.sol This contract is included as a reference that is used throughout the SDK documentation to explain core functionality such as encrypting inputs, decrypting for view, and decrypting for transactions. ```solidity [EncryptedCounter.sol] // [!include ~/snippets/EncryptedCounter.sol] ``` ### Contract walkthrough #### Submit an encrypted value `setCount` accepts an encrypted input (`InEuint32`) and stores it as the new counter value. The caller submits an already-encrypted value — the contract converts it into an FHE ciphertext, updates allowances, and resets any previously revealed plaintext. :::info [Encrypting Inputs](/sdk/encrypting-inputs) documentation ::: ```solidity // [!include ~/snippets/EncryptedCounter.sol:encrypt-input] ``` #### Retrieve an encrypted value The counter is stored as an `euint32` — an encrypted handle. `getValue` returns this handle (a `bytes32` on-chain), which can then be decrypted off-chain. The `count` state variable is also `public`, so it can be read directly. :::info [Permits](/sdk/permits) and [Decrypting to View](/sdk/decrypt-to-view) documentation ::: ```solidity // [!include ~/snippets/EncryptedCounter.sol:encrypted-state-variable] // [!include ~/snippets/EncryptedCounter.sol:get-count] ``` #### Manipulate an encrypted value `incrementCount` performs an FHE addition on the encrypted counter, adding 1 without ever decrypting the value. The result is a new ciphertext that replaces the old one. ```solidity // [!include ~/snippets/EncryptedCounter.sol:increment-count] ``` #### Reveal an encrypted value `revealCount` accepts a plaintext value and a cryptographic signature proving it matches the current encrypted counter. The contract verifies the proof on-chain via `FHE.verifyDecryptResult` and, if valid, stores the plaintext. `allowCountPublicly` can be called beforehand to allow anyone to generate the decryption proof (no permit required). :::info [Decrypting to Transact](/sdk/decrypt-to-tx) documentation ::: ```solidity // [!include ~/snippets/EncryptedCounter.sol:allow-count-publicly] ``` ```solidity // [!include ~/snippets/EncryptedCounter.sol:decrypt-for-tx-usage] ``` ## Client The plugin extends `hre` with `hre.cofhe`, providing three ways to create and connect a `CofheClient` in your Hardhat tests. ### Batteries included (recommended) `hre.cofhe.createClientWithBatteries(signer?)` is the one-liner that handles everything: 1. Creates a CoFHE config with `environment: 'hardhat'` and `supportedChains: [hardhat]` 2. Creates a `CofheClient` 3. Connects it using the provided Hardhat signer (defaults to the first signer) 4. Generates a self-usage permit for the signer ```ts twoslash // @noErrors import '@cofhe/hardhat-plugin'; import '@nomicfoundation/hardhat-ethers'; // ---cut--- import hre from 'hardhat'; const [signer] = await hre.ethers.getSigners(); const cofheClient = await hre.cofhe.createClientWithBatteries(signer); // [!code focus] cofheClient.connected; // true ``` :::info `createClientWithBatteries` generates a self-permit automatically, so most test operations (encrypting inputs, decrypting for view, decrypting for tx) work immediately without any extra setup. ::: If `signer` is omitted, the first signer from `hre.ethers.getSigners()` is used. ### Manual setup For more control — custom config options, multiple signers, or adjusting `encryptDelay` — you can set up the client step by step. :::steps #### Create config ```ts twoslash // @noErrors import '@cofhe/hardhat-plugin'; import '@nomicfoundation/hardhat-ethers'; // ---cut--- import hre from 'hardhat'; import { hardhat } from '@cofhe/sdk/chains'; const config = await hre.cofhe.createConfig({ // [!code focus] supportedChains: [hardhat], // [!code focus] }); // [!code focus] ``` `hre.cofhe.createConfig` wraps `createCofheConfig` from `@cofhe/sdk/node` with two Hardhat-specific additions: * Sets `environment: 'hardhat'` automatically * Defaults `mocks.encryptDelay` to `0` so tests run without artificial wait times For all available config options, see [Client](/sdk/client). #### Create the client ```ts twoslash // @noErrors import '@cofhe/hardhat-plugin'; import '@nomicfoundation/hardhat-ethers'; import hre from 'hardhat'; import { hardhat } from '@cofhe/sdk/chains'; const config = await hre.cofhe.createConfig({ supportedChains: [hardhat] }); // ---cut--- const cofheClient = hre.cofhe.createClient(config); // [!code focus] ``` #### Connect with a Hardhat signer ```ts twoslash // @noErrors import '@cofhe/hardhat-plugin'; import '@nomicfoundation/hardhat-ethers'; import hre from 'hardhat'; import { hardhat } from '@cofhe/sdk/chains'; const [signer] = await hre.ethers.getSigners(); const config = await hre.cofhe.createConfig({ supportedChains: [hardhat] }); const cofheClient = hre.cofhe.createClient(config); // ---cut--- await hre.cofhe.connectWithHardhatSigner(cofheClient, signer); // [!code focus] ``` `connectWithHardhatSigner` uses `HardhatSignerAdapter` under the hood to convert a `HardhatEthersSigner` into the viem `PublicClient` + `WalletClient` pair that the SDK expects. ::: ### Low-level adapter If you need direct access to the underlying viem clients, call the adapter directly: ```ts twoslash // @noErrors import '@cofhe/hardhat-plugin'; import '@nomicfoundation/hardhat-ethers'; import hre from 'hardhat'; import { CofheClient } from '@cofhe/sdk'; const [signer] = await hre.ethers.getSigners(); declare const cofheClient: CofheClient; // ---cut--- const { publicClient, walletClient } = await hre.cofhe.hardhatSignerAdapter(signer); // [!code focus] await cofheClient.connect(publicClient, walletClient); ``` ### Using the client Once connected, the client works identically to the standard SDK client. See: * [Encrypting Inputs](/sdk/encrypting-inputs) * [Decrypting to View](/sdk/decrypt-to-view) * [Decrypting to Transact](/sdk/decrypt-to-tx) * [Permits](/sdk/permits) ## Getting Started This guide walks through integrating `@cofhe/hardhat-plugin` into an existing Hardhat project — from installation to your first passing FHE test. ### Prerequisites * A Hardhat project with `hardhat >= 2.0` * `@nomicfoundation/hardhat-toolbox` or `@nomicfoundation/hardhat-ethers` already installed ### 1) Install dependencies :::code-group ```bash [npm] npm install @cofhe/hardhat-plugin @cofhe/sdk @fhenixprotocol/cofhe-contracts ``` ```bash [pnpm] pnpm add @cofhe/hardhat-plugin @cofhe/sdk @fhenixprotocol/cofhe-contracts ``` ```bash [yarn] yarn add @cofhe/hardhat-plugin @cofhe/sdk @fhenixprotocol/cofhe-contracts ``` ::: * **`@cofhe/hardhat-plugin`** — extends `hre` with `hre.cofhe`, deploys mock contracts, and registers Hardhat tasks * **`@cofhe/sdk`** — the CoFHE client used in tests for encrypting inputs and decrypting handles * **`@fhenixprotocol/cofhe-contracts`** — the `FHE.sol` library imported by your Solidity contracts ### 2) Update `hardhat.config.ts` Import the plugin and set `evmVersion` to `cancun`. The `cancun` EVM version is required for the transient storage opcodes used by the FHE contracts. ```ts [hardhat.config.ts] import { HardhatUserConfig } from 'hardhat/config'; import '@nomicfoundation/hardhat-toolbox'; import '@cofhe/hardhat-plugin'; // [!code ++] const config: HardhatUserConfig = { solidity: { version: '0.8.28', settings: { evmVersion: 'cancun', // [!code ++] }, }, }; export default config; ``` :::warning Without `evmVersion: 'cancun'`, compilation will fail with errors from `@fhenixprotocol/cofhe-contracts`. ::: ### 3) Write an FHE contract Import `FHE.sol` and use the FHE types in your contract. The key types are: * **`euint32`** — an encrypted `uint32` stored on-chain * **`InEuint32`** — the encrypted input type passed in from the SDK ```solidity [contracts/MyContract.sol] // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.28; import '@fhenixprotocol/cofhe-contracts/FHE.sol'; contract MyContract { euint32 public storedValue; function setValue(InEuint32 memory inValue) external { storedValue = FHE.asEuint32(inValue); // [!code focus] FHE.allowThis(storedValue); // [!code focus] FHE.allowSender(storedValue); // [!code focus] } } ``` `FHE.allowThis` and `FHE.allowSender` grant the contract and the caller permission to read the encrypted value. Without these, decryption will be rejected by the ACL. ### 4) Write a test Use `hre.cofhe.createClientWithBatteries` to get a fully configured client, then encrypt an input, call your contract, and decrypt the result. ```ts [test/MyContract.test.ts] import hre from 'hardhat'; import { CofheClient, Encryptable, FheTypes } from '@cofhe/sdk'; import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers'; import { expect } from 'chai'; describe('MyContract', () => { let cofheClient: CofheClient; let signer: HardhatEthersSigner; before(async () => { [signer] = await hre.ethers.getSigners(); cofheClient = await hre.cofhe.createClientWithBatteries(signer); // [!code focus] }); it('should store and decrypt an encrypted value', async () => { const MyContract = await hre.ethers.getContractFactory('MyContract'); const contract = await MyContract.deploy(); // Encrypt the input const [encrypted] = await cofheClient .encryptInputs([Encryptable.uint32(42n)]) .execute(); // Send to contract await (await contract.setValue(encrypted)).wait(); // Read the stored handle and decrypt const ctHash = await contract.storedValue(); const decrypted = await cofheClient .decryptForView(ctHash, FheTypes.Uint32) .execute(); expect(decrypted).to.equal(42n); }); }); ``` ### 5) Run the tests ```bash npx hardhat test ``` The plugin automatically deploys the mock contracts before running — no extra setup needed. ``` MyContract ✓ should store and decrypt an encrypted value 1 passing (1s) ``` ### Next steps * [Client](/hardhat/client) — manual client setup and config options * [Mock Contracts](/hardhat/mock-contracts) — reading plaintext values directly in tests * [Logging](/hardhat/logging) — inspecting FHE operations in test output * [Testing](/hardhat/testing) — permits, `decryptForTx`, and more test patterns ## Logging The mock contracts log every FHE operation to the console. This makes it easy to inspect what your contracts are doing under the hood during tests. ### What gets logged Each FHE operation emits a formatted log entry showing the operation name, the input and output operand hashes (truncated), and the security zone: ``` ┌──────────────────┬────────────────────────────────────────────────── │ [COFHE-MOCKS] │ "counter.increment()" logs: ├──────────────────┴────────────────────────────────────────────────── ├ FHE.add | euint32(4473..3424)[0] + euint32(1157..3648)[1] => euint32(1106..1872)[1] ├ FHE.allowThis | euint32(1106..1872)[1] -> 0x663f..6602 ├ FHE.allow | euint32(1106..1872)[1] -> 0x3c44..93bc └───────────────────────────────────────────────────────────────────── ``` ### `withLogs(name, fn)` — recommended Wraps a block of code with logging enabled and prints a labeled box around the output. The `name` appears as the header of the log box so you can identify which call produced which operations at a glance. ```ts twoslash // @noErrors import '@cofhe/hardhat-plugin'; import '@nomicfoundation/hardhat-ethers'; import hre from 'hardhat'; import { Contract } from 'ethers'; declare const counter: Contract; // ---cut--- await hre.cofhe.mocks.withLogs('counter.increment()', async () => { // [!code focus] await counter.increment(); }); // [!code focus] ``` `withLogs` enables logging before the closure runs and disables it after, so only operations from within that block appear in the output. ### `enableLogs()` / `disableLogs()` — manual For finer-grained control, you can enable and disable logging manually: ```ts twoslash // @noErrors import '@cofhe/hardhat-plugin'; import '@nomicfoundation/hardhat-ethers'; import hre from 'hardhat'; import { Contract } from 'ethers'; declare const counter: Contract; // ---cut--- await hre.cofhe.mocks.enableLogs('counter.increment()'); await counter.increment(); await hre.cofhe.mocks.disableLogs(); ``` :::info `enableLogs` accepts an optional label string. If provided, it prints a labeled header immediately — useful when you want to mark a section of output at a known point and disable logging asynchronously later. ::: ### Default logging behavior Logging is **enabled by default**. You can turn it off globally in your Hardhat config: ```ts [hardhat.config.ts] import '@cofhe/hardhat-plugin'; export default { solidity: '0.8.28', cofhe: { logMocks: false, // disable FHE op logs globally // [!code focus] }, }; ``` You can also toggle logging for a specific test run using the Hardhat task: ```bash npx hardhat task:cofhe-mocks:setlogops --enable true npx hardhat task:cofhe-mocks:setlogops --enable false ``` ## Mock Contracts The plugin deploys a suite of mock contracts that simulate the full CoFHE coprocessor stack on the Hardhat network. This lets you develop and test FHE contracts without running the off-chain FHE engine. ### What the mocks simulate | Contract | Role | | ---------------------- | ------------------------------------------------------------------------------------- | | `MockTaskManager` | Manages FHE operations; stores plaintext values on-chain for testing | | `MockACL` | Access control for encrypted handles | | `MockZkVerifier` | Simulates ZK proof verification for encrypted inputs | | `MockThresholdNetwork` | Handles decryption requests | | `TestBed` | Helper contract for testing — exposes trivial value setters and a `numberHash` getter | The SDK automatically detects when it's running against the mock environment (by checking bytecode at the `MockZkVerifier` fixed address) and adapts its behavior accordingly — ZK proof generation is skipped and verification is handled by the mock contracts. ### Auto-deployment Mock contracts are deployed automatically before every `npx hardhat test` and `npx hardhat node` run. To skip auto-deployment: ```bash COFHE_SKIP_MOCKS_DEPLOY=1 npx hardhat test ``` :::note You only need `COFHE_SKIP_MOCKS_DEPLOY=1` if your tests exclusively target an external RPC and don't use the in-process Hardhat network at all. ::: You can also deploy mock contracts manually via the Hardhat task: ```bash npx hardhat task:cofhe-mocks:deploy npx hardhat task:cofhe-mocks:deploy --deployTestBed false # skip TestBed npx hardhat task:cofhe-mocks:deploy --silent true # suppress output ``` Or programmatically from a test or script: ```ts twoslash // @noErrors import '@cofhe/hardhat-plugin'; import '@nomicfoundation/hardhat-ethers'; // ---cut--- import hre from 'hardhat'; await hre.cofhe.mocks.deployMocks(); ``` ### Accessing mock contracts `hre.cofhe.mocks` exposes typed accessors for each mock contract. Each method returns a fully typed Typechain contract instance: ```ts twoslash // @noErrors import '@cofhe/hardhat-plugin'; import '@nomicfoundation/hardhat-ethers'; // ---cut--- import hre from 'hardhat'; const taskManager = await hre.cofhe.mocks.getMockTaskManager(); const acl = await hre.cofhe.mocks.getMockACL(); const thresholdNetwork = await hre.cofhe.mocks.getMockThresholdNetwork(); const zkVerifier = await hre.cofhe.mocks.getMockZkVerifier(); const testBed = await hre.cofhe.mocks.getTestBed(); ``` ### Reading plaintext values Because `MockTaskManager` stores plaintext values on-chain, you can read the underlying plaintext of any encrypted handle directly in tests — no permit needed. #### `getPlaintext(ctHash)` Returns the plaintext `bigint` for a given [handle](/reference/dictionary#handle) (`ctHash` in code): ```ts twoslash // @noErrors import '@cofhe/hardhat-plugin'; import '@nomicfoundation/hardhat-ethers'; // ---cut--- import hre from 'hardhat'; import { expect } from 'chai'; const testBed = await hre.cofhe.mocks.getTestBed(); await testBed.setNumberTrivial(7); const ctHash = await testBed.numberHash(); const plaintext = await hre.cofhe.mocks.getPlaintext(ctHash); // [!code focus] expect(plaintext).to.equal(7n); ``` #### `expectPlaintext(ctHash, expectedValue)` Assertion shorthand — wraps `getPlaintext` with a Chai `expect`: ```ts twoslash // @noErrors import '@cofhe/hardhat-plugin'; import '@nomicfoundation/hardhat-ethers'; // ---cut--- import hre from 'hardhat'; const testBed = await hre.cofhe.mocks.getTestBed(); await testBed.setNumberTrivial(7); const ctHash = await testBed.numberHash(); await hre.cofhe.mocks.expectPlaintext(ctHash, 7n); // [!code focus] ``` :::warning `getPlaintext` and `expectPlaintext` only work on the Hardhat network (where `MockTaskManager` stores plaintexts). They will throw on `localcofhe` or any real network. ::: ## Testing This page shows the common patterns for writing Hardhat tests with the CoFHE plugin. ### Test setup Use `hre.cofhe.createClientWithBatteries` in a `before` hook. It creates and connects a fully configured `CofheClient` — including a self-permit — so the client is ready for every test in the suite: ```ts twoslash // @noErrors import '@cofhe/hardhat-plugin'; import '@nomicfoundation/hardhat-ethers'; // ---cut--- import hre from 'hardhat'; import { CofheClient } from '@cofhe/sdk'; import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers'; let cofheClient: CofheClient; let signer: HardhatEthersSigner; before(async () => { [signer] = await hre.ethers.getSigners(); cofheClient = await hre.cofhe.createClientWithBatteries(signer); // [!code focus] }); ``` See [Client](/hardhat/client) for manual setup options. ### Encrypt → store → decrypt The core test loop: encrypt a value, pass it to a contract, then decrypt the stored handle. ```ts twoslash // @noErrors import '@cofhe/hardhat-plugin'; import '@nomicfoundation/hardhat-ethers'; import hre from 'hardhat'; import { CofheClient, Encryptable, FheTypes } from '@cofhe/sdk'; import { Contract } from 'ethers'; import { expect } from 'chai'; declare const cofheClient: CofheClient; declare const testContract: Contract; // ---cut--- // 1. Encrypt the input const encrypted = await cofheClient .encryptInputs([Encryptable.uint32(100n)]) .execute(); // 2. Send to contract const tx = await testContract.setValue(encrypted[0]); await tx.wait(); // 3. Read the stored handle const ctHash = await testContract.storedValue(); // 4. Decrypt for display const decrypted = await cofheClient .decryptForView(ctHash, FheTypes.Uint32) .execute(); expect(decrypted).to.equal(100n); ``` For more on each step, see [Encrypting Inputs](/sdk/encrypting-inputs) and [Decrypting to View](/sdk/decrypt-to-view). ### Reading plaintext directly In tests you can bypass the normal decrypt flow and read the raw plaintext stored by the mock contracts. This is useful for asserting contract state without needing a permit: ```ts twoslash // @noErrors import '@cofhe/hardhat-plugin'; import '@nomicfoundation/hardhat-ethers'; import hre from 'hardhat'; declare const ctHash: string; // ---cut--- // Get raw plaintext value const plaintext = await hre.cofhe.mocks.getPlaintext(ctHash); // Or use the assertion shorthand await hre.cofhe.mocks.expectPlaintext(ctHash, 100n); ``` See [Mock Contracts](/hardhat/mock-contracts) for details. ### Permits `createClientWithBatteries` pre-generates a self-permit, so `decryptForView` and `decryptForTx().withPermit()` work immediately. For tests that need named permits or multiple signers, create them explicitly: ```ts twoslash // @noErrors import { CofheClient } from '@cofhe/sdk'; import { PermitUtils } from '@cofhe/sdk/permits'; import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers'; declare const cofheClient: CofheClient; declare const signer: HardhatEthersSigner; // ---cut--- const permit = await cofheClient.permits.createSelf({ issuer: signer.address, name: 'My Test Permit', }); // Select it as the active permit const permitHash = PermitUtils.getHash(permit); cofheClient.permits.selectActivePermit(permitHash); ``` Alternatively, you may find it easier to create a cofheClient for each of the multiple signers you need to test. ```ts twoslash // @noErrors import '@cofhe/hardhat-plugin'; import '@nomicfoundation/hardhat-ethers'; import hre from 'hardhat'; import { CofheClient } from '@cofhe/sdk'; declare const cofheClient: CofheClient; // ---cut--- const [bob, alice] = await hre.ethers.getSigners(); const bobClient = await hre.cofhe.createClientWithBatteries(bob); const aliceClient = await hre.cofhe.createClientWithBatteries(alice); ``` ### `decryptForTx` patterns [`decryptForTx`](/sdk/decrypt-to-tx) returns a `{ ctHash, decryptedValue, signature }` tuple for on-chain submission. The permit mode must be selected explicitly. #### Globally allowed values (`.withoutPermit()`) When a contract calls `FHE.allowPublic(handle)`, anyone can decrypt without a permit: ```ts twoslash // @noErrors import { CofheClient } from '@cofhe/sdk'; import { expect } from 'chai'; declare const cofheClient: CofheClient; declare const publicCtHash: string; // ---cut--- const result = await cofheClient .decryptForTx(publicCtHash) .withoutPermit() // [!code focus] .execute(); expect(result.decryptedValue).to.equal(55n); ``` #### Access-controlled values (`.withPermit()`) For handles restricted by ACL policy, supply a permit: :::code-group ```ts twoslash [Explicit permit] // @noErrors import { CofheClient } from '@cofhe/sdk'; import { Permit } from '@cofhe/sdk/permits'; import { expect } from 'chai'; declare const cofheClient: CofheClient; declare const ctHash: string; declare const permit: Permit; // ---cut--- const result = await cofheClient .decryptForTx(ctHash) .withPermit(permit) // [!code focus] .execute(); expect(result.decryptedValue).to.equal(99n); ``` ```ts twoslash [Active permit] // @noErrors import { CofheClient } from '@cofhe/sdk'; import { expect } from 'chai'; declare const cofheClient: CofheClient; declare const ctHash: string; // ---cut--- // resolves the active permit automatically const result = await cofheClient .decryptForTx(ctHash) .withPermit() // [!code focus] .execute(); expect(result.decryptedValue).to.equal(99n); ``` ::: #### Submitting the result on-chain Pass the result directly to your contract: ```ts twoslash // @noErrors import { Contract } from 'ethers'; import { DecryptForTxResult } from '@cofhe/sdk'; declare const myContract: Contract; declare const result: DecryptForTxResult; // ---cut--- await myContract.revealValue( result.ctHash, result.decryptedValue, result.signature ); ``` For a full walkthrough of `decryptForTx`, see [Decrypting to Transact](/sdk/decrypt-to-tx).