diff --git a/client-sdk/hardhat-3-plugin/client.mdx b/client-sdk/hardhat-3-plugin/client.mdx new file mode 100644 index 0000000..e5b855f --- /dev/null +++ b/client-sdk/hardhat-3-plugin/client.mdx @@ -0,0 +1,101 @@ +--- +title: Client +description: "Create and connect a CofheClient inside a Hardhat 3 test" +--- + +The plugin extends every Hardhat 3 connection with a `cofhe` namespace. There are three ways to create and connect a `CofheClient` from a test, in increasing order of control. + +## Batteries included (recommended) + +`cofhe.createClientWithBatteries(walletClient?)` is the one-call setup. It: + +1. Creates a CoFHE config with `environment: 'hardhat'`. +2. Creates a `CofheClient`. +3. Connects it using the wallet client you pass (or the first wallet client on the connection if you don't). +4. Generates and signs a self permit so the client can immediately decrypt encrypted values. + +```typescript +import { describe, it } from 'node:test'; +import { network } from 'hardhat'; + +describe('My FHE contract', async () => { + const { viem, cofhe } = await network.connect(); + const [walletClient] = await viem.getWalletClients(); + + it('decrypts a value', async () => { + const client = await cofhe.createClientWithBatteries(walletClient); + client.connected; // true + }); +}); +``` + +Or call it with no arguments to use the first wallet client on the connection: + +```typescript +const client = await cofhe.createClientWithBatteries(); +``` + + +Because the self permit is created up-front, encrypt / decrypt / decryptForView / decryptForTx all work immediately without any additional setup. + + +## Manual setup + +For more control — custom config options, multiple signers, adjusting `encryptDelay` — set up the client step by step. + + + + + +```typescript +const config = await cofhe.createConfig(); +// With overrides: +const config = await cofhe.createConfig({ mocks: { encryptDelay: 0 } }); +``` + +`cofhe.createConfig` wraps `createCofheConfig` from `@cofhe/sdk/node` with two Hardhat-3-specific additions: + +- Sets `environment: 'hardhat'` automatically. +- Defaults `mocks.encryptDelay` to `0` so tests run without artificial wait times. + + + + + +```typescript +const client = cofhe.createClient(config); +``` + +The client is **not connected yet** — call `client.connect(...)` before using it. + + + + + +```typescript +const publicClient = await viem.getPublicClient(); +const [walletClient] = await viem.getWalletClients(); + +await client.connect(publicClient, walletClient); +``` + + + + + +## API summary + +| Method | Returns | Description | +| --- | --- | --- | +| `cofhe.createConfig(overrides?)` | `Promise` | CoFHE config pre-wired for the Hardhat mock environment (`environment: 'hardhat'`, `mocks.encryptDelay: 0`). | +| `cofhe.createClient(config)` | `CofheClient` | Unconnected client. Call `client.connect(publicClient, walletClient)` before use. | +| `cofhe.createClientWithBatteries(walletClient?)` | `Promise` | Fully configured + connected + self-permit signed. Defaults to the first wallet client on the connection. | + +## Using the client + +Once connected, the client works identically to the standard SDK client. See: + +- [Encrypting Inputs](/client-sdk/guides/encrypting-inputs) +- [Decrypt to View](/client-sdk/guides/decrypt-to-view) +- [Decrypt to Transact](/client-sdk/guides/decrypt-to-tx) +- [Permits](/client-sdk/guides/permits) diff --git a/client-sdk/hardhat-3-plugin/getting-started.mdx b/client-sdk/hardhat-3-plugin/getting-started.mdx new file mode 100644 index 0000000..6a96c78 --- /dev/null +++ b/client-sdk/hardhat-3-plugin/getting-started.mdx @@ -0,0 +1,140 @@ +--- +title: Getting Started +description: "Set up @cofhe/hardhat-3-plugin for local FHE contract development under Hardhat 3" +--- + +`@cofhe/hardhat-3-plugin` is the Hardhat 3 counterpart to [`@cofhe/hardhat-plugin`](/client-sdk/hardhat-plugin/getting-started). It deploys the CoFHE mock contracts to an in-process Hardhat network on every `network.connect()` call and exposes a `cofhe` namespace on the connection object — ready to use immediately, no boilerplate required. + + +Use this plugin if you've adopted the [Hardhat 3 plugin/hook model](https://hardhat.org). For Hardhat v2 projects, use [`@cofhe/hardhat-plugin`](/client-sdk/hardhat-plugin/getting-started) instead. + + +## What the plugin provides + +- **Auto-deploys mocks** on every `network.connect()` — `MockTaskManager`, `MockACL`, `MockZkVerifier`, `MockThresholdNetwork`, and `TestBed`. +- **`conn.cofhe` namespace** on the Hardhat 3 connection object (sits alongside `conn.viem` from `@nomicfoundation/hardhat-viem`). +- **`cofhe.createClientWithBatteries()`** — one-call SDK client setup with a pre-signed self permit. +- **Mock helpers** — Viem contract descriptors for every mock, plaintext inspection (`getPlaintext` / `expectPlaintext`), and logging control. + +## Installation + + + + + + + +```bash npm +npm install @cofhe/hardhat-3-plugin @cofhe/sdk @fhenixprotocol/cofhe-contracts +``` + +```bash pnpm +pnpm add @cofhe/hardhat-3-plugin @cofhe/sdk @fhenixprotocol/cofhe-contracts +``` + +```bash yarn +yarn add @cofhe/hardhat-3-plugin @cofhe/sdk @fhenixprotocol/cofhe-contracts +``` + + + + + + + +```typescript hardhat.config.ts +import { defineConfig } from 'hardhat/config'; +import cofhePlugin from '@cofhe/hardhat-3-plugin'; +import hardhatViem from '@nomicfoundation/hardhat-viem'; +import hardhatNodeTestRunner from '@nomicfoundation/hardhat-node-test-runner'; + +export default defineConfig({ + plugins: [cofhePlugin, hardhatViem, hardhatNodeTestRunner], +}); +``` + + + + + +## Configuration + +The plugin adds an optional `cofhe` key to your Hardhat 3 config. All values shown below are their defaults: + +```typescript hardhat.config.ts +export default defineConfig({ + plugins: [cofhePlugin, hardhatViem, hardhatNodeTestRunner], + + cofhe: { + gasWarning: true, // warn that mock gas costs differ from live FHE + logMocks: true, // enable event-based logging in mock contracts + mocksDeployVerbosity: 'v', // '' silent | 'v' summary | 'vv' full per-contract + }, +}); +``` + +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| `gasWarning` | `boolean` | `true` | Print a one-line warning after mock deployment reminding that mock gas costs differ from live FHE. | +| `logMocks` | `boolean` | `true` | Enable event-based logging inside mock contracts. | +| `mocksDeployVerbosity` | `'' \| 'v' \| 'vv'` | `'v'` | How much output to print while deploying mocks. `''` silent, `'v'` one summary line per mock, `'vv'` full per-contract deployment log. | + +## How auto-deployment works + +Every call to `network.connect()` automatically: + +1. Deploys all CoFHE mock contracts to the fresh in-process EVM. +2. Attaches a `cofhe` namespace to the returned connection object. + +Because `network.connect()` is awaitable at the top level of an `async describe`, you can set everything up without lifecycle hooks: + +```typescript +import { describe, it } from 'node:test'; +import { network } from 'hardhat'; + +describe('My FHE contract', async () => { + const { viem, cofhe } = await network.connect(); + const publicClient = await viem.getPublicClient(); + const [walletClient] = await viem.getWalletClients(); + + it('encrypts and decrypts a value', async () => { + const client = await cofhe.createClientWithBatteries(walletClient); + // ... test logic + }); +}); +``` + + +**Why `async describe`?** Hardhat 3's `node:test` runner supports top-level `await` inside the describe callback. This lets you resolve the connection (and deploy mocks) exactly once per test file without needing a `before()` hook. + + +## Mock contracts deployed + +| Contract | Address | Description | +| --- | --- | --- | +| `MockTaskManager` | `0xeA30c4B8b44078Bbf8a6ef5b9f1eC1626C7848D9` | Coordinates FHE operations and ACL | +| `MockACL` | dynamic | Access Control List — address resolved from TaskManager | +| `MockZkVerifier` | `0x0000000000000000000000000000000000005001` | Verifies ZK proofs for encrypted inputs | +| `MockThresholdNetwork` | `0x0000000000000000000000000000000000005002` | Simulates the threshold decryption network | +| `TestBed` | `0x0000000000000000000000000000000000005003` | Utility contract for storing and retrieving encrypted values in tests | + +Fixed-address contracts are deployed via `hardhat_setCode`, so they are always at the same address regardless of deployment order. `MockACL` is deployed as a normal contract (so its EIP-712 domain constructor runs correctly) and its address is registered in `MockTaskManager`. + +## Differences from `@cofhe/hardhat-plugin` + +| Concern | `@cofhe/hardhat-plugin` (v2) | `@cofhe/hardhat-3-plugin` | +| --- | --- | --- | +| Entry point | `hre.cofhe` (global) | `conn.cofhe` (per `network.connect()` call) | +| Lifecycle | Pre-task hook (`npx hardhat test` / `node`) | `network.connect()` returns a fresh deployment | +| Test runner expectation | Mocha-style (`before`, `it`) | `node:test` with `async describe` | +| Mock compilation for custom-error decoding | Overrides `TASK_COMPILE_SOLIDITY_GET_SOURCE_PATHS` | Runs `hre.solidity.build()` during the `hre.created` hook | +| Bytecode sourcing | `hre.artifacts.readArtifact()` | `hre.artifacts.readArtifact()` (same — for variable-address mocks like `MockACL`) | + +The Solidity surface (mock contracts, plaintext semantics, `FHE.verifyDecryptResult` acceptance) is **identical** between the two plugins; the differences are all in how Hardhat hosts the plugin. + +## Next steps + +- [Client](/client-sdk/hardhat-3-plugin/client) — `conn.cofhe.createConfig`, `createClient`, `createClientWithBatteries`. +- [Mock Contracts](/client-sdk/hardhat-3-plugin/mock-contracts) — Viem contract descriptors and plaintext inspection helpers. +- [Logging](/client-sdk/hardhat-3-plugin/logging) — `enableLogs`, `disableLogs`, `withLogs`. +- [Testing](/client-sdk/hardhat-3-plugin/testing) — end-to-end test patterns. diff --git a/client-sdk/hardhat-3-plugin/logging.mdx b/client-sdk/hardhat-3-plugin/logging.mdx new file mode 100644 index 0000000..f76c16a --- /dev/null +++ b/client-sdk/hardhat-3-plugin/logging.mdx @@ -0,0 +1,42 @@ +--- +title: Logging +description: "Inspect FHE operations in your Hardhat 3 test output" +--- + +The mock contracts emit structured logs for every FHE operation. This makes it easy to see what your contracts are doing under the hood during tests. + +## `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 so you can identify which call produced which operations. + +```typescript +await cofhe.mocks.withLogs('counter.increment()', async () => { + await walletClient.writeContract({ + ...counterContract, + functionName: 'increment', + }); +}); +``` + +`withLogs` enables logging before the closure runs and disables it after, so only operations from within that block appear in the output. The previous on/off state is restored when it returns. + +## `enableLogs()` / `disableLogs()` — manual + +For finer-grained control, you can enable and disable logging manually: + +```typescript +await cofhe.mocks.enableLogs(); + +await walletClient.writeContract({ + ...counterContract, + functionName: 'increment', +}); + +await cofhe.mocks.disableLogs(); +``` + +## Default behavior + +Logging is controlled by the `cofhe.logMocks` config option (see [Getting Started → Configuration](/client-sdk/hardhat-3-plugin/getting-started#configuration)). When the option is `true` (the default), mock-contract log events are emitted; when `false`, they're suppressed even if you call `enableLogs()`. + +`withLogs`, `enableLogs`, and `disableLogs` only flip the runtime gate at the mock-contract level — they do **not** override the config. diff --git a/client-sdk/hardhat-3-plugin/mock-contracts.mdx b/client-sdk/hardhat-3-plugin/mock-contracts.mdx new file mode 100644 index 0000000..113b807 --- /dev/null +++ b/client-sdk/hardhat-3-plugin/mock-contracts.mdx @@ -0,0 +1,73 @@ +--- +title: Mock Contracts +description: "Inspect plaintext and call mock CoFHE contracts in Hardhat 3 tests" +--- + +`conn.cofhe.mocks` exposes Viem contract descriptors for every deployed mock contract and helpers for reading the on-chain plaintext that the mock task manager stores. None of this requires the SDK client — the mocks are reachable from any Viem `publicClient` / `walletClient`. + +## Contract descriptors + +Each mock is exposed as a synchronous `{ address, abi }` object — spread it directly into Viem's `readContract` / `writeContract`: + +```typescript +cofhe.mocks.MockTaskManager; // { address: '0x...', abi: [...] } +cofhe.mocks.MockACL; // { address: '0x...', abi: [...] } +cofhe.mocks.MockZkVerifier; // { address: '0x...', abi: [...] } +cofhe.mocks.MockThresholdNetwork; // { address: '0x...', abi: [...] } +cofhe.mocks.TestBed; // { address: '0x...', abi: [...] } +``` + +### Calling a mock directly + +```typescript +const ctHash = await publicClient.readContract({ + ...cofhe.mocks.TestBed, + functionName: 'numberHash', +}); + +await walletClient.writeContract({ + ...cofhe.mocks.MockTaskManager, + functionName: 'setSecurityZones', + args: [0, 1], +}); +``` + +This is the lowest-friction way to assert mock state without going through the SDK. + +## Reading plaintext values + +Because `MockTaskManager` stores plaintext values on-chain, you can read the underlying plaintext of any encrypted handle directly in tests — no permit needed. + +### `getPlaintext(ctHash)` + +Returns the plaintext `bigint` for a given ciphertext hash. Accepts either a `bigint` or a hex `string`. + +```typescript +const plaintext = await cofhe.mocks.getPlaintext(ctHash); +``` + +### `expectPlaintext(ctHash, expected)` + +Assertion shorthand — throws if the on-chain plaintext doesn't match `expected`: + +```typescript +await cofhe.mocks.expectPlaintext(ctHash, 42n); +``` + + +`getPlaintext` and `expectPlaintext` only work on the in-process Hardhat network where `MockTaskManager.mockStorage` exists. They will throw on real CoFHE networks. + + +## Re-deploying mocks mid-test + +Normally you don't need this — mocks are deployed automatically on every `network.connect()`. For advanced scenarios where you want to reset mock state inside a single test: + +```typescript +await cofhe.mocks.deployMocks({ deployTestBed: true, silent: true }); +``` + +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| `deployTestBed` | `boolean` | `true` | Whether to deploy the `TestBed` utility contract. | +| `gasWarning` | `boolean` | inherits from config | Print the gas-warning line after deployment. | +| `silent` | `boolean` | `false` | Suppress all deployment output (overrides `mocksDeployVerbosity`). | diff --git a/client-sdk/hardhat-3-plugin/testing.mdx b/client-sdk/hardhat-3-plugin/testing.mdx new file mode 100644 index 0000000..26b3b24 --- /dev/null +++ b/client-sdk/hardhat-3-plugin/testing.mdx @@ -0,0 +1,139 @@ +--- +title: Testing +description: "Canonical test-writing patterns for FHE contracts under Hardhat 3" +--- + +This page shows the load-bearing patterns for writing FHE contract tests under Hardhat 3 with `@cofhe/hardhat-3-plugin`. Hardhat v2 counterpart: [Hardhat Plugin → Testing](/client-sdk/hardhat-plugin/testing). + +## Skeleton + +```typescript test/Counter.test.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(); + + it('encrypts, stores, and decrypts a uint32', async () => { + const client = await cofhe.createClientWithBatteries(walletClient); + + // 1. Encrypt + const [enc] = await client + .encryptInputs([Encryptable.uint32(42n)]) + .execute(); + + // 2. Store on-chain via TestBed + await walletClient.writeContract({ + ...cofhe.mocks.TestBed, + functionName: 'setNumber', + args: [{ + ctHash: enc.ctHash, + securityZone: enc.securityZone, + utype: enc.utype, + signature: enc.signature, + }], + }); + + const ctHash = (await publicClient.readContract({ + ...cofhe.mocks.TestBed, + functionName: 'numberHash', + })) as `0x${string}`; + + // 3. Decrypt via view (off-chain, instant) + const decryptedView = await client + .decryptForView(ctHash, FheTypes.Uint32) + .execute(); + assert.equal(decryptedView, 42n); + + // 4. Alternatively, verify plaintext without decryption + await cofhe.mocks.expectPlaintext(ctHash, 42n); + }); +}); +``` + +That's the load-bearing shape. Everything below is what to add when the contract gets non-trivial. + +## Rules + +### 1. `async describe` runs `network.connect()` once per file + +Hardhat 3's `node:test` runner supports top-level `await` inside the describe callback. Resolve the connection (and deploy mocks) at the top of the describe — every `it` inside the same block shares the same fresh deployment. + +```typescript +describe('My contract', async () => { + const { viem, cofhe } = await network.connect(); + // ... +}); +``` + +If you want a **completely fresh** mock deployment between tests, call `await cofhe.mocks.deployMocks()` inside a `beforeEach`. + +### 2. Use `cofhe.createClientWithBatteries()` for most tests + +It handles config + connect + self-permit in one call. The self permit lets `decryptForView` work immediately: + +```typescript +const client = await cofhe.createClientWithBatteries(walletClient); +const value = await client.decryptForView(ctHash, FheTypes.Uint32).execute(); +``` + +Drop down to `cofhe.createConfig` / `cofhe.createClient` only when you need to override defaults (e.g. a non-zero `mocks.encryptDelay`). + +### 3. Assert plaintext with `expectPlaintext` whenever possible + +```typescript +await cofhe.mocks.expectPlaintext(ctHash, 42n); +``` + +Faster than `decryptForView` and needs no permit. Reserve the SDK path for tests where the SDK behavior itself is under test. + +### 4. Call mock contracts directly through their descriptors + +For state setup or assertions that don't need the SDK, spread the descriptor into Viem: + +```typescript +await walletClient.writeContract({ + ...cofhe.mocks.MockTaskManager, + functionName: 'setSecurityZones', + args: [0, 1], +}); +``` + +### 5. Wrap noisy code in `withLogs(name, fn)` for debugging + +When a test is misbehaving, scope mock logging to the suspicious block: + +```typescript +await cofhe.mocks.withLogs('counter.increment()', async () => { + await walletClient.writeContract({ + ...counterContract, + functionName: 'increment', + }); +}); +``` + +## Common pitfalls + + + +`network.connect()` returns a **fresh** deployment each call. If you `await network.connect()` once and share the result across `describe` blocks, those blocks share mock state — which is sometimes what you want and sometimes not. When in doubt, call `network.connect()` per `describe`. + + + +Tests pass on the first op, then a second op reverts with `ACLNotAllowed` because the contract itself isn't on the ACL. Toggle logging (`await cofhe.mocks.enableLogs()`) to see the missing grant — every op prints a line showing whether `allowThis` / `allow` was called. + + + +The client is bound to whichever `walletClient` you passed to `createClientWithBatteries`. Calling the contract from a *different* wallet client (e.g. via `vm.prank`-style impersonation) will fail the ZK-verifier signature check — the input was signed for the original wallet, not the new caller. + + + +## Related + +- [Hardhat 3 Plugin → Getting Started](/client-sdk/hardhat-3-plugin/getting-started) — install + config. +- [Hardhat 3 Plugin → Mock Contracts](/client-sdk/hardhat-3-plugin/mock-contracts) — descriptors + plaintext inspection. +- [Hardhat Plugin (v2) → Testing](/client-sdk/hardhat-plugin/testing) — the same patterns under Hardhat v2. diff --git a/docs.json b/docs.json index 5c8b34d..9aa162f 100644 --- a/docs.json +++ b/docs.json @@ -103,6 +103,16 @@ "client-sdk/hardhat-plugin/testing" ] }, + { + "group": "Hardhat 3 Plugin", + "pages": [ + "client-sdk/hardhat-3-plugin/getting-started", + "client-sdk/hardhat-3-plugin/client", + "client-sdk/hardhat-3-plugin/mock-contracts", + "client-sdk/hardhat-3-plugin/logging", + "client-sdk/hardhat-3-plugin/testing" + ] + }, { "group": "Foundry Plugin", "pages": [