@0xbojack/fhevm-core
v0.2.1
Published
Framework-agnostic FHEVM SDK core library
Maintainers
Readme
@fhevm/core
Framework-agnostic FHEVM SDK core library for building privacy-preserving applications with Fully Homomorphic Encryption.
Features
- 🔒 Fully Homomorphic Encryption: Compute on encrypted data without decryption
- 🎯 Framework Agnostic: Works with any JavaScript framework or vanilla JS
- 💾 Flexible Storage: Built-in support for Memory, localStorage, and IndexedDB
- 🔐 EIP-712 Signatures: Secure 365-day decryption authorization caching
- 🧪 Mock Support: Local development with Hardhat using
@fhevm/mock-utils - 📦 Tree-shakeable: Optimized bundle size with ESM and CJS builds
Installation
npm install @fhevm/core ethers
# or
pnpm add @fhevm/core ethers
# or
yarn add @fhevm/core ethersFor local development with Hardhat:
npm install -D @fhevm/mock-utilsQuick Start
Basic Usage
import { FhevmClient } from '@fhevm/core';
import { BrowserProvider } from 'ethers';
// Create client
const client = new FhevmClient();
// Initialize with provider
const provider = new BrowserProvider(window.ethereum);
await client.init({ provider });
// Encrypt data
const encrypted = await client.encrypt({
value: 42,
type: 'euint32',
contractAddress: '0x...',
userAddress: '0x...',
});
// Decrypt data (requires signer)
const signer = await provider.getSigner();
const decrypted = await client.decrypt(
[{ handle: '0x...', contractAddress: '0x...' }],
signer
);Custom Storage
import { FhevmClient, IndexedDBStorage } from '@fhevm/core';
const client = new FhevmClient({
storage: new IndexedDBStorage(), // Persistent browser storage
});Local Development (Hardhat)
import { FhevmClient } from '@fhevm/core';
const client = new FhevmClient({
mockChains: {
31337: 'http://localhost:8545', // Hardhat local network
},
});
await client.init({
provider: 'http://localhost:8545',
chainId: 31337,
});API Reference
FhevmClient
Constructor
new FhevmClient(config?: FhevmConfig)Config Options:
storage?: StorageAdapter- Custom storage implementation (default: MemoryStorage)mockChains?: Record<number, string>- Mock chain configurations for local development
Methods
init(params: InitParams, signal?: AbortSignal): Promise<FhevmInstance>
Initialize the FHEVM instance.
Parameters:
params.provider: Eip1193Provider | string- Ethereum provider or RPC URLparams.chainId?: number- Chain ID (auto-detected if not provided)signal?: AbortSignal- Optional abort signal for cancellation
Returns: Promise
getStatus(): FhevmStatus
Get current initialization status.
Returns: 'idle' | 'loading' | 'ready' | 'error'
onStatusChange(listener: (status: FhevmStatus) => void): () => void
Subscribe to status changes.
Returns: Unsubscribe function
getPublicKey(): string
Get the public key for encryption.
Returns: Public key string
encrypt(params: EncryptParams): Promise<EncryptResult>
Encrypt data for on-chain computation.
Parameters:
value: number | bigint | boolean | string- Value to encrypttype: EncryptedType- Type of encrypted valuecontractAddress: 0x${string}- Contract addressuserAddress: 0x${string}- User address
Supported Types:
ebool- Encrypted booleaneuint8- Encrypted 8-bit unsigned integereuint16- Encrypted 16-bit unsigned integereuint32- Encrypted 32-bit unsigned integereuint64- Encrypted 64-bit unsigned integereuint128- Encrypted 128-bit unsigned integereuint160- Encrypted 160-bit unsigned integereuint256- Encrypted 256-bit unsigned integereaddress- Encrypted address
Returns: { handles: Uint8Array[], inputProof: Uint8Array }
decrypt(requests: DecryptRequest[], signer: JsonRpcSigner): Promise<DecryptResult>
Decrypt encrypted data (requires user signature).
Parameters:
requests- Array of decrypt requestssigner- ethers.JsonRpcSigner for EIP-712 signature
Returns: Decrypted values as Record<string, string | bigint | boolean>
Storage Adapters
MemoryStorage (Default)
In-memory storage, data persists only for the lifetime of the instance.
import { MemoryStorage } from '@fhevm/core';
const storage = new MemoryStorage();LocalStorageStorage
Browser localStorage with prefix support.
import { LocalStorageStorage } from '@fhevm/core';
const storage = new LocalStorageStorage('my-app:'); // Optional prefixIndexedDBStorage
Browser IndexedDB for larger storage capacity.
import { IndexedDBStorage } from '@fhevm/core';
const storage = new IndexedDBStorage();Custom Storage
Implement the StorageAdapter interface:
interface StorageAdapter {
getItem(key: string): Promise<string | null>;
setItem(key: string, value: string): Promise<void>;
removeItem(key: string): Promise<void>;
clear(): Promise<void>;
}Error Handling
import { FhevmError, FhevmAbortError } from '@fhevm/core';
try {
await client.init({ provider });
} catch (error) {
if (error instanceof FhevmAbortError) {
console.log('Operation was cancelled');
} else if (error instanceof FhevmError) {
console.error('FHEVM Error:', error.code, error.message);
}
}TypeScript Support
Full TypeScript support with comprehensive type definitions.
import type {
FhevmConfig,
FhevmStatus,
InitParams,
EncryptParams,
EncryptResult,
DecryptRequest,
DecryptResult,
StorageAdapter,
} from '@fhevm/core';Advanced Usage
Abort Operations
const controller = new AbortController();
// Start initialization
const initPromise = client.init({ provider }, controller.signal);
// Cancel if needed
controller.abort();
try {
await initPromise;
} catch (error) {
if (error instanceof FhevmAbortError) {
console.log('Initialization cancelled');
}
}Status Monitoring
const unsubscribe = client.onStatusChange((status) => {
console.log('Status:', status);
// idle -> loading -> ready
});
await client.init({ provider });
unsubscribe();Multiple Contract Decryption
const results = await client.decrypt(
[
{ handle: '0x...', contractAddress: '0xContract1...' },
{ handle: '0x...', contractAddress: '0xContract2...' },
],
signer
);Batch Encryption
// Encrypt multiple values for the same contract
const client = new FhevmClient();
await client.init({ provider });
const contractAddress = '0x...';
const userAddress = '0x...';
// Encrypt multiple values
const age = await client.encrypt({
value: 25,
type: 'euint8',
contractAddress,
userAddress,
});
const balance = await client.encrypt({
value: 1000n,
type: 'euint64',
contractAddress,
userAddress,
});
const isActive = await client.encrypt({
value: true,
type: 'ebool',
contractAddress,
userAddress,
});
// Use in contract call
await contract.setUserData(
age.handles[0],
balance.handles[0],
isActive.handles[0],
age.inputProof // All values share the same proof
);Custom Error Handling
import { FhevmClient, FhevmError, FhevmAbortError } from '@fhevm/core';
try {
await client.encrypt({ value: 42, type: 'euint32', contractAddress, userAddress });
} catch (error) {
if (error instanceof FhevmAbortError) {
console.log('Operation cancelled by user');
} else if (error instanceof FhevmError) {
switch (error.code) {
case 'NOT_INITIALIZED':
console.error('Client not initialized - call init() first');
break;
case 'INVALID_TYPE':
console.error('Invalid encrypted type provided');
break;
case 'BROWSER_REQUIRED':
console.error('Browser environment required for production chains');
break;
default:
console.error(`FHEVM Error (${error.code}):`, error.message);
}
} else {
console.error('Unexpected error:', error);
}
}Production Deployment Example
import { FhevmClient, IndexedDBStorage } from '@fhevm/core';
import { BrowserProvider } from 'ethers';
// Production configuration
const client = new FhevmClient({
storage: new IndexedDBStorage(), // Persistent storage
mockChains: {}, // Disable mock chains in production
});
// Initialize with MetaMask/WalletConnect
const provider = new BrowserProvider(window.ethereum);
await client.init({ provider });
// Subscribe to status changes for UI feedback
client.onStatusChange((status) => {
switch (status) {
case 'loading':
showLoadingSpinner();
break;
case 'ready':
hideLoadingSpinner();
enableEncryptButton();
break;
case 'error':
hideLoadingSpinner();
showErrorMessage();
break;
}
});Performance Best Practices
1. Optimize Encrypted Type Selection
Choose the smallest encrypted type that fits your data to minimize gas costs:
// ❌ BAD: Using oversized type
const age = await client.encrypt({
value: 25, // 0-255 range
type: 'euint256', // Wastes gas
contractAddress,
userAddress,
});
// ✅ GOOD: Using appropriate type
const age = await client.encrypt({
value: 25,
type: 'euint8', // Perfect fit for 0-255 range
contractAddress,
userAddress,
});2. Storage Adapter Selection
| Storage Type | Use Case | Persistence | Capacity | Performance | |--------------|----------|-------------|----------|-------------| | MemoryStorage | Testing, ephemeral sessions | Session only | Low (~5MB) | Fastest | | LocalStorageStorage | Simple apps, small data | Browser restart | ~10MB | Fast | | IndexedDBStorage | Production apps | Browser restart | ~50MB+ | Good |
Recommendation: Use IndexedDBStorage for production to cache public keys and signatures persistently.
3. Signature Reuse
Decryption signatures are valid for 365 days and automatically cached:
// First decrypt - prompts user to sign
const result1 = await client.decrypt([{ handle: '0x...', contractAddress }], signer);
// Subsequent decrypts within 365 days - no signature prompt!
const result2 = await client.decrypt([{ handle: '0x...', contractAddress }], signer);
const result3 = await client.decrypt([{ handle: '0x...', contractAddress }], signer);Impact: Reduces user friction by 99% after initial signature.
4. Public Key Caching
Public keys are automatically cached using PublicKeyStorage:
- First init: Downloads public key from network (~1-2s)
- Subsequent inits: Loads from storage (<50ms)
Recommendation: Always use persistent storage (IndexedDB) to maximize cache hit rate across sessions.
5. Abort Long-Running Operations
Use AbortController to cancel initialization if user navigates away:
const controller = new AbortController();
// Cancel on component unmount
useEffect(() => {
const initPromise = client.init({ provider }, controller.signal);
return () => controller.abort();
}, []);Troubleshooting
MetaMask Nonce Mismatch After Hardhat Restart
Symptom: Transactions fail with "nonce too high" or "nonce too low" after restarting local Hardhat node.
Solution:
- Open MetaMask → Settings → Advanced → Clear Activity Tab Data
- Restart your browser (clears view function cache)
- Reconnect to
http://localhost:8545 - Redeploy contracts with
pnpm contract:deploy
Public Key Not Found Error
Symptom: FhevmError: Public key not found during decryption.
Causes & Solutions:
- Signature expired (>365 days old): Re-initialize client
await client.init({ provider }); // Refreshes public key - Storage cleared: Signature was deleted from browser storage
// User will be prompted to sign again await client.decrypt([...], signer); - Wrong contract address: Signature is per-contract
// Generate new signature for new contract await client.decrypt( [{ handle: '0x...', contractAddress: '0xNEW_CONTRACT' }], signer );
Mock Utils Not Found (Local Development)
Symptom: Cannot find module '@fhevm/mock-utils' when using Hardhat (chainId 31337).
Solution: Install as dev dependency:
npm install -D @fhevm/mock-utils
# or
pnpm add -D @fhevm/mock-utilsBrowser Environment Required
Symptom: FhevmError: BROWSER_REQUIRED when running on server.
Explanation: Production chains (Sepolia, mainnet) require browser environment to load Relayer SDK.
Solutions:
- Next.js: Use client components (
'use client') for FHEVM operations - Node.js backend: Use mock chains for testing, or implement server-side decryption (advanced)
- SSR: Defer FHEVM initialization to
useEffect(client-side only)
'use client'; // Next.js client component
import { useEffect } from 'react';
import { FhevmClient } from '@fhevm/core';
export function EncryptForm() {
useEffect(() => {
// Client-side only
const client = new FhevmClient();
client.init({ provider: window.ethereum });
}, []);
}ChainId Mismatch
Symptom: Client initializes but encryption fails.
Cause: Provider and config chainId don't match.
Solution: Let the SDK auto-detect chainId:
// ❌ BAD: Manual chainId may mismatch
await client.init({ provider: window.ethereum, chainId: 11155111 });
// ✅ GOOD: Auto-detect from provider
await client.init({ provider: window.ethereum });Slow Initialization
Symptom: init() takes >5 seconds on subsequent loads.
Causes:
- No persistent storage: Using
MemoryStorage(default)- Solution: Switch to
IndexedDBStorage
- Solution: Switch to
- Network latency: Downloading public key every time
- Solution: Verify storage adapter is working
- Relayer SDK loading: CDN download on first load
- Expected: First load slower (~2-3s), subsequent loads fast
// Enable persistent caching
const client = new FhevmClient({
storage: new IndexedDBStorage(),
});
// Verify caching works
console.time('First init');
await client.init({ provider });
console.timeEnd('First init'); // ~2000ms
console.time('Second init');
await client.init({ provider });
console.timeEnd('Second init'); // ~50ms (cached!)Migration Guide
From fhevmjs Direct Usage
Before (fhevmjs):
import { createInstance } from 'fhevmjs';
const instance = await createInstance({
network: window.ethereum,
aclContractAddress: '0x...',
kmsVerifierContractAddress: '0x...',
inputVerifierContractAddress: '0x...',
});
const input = instance.createEncryptedInput('0xContract', '0xUser');
input.add32(42);
const encrypted = await input.encrypt();After (@fhevm/core):
import { FhevmClient } from '@fhevm/core';
const client = new FhevmClient();
await client.init({ provider: window.ethereum });
// ACL addresses auto-detected from Relayer SDK config
const encrypted = await client.encrypt({
value: 42,
type: 'euint32',
contractAddress: '0xContract',
userAddress: '0xUser',
});Key Changes:
- ✅ Wrapped in class API with lifecycle management (
init(),getStatus()) - ✅ Automatic ACL address detection from network config
- ✅ Built-in storage adapters for public key/signature caching
- ✅ Type-safe encrypt/decrypt with
EncryptedTypeenum - ✅ Error handling with custom error classes (
FhevmError,FhevmAbortError)
Examples
See full working examples in our monorepo:
- Next.js App - Complete dApp with RainbowKit + wagmi integration
- Vue App - Vue 3 Composition API example
- Smart Contracts - Solidity contracts with encryption
Live Demos:
- Next.js Demo - Encrypted counter example
- Vue Demo - Same example in Vue 3
License
BSD-3-Clause-Clear
