# Cofhe SDK Docs > Documentation for the Cofhe SDK ## Foundry Plugin `@cofhe/foundry-plugin` provides two contracts for writing CoFHE tests with Foundry: * **`CofheTest`** — abstract base contract. Inherits `forge-std/Test`. Exposes `deployMocks()`, `createCofheClient()`, `getPlaintext()`, and `expectPlaintext()`. * **`CofheClient`** — SDK-like client. Manages a private key / account pair and exposes encrypt, decrypt, and ACP helpers. ### Installation #### npm / pnpm :::code-group ```bash [npm] npm install --save-dev @cofhe/foundry-plugin ``` ```bash [pnpm] pnpm add --save-dev @cofhe/foundry-plugin ``` ::: Then add the remappings to your `foundry.toml`: ```toml [foundry.toml] [profile.default] src = "contracts" out = "out" libs = ["node_modules", "lib"] remappings = [ "@cofhe/foundry-plugin/=node_modules/@cofhe/foundry-plugin/", "@cofhe/mock-contracts/=node_modules/@cofhe/mock-contracts/", "@fhenixprotocol/cofhe-contracts/=node_modules/@fhenixprotocol/cofhe-contracts/", "@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/", "forge-std/=node_modules/forge-std/src/", ] ``` #### Git submodule ```bash forge install FhenixProtocol/cofhesdk # @cofhe/foundry-plugin + @cofhe/mock-contracts ``` Both packages live in the `cofhesdk` monorepo, so a single install covers them. Forge will check it out under `lib/`, so point the `@cofhe/foundry-plugin/` and `@cofhe/mock-contracts/` remappings at `lib/cofhesdk/packages//` instead of `node_modules/@cofhe//`. ### Quick start ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import { CofheTest } from "@cofhe/foundry-plugin/contracts/CofheTest.sol"; import { CofheClient } from "@cofhe/foundry-plugin/contracts/CofheClient.sol"; import "@fhenixprotocol/cofhe-contracts/FHE.sol"; contract CounterTest is CofheTest { Counter private counter; CofheClient private cofheClient; uint256 constant USER_PKEY = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80; function setUp() public { deployMocks(); // deploy full mock stack cofheClient = createCofheClient(); // create SDK-like client cofheClient.connect(USER_PKEY); // set account/signer counter = new Counter(); } function test_increment() public { // The last argument binds the input to the contract that will consume it. (externalEuint32 hash, bytes memory proof) = cofheClient.createExternalEuint32(5, address(counter)); externalEuint32[] memory hashes = new externalEuint32[](1); hashes[0] = hash; vm.prank(cofheClient.account()); counter.setNumberBatch(hashes, proof); expectPlaintext(counter.numberHash(), 5); } } ``` ### Gas reporting Mock FHE operations replicate the off-chain CoFHE work on-chain, which would normally inflate reported gas 2–3×. Under forge, `deployMocks()` excludes that mock-only work from gas metering by default (via `pauseGasMetering` cheatcodes), so `forge test --gas-report`, `forge snapshot`, and `gasleft()` measurements report numbers close to real-network costs — no configuration needed. * Requires forge ≥ 1.0 (older versions had unreliable `pauseGasMetering` behavior). If cheatcodes are unavailable, the mocks silently fall back to normal metering — correctness is never affected. * To see raw mock gas instead, opt out with `mockTaskManager.setMockGasExcluded(false)`. :::note Upgrading to a plugin version with this feature will lower existing `forge snapshot` numbers — the drop is the mock overhead disappearing from the report, not a change in your contracts. ::: ### `CofheTest` API `CofheTest` is `abstract` and provides mock infrastructure. Inherit it in your test contract. | Function | Description | | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `deployMocks()` | Deploys the complete CoFHE mock stack: `MockTaskManager`, `MockACL`, `MockZkVerifier`, `MockZkVerifierSigner`, `MockThresholdNetwork`, `MockThresholdNetworkSigner`. Mirrors the Hardhat plugin deployment order. Also enables mock-gas exclusion (see [Gas reporting](#gas-reporting)). | | `createCofheClient()` | Returns a new `CofheClient` instance. Call `connect(pkey)` on the result before use. | | `enableLogs()` | Turns on plaintext operation logs from `MockTaskManager`. | | `disableLogs()` | Turns off plaintext operation logs. | | `getPlaintext(bytes32 ctHash)` | Returns the stored plaintext for a ciphertext hash as `uint256`. Reverts if the hash is not in mock storage. | | `getPlaintext(euint32 eValue)` | Typed overloads — returns the correct Solidity type (`bool`, `uint8`…`uint128`, `address`) for each FHE type. `internal` visibility only. | | `expectPlaintext(bytes32, uint256)` | Asserts `ctHash` exists in mock storage and equals `value`. | | `expectPlaintext(bytes32, uint256, string)` | Same with a custom failure message. | | `expectPlaintext(euint32, uint32)` | Typed overloads for each FHE type. | | `expectPlaintext(euint32, uint32, string)` | Typed overloads with failure message. | ### `CofheClient` API `CofheClient` is a standalone contract deployed by `createCofheClient()`. It wraps mock FHE operations behind the same conceptual interface as the JS SDK. #### Setup | Function | Description | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | `connect(uint256 pkey)` | Stores the private key, derives the account address, and marks the client as ready. Must be called before all other functions. | | `account()` | Returns the address derived from the connected private key. | #### Encryption `CofheClient.createEncryptedInputsBatch` is the root of every encryption helper on the client — a whole array of ciphertexts authenticated by **one shared signature**, verified via `ITaskManager.batchVerifyInputs` (matching [`FhenixProtocol/cofhe-contracts#78`](https://github.com/FhenixProtocol/cofhe-contracts/pull/78)). `createExternalEbool`...`createExternalEaddress` are thin single-item wrappers over a batch of size one, not a separate signing scheme — every hash they return is only valid when consumed via the batch path (`FHE.asEuint32s([hash], signature)`, not the legacy single-item `FHE.asEuint32(hash, proof)`). Every helper takes the consuming contract as a required **last argument** — the verifier binds that address into the signature, matching [`FhenixProtocol/cofhe-contracts#77`](https://github.com/FhenixProtocol/cofhe-contracts/pull/77). There is no client-level setter; passing it per call lets one test encrypt for several contracts without tracking hidden state. Passing `address(0)` reverts. | Function | Returns | | ------------------------------------------------------------ | ---------------------------------- | | `createExternalEbool(bool, address consumingContract)` | `(externalEbool, bytes memory)` | | `createExternalEuint8(uint8, address consumingContract)` | `(externalEuint8, bytes memory)` | | `createExternalEuint16(uint16, address consumingContract)` | `(externalEuint16, bytes memory)` | | `createExternalEuint32(uint32, address consumingContract)` | `(externalEuint32, bytes memory)` | | `createExternalEuint64(uint64, address consumingContract)` | `(externalEuint64, bytes memory)` | | `createExternalEuint128(uint128, address consumingContract)` | `(externalEuint128, bytes memory)` | | `createExternalEaddress(address, address consumingContract)` | `(externalEaddress, bytes memory)` | Example — note the contract's entry point must accept an array (even for a single value) plus the shared signature: ```solidity function testSetValue(uint32 n) public { (externalEuint32 hash, bytes memory signature) = cofheClient.createExternalEuint32(n, address(myContract)); externalEuint32[] memory hashes = new externalEuint32[](1); hashes[0] = hash; vm.prank(cofheClient.account()); myContract.setValueBatch(hashes, signature); expectPlaintext(myContract.getValue(), n); } ``` ##### Batch variants For batches of more than one ciphertext, use `createEncryptedInputsBatch` directly (mixed `utype`s allowed) or the typed `createEuint32sBatch` convenience wrapper. | Function | Returns | | --------------------------------------------------------------------------- | ----------------------------------------------------------------- | | `createEncryptedInputsBatch(uint8[], uint256[], address consumingContract)` | `(UnsignedEncryptedInput[], bytes memory)` — mixed utypes allowed | | `createEuint32sBatch(uint32[], address consumingContract)` | `(externalEuint32[] memory, bytes memory)` | Example: ```solidity function testSetValueBatch(uint32[] memory values) public { (externalEuint32[] memory hashes, bytes memory signature) = cofheClient.createEuint32sBatch(values, address(myContract)); vm.prank(cofheClient.account()); myContract.setValueBatch(hashes, signature); } ``` This is the canonical signing path, backed by `MockZkVerifierSigner.zkVerifyBatchSign` — this repo's own tooling routes all encryption (including batches of size 1) through it, rather than maintaining a second independent signing implementation. #### Decryption | Function | Returns | Description | | ------------------------------------------------------------- | --------------------------- | --------------------------------------------------------------------------------- | | `decryptForTx_withoutACP(bytes32 ctHash)` | `(bytes32, uint256, bytes)` | Decrypts a globally-allowed ciphertext. Returns `(ctHash, plaintext, signature)`. | | `decryptForTx_withACP(bytes32 ctHash, Permission permission)` | `(bytes32, uint256, bytes)` | Decrypts using an ACP. Same return shape. | | `decryptForView(bytes32 ctHash, Permission permission)` | `uint256` | Seals and unseals with the ACP's sealing key for off-chain reading. | #### ACPs | Function | Returns | Description | | ------------------------------------- | ----------------- | --------------------------------------------------------------------------------------------- | | `acp_createSelf()` | `Permission` | Creates a self-ACP signed by the connected account. | | `acp_createShared(address recipient)` | `Permission` | Creates the issuer half of a shared ACP. | | `acp_exportShared(Permission)` | `SharedACPExport` | Strips private fields for safe transmission. | | `acp_importShared(SharedACPExport)` | `Permission` | Reconstructs a `Permission` as the recipient, adding the sealing key and recipient signature. | ### Hardhat 3 Solidity tests `@cofhe/foundry-plugin` ships a `remappings.txt` that Hardhat 3 automatically discovers. Add the package as a `devDependency` in your `hardhat-3-plugin-test` project and configure the Solidity test path: ```ts [hardhat.config.ts] export default defineConfig({ plugins: [cofhePlugin, hardhatViem, hardhatNodeTestRunner], solidity: '0.8.29', paths: { tests: { nodejs: './test', solidity: './test/solidity', }, }, }); ``` Then import `CofheTest` and `CofheClient` exactly as in standard Foundry tests — the `@cofhe/foundry-plugin/` prefix resolves via the bundled remappings: ```solidity import { CofheTest } from "@cofhe/foundry-plugin/contracts/CofheTest.sol"; import { CofheClient } from "@cofhe/foundry-plugin/contracts/CofheClient.sol"; ``` Run with: ```bash hardhat test solidity ``` ## Get started Hello world! ## Hardhat 3 Plugin `@cofhe/hardhat-3-plugin` is the CoFHE plugin for [Hardhat 3](https://hardhat.org). It deploys the full CoFHE mock stack on every `network.connect()` call and attaches a `cofhe` namespace to the connection object — no setup boilerplate required. ### Installation ::::steps #### Install the package :::code-group ```bash [npm] npm install --save-dev @cofhe/hardhat-3-plugin ``` ```bash [pnpm] pnpm add --save-dev @cofhe/hardhat-3-plugin ``` ::: #### Register in your config ```ts [hardhat.config.ts] import { defineConfig } from 'hardhat/config'; import cofhePlugin from '@cofhe/hardhat-3-plugin'; // [!code focus] import hardhatViem from '@nomicfoundation/hardhat-viem'; import hardhatNodeTestRunner from '@nomicfoundation/hardhat-node-test-runner'; export default defineConfig({ plugins: [cofhePlugin, hardhatViem, hardhatNodeTestRunner], // [!code focus] }); ``` :::: ### Configuration All options are optional. The `cofhe` block can be omitted entirely if the defaults are acceptable. ```ts [hardhat.config.ts] export default defineConfig({ plugins: [cofhePlugin, hardhatViem, hardhatNodeTestRunner], cofhe: { logMocks: false, // log FHE ops to the console (default: false) gasWarning: false, // warn that mock gas differs from live FHE (default: false) gasSummary: false, // print a per-method adjusted-gas summary after tests (default: false) mocksDeployVerbosity: 'v', // '' | 'v' | 'vv' — deployment output (default: 'v') }, }); ``` | Option | Type | Default | Description | | ---------------------- | ------------------- | ------- | -------------------------------------------------------------------------------- | | `logMocks` | `boolean` | `false` | Enable structured FHE operation logs from mock contracts | | `gasWarning` | `boolean` | `false` | Print a reminder that mock gas costs differ from live FHE | | `gasSummary` | `boolean` | `false` | Print a per-method gas summary after `hardhat test`, with mock overhead excluded | | `mocksDeployVerbosity` | `'' \| 'v' \| 'vv'` | `'v'` | `''` silent · `'v'` summary line · `'vv'` full deployment log | ### Usage #### The `cofhe` connection object Every call to `network.connect()` deploys all CoFHE mock contracts and attaches a `cofhe` namespace to the returned connection: ```ts import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { network } from 'hardhat'; describe('My FHE contract', async () => { const { viem, cofhe } = await network.connect(); // [!code focus] const publicClient = await viem.getPublicClient(); const [walletClient] = await viem.getWalletClients(); it('stores an encrypted value', async () => { const client = await cofhe.createClientWithBatteries(walletClient); // ... }); }); ``` :::info `network.connect()` is awaited at the top of the `describe` callback. Hardhat 3's Node.js test runner supports top-level `await` here, so mocks are deployed exactly once per test file without a `before()` hook. ::: #### `createClientWithBatteries(walletClient?)` Creates a fully initialized `CofheClient` in one call — configured for Hardhat, connected, and with a self ACP already issued: ```ts // Use the first account in the connection: const client = await cofhe.createClientWithBatteries(); // Or pass a specific wallet client: const [walletClient] = await viem.getWalletClients(); const client = await cofhe.createClientWithBatteries(walletClient); ``` For manual setup, use `cofhe.createConfig()` and `cofhe.createClient()` separately. See the [SDK Client](/sdk/client) docs. ### Mock utilities #### Plaintext inspection In the mock environment every ciphertext has a corresponding plaintext stored on-chain. These helpers let you assert contract state without going through the full decrypt flow: ```ts // Returns the raw plaintext as a bigint const value = await cofhe.mocks.getPlaintext(ctHash); // Throws if the plaintext doesn't match await cofhe.mocks.expectPlaintext(ctHash, 42n); ``` Both accept `ctHash` as a `bigint` or hex `string`. #### Logging FHE operation logs are off by default. Turn them on for a specific block using `withLogs`: ```ts await cofhe.mocks.withLogs('counter.increment()', async () => { await counter.increment(); }); ``` Or toggle manually: ```ts await cofhe.mocks.enableLogs(); await counter.increment(); await cofhe.mocks.disableLogs(); ``` #### Gas reporting Mock FHE operations consume extra gas replicating the off-chain CoFHE work on-chain. The mock task manager emits a `MockGasConsumed(uint256)` event per block of mock-only work, letting the plugin report corrected numbers: ```ts const hash = await walletClient.writeContract({ ...simpleTest, functionName: 'setValueTrivial', args: [7n] }); const receipt = await publicClient.waitForTransactionReceipt({ hash }); // Gas usage excluding mock overhead — an estimate of real-network cost: const adjusted = cofhe.getAdjustedGasUsed(receipt); // Or the full breakdown: const { gasUsed, mockGas, adjustedGasUsed, mockGasEvents } = cofhe.getAdjustedGasBreakdown(receipt); ``` Both are pure functions of the receipt — no RPC calls — and pass raw `gasUsed` through unchanged on a real network. With `cofhe: { gasSummary: true }` in the config, a per-method table (raw vs adjusted gas) is printed after `hardhat test`. The table is reconstructed from chain history at the end of each test worker: tests that roll back state via snapshots won't appear, and it is best-effort after forced exits — use the receipt helpers for exact numbers. :::note `eth_estimateGas` is not adjusted — the mock work really does execute, so transactions still need the raw gas limit. ::: #### Contract descriptors Each mock contract is exposed as a `{ address, abi }` object that can be spread directly into Viem's `readContract` / `writeContract`: ```ts // Core mock contracts are synchronous { address, abi } descriptors: cofhe.mocks.MockTaskManager; cofhe.mocks.MockACL; cofhe.mocks.MockZkVerifier; cofhe.mocks.MockThresholdNetwork; ``` `SimpleTest` is deployed explicitly when needed and bound locally from its fixed address and ABI. ### Complete example ```ts import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { network } from 'hardhat'; import { Encryptable, FheTypes } from '@cofhe/sdk'; describe('Encrypted counter', async () => { const { viem, cofhe } = await network.connect(); const publicClient = await viem.getPublicClient(); const [walletClient] = await viem.getWalletClients(); const simpleTestArtifact = /* your compiled SimpleTest artifact */; const deployHash = await walletClient.deployContract({ abi: simpleTestArtifact.abi, bytecode: simpleTestArtifact.bytecode.object as `0x${string}`, }); const deployReceipt = await publicClient.waitForTransactionReceipt({ hash: deployHash }); if (!deployReceipt.contractAddress) throw new Error('SimpleTest deployment failed'); const simpleTest = { address: deployReceipt.contractAddress, abi: simpleTestArtifact.abi } as const; it('encrypts, stores, and reads back a uint32', async () => { const client = await cofhe.createClientWithBatteries(walletClient); // [hash, signature] - one hash per input, followed by the shared batch signature const [hash, signature] = await client .encryptInputs([Encryptable.uint32(42n)]) .setConsumingContract(simpleTest.address) .execute(); await walletClient.writeContract({ ...simpleTest, functionName: 'setValueBatch', args: [[hash], signature], }); const ctHash = await publicClient.readContract({ ...simpleTest, functionName: 'getValueHash', }); await cofhe.mocks.expectPlaintext(ctHash, 42n); }); }); ``` ### Differences from `@cofhe/hardhat-plugin` | | `@cofhe/hardhat-plugin` (v2) | `@cofhe/hardhat-3-plugin` | | ------------- | ---------------------------- | ------------------------- | | Access | `hre.cofhe` | `conn.cofhe` | | Trigger | `npx hardhat test` | `network.connect()` | | Test runner | Mocha | Node.js `node:test` | | Ethers / Viem | ethers.js | Viem | | Config | `import '...'` side-effect | `plugins: [...]` array | ## 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: false) gasWarning: true, // warn when mock gas usage is high (default: false) gasSummary: true, // print a per-method adjusted-gas summary after tests (default: false) mocksDeployVerbosity: 'v', // '' | 'v' | 'vv' — mock deployment output (default: 'v') }, }; ``` #### Gas reporting Mock FHE operations consume extra gas replicating the off-chain CoFHE work on-chain. The mock task manager emits a `MockGasConsumed(uint256)` event per block of mock-only work, letting the plugin report corrected numbers: ```ts const tx = await simpleTest.setValueTrivial(7); const receipt = await tx.wait(); // Gas usage excluding mock overhead — an estimate of real-network cost: const adjusted = hre.cofhe.getAdjustedGasUsed(receipt); // Or the full breakdown: const { gasUsed, mockGas, adjustedGasUsed, mockGasEvents } = hre.cofhe.getAdjustedGasBreakdown(receipt); ``` Both are pure functions of the receipt — no RPC calls — and pass raw `gasUsed` through unchanged on a real network. With `cofhe: { gasSummary: true }` in the config, a per-method table (raw vs adjusted gas) is printed after `hardhat test`. The table is reconstructed from chain history after the run, so transactions rolled back by `loadFixture`/`evm_revert` won't appear — use the receipt helpers for exact numbers. See [Mock Contracts → Gas reporting](/hardhat/mock-contracts#gas-reporting) for details. :::note `eth_estimateGas` is not adjusted — the mock work really does execute, so transactions still need the raw gas limit. ::: #### `mocksDeployVerbosity` Controls how much is printed when the mock contracts are deployed at the start of each test run: | Value | Output | | ------ | ------------------------------------------------------------------------------ | | `''` | Silent — nothing is printed | | `'v'` | Single summary line: `✓ cofhe-hardhat-plugin :: mocks deployed` (default) | | `'vv'` | Full deployment log — each contract address, signer setup, and funded balances | ### 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 ## Cofhe SDK `@cofhe/sdk` is the TypeScript client for [CoFHE](https://cofhe-docs.fhenix.zone/deep-dive/cofhe-components/overview). It handles the client-side operations required to interact with FHE-enabled smart contracts: encrypting inputs with ZK proofs, decrypting [handles](/reference/dictionary#handle) via the [Threshold Network](/reference/dictionary#threshold-network), and managing EIP-712 [ACPs](/reference/dictionary#acp) for access control. On-chain, contracts use [`FHE.sol`](https://cofhe-docs.fhenix.zone/fhe-library/introduction/overview) to operate on encrypted data. Off-chain, this SDK prepares the inputs and reads the outputs. ### What the SDK does | Operation | Function | Description | | ---------------- | ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Encrypt inputs | `client.encryptInputs([...]).setConsumingContract(addr).execute()` | Packs plaintext values, generates a ZKPoK, and submits them to the CoFHE verifier's batch endpoint. Returns `[...hashes, signature]` — one ciphertext hash per input plus one shared signature — for use in contract calls. `setConsumingContract` is required: the verifier binds the target contract into the signature. | | Decrypt for view | `client.decryptForView(ctHash, utype).execute()` | Requests decryption of a [ciphertext handle](/reference/dictionary#handle) using an ACP. Returns the plaintext locally (not published on-chain). | | Decrypt for tx | `client.decryptForTx(ctHash).execute()` | Requests decryption and returns the plaintext with a Threshold Network signature. The signature can be verified on-chain via `FHE.verifyDecryptResult(...)`. | | Manage ACPs | `client.acp.createSelf(...)` / `getOrCreateSelfACP()` | Creates, stores, and manages EIP-712 ACPs that authorize decryption of specific ciphertext handles. | ### Entrypoints | Entrypoint | Contents | | --------------------- | ----------------------------------------------------------------------------------------------------------- | | `@cofhe/sdk` | Core types (`Encryptable`, `FheTypes`, `EncryptStep`, `CofheError`), shared across runtimes | | `@cofhe/sdk/web` | `createCofheConfig` / `createCofheClient` with browser defaults (IndexedDB storage, TFHE WASM, Web Workers) | | `@cofhe/sdk/node` | `createCofheConfig` / `createCofheClient` with Node.js defaults (filesystem storage, `node-tfhe`) | | `@cofhe/sdk/acps` | ACP creation, validation, serialization, and storage utilities | | `@cofhe/sdk/adapters` | `Ethers5Adapter`, `Ethers6Adapter`, `WagmiAdapter`, `HardhatSignerAdapter` | | `@cofhe/sdk/chains` | Built-in chain definitions and `getChainById` / `getChainByName` helpers | ```ts twoslash // @noErrors // Browser import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; // Node.js import { createCofheConfig as createCofheConfigNode, createCofheClient as createCofheClientNode, } from '@cofhe/sdk/node'; // Shared import { Encryptable, FheTypes } from '@cofhe/sdk'; import { chains } from '@cofhe/sdk/chains'; import { Ethers6Adapter } from '@cofhe/sdk/adapters'; ``` ### Client lifecycle ``` createCofheConfig({ supportedChains }) → createCofheClient(config) → client.connect(publicClient, walletClient) ``` ```ts twoslash // @noErrors import { type PublicClient, type WalletClient } from 'viem'; declare const publicClient: PublicClient; declare const walletClient: WalletClient; declare const contract: { address: `0x${string}`; deposit: (hash: `0x${string}`, signature: `0x${string}`) => Promise; getBalance: () => Promise; }; // ---cut--- import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web'; import { Encryptable, FheTypes } from '@cofhe/sdk'; import { chains } from '@cofhe/sdk/chains'; const config = createCofheConfig({ supportedChains: [chains.sepolia], }); const client = createCofheClient(config); await client.connect(publicClient, walletClient); // Encrypt and send: [hash, signature] const [hash, signature] = await client .encryptInputs([Encryptable.uint64(100n)]) .setConsumingContract(contract.address) .execute(); await contract.deposit(hash, signature); // Decrypt for UI await client.acp.getOrCreateSelfACP(); const ctHash = await contract.getBalance(); const balance = await client .decryptForView(ctHash, FheTypes.Uint64) .execute(); ``` ### Hardhat plugin `@cofhe/hardhat-plugin` deploys mock versions of the CoFHE on-chain components (TaskManager, ACL, ZK verifier, Threshold Network) onto the Hardhat network, so you can compile and test FHE contracts without the off-chain infrastructure. The plugin extends `hre` with `hre.cofhe`: ```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); ``` The mock `TaskManager` stores plaintext values on-chain, so you can assert encrypted state directly: ```ts twoslash // @noErrors import '@cofhe/hardhat-plugin'; import '@nomicfoundation/hardhat-ethers'; import hre from 'hardhat'; declare const ctHash: string; // ---cut--- await hre.cofhe.mocks.expectPlaintext(ctHash, 42n); ``` Mocks are deployed automatically before `npx hardhat test`. FHE operation logging is built in. ### Pages * [Quick Start](/quick-start) — install, write a contract, run a test. * [Client](/sdk/client) — config, connect, adapters, connection lifecycle. * [Encrypting Inputs](/sdk/encrypting-inputs) — prepare encrypted inputs for a contract call. * [Decrypting to View](/sdk/decrypt-to-view) — decrypt ciphertext handles for UI display. * [Decrypting to Transact](/sdk/decrypt-to-tx) — decrypt with a verifiable signature for on-chain use. * [ACPs](/sdk/acp) — create and manage decryption authorization. * [Hardhat Getting Started](/hardhat) — plugin setup and first test. * [Mock Contracts](/hardhat/mock-contracts) — `getPlaintext`, `expectPlaintext`, mock contract access. * [Migrating from `cofhejs`](/migrating-from-cofhejs) — side-by-side API mapping for existing cofhejs users. ## 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 ACP (Access Control Permission) 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([...]).setConsumingContract(address).execute()`. `setConsumingContract` is required — the verifier binds that address into the signature, so a batch signed for one contract can't be replayed into another. The builder also lets you chain optional overrides (account, chain, worker, progress callback) before executing. ::::note :::details[Changes] | | `cofhejs` | `@cofhe/sdk` | | --------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | | **Function** | `cofhejs.encrypt([...], callback)` | `client.encryptInputs([...]).setConsumingContract(addr).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. | `[...hashes, signature]` — one `external*` hash per input, followed by one shared batch signature | | **Overrides** | Not available | `.setConsumingContract(...)` (required), `.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; declare const myContractAddress: `0x${string}`; // ---cut--- import { Encryptable, EncryptStep } from '@cofhe/sdk'; // [hash1, hash2, signature] - one hash per input, followed by the shared batch signature const [eAmountHash, eFlagHash, signature] = await client .encryptInputs([Encryptable.uint64(42n), Encryptable.bool(true)]) .setConsumingContract(myContractAddress) .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)` | | **ACP handling** | Automatic (uses most recent permit) | Explicit — `.withACP()` / `.withACP(permit)` / `.withACP(hash)` / `.withoutACP()` | | **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) .withoutACP() .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 → ACPs Permits are now called **ACPs** (Access Control Permissions) and are no longer auto-generated during initialization. All ACP operations are explicit through `client.acp`, with separate methods for self ACPs, sharing ACPs, and importing shared ACPs. ::::note :::details[Changes] | | `cofhejs` | `@cofhe/sdk` | | ------------------- | ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | | **Auto-generation** | `generatePermit: true` (default) | Never — always explicit | | **Creation** | `cofhejs.createPermit({ type, issuer })` | `client.acp.createSelf(...)`, `client.acp.createSharing(...)`, `client.acp.importShared(...)` | | **Return type** | `Result` | Direct `ACP` object | | **Active ACP** | Implicitly used by `unseal` | `getOrCreateSelfACP()` sets the active ACP; `decryptForView/Tx` uses it automatically | | **Sharing** | `createPermit({ type: "sharing", recipient })` → `createPermit({ type: "recipient", ...permit })` | `createSharing(...)` → `importShared(...)` | | **Selection** | Implicit | `client.acp.selectActiveACP(hash)` | | **Retrieval** | `cofhejs.getPermit(...)` | `client.acp.getACP(hash)`, `client.acp.getACPs()`, `client.acp.getActiveACP()` | ::: :::: #### 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`) ACPs are never auto-generated. You explicitly create, retrieve, and select them through `client.acp`: ```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 ACP (prompts for wallet signature) const permit = await client.acp.createSelf({ issuer: account, name: 'My dApp ACP', }); // Or use the convenience method that creates one only if needed const permit2 = await client.acp.getOrCreateSelfACP(); // Use with decryptForView (active ACP is used automatically) const value = await client .decryptForView(ctHash, FheTypes.Uint32) .execute(); // Or pass a specific ACP const value2 = await client .decryptForView(ctHash, FheTypes.Uint32) .withACP(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; declare const myContractAddress: `0x${string}`; // ---cut--- import { isCofheError, CofheErrorCode } from '@cofhe/sdk'; try { const encrypted = await client .encryptInputs([Encryptable.uint32(42n)]) .setConsumingContract(myContractAddress) .execute(); } catch (err) { if (isCofheError(err)) { console.error(err.code); // CofheErrorCode enum console.error(err.message); // human-readable message console.error(err.apiErrorCode); // raw backend error code, if this came from a threshold-network response } } ``` ### 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/acps` | | N/A | `@cofhe/sdk/adapters` | | N/A | `@cofhe/sdk/chains` | ### 7. Type renames `cofhejs`'s per-type result structs (`CoFheInBool`, `CoFheInUint8`, etc.) have no direct equivalent — `@cofhe/sdk` returns a single `[...hashes, signature]` tuple instead, typed as `HashPlusProofResult`. Each hash is branded by its type (`ExternalBoolHash`, `ExternalUint8Hash`, `ExternalUint16Hash`, `ExternalUint32Hash`, `ExternalUint64Hash`, `ExternalUint128Hash`, `ExternalAddressHash`), and the final element is a single `ExternalHashProof` (the shared batch signature) rather than a per-item signature. ### 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: [hash, signature] const [eAmountHash, signature] = await client .encryptInputs([Encryptable.uint64(100n)]) .setConsumingContract(await contract.getAddress()) .onStep((step) => console.log(step)) .execute(); // 3. Send transaction await contract.deposit(eAmountHash, signature); // 4. Create permit & decrypt await client.acp.getOrCreateSelfACP(); 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(externalEuint64[] calldata encryptedAmounts, bytes calldata signature) external { euint64 amount = FHE.asEuint64s(encryptedAmounts, signature)[0]; _balances[msg.sender] = FHE.add(_balances[msg.sender], amount); } function getBalance() public view returns (euint64) { return _balances[msg.sender]; } } ``` ::: ## Migrating to `0.7.0` `0.7.0` renames Permit to ACP, replaces per-ciphertext input signatures with **one signature per batch**, and requires every encryption to declare the contract that will consume it. It also moves to `@fhenixprotocol/cofhe-contracts` `0.2.x`, **which deletes the `InEuintXX` structs** — so most projects need Solidity changes, not just TypeScript changes. There are no deprecation shims. Old names are removed, so your compiler finds the work. :::warning Migrate from `0.6.x`. If you are on an earlier version, upgrade to `0.6.1` first using the package changelog. If you are coming from `cofhejs`, read [Migrating from cofhejs](/migrating-from-cofhejs) first. ::: ### The agent skill Most of this migration is mechanical, and the parts that aren't are worth a conversation rather than a find-and-replace. We ship a skill that drives the whole thing: it detects what your project uses, works through the changes in dependency order, shows you a diff before touching anything, and tells you what it couldn't decide for you. #### Install It's an [Agent Skill](https://agentskills.io) — an open format supported by Claude Code, Cursor, GitHub Copilot, VS Code, Codex, Gemini CLI, OpenCode, Roo, Kiro, Goose and others. Drop the folder into wherever your agent reads skills from (commonly `.claude/skills/` or `.cursor/skills/`): ```bash mkdir -p .claude/skills curl -L https://github.com/FhenixProtocol/cofhesdk/archive/refs/heads/main.tar.gz \ | tar -xz --strip-components=2 -C .claude/skills \ '*/skills/cofhe-migrate-0-6-to-0-7' ``` Or [browse it on GitHub](https://github.com/FhenixProtocol/cofhesdk/tree/main/skills/cofhe-migrate-0-6-to-0-7) and copy the directory manually. #### Run ``` Migrate this project to @cofhe/sdk 0.7.0 ``` The skill proposes each change as a diff and waits for your approval by default. Ask it to apply everything if you'd rather review at the end. It will stop and ask you — rather than guess — when a function takes two or more encrypted parameters, when encrypted values were persisted somewhere, or when the contract you call isn't one you control. ### What changes #### Contracts `InEuint32` and friends are gone, along with `FHE.asEuint32(InEuint32)` and `ITaskManager.verifyInput`. Which case you're in depends on how many encrypted parameters a function takes: ```solidity // BEFORE - the shape the 0.6.x docs taught function setValue(InEuint32 memory inValue) external { storedValue = FHE.asEuint32(inValue); } // AFTER - one encrypted value function setValue(externalEuint32 inValue, bytes memory proof) external { storedValue = FHE.asEuint32(inValue, proof); } ``` If a function takes **two or more** encrypted values, they must become an array sharing one signature — you can't keep separate `(hash, proof)` pairs, because the signature covers all the hashes together: ```solidity function transfer(address to, externalEuint32[] calldata values, bytes calldata signature) external { euint32[] memory v = FHE.asEuint32s(values, signature); euint32 amount = v[0]; euint32 fee = v[1]; } ``` Already using `(externalEuint32, bytes)` with a single encrypted value? **No contract change and no redeploy** — `FHE.asEuint32(hash, proof)` now verifies as a one-element batch, so the new signature works with your existing ABI. #### Encrypting inputs ```ts // BEFORE const [encA, encB] = await client.encryptInputs([a, b]).execute(); await contract.f(encA, encB); // AFTER const [hashA, hashB, signature] = await client .encryptInputs([a, b]) .setConsumingContract(contractAddress) .execute(); await contract.f([hashA, hashB], signature); ``` `execute()` returns `[...hashes, signature]` — note that's one element *more* than your input count. `setConsumingContract` is required; omitting it is a compile error, because the verifier binds that address into the signature so a batch signed for one contract can't be replayed into another. #### Passing encrypted values between contracts If a contract hands an encrypted value to another contract — as a parameter, or as the return value of a non-`view` function — that has to move onto the new `sharedEuintXX` types. ```solidity // BEFORE - permission granted out of band, and the receiver cannot tell who sent it FHE.allowTransient(amount, address(token)); token.pull(amount); // AFTER - the type carries the permission, and the sharer is checked token.pull(FHE.shareEuint64(amount, address(token))); ``` ```solidity // in Token function pull(sharedEuint64 shared) external { euint64 amount = FHE.receiveEuint64Param(shared); // reverts unless our caller shared it } ``` :::danger The old spelling still compiles and still runs, so **your compiler will not find these**. It is also unsafe: a function taking a bare `euintXX` can be called by anyone, with any handle your contract is allowed on — including your contract's own stored ciphertexts. FHE operations check the permission of the *contract*, not the caller, so it will happily compute on them and hand the result back. That makes it an oracle over your encrypted state. ::: Both sides must migrate together — `pull(euint64)` and `pull(sharedEuint64)` are both `bytes32` on the wire, so an unmigrated caller compiles fine and fails at runtime with `NotShared`. `view` functions returning an encrypted value are unaffected: they never granted anything. Full details, including which `receive` function to use and why it matters, are in [Sharing Encrypted Values](/reference/sharing-encrypted-values). #### Permit → ACP `@cofhe/sdk/permits` → `@cofhe/sdk/acps`, `Permit` → `ACP`, `Permission` → `ACPPublic`, `PermitUtils` → `ACPUtils`, and `client.permits` → **`client.acp`** (singular). React hooks follow: `useCofhePermits` → `useCofheACPs`, `useCofheActivePermit` → `useCofheActiveACP`, and so on. Previously signed permits can't verify against the upgraded ACL, so stored ones are dropped and recreated — **your users will be prompted to sign again.** #### Config keys Five keys were renamed. In `0.6.x` an unknown key was silently ignored; now it throws and names its replacement. | Before | After | | -------------------------------------- | ----------------------------------- | | `defaultPermitExpiration` | `defaultACPExpiration` | | `react.shareablePermits` | `react.shareableACPs` | | `react.autogeneratePermits` | `react.autogenerateACPs` | | `react.permitExpirationOptions` | `react.acpExpirationOptions` | | `react.defaultPermitExpirationSeconds` | `react.defaultACPExpirationSeconds` | ### Known issue The verifier's batch endpoint is live on **CoFHE staging** and the host chain, and encryption works end-to-end there and against hardhat mocks. It is **not yet deployed on the public testnets** (Sepolia, Arbitrum Sepolia, Base Sepolia), where encrypting still fails with `ZK_VERIFY_FAILED`. This is not a problem with your migration. Point at staging to confirm your code is correct; the testnets will start working with no further code changes once the endpoint ships there. ## 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 ACP 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(externalEuint32[] memory inValues, bytes memory signature) external { storedValue = FHE.asEuint32s(inValues, signature)[0]; FHE.allowThis(storedValue); FHE.allowSender(storedValue); } } ``` * `euint32` — an encrypted `uint32` stored on-chain as a [ciphertext handle](/reference/dictionary#handle). * `externalEuint32` — the encrypted input handle produced by the SDK; `bytes signature` is the shared batch signature that authenticates it. * `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-ACP, 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: [hash, signature] const [encHash, signature] = await cofheClient .encryptInputs([Encryptable.uint32(42n)]) .setConsumingContract(await contract.getAddress()) .execute(); // 2. Send to contract await (await contract.setValue([encHash], signature)).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-ACP 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 ACP 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 ACPs. * [Decrypting to Transact](/sdk/decrypt-to-tx) — reveal values on-chain with a verifiable signature. * [ACPs](/sdk/acp) — 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. ## ACPs ACPs (Access Control Permissions, formerly called *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 an ACP is used, CoFHE validates it against the ACL contract to confirm that the issuer has access to the requested encrypted handle. Each ACP includes a sealing keypair. The public key is sent to CoFHE so it can re-encrypt the data for the ACP holder. The private key stays client-side and is used to unseal the returned data. ACPs are stored locally and identified by a deterministic hash of their fields. ACPs can be used to decrypt your own data (self ACPs), or to delegate your access to another party (sharing ACPs). :::info The examples below show two approaches for working with ACPs. The `client.acp` API is the recommended approach -- it automatically signs ACPs with the connected wallet and manages the ACP store. The `ACPUtils` API is a lower-level alternative that gives you direct control over signing and storage. ::: ### When do you need an ACP? * **`decryptForView`**: always requires an ACP. * **`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 `.withoutACP()`. * If the policy restricts decryption, you must use `.withACP(...)`. ### Prerequisites 1. Create and connect a client (see the [client page](./client.mdx)). ACPs 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 ACP. Once created, it is stored and set as the active ACP for the connected chain and account. :::code-group ```ts twoslash [client.acp] // @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 ACP if one exists, otherwise creates and signs a new one. const acp = await client.acp.getOrCreateSelfACP(); ``` ```ts twoslash [ACPUtils] // @noErrors import { ACPUtils, setACP, setActiveACPHash, } from '@cofhe/sdk/acps'; import type { PublicClient, WalletClient } from 'viem'; declare const publicClient: PublicClient; declare const walletClient: WalletClient; // ---cut--- const acp = await ACPUtils.createSelfAndSign( { issuer: walletClient.account!.address }, publicClient, walletClient ); // Manually store and activate the ACP const chainId = await publicClient.getChainId(); const account = walletClient.account!.address; setACP(chainId, account, acp); setActiveACPHash(chainId, account, acp.hash); ``` ::: After this, the active ACP is picked up automatically: * `decryptForView(...).execute()` uses the active ACP. * `decryptForTx(...).withACP().execute()` uses the active ACP. ### ACP types The SDK supports three ACP 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 ACP after the recipient signs it | Notes: * An ACP includes a **sealing keypair**. The public key is sent to CoFHE for re-encryption. The private key stays client-side for unsealing. * ACP `expiration` is a unix timestamp in **seconds**. The default is **7 days from creation**. * When an ACP is created via `client.acp.*`, it is automatically stored and set as the active ACP for the current chain and account. ### Creating a self ACP A self ACP lets you decrypt data that was allowed to your address. Use `createSelf` to always create a new ACP, or `getOrCreateSelfACP` to reuse an existing active ACP. #### createSelf :::code-group ```ts twoslash [client.acp] // @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 acp = await client.acp.createSelf({ issuer: walletClient.account!.address, name: 'My self ACP', }); acp.type; acp.hash; ``` ```ts twoslash [ACPUtils] // @noErrors import { ACPUtils } from '@cofhe/sdk/acps'; import type { PublicClient, WalletClient } from 'viem'; declare const publicClient: PublicClient; declare const walletClient: WalletClient; // ---cut--- const acp = await ACPUtils.createSelfAndSign( { issuer: walletClient.account!.address, name: 'My self ACP', }, publicClient, walletClient ); acp.type; acp.hash; ``` ::: #### getOrCreateSelfACP Only available via the `client.acp` API. Returns the active self ACP if one exists. Otherwise creates and signs a new one. This is the recommended approach for most applications. ```ts twoslash [client.acp] // @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 acp = await client.acp.getOrCreateSelfACP(); acp.type; ``` ### Sharing ACPs Sharing ACPs 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 ACP The issuer creates a sharing ACP specifying the recipient's address. The issuer's signature does not include a sealing key. :::code-group ```ts twoslash [client.acp] // @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 sharingACP = await client.acp.createSharing({ issuer: walletClient.account!.address, recipient, name: 'Share with recipient', }); ``` ```ts twoslash [ACPUtils] // @noErrors import { ACPUtils } from '@cofhe/sdk/acps'; import type { PublicClient, WalletClient } from 'viem'; declare const publicClient: PublicClient; declare const walletClient: WalletClient; declare const recipient: `0x${string}`; // ---cut--- const sharingACP = await ACPUtils.createSharingAndSign( { issuer: walletClient.account!.address, recipient, name: 'Share with recipient', }, publicClient, walletClient ); ``` ::: #### Issuer exports the ACP Export the ACP as a JSON blob and share it with the recipient. ```ts twoslash [ACPUtils] // @noErrors import { ACPUtils } from '@cofhe/sdk/acps'; import type { ACP } from '@cofhe/sdk/acps'; declare const sharingACP: ACP; // ---cut--- const exported = ACPUtils.export(sharingACP); ``` :::info The exported JSON does not contain any sensitive data and can be shared via any channel. ::: :::warning Do not share `serialize(acp)` 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 ACP is stored and set as active. :::code-group ```ts twoslash [client.acp] // @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 recipientACP = await client.acp.importShared(exported); recipientACP.type; recipientACP.hash; ``` ```ts twoslash [ACPUtils] // @noErrors import { ACPUtils, setACP, setActiveACPHash, } from '@cofhe/sdk/acps'; import type { PublicClient, WalletClient } from 'viem'; declare const publicClient: PublicClient; declare const walletClient: WalletClient; declare const exported: string; // ---cut--- const recipientACP = await ACPUtils.importSharedAndSign( exported, publicClient, walletClient ); const chainId = await publicClient.getChainId(); const account = walletClient.account!.address; setACP(chainId, account, recipientACP); setActiveACPHash(chainId, account, recipientACP.hash); ``` ::: To store the imported ACP without making it active, pass `activate: false`. Decrypts that use the active ACP keep using the current one, and the imported ACP is used only where you pass it, for example `.withACP(recipientACP)`. This fits an app that shows someone else's shared data in one view while the rest of the app keeps using the user's own ACP. ```ts twoslash // @noErrors declare const client: import('@cofhe/sdk').CofheClient; declare const exported: string; // ---cut--- const recipientACP = await client.acp.importShared(exported, { activate: false }); ``` In React, see [ACPs in React](/react/acp). :::: ### Sharing on-chain Instead of exporting JSON and sending it over a second channel, the issuer can post the share to the on-chain **ACP share registry**. The recipient discovers it from any cofhesdk-enabled app and imports it directly — no copy-paste. Configure the registry address per chain: ```ts const config = createCofheConfig({ // ... acp: { sharingRegistry: { 11155111: '0x…' }, }, }); ``` #### Issuer shares on-chain ```ts twoslash [client.acp] // @noErrors declare const client: import('@cofhe/sdk').CofheClient; declare const recipientAddress: `0x${string}`; // ---cut--- const sharingAcp = await client.acp.createSharing({ issuer: '0x…', // connected account recipient: recipientAddress, }); // posts the signed share payload to the registry const { txHash, shareId } = await client.acp.shareOnChain(sharingAcp); ``` Only signed sharing ACPs can be posted (same rule as `export()`), and only by their issuer. The issuer can retract a pending share with `client.acp.cancelShare(shareId)`. #### Recipient discovers and imports ```ts twoslash [client.acp] // @noErrors declare const client: import('@cofhe/sdk').CofheClient; // ---cut--- // importable shares addressed to the connected account (unexpired, not revoked) const incoming = await client.acp.getIncomingShares(); // fills in the recipient's sealing key, signs, stores and activates // (pass { activate: false } to only store it) const acp = await client.acp.importFromChain(incoming[0]); // optionally clean up the on-chain entry afterwards (or decline a share) await client.acp.dismissShare(incoming[0].shareId); ``` An app that sends these writes through its own transaction path can use the same pieces from `@cofhe/sdk/acps`: `ACP_SHARE_REGISTRY_ABI`, `ACP_REVOKER_ABI`, `toChainShare(acp)` (the registry payload), `computeShareId(acp)` (the id the registry assigns) and `getAclServedAddresses(publicClient, chainId)` (the registry and default revoker the chain ACL serves). The registry lists only importable shares: expired shares and shares whose underlying acp was revoked are filtered out. Everything posted is cleartext by design (the same data as the exported JSON) — note that posting on-chain makes the issuer→recipient relationship public. ### Checking an ACP on-chain The ACL reverts when an ACP is expired, revoked or wrongly signed. `client.acp.checkAccess` runs the same checks and returns the outcome as a status, so an app can tell the user why instead of catching reverts: ```ts twoslash // @noErrors declare const client: import('@cofhe/sdk').CofheClient; declare const acp: import('@cofhe/sdk/acps').ACP; declare const ctHash: bigint; // ---cut--- // the ACP itself const status = await client.acp.checkAccess(acp); // the ACP and one encrypted value const access = await client.acp.checkAccess(acp, ctHash); ``` | Status | Meaning | | ---------------------------------------------------------- | ------------------------------------------------------------ | | `valid` | The ACP passes the ACL checks (no handle given). | | `allowed` | The ACP may read the handle. | | `out-of-scope` | The ACP is valid, but its scope does not cover the handle. | | `issuer-not-allowed` | The issuer may not read the handle, so no ACP of theirs can. | | `expired` | Past its expiration. | | `revoked` | Disabled by its revoker (e.g. `client.acp.revokeACP`). | | `invalid-issuer-signature` / `invalid-recipient-signature` | A signature does not match. | Each check is one view call to the ACL, plus a second only to tell `out-of-scope` from `issuer-not-allowed`. A network error still throws. ### Active ACP management The SDK tracks all stored ACPs and an **active ACP hash** per `chainId + account`. Creating or importing an ACP via `client.acp.*` automatically stores it and selects it as active, except for sharing ACPs and imports made with `activate: false`, which are only stored. #### List stored ACPs ```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 acps = client.acp.getACPs(); Object.keys(acps); ``` #### Read / select the active ACP ```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 someACPHash: string; // ---cut--- await client.connect(publicClient, walletClient); const active = client.acp.getActiveACP(); active?.hash; client.acp.selectActiveACP(someACPHash); ``` #### Removing ACPs ```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 acpHash: string; // ---cut--- await client.connect(publicClient, walletClient); client.acp.removeACP(acpHash); client.acp.removeActiveACP(); ``` ### Persistence and security * The SDK persists ACPs in a store keyed by `chainId + account`. * In web and React environments, this store uses `localStorage` under the key `cofhesdk-acps`. * A stored ACP includes the **sealing private key**. Treat it like a secret. * Never share serialized ACPs with other users. * To share access, use `ACPUtils.export(...)` which strips sensitive fields. ## 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 // defaultACPExpiration: 60 * 60 * 24 * 30, // useWorkers: true, // tfheThreads: 'auto', }); ``` :::tip **Encryption can be \~5× faster** If you serve your app with two extra HTTP headers, TFHE generates ZK proofs across multiple threads instead of one. The SDK enables this automatically when the browser allows it, and silently stays single-threaded when it doesn't. The headers restrict how your page interacts with other origins, so check the trade-offs first — see [Faster Encryption](/reference/faster-encryption). ::: ### 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 acps 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. :::note The reference page [Decryption Lifecycle](/reference/decryption-lifecycle) gives a deep dive into the internal decryption process and the states that may be encountered when decrypting. ::: 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 an [ACP](/reference/dictionary#acp) (see [ACPs](./acp.mdx)). * If the policy allows **anyone** to decrypt it (a common setup), an ACP is not required and `.withoutACP()` will work. * If the policy restricts decryption, you must use `.withACP(...)` 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] import { transformEncryptedReturnTypes } from '@cofhe/abi'; import { createPublicClient, http } from 'viem'; import { sepolia } from 'viem/chains'; const rpcUrl = (globalThis as any).process?.env?.SEPOLIA_RPC_URL ?? (globalThis as any).process?.env?.RPC_URL ?? 'https://rpc.sepolia.org'; // Predeployed test contract on Sepolia. const contractAddress = '0x9e599dA5c7BA756641bA59DbecCc394CEdFC19f0' as const; // ---cut--- // Viem: public client + contract call const publicClient = createPublicClient({ chain: sepolia, transport: http(rpcUrl), }); const abi = [ { type: 'function', name: 'getValue', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'bytes32', internalType: 'euint32' }], }, ] as const; // Read the raw encrypted return value. const raw = await publicClient.readContract({ address: contractAddress, abi, functionName: 'getValue', args: [], }); // Convert raw ABI values into `{ ctHash, utype }`. const encrypted = transformEncryptedReturnTypes(abi, 'getValue', raw); encrypted.ctHash; // ^? encrypted.utype; // ^? ``` ```ts twoslash [Ethers] import { transformEncryptedReturnTypes } from '@cofhe/abi'; import { Contract, JsonRpcProvider } from 'ethers'; // Predeployed test contract on Sepolia. const contractAddress = '0x9e599dA5c7BA756641bA59DbecCc394CEdFC19f0' as const; const rpcUrl = (globalThis as any).process?.env?.SEPOLIA_RPC_URL ?? (globalThis as any).process?.env?.RPC_URL ?? 'https://rpc.sepolia.org'; // ---cut--- // Ethers: provider + contract call const provider = new JsonRpcProvider(rpcUrl); const abi = [ { type: 'function', name: 'getValue', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'bytes32', internalType: 'euint32' }], }, ] as const; const contract = new Contract(contractAddress, abi, provider); // Read the raw encrypted return value. const raw = await contract.getValue(); // Convert raw ABI values into `{ ctHash, utype }`. const encrypted = transformEncryptedReturnTypes(abi, 'getValue', raw); encrypted.ctHash; // ^? encrypted.utype; // ^? ``` ::: ### Preparations: ACP (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 ACPs and use `.withoutACP()`. If the ACL policy restricts decryption for this handle, obtain an ACP before calling `decryptForTx`: * If decryption is restricted to your address, generate a self-ACP. * If decryption is restricted to a different address, import an ACP from the address that is allowed to decrypt. For details, see [ACPs](./acp.mdx). Decision guide: ``` ACL policy for this ctHash │ ├─ Anyone can decrypt (no restriction) │ └─ No acp needed │ └─ decryptForTx(ctHash) │ .withoutACP() │ .execute() │ └─ Restricted to a specific address │ ├─ Your address is allowed │ └─ Generate a self-acp │ └─ decryptForTx(ctHash) │ .withACP() │ .execute() │ └─ A different address is allowed └─ Import an ACP from them └─ decryptForTx(ctHash) .withACP(importedACP) .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 ACP mode) Choose the mode that matches the contract’s ACL policy for this handle: :::code-group ```ts twoslash [No acp] 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) .withoutACP() .execute(); decryptResult; // ^? decryptResult.decryptedValue; decryptResult.signature; ``` ```ts twoslash [ACP (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) .withACP() .execute(); decryptResult; // ^? ``` ```ts twoslash [ACP (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 acp = await client.acp.getOrCreateSelfACP(); const decryptResult = await client .decryptForTx(ctHash) .withACP(acp) .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 ACP mode: `.withACP(...)` or `.withoutACP()`. #### `.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).withACP().execute(); // [!code focus] result.decryptedValue; result.signature; ``` #### `.onPoll(callback)` — optional Registers a callback that runs once per poll attempt while waiting for the threshold network to complete the request. During submit retries, `requestId` is an empty string because the backend has not created a request yet. This can happen while the SDK is retrying transient `204` or short-lived `404` submit responses. For the full meaning of submit retries versus request-status polling, see [Decryption Lifecycle](/reference/decryption-lifecycle). ```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) .onPoll(({ attemptIndex, elapsedMs }) => { // [!code focus] console.log('decrypt polling', { attemptIndex, elapsedMs }); // [!code focus] }) // [!code focus] .withACP() .execute(); result.signature; ``` #### `.set404RetryTimeout(timeoutMs)` — optional Controls how long the SDK should keep retrying submit-time `404 Not Found` responses before failing. The default is `10000` ms. This retry only applies while the backend reports the ciphertext isn't indexed yet (`ct_not_found`); a `404` with any other error code fails immediately. Use this when your backend is eventually consistent and may need a few seconds to index a freshly-created ciphertext 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) .set404RetryTimeout(15_000) // [!code focus] .withACP() .execute(); result.signature; ``` #### `.withACP(...)` — required unless using `.withoutACP()` Decrypt using an ACP: * `.withACP()` uses the active ACP for the resolved `chainId + account`. * `.withACP(acpHash)` fetches a stored ACP by hash. * `.withACP(acp)` uses the provided ACP object. :::code-group ```ts twoslash [Active acp] 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) .withACP() // [!code focus] .execute(); result.signature; ``` ```ts twoslash [ACP 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 acp = await client.acp.getOrCreateSelfACP(); const result = await client .decryptForTx(ctHash) .withACP(acp) // [!code focus] .execute(); result.decryptedValue; ``` ```ts twoslash [ACP 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 acpHash: `0x${string}`; // ---cut--- const result = await client .decryptForTx(ctHash) .withACP(acpHash) // [!code focus] .execute(); result; ``` ::: #### `.withoutACP()` — required unless using `.withACP(...)` Decrypt via global allowance (no ACP). 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) .withoutACP() // [!code focus] .execute(); result.decryptedValue; ``` #### `.setAccount(address)` — optional Overrides the account used to resolve the active ACP / stored ACP. ```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] .withACP() .execute(); const handleFromResult = result.ctHash; handleFromResult; ``` #### `.setChainId(chainId)` — optional Overrides the chain used to resolve the Threshold Network URL and ACPs. ```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] .withACP() .execute(); result.signature; ``` ### Common pitfalls * **ACP mode must be selected**: you must call exactly one of `.withACP(...)` or `.withoutACP()` before `.execute()`. * **Wrong chain/account**: ACPs are scoped to `chainId + account`. If you use `.withACP()` and get an ACL/ACP 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. :::note The reference page [Decryption Lifecycle](/reference/decryption-lifecycle) gives a deep dive into the internal decryption process and the states that may be encountered when decrypting. ::: 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 an [ACP](/reference/dictionary#acp) that authorizes decryption of that value (see [ACP quickstart](#ACP-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 an ACP (there is no `.withoutACP()` 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 an ACP available for the connected `chainId + account` (see [ACPs](./acp.mdx)). If you don’t have one yet, the quickest way is to create a self ACP 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] import { transformEncryptedReturnTypes } from '@cofhe/abi'; import { createPublicClient, http } from 'viem'; import { sepolia } from 'viem/chains'; const rpcUrl = (globalThis as any).process?.env?.SEPOLIA_RPC_URL ?? (globalThis as any).process?.env?.RPC_URL ?? 'https://rpc.sepolia.org'; // Predeployed test contract on Sepolia. const contractAddress = '0x9e599dA5c7BA756641bA59DbecCc394CEdFC19f0' as const; // ---cut--- // Viem: public client + contract call const publicClient = createPublicClient({ chain: sepolia, transport: http(rpcUrl), }); const abi = [ { type: 'function', name: 'getValue', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'bytes32', internalType: 'euint32' }], }, ] as const; // Read the raw encrypted return value. const raw = await publicClient.readContract({ address: contractAddress, abi, functionName: 'getValue', args: [], }); // Convert raw ABI values into `{ ctHash, utype }`. const encrypted = transformEncryptedReturnTypes(abi, 'getValue', raw); encrypted.ctHash; // ^? encrypted.utype; // ^? ``` ```ts twoslash [Ethers] import { transformEncryptedReturnTypes } from '@cofhe/abi'; import { Contract, JsonRpcProvider } from 'ethers'; // Predeployed test contract on Sepolia. const contractAddress = '0x9e599dA5c7BA756641bA59DbecCc394CEdFC19f0' as const; const rpcUrl = (globalThis as any).process?.env?.SEPOLIA_RPC_URL ?? (globalThis as any).process?.env?.RPC_URL ?? 'https://rpc.sepolia.org'; // ---cut--- // Ethers: provider + contract call const provider = new JsonRpcProvider(rpcUrl); const abi = [ { type: 'function', name: 'getValue', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'bytes32', internalType: 'euint32' }], }, ] as const; const contract = new Contract(contractAddress, abi, provider); // Read the raw encrypted return value. const raw = await contract.getValue(); // Convert raw ABI values into `{ ctHash, utype }`. const encrypted = transformEncryptedReturnTypes(abi, 'getValue', raw); encrypted.ctHash; // ^? encrypted.utype; // ^? ``` ::: :::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())`). ::: ### ACP 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 an ACP if needed, stores it, and selects it as the active acp. await client.acp.getOrCreateSelfACP(); ``` After this, `decryptForView(...)` can automatically use the active ACP when you run `.execute()`. ### Decrypt for UI Choose the pattern that matches how your app manages ACPs: :::code-group ```ts twoslash [Active acp] 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.acp.getOrCreateSelfACP(); const plaintext = await client .decryptForView(ctHash, FheTypes.Uint32) .execute(); plaintext; // ^? ``` ```ts twoslash [ACP 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 acp = await client.acp.getOrCreateSelfACP(); const plaintext = await client .decryptForView(ctHash, FheTypes.Uint64) .withACP(acp) .execute(); plaintext; // ^? ``` ```ts twoslash [ACP 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 acpHash: `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) .withACP(acpHash) .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 an ACP (there is no `.withoutACP()` 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; ``` #### `.onPoll(callback)` — optional Registers a callback that runs once per poll attempt while waiting for the threshold network to complete the request. This is useful for showing “decrypting…” UI, measuring latency, or logging. During submit retries, `requestId` is an empty string because the backend has not created a request yet. This can happen while the SDK is retrying transient `204` or short-lived `404` submit responses. ```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) .onPoll(({ attemptIndex, elapsedMs }) => { // [!code focus] console.log('sealoutput polling', { attemptIndex, elapsedMs }); // [!code focus] }) // [!code focus] .execute(); plaintext; ``` #### `.set404RetryTimeout(timeoutMs)` — optional Controls how long the SDK should keep retrying submit-time `404 Not Found` responses before failing. The default is `10000` ms. This retry only applies while the backend reports the ciphertext isn't indexed yet (`ct_not_found`); a `404` with any other error code fails immediately. Use this when your backend is eventually consistent and may need a few seconds to index a freshly-created ciphertext handle. ```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) .set404RetryTimeout(15_000) // [!code focus] .execute(); plaintext; ``` #### `.withACP(...)` — optional Select which ACP to use: * `.withACP()` uses the active ACP for the resolved `chainId + account`. * `.withACP(acpHash)` fetches a stored ACP by hash. * `.withACP(acp)` uses the provided ACP object. If you don’t call `.withACP(...)`, `decryptForView` also uses the active ACP for the resolved `chainId + account`. :::code-group ```ts twoslash [Active acp] 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) .withACP() // [!code focus] .execute(); plaintext; ``` ```ts twoslash [ACP 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 acp = await client.acp.getOrCreateSelfACP(); const plaintext = await client .decryptForView(ctHash, FheTypes.Uint64) .withACP(acp) // [!code focus] .execute(); plaintext; ``` ```ts twoslash [ACP 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 acpHash: `0x${string}`; // ---cut--- const plaintext = await client .decryptForView(ctHash, FheTypes.Bool) .withACP(acpHash) // [!code focus] .execute(); plaintext; ``` ::: #### `.setAccount(address)` — optional Overrides the account used to resolve the active ACP / stored ACP. ```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] .withACP() .execute(); plaintext; ``` #### `.setChainId(chainId)` — optional Overrides the chain used to resolve the Threshold Network URL and ACPs. ```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] .withACP() .execute(); plaintext; ``` ### Common pitfalls * **Missing ACP**: `decryptForView` will fail if there is no active ACP for the current `chainId + account` (or if the ACP 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**: ACPs are scoped to `chainId + account`. If the user switches wallets or networks, create/select the correct ACP (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 `.setConsumingContract(address)` with the address of the contract you'll submit the result to — the verifier binds this into the signed digest, so a batch signed for one contract can't be replayed into another. 4. Call `client.encryptInputs([...]).setConsumingContract(...).execute()` to produce a [batch-verified result](#what-encryptinputs-returns) — one ciphertext hash per input, followed by a single shared signature. 5. Submit a transaction to your contract, passing the hashes and signature as `external*` + `bytes` 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. `externalEuint32` vs `externalEuint64`). ### 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'), ]) .setConsumingContract('0x5FbDB2315678afecb367f032d93F642f64180aa3') .execute(); const [eUint32Hash, eBoolHash, eAddressHash, signature] = encrypted; ``` The return value is a typed tuple: one hash per encryptable you passed in (in order), followed by a single signature that authenticates the whole batch. For the exact shape, see [What `encryptInputs` returns](#what-encryptinputs-returns). ### What `encryptInputs` returns Running `.execute()` returns a `HashPlusProofResult` tuple: `[...hashes, signature]`. * One `0x`-prefixed [handle](/reference/dictionary#handle) hash per input, in the same order as the array you passed to `encryptInputs([...])` — typed as `externalEuint*` / `externalEbool` / `externalEaddress` on the Solidity side. * One trailing `bytes` signature, shared by the whole batch. The CoFHE verifier signs `keccak256(h_0 || h_1 || ... || h_n)`, where each `h_i` binds that ciphertext's hash to its type, security zone, account, and chain ID — a single call to your contract must therefore submit **all** of a batch's hashes together with that one signature; you can't split a batch apart after the fact. For the exact shape, see [`HashPlusProofResult` — the result type](#hashplusproofresult--the-result-type). ### Builder API #### `.execute()` — required, call last Runs the encryption pipeline and returns the `[...hashes, signature]` 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, signature] = await cofheClient .encryptInputs([Encryptable.uint8(25n), Encryptable.bool(true)]) .setConsumingContract('0x5FbDB2315678afecb367f032d93F642f64180aa3') .execute(); // [!code focus] ``` #### `.setConsumingContract(address)` — required Sets the contract that will consume the resulting hashes+signature — i.e. the contract whose function call will pass them into `FHE.asEuint*s(...)`. The CoFHE verifier binds this address into the signed digest, so a batch signed for one contract cannot be replayed into another. This is enforced by the type system: `client.encryptInputs(...)` returns a builder with no `.execute()` method, and `.setConsumingContract(...)` is what returns the builder that has one. Omitting it is a compile error rather than a runtime failure. (JavaScript callers, who get no such check, still hit a `ConsumingContractUninitialized` throw at `.execute()`.) ```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)]) .setConsumingContract('0x5FbDB2315678afecb367f032d93F642f64180aa3') // [!code focus] .execute(); ``` #### `.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)]) .setConsumingContract('0x5FbDB2315678afecb367f032d93F642f64180aa3') .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)]) .setConsumingContract('0x5FbDB2315678afecb367f032d93F642f64180aa3') .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)]) .setConsumingContract('0x5FbDB2315678afecb367f032d93F642f64180aa3') .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)]) .setConsumingContract('0x5FbDB2315678afecb367f032d93F642f64180aa3') .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's `/verifyBatch` endpoint. Returns the hashes and one shared batch signature. | `Prove` normally dominates the total time. On the web it can run across multiple threads — roughly 5× faster — if your app is served with the right headers. See [Faster Encryption](/reference/faster-encryption). ### `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` | `externalEbool` | | `Encryptable.uint8(value)` | `bigint \| string` | `externalEuint8` | | `Encryptable.uint16(value)` | `bigint \| string` | `externalEuint16` | | `Encryptable.uint32(value)` | `bigint \| string` | `externalEuint32` | | `Encryptable.uint64(value)` | `bigint \| string` | `externalEuint64` | | `Encryptable.uint128(value)` | `bigint \| string` | `externalEuint128` | | `Encryptable.address(value)` | `bigint \| string` | `externalEaddress` | 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. ### Result types #### `HashPlusProofResult` — the result type `.execute()` always returns a typed tuple of the form `[...hashes, signature]`: * One `0x`-prefixed 32-byte hash string per input (typed as `externalEuint*` / `externalEbool` / `externalEaddress` on the Solidity side). * One `bytes` signature at the end, authenticating the entire batch of hashes together. ```ts // For one input: // [externalHash, signature] // For two inputs: // [externalHash1, externalHash2, signature] ``` Use this with Solidity functions that accept `external*` handles plus a `bytes` parameter for the shared signature — see [Writing Encrypted Data to a Contract](./writing-encrypted-data-to-contract.mdx) for contract examples, and the [`@cofhe/abi` reference](/reference/dictionary) for how `insertEncryptedValues` threads the hashes/signature into arbitrary contract calls automatically. ### Common pitfalls * **Missing `.setConsumingContract(...)`**: required before `.execute()` — throws `ConsumingContractUninitialized` otherwise. It must be the exact contract you submit the hashes/signature to; the verifier binds this address into the signature. * **Wrong `Encryptable` type**: `Encryptable.uint32(...)` must match what your Solidity function expects (e.g. `externalEuint32`). * **Splitting a batch**: the signature authenticates *all* the hashes in that `execute()` call together, in order. You can't reuse a subset of the hashes with a different signature, or reorder them. * **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`. ## Verifying Decrypt Results When you use [`decryptForTx`](./decrypt-to-tx.mdx), the Threshold Network returns: * the plaintext (`decryptedValue`), and * a signature (`signature`) that your contract can later verify on-chain. If you ever receive a decrypt result from somewhere else (e.g. your backend, a cache, or another client), you can validate that the `(ctHash, plaintext, signature)` triple is authentic using `client.verifyDecryptResult(...)`. ### What “verify” means `verifyDecryptResult(ctHash, plaintext, signature)` checks that: * the `signature` is a **valid Threshold Network signature** for the given `ctHash` (handle) and `plaintext`, and * the signature matches the **expected Threshold Network signer set for that chain**. In other words: it makes sure you’re not about to submit `(ctHash, plaintext, signature)` where the signature is malformed, tampered with, or produced by the wrong signer. The “signer is correct” part refers to the **Threshold Network’s signing key** (a distributed key controlled by the Threshold Network committee), not your wallet address. A `true` result means the chain’s verifier considers this signature a valid attestation from that Threshold Network key for the specific `(ctHash, plaintext)` pair. Under the hood, the SDK: * does an `eth_call` to read the expected `decryptResultSigner` from the chain’s TaskManager (no gas), then * performs the signature check locally via `ecrecover`-equivalent recovery against `keccak256(abi.encodePacked(ctHash, plaintext))`. This means your `(ctHash, plaintext, signature)` is **not** sent to the RPC node for verification. :::note This does **not** “re-decrypt” the ciphertext. It only checks that the signature you already have is cryptographically valid for the `(ctHash, plaintext)` pair. ::: :::note Verification is a **cryptographic sanity check**. It does not validate acps/ACL policy, and it does not guarantee your contract will accept the plaintext (e.g. range/type constraints). ::: :::note If you decrypted locally via `decryptForTx` and you’re about to submit a transaction that calls `FHE.verifyDecryptResult(...)` / `FHE.publishDecryptResult(...)`, you usually don’t need to call `verifyDecryptResult` separately — the on-chain verification (and a normal transaction simulation) will fail if the signature is wrong. ::: ### When to use it Use `verifyDecryptResult` when you want to validate decrypt results that didn’t come directly from your own `decryptForTx` call, for example: * when you received a decrypt result from an untrusted off-chain source (e.g. a backend, cache, or another client), * when debugging mismatches between what you decrypted and what your contract expects. ### Example ```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 decryptResult: { ctHash: bigint | `0x${string}`; decryptedValue: bigint; signature: `0x${string}`; }; // ---cut--- // `client` must be connected (see the SDK client page) // `decryptResult` came from an untrusted off-chain source (e.g. backend/cache/another client) const isValid = await client.verifyDecryptResult( decryptResult.ctHash, decryptResult.decryptedValue, decryptResult.signature ); if (!isValid) throw new Error('Rejecting untrusted decrypt result'); // Safe to trust that (ctHash, plaintext, signature) is authentic ``` ### Common gotchas * **Wrong chain**: verification must be performed against the same chain that produced the handle (`ctHash`). * **Wrong plaintext**: even a single-bit change in `plaintext` will make verification return `false`. * **Signature formatting**: the signature must be a `0x...` hex string exactly as returned by `decryptForTx`. ## 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 ciphertext hashes and pass them, plus one shared signature, directly into a contract call. `encryptInputs` returns a `[...hashes, signature]` tuple — one `external*` handle per input, followed by a single signature that authenticates the whole batch together. The on-chain CoFHE library validates that signature before the contract can use any of the ciphertexts. The flow is as follows: 1. Ensure your contract function accepts `external*` handles, each followed by a `bytes` parameter carrying the shared signature. 2. Encrypt the plaintext values with [`encryptInputs`](./encrypting-inputs.mdx). 3. Send a transaction, passing the hashes and the signature as arguments. ### Prerequisites 1. Create and connect a client (see the [client page](./client.mdx)). 2. Your contract function must accept `external*` handles (`externalEbool`, `externalEuint32`, etc.) plus a `bytes` parameter carrying the shared batch signature. Convention is to place the `bytes` immediately after the handle it authenticates — `FHE.asEuintXX(hash, proof)` is a pair. Other arguments may follow. The encrypted type you choose in JavaScript/TypeScript must match the Solidity parameter type: * `Encryptable.uint32(...)` → `externalEuint32` * `Encryptable.bool(...)` → `externalEbool` * `Encryptable.address(...)` → `externalEaddress` ### Example: encrypt and call a contract The reference contract used throughout these docs is [`EncryptedCounter.sol`](/reference/encrypted-counter-sol). It exposes `setCount(externalEuint32[], bytes)` 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(externalEuint32[] memory _inCounts, bytes memory _signature) external onlyOwner { count = FHE.asEuint32s(_inCounts, _signature)[0]; FHE.allowThis(count); FHE.allowSender(count); decrypted = false; decryptedCount = 0; } ``` ```ts twoslash [TypeScript] import { Encryptable } 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(bytes32[] _inCounts, bytes _signature)', ]); // 1) Encrypt right before sending the transaction: [hash, signature] const [inCountHash, signature] = await cofheClient .encryptInputs([Encryptable.uint32(42n)]) .setConsumingContract(encryptedCounterAddress) .execute(); // 2) Pass the hash(es) and the shared signature as separate arguments const hash = await walletClient.writeContract({ chain: sepolia, account, address: encryptedCounterAddress, abi: encryptedCounterAbi, functionName: 'setCount', args: [[inCountHash], signature], }); await publicClient.waitForTransactionReceipt({ hash }); ``` ::: ### Automatic argument wiring with `@cofhe/abi` For contracts with a mix of plain and encrypted arguments, `@cofhe/abi`'s `extractEncryptableValues`/`insertEncryptedValues` will detect the `external*` parameters and inject the signature for you, so you can call `encryptInputs` and build the final call args from the ABI alone — see the `@cofhe/react` `useCofheEncryptAndWriteContract` hook for a ready-made version of this pattern. :::info The helpers take the `bytes` immediately after the encrypted parameters as the signature slot, so ERC-7984-style `*AndCall` overloads — where a `data` payload follows the proof — wire up without any special handling. The one layout they reject is `external*` parameters that are not adjacent to each other: they share a single signature, so there is no unambiguous slot to pair them with. ::: ### Common pitfalls * **Wrong `Encryptable` type**: the `Encryptable.*` factory must match the Solidity parameter type (`externalEuint32` vs `externalEuint64`, etc). * **Missing or misplaced signature parameter**: a contract function with `external*` inputs needs a plain `bytes` parameter immediately after them to receive the shared batch signature. Other arguments may follow it, but the encrypted parameters themselves must be adjacent. * **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. * **Splitting a batch**: don't reorder the hashes relative to how they were encrypted, and don't pass a subset of a batch's hashes with its signature — the signature covers the exact ordered set produced by that `execute()` call. ## Decryption Lifecycle This page explains the shared production-side lifecycle behind both [Decrypting to View](/sdk/decrypt-to-view) and [Decrypting to Transact](/sdk/decrypt-to-tx). It is not about ACPs, return types, or builder methods. It is about what happens after you already have a `ctHash` and ask CoFHE to decrypt it. ### Why decryption is not always immediate In production, decryption depends on the CoFHE coprocessor observing on-chain activity and preparing work for the Threshold Network. That means there are three distinct states your request can pass through: 1. The coprocessor has not seen the `ctHash` on-chain yet. 2. The coprocessor knows the `ctHash`, but has not produced a decryption `request_id` yet. 3. A `request_id` exists, and the Threshold Network is processing the request. The SDK behavior differs in each state. ### State 1: `ctHash` not known yet If the coprocessor has not observed the `ctHash` on-chain yet, the submit request can fail with `404`. In practice, this usually means one of the following: * the encrypting transaction was sent very recently, * the relevant event has not been indexed or processed yet, * you are trying to decrypt a handle from the wrong chain or environment, * the `ctHash` is invalid for that network. At this stage there is no decryption request yet, so there is no `request_id` to poll. The SDK does not automatically retry this `404` case for you. It surfaces the error, and the caller should retry later if the handle is expected to become available soon. ### State 2: `ctHash` known, but no `request_id` yet Once the coprocessor is aware of the `ctHash`, it still may not be ready to start the Threshold Network request immediately. In that case the submit endpoint can return `204 No Content` without a `request_id`. This means: * the handle is known, * the request is not ready yet, * retrying the submit call makes sense. This is the case the SDK now handles automatically. The SDK also treats short-lived submit-time `404 Not Found` responses as retryable during an initial indexing window — but only while the backend's error body reports `ct_not_found` (the "ciphertext not indexed yet" case). A `404` carrying any other error code is surfaced immediately as that code's dedicated error, not retried. By default, the `ct_not_found` retry window is 10 seconds and can be changed via `.set404RetryTimeout(timeoutMs)` on either decrypt builder. For both `decryptForTx` and `decryptForView`, the SDK will: * retry the submit call, * wait between attempts, * keep using the same overall timeout budget, * emit `.onPoll(...)` callbacks during these retries. During this stage, the `.onPoll(...)` callback receives an empty `requestId` because no request has been created yet. ### State 3: `request_id` assigned Once the submit endpoint returns a `request_id`, the decryption request has been accepted and the SDK switches to status polling. From this point on, the SDK polls the request status endpoint until one of these happens: * the request completes successfully, * the request completes with an error, * the request times out, * the request is no longer found. This is the normal long-polling phase most users think of as `decryption polling`. ### Shared timeout budget Submit retries and status polling do not have separate timers. The SDK uses one shared timeout budget across the full production lifecycle: * initial submit attempts, * repeated `204` submit retries, * transient `404` submit retries within the configured 404 retry window, * status polling after `request_id` creation. This matters because a request can spend most of its time budget before the `request_id` even exists. ### How `.onPoll(...)` fits in Both decryption builders support `.onPoll(...)`: * [Decrypting to View](/sdk/decrypt-to-view) * [Decrypting to Transact](/sdk/decrypt-to-tx) The callback context is: ```ts { operation: 'decrypt' | 'sealoutput'; requestId: string; attemptIndex: number; elapsedMs: number; intervalMs: number; timeoutMs: number; } ``` Interpretation: * `requestId === ''`: the SDK is still retrying submit because no request has been created yet. * `requestId !== ''`: the SDK is polling an existing request. * `operation === 'decrypt'`: `decryptForTx` flow. * `operation === 'sealoutput'`: `decryptForView` flow. ### End-to-end summary ```text decrypt call starts -> submit request -> 404, error: ct_not_found, within retry window: SDK retries submit automatically -> 404, error: ct_not_found, after retry window: fail and surface submit-time error -> 404, any other error code: fail immediately, not retried -> 204 without request_id: SDK retries submit automatically -> 200 with request_id: SDK starts status polling -> PROCESSING: keep polling -> COMPLETED: return decrypt result ``` ### Practical guidance * If you just submitted the encrypting transaction, expect decryption to be eventually consistent rather than immediate. * If you get a submit-time `404` reporting `ct_not_found`, the SDK now retries automatically for a short window; increase `.set404RetryTimeout(...)` if your backend indexes handles more slowly. A `404` reporting anything else fails immediately. * If you want progress UI, use `.onPoll(...)` and treat an empty `requestId` as `waiting for request creation`. * If you are debugging slow decrypts, separate `coprocessor has not created a request yet` from `Threshold Network is processing an existing request`. ### Error codes Decrypt/sealoutput failures throw a typed `CofheError` (see [Migrating from cofhejs](/migrating-from-cofhejs) for the general `isCofheError`/`err.code` pattern). The threshold network's stable backend error codes each map to a dedicated `CofheErrorCode`, for example: * `acp_denied`, `acp_expired`, `acp_invalid`, `acp_malformed` — ACP was rejected or malformed. * `not_publicly_allowed` — no ACP was supplied and the handle isn't publicly decryptable. * `ct_not_found` — the ciphertext handle is unknown to the ct-server (after any indexing retry window has elapsed). * `unsupported_security_zone`, `unsupported_type` — the handle's zone or encryption type isn't supported. * `internal_error`, `signing_failed`, `ct_source_error`, `acp_verifier_error`, `ct_source_timeout`, `acp_verifier_timeout` — backend-side failures. * `acp_required`, `seal_failed` — sealoutput-specific. `err.apiErrorCode` carries the raw backend string verbatim (e.g. `'acp_denied'`), independent of which `CofheErrorCode` it mapped to — useful for logging, or if the backend introduces a new code before the SDK adds a dedicated enum member for it (in that case `err.code` falls back to the operation's generic failure code, but `err.apiErrorCode` still tells you exactly what the backend reported). ### See also * [Decrypting to View](/sdk/decrypt-to-view) * [Decrypting to Transact](/sdk/decrypt-to-tx) * [ACPs](/sdk/acp) ## 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([...]).setConsumingContract(contractAddress).execute()` as a `[...hashes, signature]` tuple — one ciphertext hash per input, followed by a single signature authenticating the whole batch together. `setConsumingContract` is required — the signature is bound to the specific contract that will consume it. * On-chain: passed into functions as `externalEuint32`, `externalEuint64`, etc. handles plus a `bytes` signature parameter, conventionally placed immediately after the handle it authenticates (validated by the CoFHE library, e.g. `FHE.asEuint32s(hashes, signature)`, before use). ### ACP An **ACP** (Access Control Permission, formerly called a *permit*) is an EIP-712 signature that authorizes decryption of one or more handles. * The ACP’s **issuer** is the address doing the decryption. * ACPs are checked against the on-chain ACL policy (e.g. via `FHE.allow(handle, address)`). See: [/sdk/acp](/sdk/acp). ### Sealing keypair ACPs include a sealing keypair used for transport security: * The **public key** is sent to CoFHE so it can **re-encrypt** decrypted data for the ACP 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 ACP holder can unseal (using the ACP’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 an ACP.** ### 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 an ACP 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 a batch-verified encrypted input (`externalEuint32[]` + a shared `bytes` signature) and stores the first value as the new counter value. The caller submits an already-encrypted hash plus the signature authenticating it — 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 [ACPs](/sdk/acp) 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 ACP 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] ``` ## Faster Encryption (Multi-Threaded Proving) Generating the ZK proof of knowledge is by far the most expensive part of `encryptInputs` — typically **90%+ of the total time**. TFHE's WASM build is compiled with [rayon](https://github.com/rayon-rs/rayon), so that proof can be computed across several threads instead of one. The SDK turns this on automatically when the browser allows it. Whether it's allowed is decided entirely by **your page's HTTP headers**, so this is something an app opts into by how it's served. ### What it's worth Measured in Chromium on a 12-core machine — median of 5 proofs packing a `uint128`, `uint64`, and `uint32`: | Threads | Proof time | Speedup | | ----------- | ---------: | ------: | | 1 (default) | 3199 ms | 1.00× | | 2 | 1687 ms | 1.90× | | 4 | 964 ms | 3.32× | | 6 | 728 ms | 4.39× | | **8** | **657 ms** | 4.87× | | 12 | 624 ms | 5.13× | Roughly **5× faster proving**, turning a multi-second encryption into well under a second. Starting the thread pool costs about 37 ms, once, on first encryption. The gains flatten past 8 threads, so `'auto'` caps there rather than using every core — each thread is a real Web Worker competing with your app for CPU. ### The requirement: cross-origin isolation Threads share TFHE's WASM memory, and browsers only permit sharing memory between threads in a **cross-origin isolated** context. That needs two response headers on the document: ``` Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: credentialless ``` Without them, `self.crossOriginIsolated` is `false`, `SharedArrayBuffer` is unavailable, and the SDK **silently stays single-threaded**. Encryption still works exactly as before — you just don't get the speedup. There is nothing to catch or handle. :::warning **These headers have real trade-offs. Read the next section before enabling them.** Cross-origin isolation restricts how your page interacts with other origins. For some dApps — particularly those using wallet popups — that's a breaking change, and the speedup isn't worth it. ::: ### Trade-offs to check first #### `Cross-Origin-Opener-Policy: same-origin` breaks cross-origin popups This is the one most likely to bite a dApp. COOP `same-origin` severs the `window.opener` relationship with cross-origin popups. If your app depends on `postMessage` to or from a popup on another origin — some wallet connectors, social logins, and OAuth flows do — **that communication will break**. `same-origin-allow-popups` preserves popups but does *not* enable cross-origin isolation. You cannot have both. If your wallet flow relies on cross-origin popup messaging, leave the headers off. Wallet connections that use WebSockets, browser extensions, or deep links (including most WalletConnect setups) are unaffected. #### `Cross-Origin-Embedder-Policy` restricts embedded resources Both values require cross-origin subresources to opt in, and cross-origin iframes (wallet widgets, price charts, analytics embeds) must send COEP themselves or they'll be blocked. | Value | Cross-origin subresources | Browser support | | ---------------- | ------------------------------------------------ | --------------------------------------------- | | `credentialless` | Load without credentials — no cooperation needed | Chrome/Edge 96+, Firefox 119+, **not Safari** | | `require-corp` | Must send `Cross-Origin-Resource-Policy` or CORS | All isolation-capable browsers | `credentialless` is usually the pragmatic choice: third-party images, fonts, and CDN assets keep working without the other origin changing anything. The cost is that Safari doesn't support it, so Safari users fall back to single-threaded proving. Use `require-corp` if you need Safari and control (or can vouch for) every cross-origin resource you load. ### Setting the headers #### Vite Headers must be set for both the dev server and `vite preview` — they're separate config blocks, and missing the second makes production builds look mysteriously slower than dev. ```ts [vite.config.ts] import { defineConfig } from 'vite'; const crossOriginIsolation = { 'Cross-Origin-Embedder-Policy': 'credentialless', 'Cross-Origin-Opener-Policy': 'same-origin', }; export default defineConfig({ optimizeDeps: { exclude: ['tfhe'] }, worker: { format: 'es' }, server: { headers: crossOriginIsolation }, preview: { headers: crossOriginIsolation }, }); ``` #### Next.js ```js [next.config.js] module.exports = { async headers() { return [ { source: '/:path*', headers: [ { key: 'Cross-Origin-Embedder-Policy', value: 'credentialless' }, { key: 'Cross-Origin-Opener-Policy', value: 'same-origin' }, ], }, ]; }, }; ``` #### Vercel ```json [vercel.json] { "headers": [ { "source": "/(.*)", "headers": [ { "key": "Cross-Origin-Embedder-Policy", "value": "credentialless" }, { "key": "Cross-Origin-Opener-Policy", "value": "same-origin" } ] } ] } ``` #### nginx ```nginx add_header Cross-Origin-Embedder-Policy credentialless always; add_header Cross-Origin-Opener-Policy same-origin always; ``` ### Verifying it worked In the browser console: ```js crossOriginIsolated; // must be true ``` If that's `true`, the next encryption starts the thread pool. The SDK's proof worker logs which path it took: ``` [Worker] TFHE initialized (rayon thread pool: 8 threads) ``` or, when isolation is missing: ``` [Worker] TFHE initialized (single-threaded: not cross-origin isolated — serve the page with …) ``` If the main thread generates proofs itself (`useWorkers: false`, or the worker failed and the SDK fell back), you can read its pool status programmatically. With workers on, the main thread never starts a pool, so this stays `null`: ```ts import { getTfheThreadPoolStatus } from '@cofhe/sdk/web'; // null until the main thread has had to generate a proof const status = getTfheThreadPoolStatus(); // { enabled: true, threads: 8 } // or { enabled: false, threads: 1, reason: '…why it bailed' } ``` The most direct signal is the `Prove` step duration in [`.onStep(...)`](/sdk/encrypting-inputs#onstepcallback--optional) dropping several-fold. ### Tuning with `tfheThreads` `tfheThreads` on `CofheConfig` controls the thread count. It's web-only and ignored on Node/Hardhat. ```ts twoslash import { createCofheConfig } from '@cofhe/sdk/web'; import { chains } from '@cofhe/sdk/chains'; const config = createCofheConfig({ supportedChains: [chains.sepolia], // 'auto' → navigator.hardwareConcurrency, capped at 8 (default) // number → request exactly this many threads // false → stay single-threaded tfheThreads: 'auto', }); ``` | Value | Behaviour | | -------- | ----------------------------------------------------- | | `'auto'` | Default. `navigator.hardwareConcurrency`, capped at 8 | | a number | Exactly that many threads, ignoring the cap | | `false` | Never start a pool — proving stays on one thread | Lowering it (for example `tfheThreads: 4`) leaves more CPU for the rest of your app during encryption. Setting `false` opts out entirely. :::note TFHE's WASM instance is a singleton per page and its thread pool can only be started once. If your app creates several clients with different `tfheThreads` values, whichever encrypts first decides the count for the page. ::: ### Notes * **Proving already runs off the main thread.** With `useWorkers: true` (the default), proofs are generated in a Web Worker regardless, so your UI never blocks. Threading makes that work *finish sooner*; it doesn't change what blocks. * **No fallback needed.** Non-isolated pages, Safari with `credentialless`, single-core devices, and thread-pool startup failures all degrade to single-threaded proving automatically. * **Node is unaffected.** `@cofhe/sdk/node` uses `node-tfhe`, which has no browser thread pool. ## Sharing Encrypted Values Between Contracts Passing an encrypted handle to another contract is easy — it is a `bytes32`. Letting that contract *compute* on it is the part that needs care, because the ACL decides who may use a ciphertext and by default only your contract can. `sharedEuintXX` carries a handle **and** permission to use it, for one transaction. Nothing is decrypted or re-encrypted; the receiver gets access to the same ciphertext. ### Which type do I need? | Type | Comes from | Used for | | ----------------- | ------------------------------- | ------------------------------------------------- | | `externalEuint64` | a user, offchain | encrypted input submitted with a proof | | `sharedEuint64` | another contract | handing a value to a contract in this transaction | | `euint64` | your own storage or computation | everything you do internally | One `shared*` type exists per encrypted type: `sharedEbool`, `sharedEuint8`, `sharedEuint16`, `sharedEuint32`, `sharedEuint64`, `sharedEuint128`, `sharedEaddress`. ### When to use it | You want to… | Use | | ------------------------------------------------------ | ------------------------------------------------- | | Accept an encrypted value from a user | `externalEuint64` + `FHE.asEuint64(value, proof)` | | Hand a value to another contract in this transaction | `sharedEuint64` + share / receive | | Keep using a value in a later transaction | `FHE.allowThis(value)` | | Let another address use a value in a later transaction | `FHE.allow(value, account)` | | Make a value readable by anyone | `FHE.allowGlobal(value)` | The deciding factor is **lifetime**: sharing lasts one transaction, the `allow*` family is persistent. ### Usage ```solidity [Vault.sol] contract Vault { function deposit(Token token, externalEuint64 inAmount, bytes memory proof) external { euint64 amount = FHE.asEuint64(inAmount, proof); token.accept(FHE.shareEuint64(amount, address(token))); } } ``` ```solidity [Token.sol] contract Token { function accept(sharedEuint64 shared) external { euint64 amount = FHE.receiveEuint64Param(shared); // ... compute with `amount` } } ``` `FHE.shareEuint64` reverts unless your contract is allowed on the handle — you cannot share what you cannot use. ### Keeping a received value A received value is **transient**: usable for the rest of this transaction, gone afterwards. To keep it, call `FHE.allowThis` on the unwrapped `euint64` — not on the `sharedEuint64`: ```solidity [Token.sol] mapping(address => euint64) public deposits; function accept(sharedEuint64 shared) external { euint64 amount = FHE.receiveEuint64Param(shared); FHE.allowThis(amount); deposits[msg.sender] = amount; } ``` :::danger Storing a value without `FHE.allowThis` is the most common mistake. The write succeeds and the transaction succeeds — the failure appears in the *next* transaction that touches the value, when your contract is no longer allowed on it. ::: The same applies to anything derived from a received value: `FHE.add(balance, amount)` produces a new handle that needs its own `FHE.allowThis` before being stored. ### Choosing the receive function Which one you need depends only on how the value reached you: | The value arrived as… | Use | | -------------------------------------- | ---------------------------------------- | | a parameter of the function you are in | `receiveEuint64Param(shared)` | | the return value of a call you made | `receiveEuint64FromCall(shared, callee)` | `receiveEuint64Param` needs no address — use it whenever the value came in through your function signature. :::warning The address passed to `receiveEuint64FromCall` must be the contract called **in that same expression**. If nothing is called on that line, use `receiveEuint64Param`. Passing anything else — a stored `officialVault`, a constant — compiles and usually works, but checks the wrong thing: who *created* the share rather than who *handed it to you*. That lets an unrelated caller present a value never meant for them. ::: ### Returning a value Share the result back with `msg.sender`, and unwrap it with `FromCall`: ```solidity [Token.sol] function swap(sharedEuint64 shared) external returns (sharedEuint64) { euint64 amountIn = FHE.receiveEuint64Param(shared); return FHE.shareEuint64(FHE.div(amountIn, FHE.asEuint64(2)), msg.sender); } ``` ```solidity [Vault.sol] euint64 out = FHE.receiveEuint64FromCall(token.swap(shared), address(token)); FHE.allowThis(out); ``` ### Limits * **One transaction.** A `sharedEuintXX` cannot be stored and received later. * **One use.** The first successful receive consumes it. * **One receiver.** Only the address named in `shareEuint64` can receive it. * **No batching.** Each share and each receive is one TaskManager call. ### Errors | Revert | Meaning | | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `SenderNotAllowed(sharer)` | You shared a handle your contract is not allowed to use. | | `NotShared(handle, receiver)` | Nothing shared with you for this handle — never shared, already consumed, shared with a different address, or an earlier transaction. | | `UnexpectedSharer(expected, actual)` | A share exists but came from a different contract than the one named. Usually the wrong receive function, or the wrong address passed to `FromCall`. | ### Access control is still yours Receiving proves the caller deliberately handed you the value. It says nothing about whether that caller should be talking to you: ```solidity require(msg.sender == address(officialVault), "token: unknown vault"); // who may call euint64 amount = FHE.receiveEuint64Param(shared); // did they really share it ``` ### Relaying A share names one receiver and must be handed over by the contract that created it — "A shares with C, B passes it along" does not work. Chain the hops instead: each contract receives, then shares onward. ```solidity euint64 value = FHE.receiveEuint64Param(fromA); c.consume(FHE.shareEuint64(value, address(c))); ``` ## ACPs in React The React hooks decrypt with an ACP the same way the SDK does. By default that is the active ACP of the connected account, so a signed-in user sees their own data with no extra code. This page covers the case where one view should decrypt with a different ACP: data someone shared with the user. For creating, sharing and importing ACPs themselves, see [ACPs](/sdk/acp). ### Viewing shared data A recipient usually wants the issuer data in one view while the rest of the app keeps decrypting their own. Import the share without activating it, find it in the store when the user opens it, and wrap that view in ``. The pieces fit together like this; each component is defined in the steps below: ```tsx function AccessPage({ exported, me }: { exported: string; me: Address }) { const [openHash, setOpenHash] = useState(); return ( <> {/* 1. once: import the share, the active ACP stays the user own */} {/* 2. any later visit: list received shares from the store */} {openHash && ( <> {/* 3. everything inside decrypts with the share */} {/* no scope: one hook picks the ACP itself */} )} ); } ``` #### 1. Import without activating ```tsx import { useCofheImportShared } from '@cofhe/react'; function AcceptShare({ exported }: { exported: string }) { // Stored, not activated: the user own ACP keeps serving the rest of the app. const accept = useCofheImportShared({ activate: false }); return ; } ``` The same hook imports a share found in the on-chain registry: pass the `IncomingShare` instead of the JSON. See [On-chain inbox](#on-chain-inbox) below. #### 2. Find it again later Imported ACPs are stored with type `'recipient'` and survive a reload, so a later visit lists them from the store. `useCofheACPs` returns the connected account ACPs on the connected chain (or `chainId`), narrowed by `type`: ```tsx import { useCofheACPs } from '@cofhe/react'; function SharedWithMe({ onOpen }: { onOpen: (hash: string) => void }) { const received = useCofheACPs({ type: 'recipient' }); return received.map((acp) => ( )); } ``` #### 3. Scope the view ```tsx ``` `acp` takes the ACP or its hash. Inside the scope: * `useCofheTokenDecryptedBalance`, `useCofheReadContractAndDecrypt` and `useCofheReadContract(s)` gate on and decrypt with the scope ACP. * `useCofheTokenDecryptedBalance` reads the issuer balance when no `accountAddress` is given. * The scope checks its ACP on chain when it mounts, every minute and on window focus. Until the check passes, and once the ACP is revoked, expired, removed or unknown, the hooks are disabled (`disabledDueToMissingValidACP`); they never fall back to the active ACP. `useCofheACPScope().status` says which: `checking`, `valid`, `expired`, `revoked`, `invalid`, or `unverified` when the check itself failed (it is retried). * For a SNAPSHOT share, a value the share does not list is flagged `isOutOfScope` and never sent for decryption. LIVE shares are not pre-checked: a value they do not cover shows as a decrypt error. * Nested scopes: the innermost wins. Outside any scope nothing changes. ```tsx import { useCofheACPScope, useCofheTokenDecryptedBalance, type ConfidentialToken } from '@cofhe/react'; function SharedBalance({ token }: { token: ConfidentialToken }) { // No account given: inside a scope this is the issuer balance. const scope = useCofheACPScope(); const { data, disabledDueToMissingValidACP, isOutOfScope } = useCofheTokenDecryptedBalance({ token }); if (scope?.status === 'checking') return

…

; if (scope?.status === 'revoked') return

The owner revoked this share.

; if (disabledDueToMissingValidACP) return

This share has expired or was removed.

; if (isOutOfScope) return

Not included in this share.

; return

{data ? `${data.formatted} ${token.symbol}` : '…'}

; } ``` Any contract read works the same way. `useCofheACPScope()` returns `{ acp, issuer, isValid }` (or `null` outside a scope), for arguments that name the issuer: ```tsx import { useCofheACPScope, useCofheReadContractAndDecrypt } from '@cofhe/react'; function SharedVaultPosition() { const scope = useCofheACPScope(); const { decrypted } = useCofheReadContractAndDecrypt({ address: VAULT_ADDRESS, abi: vaultAbi, functionName: 'confidentialBalanceOf', args: scope?.issuer ? [scope.issuer] : undefined, }); return

{decrypted.data?.toString() ?? '…'}

; } ``` A value that is only a handle, not a contract read, e.g. a transfer amount listed in a SNAPSHOT share, decrypts with `useCofheDecrypt`; inside a scope it uses the scope ACP too: ```tsx import { useCofheDecrypt } from '@cofhe/react'; import { FheTypes } from '@cofhe/sdk'; function SharedAmount({ handle }: { handle: `0x${string}` }) { const { data } = useCofheDecrypt({ input: { ctHash: handle, utype: FheTypes.Uint64 } }); return {data?.toString() ?? '…'}; } ``` #### On-chain inbox Shares posted to the on-chain registry need no copy-paste. The recipient lists them, imports one (without activating it) and dismisses the registry entry; the issuer posts, watches the status and revokes: ```tsx import { useCofheIncomingShares, useCofheImportShared, useCofheRemoveShare } from '@cofhe/react'; function Inbox() { const inbox = useCofheIncomingShares(); // polls every 15 s; shares already imported are left out const accept = useCofheImportShared({ activate: false }); const dismiss = useCofheRemoveShare(); return (inbox.data ?? []).map((share) => (
From {share.issuer}
)); } ``` ```tsx import { useCofheACPStatus, useCofheRevokeACP, useCofheShareOnChain } from '@cofhe/react'; import type { ACP } from '@cofhe/sdk/acps'; function GrantedShare({ acp }: { acp: ACP }) { const post = useCofheShareOnChain(); const revoke = useCofheRevokeACP(); const { status } = useCofheACPStatus(acp); // valid, expired, revoked, ...; re-reads after revoke mines return (
{acp.name}: {status}
); } ``` The write hooks resolve once the transaction is mined, so a read that follows sees the result. #### One value without a scope Pass `acp` to the hook itself; it wins over any enclosing scope. Here the user own balance and a shared one sit side by side: ```tsx function BothBalances({ token, me, shareHash }: { token: ConfidentialToken; me: Address; shareHash: string }) { const mine = useCofheTokenDecryptedBalance({ token, accountAddress: me }); // active ACP const theirs = useCofheTokenDecryptedBalance({ token, acp: shareHash }); // shared ACP, issuer account return (

You: {mine.data?.formatted} · Them: {theirs.data?.formatted}

); } ``` #### What happens to the decrypted values * Values decrypted with a shared ACP stay in memory; they are never written to the persisted query cache. * They are dropped when the ACP is removed (`client.acp.removeACP(hash)`) or expires, even while the view is open. * A revocation is caught by the scope re-check (on mount, every minute, on window focus), and the values are dropped the same way. For a one-off check outside React, use `client.acp.checkAccess(acp)`. ## 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 ACP 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-ACP 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) * [ACPs](/sdk/acp) ## 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 * **`externalEuint32`** — the encrypted input handle passed in from the SDK, authenticated by a shared `bytes` signature ```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(externalEuint32[] memory inValues, bytes memory signature) external { storedValue = FHE.asEuint32s(inValues, signature)[0]; // [!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: [hash, signature] const [encHash, signature] = await cofheClient .encryptInputs([Encryptable.uint32(42n)]) .setConsumingContract(await contract.getAddress()) .execute(); // Send to contract await (await contract.setValue([encHash], signature)).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) — acps, `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 | | `SimpleTest` | Optional helper contract for tests — exposes trivial value setters plus `getValueHash()` | 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 The core mock contracts are deployed automatically before every `npx hardhat test` and `npx hardhat node` run. `SimpleTest` is not auto-deployed. 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 --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 the core mock contracts. 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 simpleTestFactory = await hre.ethers.getContractFactory('SimpleTest'); const simpleTest = await simpleTestFactory.deploy(); await simpleTest.waitForDeployment(); ``` ### Reading plaintext values Because `MockTaskManager` stores plaintext values on-chain, you can read the underlying plaintext of any encrypted handle directly in tests — no acp 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 simpleTestFactory = await hre.ethers.getContractFactory('SimpleTest'); const simpleTest = await simpleTestFactory.deploy(); await simpleTest.waitForDeployment(); await simpleTest.setValueTrivial(7); const ctHash = await simpleTest.getValueHash(); 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 simpleTestFactory = await hre.ethers.getContractFactory('SimpleTest'); const simpleTest = await simpleTestFactory.deploy(); await simpleTest.waitForDeployment(); await simpleTest.setValueTrivial(7); const ctHash = await simpleTest.getValueHash(); 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. ::: ### Gas reporting Mock FHE operations consume more gas than they would on a real CoFHE network, because the off-chain FHE work is replicated on-chain. `MockTaskManager` measures this overhead and emits a `MockGasConsumed(uint256)` event for every block of mock-only work (op replication, decrypt-task storage, mock logging), so the plugin can report corrected numbers. #### `hre.cofhe.getAdjustedGasUsed(receipt)` Returns the transaction's gas usage with the mock overhead subtracted — an estimate of real-network cost: ```ts twoslash // @noErrors import '@cofhe/hardhat-plugin'; import '@nomicfoundation/hardhat-ethers'; // ---cut--- import hre from 'hardhat'; const simpleTestFactory = await hre.ethers.getContractFactory('SimpleTest'); const simpleTest = await simpleTestFactory.deploy(); await simpleTest.waitForDeployment(); const tx = await simpleTest.setValueTrivial(7); const receipt = await tx.wait(); const adjusted = hre.cofhe.getAdjustedGasUsed(receipt!); // [!code focus] ``` #### `hre.cofhe.getAdjustedGasBreakdown(receipt)` Returns the full breakdown: ```ts const { gasUsed, mockGas, adjustedGasUsed, mockGasEvents } = hre.cofhe.getAdjustedGasBreakdown(receipt); // gasUsed — raw gas (includes mock overhead) // mockGas — gas consumed by mock-only work // adjustedGasUsed — gasUsed - mockGas (≈ real-network cost) // mockGasEvents — number of mock-only blocks executed ``` Both helpers are pure functions of the receipt — no RPC calls. On a real network the receipt carries no mock events, so `adjustedGasUsed` equals `gasUsed`; the same code works everywhere. #### End-of-run summary table Set `cofhe: { gasSummary: true }` in your Hardhat config to print a per-method table after every test run: ``` [COFHE-MOCKS] Gas summary — adjusted ≈ cost excluding mock-only overhead ┌──────────────────┬──────────────────────────┬───────┬─────────────────┬────────────────────┬───────────────┐ │ Contract │ Method │ Calls │ Avg gas (mocks) │ Avg gas (adjusted) │ Mock overhead │ ├──────────────────┼──────────────────────────┼───────┼─────────────────┼────────────────────┼───────────────┤ │ SimpleTest │ setValueTrivial(uint256) │ 11 │ 179,897 │ 141,482 │ 21% │ └──────────────────┴──────────────────────────┴───────┴─────────────────┴────────────────────┴───────────────┘ ``` :::warning The summary is reconstructed from chain history after the run. Tests using `loadFixture` or manual `evm_snapshot`/`evm_revert` roll back their blocks, so those transactions won't appear in the table — use `getAdjustedGasUsed(receipt)` inside tests for exact per-transaction numbers. The summary is also best-effort: it may be incomplete after forced exits (`process.exit`, Ctrl+C). ::: :::note `eth_estimateGas` is not adjusted — the mock work really does execute, so transactions still need the raw gas limit. Use a testnet for estimate-sensitive flows. (In Foundry, the equivalent overhead is excluded from gas metering entirely — see the [Foundry plugin](/foundry).) ::: ## 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-ACP — 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: [hash, signature] const [hash, signature] = await cofheClient .encryptInputs([Encryptable.uint32(100n)]) .setConsumingContract(await testContract.getAddress()) .execute(); // 2. Send to contract const tx = await testContract.setValueBatch([hash], signature); 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 an ACP: ```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. ### ACPs `createClientWithBatteries` pre-generates a self-ACP, so `decryptForView` and `decryptForTx().withACP()` work immediately. For tests that need named ACPs or multiple signers, create them explicitly: ```ts twoslash // @noErrors import { CofheClient } from '@cofhe/sdk'; import { ACPUtils } from '@cofhe/sdk/acps'; import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers'; declare const cofheClient: CofheClient; declare const signer: HardhatEthersSigner; // ---cut--- const acp = await cofheClient.acp.createSelf({ issuer: signer.address, name: 'My Test ACP', }); // Select it as the active acp const acpHash = ACPUtils.getHash(acp); cofheClient.acp.selectActiveACP(acpHash); ``` 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 ACP mode must be selected explicitly. #### Globally allowed values (`.withoutACP()`) When a contract calls `FHE.allowPublic(handle)`, anyone can decrypt without an ACP: ```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) .withoutACP() // [!code focus] .execute(); expect(result.decryptedValue).to.equal(55n); ``` #### Access-controlled values (`.withACP()`) For handles restricted by ACL policy, supply an ACP: :::code-group ```ts twoslash [Explicit acp] // @noErrors import { CofheClient } from '@cofhe/sdk'; import { ACP } from '@cofhe/sdk/acps'; import { expect } from 'chai'; declare const cofheClient: CofheClient; declare const ctHash: string; declare const acp: ACP; // ---cut--- const result = await cofheClient .decryptForTx(ctHash) .withACP(acp) // [!code focus] .execute(); expect(result.decryptedValue).to.equal(99n); ``` ```ts twoslash [Active acp] // @noErrors import { CofheClient } from '@cofhe/sdk'; import { expect } from 'chai'; declare const cofheClient: CofheClient; declare const ctHash: string; // ---cut--- // resolves the active acp automatically const result = await cofheClient .decryptForTx(ctHash) .withACP() // [!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).