diff --git a/packages/fhevm-sdk/package.json b/packages/fhevm-sdk/package.json index 47445861..ac70749d 100644 --- a/packages/fhevm-sdk/package.json +++ b/packages/fhevm-sdk/package.json @@ -1,32 +1,40 @@ { "name": "@fhevm-sdk", "version": "0.1.0", + "description": "Universal FHEVM SDK - Framework-agnostic toolkit for building confidential dApps with Fully Homomorphic Encryption", "private": true, "type": "module", "main": "dist/index.js", "module": "dist/index.js", "types": "src/index.ts", + "sideEffects": false, "exports": { ".": { "types": "./src/index.ts", + "import": "./dist/index.js", "default": "./dist/index.js" }, "./core": { "types": "./src/core/index.ts", + "import": "./dist/core/index.js", "default": "./dist/core/index.js" }, "./storage": { "types": "./src/storage/index.ts", + "import": "./dist/storage/index.js", "default": "./dist/storage/index.js" }, "./types": { "types": "./src/fhevmTypes.ts", + "import": "./dist/fhevmTypes.js", "default": "./dist/fhevmTypes.js" }, "./react": { "types": "./src/react/index.ts", + "import": "./dist/react/index.js", "default": "./dist/react/index.js" - } + }, + "./package.json": "./package.json" }, "scripts": { "build": "tsc -p tsconfig.json", diff --git a/packages/fhevm-sdk/src/core/FhevmClient.ts b/packages/fhevm-sdk/src/core/FhevmClient.ts new file mode 100644 index 00000000..6c4785cd --- /dev/null +++ b/packages/fhevm-sdk/src/core/FhevmClient.ts @@ -0,0 +1,285 @@ +import { Eip1193Provider } from "ethers"; +import { FhevmInstance } from "../fhevmTypes"; +import { createFhevmInstance, FhevmAbortError } from "../internal/fhevm"; +import { RelayerEncryptedInput } from "@zama-fhe/relayer-sdk/web"; +import { FhevmError, ErrorCodes, createError } from "./errors"; + +/** + * Configuration options for creating an FHEVM client + */ +export interface FhevmClientConfig { + /** + * Ethereum provider (EIP-1193 compatible) or RPC URL + */ + provider: Eip1193Provider | string; + + /** + * Optional mock chain configurations for testing + * Maps chainId to RPC URL + * + * @example + * ```typescript + * { + * 31337: "http://localhost:8545" + * } + * ``` + */ + mockChains?: Record; + + /** + * Optional abort signal to cancel initialization + */ + signal?: AbortSignal; + + /** + * Optional callback for status updates during initialization + */ + onStatusChange?: (status: FhevmClientStatus) => void; +} + +/** + * Status of the FHEVM client during initialization + */ +export type FhevmClientStatus = + | "idle" + | "sdk-loading" + | "sdk-loaded" + | "sdk-initializing" + | "sdk-initialized" + | "creating" + | "ready" + | "error"; + +/** + * Result of an encryption operation + */ +export interface EncryptionResult { + /** + * Encrypted handles (one per encrypted value) + */ + handles: Uint8Array[]; + + /** + * Input proof for verification + */ + inputProof: Uint8Array; +} + +/** + * Request for decryption + */ +export interface DecryptionRequest { + /** + * The encrypted handle to decrypt + */ + handle: string; + + /** + * The contract address that owns this encrypted value + */ + contractAddress: `0x${string}`; +} + +// Re-export error utilities for convenience +export { FhevmError, ErrorCodes, createError } from "./errors"; + +/** + * FhevmClient - Core client for FHEVM operations + * + * This class provides a framework-agnostic interface for working with + * Fully Homomorphic Encryption on Ethereum. It handles: + * - Instance initialization and management + * - Encryption of values + * - Decryption of encrypted handles + * + * @example + * ```typescript + * // Create and initialize a client + * const client = new FhevmClient({ + * provider: window.ethereum, + * mockChains: { 31337: "http://localhost:8545" } + * }); + * + * await client.initialize(); + * + * // Encrypt a value + * const encrypted = await client.createEncryptedInput( + * "0x...", // contract address + * "0x..." // user address + * ); + * encrypted.add32(42); + * const result = await encrypted.encrypt(); + * ``` + */ +export class FhevmClient { + private instance: FhevmInstance | undefined; + private config: FhevmClientConfig; + private status: FhevmClientStatus = "idle"; + private initializationPromise: Promise | undefined; + private abortController: AbortController | undefined; + + /** + * Creates a new FHEVM client + * + * @param config - Configuration options + */ + constructor(config: FhevmClientConfig) { + this.config = config; + this.abortController = new AbortController(); + } + + /** + * Initialize the FHEVM client + * + * This method must be called before using any encryption/decryption features. + * It loads the FHEVM SDK, initializes it, and creates an instance. + * + * @throws {FhevmError} If initialization fails + * @throws {FhevmAbortError} If initialization is aborted + * + * @example + * ```typescript + * const client = new FhevmClient({ provider: window.ethereum }); + * await client.initialize(); + * ``` + */ + async initialize(): Promise { + // If already initializing, return the existing promise + if (this.initializationPromise) { + return this.initializationPromise; + } + + // If already initialized, return immediately + if (this.instance && this.status === "ready") { + return Promise.resolve(); + } + + this.initializationPromise = this._doInitialize(); + return this.initializationPromise; + } + + private async _doInitialize(): Promise { + try { + this.updateStatus("sdk-loading"); + + const signal = this.config.signal || this.abortController!.signal; + + this.instance = await createFhevmInstance({ + provider: this.config.provider, + mockChains: this.config.mockChains, + signal, + onStatusChange: (status) => { + this.updateStatus(status as FhevmClientStatus); + }, + }); + + this.updateStatus("ready"); + } catch (error) { + this.updateStatus("error"); + + if (error instanceof FhevmAbortError) { + throw error; + } + + throw new FhevmError( + "INITIALIZATION_FAILED", + "Failed to initialize FHEVM client", + "Please check your provider and network configuration", + { cause: error } + ); + } + } + + private updateStatus(status: FhevmClientStatus): void { + this.status = status; + if (this.config.onStatusChange) { + this.config.onStatusChange(status); + } + } + + /** + * Get the current FHEVM instance + * + * @returns The FHEVM instance, or undefined if not initialized + */ + getInstance(): FhevmInstance | undefined { + return this.instance; + } + + /** + * Get the current status of the client + * + * @returns The current status + */ + getStatus(): FhevmClientStatus { + return this.status; + } + + /** + * Check if the client is ready for encryption/decryption + * + * @returns True if the client is ready + */ + isReady(): boolean { + return this.status === "ready" && this.instance !== undefined; + } + + /** + * Create an encrypted input builder + * + * @param contractAddress - The contract address + * @param userAddress - The user address + * @returns An encrypted input builder + * + * @throws {FhevmError} If the client is not initialized + * + * @example + * ```typescript + * const input = client.createEncryptedInput( + * "0x1234...", + * "0x5678..." + * ); + * input.add32(42); + * input.add64(1000n); + * const result = await input.encrypt(); + * ``` + */ + createEncryptedInput( + contractAddress: string, + userAddress: string + ): RelayerEncryptedInput { + if (!this.instance) { + throw createError( + ErrorCodes.NOT_INITIALIZED, + "FHEVM client is not initialized" + ); + } + + return this.instance.createEncryptedInput( + contractAddress, + userAddress + ) as RelayerEncryptedInput; + } + + /** + * Abort any ongoing initialization + * + * This will cancel the initialization process if it's still running. + */ + abort(): void { + if (this.abortController) { + this.abortController.abort(); + } + } + + /** + * Dispose of the client and clean up resources + */ + dispose(): void { + this.abort(); + this.instance = undefined; + this.initializationPromise = undefined; + this.status = "idle"; + } +} + diff --git a/packages/fhevm-sdk/src/core/createFhevmClient.ts b/packages/fhevm-sdk/src/core/createFhevmClient.ts new file mode 100644 index 00000000..58856b2d --- /dev/null +++ b/packages/fhevm-sdk/src/core/createFhevmClient.ts @@ -0,0 +1,63 @@ +import { FhevmClient, FhevmClientConfig } from "./FhevmClient"; +import { getNetworkPreset } from "./presets"; + +/** + * Create and initialize an FHEVM client in one step + * + * This is a convenience function that creates a new FhevmClient + * and immediately initializes it. + * + * @param config - Configuration options for the client, or a network preset name + * @returns A promise that resolves to an initialized FhevmClient + * + * @throws {FhevmError} If initialization fails + * @throws {FhevmAbortError} If initialization is aborted + * + * @example + * ```typescript + * // Simple usage with provider + * const client = await createFhevmClient({ + * provider: window.ethereum + * }); + * + * // With mock chains for local development + * const client = await createFhevmClient({ + * provider: "http://localhost:8545", + * mockChains: { + * 31337: "http://localhost:8545" + * } + * }); + * + * // Using a network preset + * const client = await createFhevmClient("localhost"); + * + * // With status updates + * const client = await createFhevmClient({ + * provider: window.ethereum, + * onStatusChange: (status) => { + * console.log('FHEVM status:', status); + * } + * }); + * ``` + */ +export async function createFhevmClient( + config: FhevmClientConfig | string +): Promise { + let clientConfig: FhevmClientConfig; + + // If config is a string, treat it as a network preset name + if (typeof config === "string") { + const preset = getNetworkPreset(config); + clientConfig = { + provider: preset.rpcUrl, + mockChains: preset.mockChains, + }; + } else { + clientConfig = config; + } + + const client = new FhevmClient(clientConfig); + await client.initialize(); + return client; +} + diff --git a/packages/fhevm-sdk/src/core/errors.ts b/packages/fhevm-sdk/src/core/errors.ts new file mode 100644 index 00000000..079afc56 --- /dev/null +++ b/packages/fhevm-sdk/src/core/errors.ts @@ -0,0 +1,188 @@ +/** + * Error codes and helper utilities for FHEVM SDK + */ + +/** + * Error thrown by FHEVM operations + */ +export class FhevmError extends Error { + constructor( + public code: string, + message: string, + public suggestion?: string, + options?: ErrorOptions + ) { + super(message, options); + this.name = "FhevmError"; + } + + toString(): string { + let msg = `${this.name} [${this.code}]: ${this.message}`; + if (this.suggestion) { + msg += `\nšŸ’” Suggestion: ${this.suggestion}`; + } + return msg; + } +} + +/** + * Common error codes used throughout the SDK + */ +export const ErrorCodes = { + // Initialization errors + NOT_INITIALIZED: "NOT_INITIALIZED", + INITIALIZATION_FAILED: "INITIALIZATION_FAILED", + SDK_LOAD_FAILED: "SDK_LOAD_FAILED", + SDK_INIT_FAILED: "SDK_INIT_FAILED", + + // Provider errors + INVALID_PROVIDER: "INVALID_PROVIDER", + PROVIDER_NOT_CONNECTED: "PROVIDER_NOT_CONNECTED", + NETWORK_ERROR: "NETWORK_ERROR", + + // Encryption/Decryption errors + ENCRYPTION_FAILED: "ENCRYPTION_FAILED", + DECRYPTION_FAILED: "DECRYPTION_FAILED", + INVALID_HANDLE: "INVALID_HANDLE", + + // Address errors + INVALID_ADDRESS: "INVALID_ADDRESS", + INVALID_CONTRACT_ADDRESS: "INVALID_CONTRACT_ADDRESS", + + // Configuration errors + INVALID_CONFIG: "INVALID_CONFIG", + MISSING_MOCK_CHAIN: "MISSING_MOCK_CHAIN", + + // Operation errors + OPERATION_CANCELLED: "OPERATION_CANCELLED", + OPERATION_TIMEOUT: "OPERATION_TIMEOUT", +} as const; + +/** + * Error suggestions for common error codes + */ +export const ErrorSuggestions: Record = { + [ErrorCodes.NOT_INITIALIZED]: + "Call client.initialize() before using the client, or use createFhevmClient() which initializes automatically.", + + [ErrorCodes.INITIALIZATION_FAILED]: + "Check your provider connection and network configuration. Make sure the FHEVM network is accessible.", + + [ErrorCodes.SDK_LOAD_FAILED]: + "The FHEVM SDK failed to load. Check your internet connection and try again.", + + [ErrorCodes.SDK_INIT_FAILED]: + "The FHEVM SDK failed to initialize. Verify that your network supports FHEVM operations.", + + [ErrorCodes.INVALID_PROVIDER]: + "Provide a valid EIP-1193 provider (like window.ethereum) or an RPC URL string.", + + [ErrorCodes.PROVIDER_NOT_CONNECTED]: + "Make sure your wallet is connected and the provider is accessible.", + + [ErrorCodes.NETWORK_ERROR]: + "Check your network connection and RPC endpoint. The network may be down or unreachable.", + + [ErrorCodes.ENCRYPTION_FAILED]: + "Encryption failed. Verify that the FHEVM instance is ready and the input data is valid.", + + [ErrorCodes.DECRYPTION_FAILED]: + "Decryption failed. Make sure you have permission to decrypt this value and the handle is valid.", + + [ErrorCodes.INVALID_HANDLE]: + "The provided handle is not a valid encrypted value handle. Handles should be hex strings.", + + [ErrorCodes.INVALID_ADDRESS]: + "Provide a valid Ethereum address in the format 0x...", + + [ErrorCodes.INVALID_CONTRACT_ADDRESS]: + "The contract address is invalid. Make sure it's a valid Ethereum address.", + + [ErrorCodes.INVALID_CONFIG]: + "Check your configuration object. Required fields may be missing or invalid.", + + [ErrorCodes.MISSING_MOCK_CHAIN]: + "For local development, add the chain to mockChains config. Example: { 31337: 'http://localhost:8545' }", + + [ErrorCodes.OPERATION_CANCELLED]: + "The operation was cancelled. This is normal if you aborted the operation intentionally.", + + [ErrorCodes.OPERATION_TIMEOUT]: + "The operation timed out. Try again or check your network connection.", +}; + +/** + * Create an FhevmError with automatic suggestion lookup + * + * @param code - Error code from ErrorCodes + * @param message - Error message + * @param customSuggestion - Optional custom suggestion (overrides default) + * @param cause - Optional error cause + * @returns FhevmError instance + * + * @example + * ```typescript + * throw createError( + * ErrorCodes.NOT_INITIALIZED, + * "Client is not initialized" + * ); + * ``` + */ +export function createError( + code: string, + message: string, + customSuggestion?: string, + cause?: unknown +): FhevmError { + const suggestion = customSuggestion || ErrorSuggestions[code]; + return new FhevmError(code, message, suggestion, cause ? { cause } : undefined); +} + +/** + * Check if an error is an FhevmError + * + * @param error - Error to check + * @returns True if error is an FhevmError + */ +export function isFhevmError(error: unknown): error is FhevmError { + return error instanceof FhevmError; +} + +/** + * Check if an error has a specific error code + * + * @param error - Error to check + * @param code - Error code to match + * @returns True if error has the specified code + * + * @example + * ```typescript + * try { + * await client.initialize(); + * } catch (error) { + * if (isErrorCode(error, ErrorCodes.NETWORK_ERROR)) { + * console.log("Network issue detected"); + * } + * } + * ``` + */ +export function isErrorCode(error: unknown, code: string): boolean { + return isFhevmError(error) && error.code === code; +} + +/** + * Format an error for display + * + * @param error - Error to format + * @returns Formatted error string + */ +export function formatError(error: unknown): string { + if (isFhevmError(error)) { + return error.toString(); + } + if (error instanceof Error) { + return `${error.name}: ${error.message}`; + } + return String(error); +} + diff --git a/packages/fhevm-sdk/src/core/index.ts b/packages/fhevm-sdk/src/core/index.ts index 6de1bba3..bcd26689 100644 --- a/packages/fhevm-sdk/src/core/index.ts +++ b/packages/fhevm-sdk/src/core/index.ts @@ -1,6 +1,16 @@ +// Core client and factory +export * from "./FhevmClient"; +export * from "./createFhevmClient"; + +// Network presets +export * from "./presets"; + +// Error handling utilities +export * from "./errors"; + +// Re-export internal utilities for advanced use cases export * from "../internal/fhevm"; export * from "../internal/RelayerSDKLoader"; export * from "../internal/PublicKeyStorage"; export * from "../internal/fhevmTypes"; export * from "../internal/constants"; - diff --git a/packages/fhevm-sdk/src/core/presets.ts b/packages/fhevm-sdk/src/core/presets.ts new file mode 100644 index 00000000..dc6b546f --- /dev/null +++ b/packages/fhevm-sdk/src/core/presets.ts @@ -0,0 +1,131 @@ +/** + * Network presets for common FHEVM networks + * + * These presets provide default configurations for commonly used networks, + * making it easier to get started with FHEVM. + */ + +/** + * Network preset configuration + */ +export interface NetworkPreset { + /** + * Network name + */ + name: string; + + /** + * Chain ID + */ + chainId: number; + + /** + * RPC URL + */ + rpcUrl: string; + + /** + * Mock chains configuration (if applicable) + */ + mockChains?: Record; + + /** + * Description of the network + */ + description?: string; +} + +/** + * Localhost / Hardhat network preset + * + * Use this for local development with Hardhat node + */ +export const LOCALHOST_PRESET: NetworkPreset = { + name: "localhost", + chainId: 31337, + rpcUrl: "http://localhost:8545", + mockChains: { + 31337: "http://localhost:8545", + }, + description: "Local Hardhat node for development", +}; + +/** + * Sepolia testnet preset + * + * Use this for testing on Sepolia testnet + */ +export const SEPOLIA_PRESET: NetworkPreset = { + name: "sepolia", + chainId: 11155111, + rpcUrl: "https://sepolia.infura.io/v3/YOUR_INFURA_KEY", + description: "Sepolia Ethereum testnet", +}; + +/** + * All available network presets + */ +export const NETWORK_PRESETS: Record = { + localhost: LOCALHOST_PRESET, + sepolia: SEPOLIA_PRESET, +}; + +/** + * Get a network preset by name + * + * @param name - Network name (e.g., "localhost", "sepolia") + * @returns Network preset configuration + * @throws Error if preset not found + * + * @example + * ```typescript + * const preset = getNetworkPreset("localhost"); + * const client = await createFhevmClient({ + * provider: preset.rpcUrl, + * mockChains: preset.mockChains + * }); + * ``` + */ +export function getNetworkPreset(name: string): NetworkPreset { + const preset = NETWORK_PRESETS[name.toLowerCase()]; + if (!preset) { + throw new Error( + `Network preset "${name}" not found. Available presets: ${Object.keys(NETWORK_PRESETS).join(", ")}` + ); + } + return preset; +} + +/** + * Check if a network preset exists + * + * @param name - Network name + * @returns True if preset exists + * + * @example + * ```typescript + * if (hasNetworkPreset("localhost")) { + * const preset = getNetworkPreset("localhost"); + * } + * ``` + */ +export function hasNetworkPreset(name: string): boolean { + return name.toLowerCase() in NETWORK_PRESETS; +} + +/** + * Get all available network preset names + * + * @returns Array of network preset names + * + * @example + * ```typescript + * const networks = getAvailableNetworks(); + * console.log("Available networks:", networks); + * // Output: ["localhost", "sepolia"] + * ``` + */ +export function getAvailableNetworks(): string[] { + return Object.keys(NETWORK_PRESETS); +} + diff --git a/packages/fhevm-sdk/src/react/index.ts b/packages/fhevm-sdk/src/react/index.ts index f96544e7..7113d023 100644 --- a/packages/fhevm-sdk/src/react/index.ts +++ b/packages/fhevm-sdk/src/react/index.ts @@ -1,5 +1,10 @@ +// Original hooks (backward compatibility) export * from "./useFhevm"; export * from "./useFHEEncryption"; export * from "./useFHEDecrypt"; export * from "./useInMemoryStorage"; +// New Wagmi-style hooks (recommended) +export * from "./useFhevmInstance"; +export * from "./useFhevmEncrypt"; +export * from "./useFhevmDecrypt"; diff --git a/packages/fhevm-sdk/src/react/useFhevm.tsx b/packages/fhevm-sdk/src/react/useFhevm.tsx index 4f31bdd6..696b9ac7 100644 --- a/packages/fhevm-sdk/src/react/useFhevm.tsx +++ b/packages/fhevm-sdk/src/react/useFhevm.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useRef, useState } from "react"; import type { FhevmInstance } from "../fhevmTypes.js"; -import { createFhevmInstance } from "../internal/fhevm.js"; +import { FhevmClient, FhevmClientStatus } from "../core/FhevmClient.js"; import { ethers } from "ethers"; function _assert(condition: boolean, message?: string): asserts condition { @@ -12,6 +12,28 @@ function _assert(condition: boolean, message?: string): asserts condition { export type FhevmGoState = "idle" | "loading" | "ready" | "error"; +/** + * Map FhevmClientStatus to FhevmGoState for backward compatibility + */ +function mapClientStatusToGoState(status: FhevmClientStatus): FhevmGoState { + switch (status) { + case "idle": + return "idle"; + case "sdk-loading": + case "sdk-loaded": + case "sdk-initializing": + case "sdk-initialized": + case "creating": + return "loading"; + case "ready": + return "ready"; + case "error": + return "error"; + default: + return "idle"; + } +} + export function useFhevm(parameters: { provider: string | ethers.Eip1193Provider | undefined; chainId: number | undefined; @@ -30,18 +52,19 @@ export function useFhevm(parameters: { const [error, _setError] = useState(undefined); const [_isRunning, _setIsRunning] = useState(enabled); const [_providerChanged, _setProviderChanged] = useState(0); - const _abortControllerRef = useRef(null); + const _clientRef = useRef(null); const _providerRef = useRef(provider); const _chainIdRef = useRef(chainId); const _mockChainsRef = useRef | undefined>(initialMockChains as any); const refresh = useCallback(() => { - if (_abortControllerRef.current) { + if (_clientRef.current) { _providerRef.current = undefined; _chainIdRef.current = undefined; - _abortControllerRef.current.abort(); - _abortControllerRef.current = null; + _clientRef.current.abort(); + _clientRef.current.dispose(); + _clientRef.current = null; } _providerRef.current = provider; @@ -66,9 +89,10 @@ export function useFhevm(parameters: { useEffect(() => { if (_isRunning === false) { - if (_abortControllerRef.current) { - _abortControllerRef.current.abort(); - _abortControllerRef.current = null; + if (_clientRef.current) { + _clientRef.current.abort(); + _clientRef.current.dispose(); + _clientRef.current = null; } _setInstance(undefined); _setError(undefined); @@ -84,36 +108,35 @@ export function useFhevm(parameters: { return; } - if (!_abortControllerRef.current) { - _abortControllerRef.current = new AbortController(); - } - - _assert(!_abortControllerRef.current.signal.aborted, "!controllerRef.current.signal.aborted"); - - _setStatus("loading"); - _setError(undefined); - - const thisSignal = _abortControllerRef.current.signal; const thisProvider = _providerRef.current; const thisRpcUrlsByChainId = _mockChainsRef.current as any; - createFhevmInstance({ - signal: thisSignal, + // Create new client + const client = new FhevmClient({ provider: thisProvider as any, mockChains: thisRpcUrlsByChainId as any, - onStatusChange: s => console.log(`[useFhevm] createFhevmInstance status changed: ${s}`), - }) - .then(i => { - if (thisSignal.aborted) return; + onStatusChange: (clientStatus) => { + console.log(`[useFhevm] FhevmClient status changed: ${clientStatus}`); + _setStatus(mapClientStatusToGoState(clientStatus)); + }, + }); + + _clientRef.current = client; + _setStatus("loading"); + _setError(undefined); + + // Initialize client + client + .initialize() + .then(() => { _assert(thisProvider === _providerRef.current, "thisProvider === _providerRef.current"); - _setInstance(i); + const inst = client.getInstance(); + _setInstance(inst); _setError(undefined); _setStatus("ready"); }) - .catch(e => { - if (thisSignal.aborted) return; - + .catch((e) => { _assert(thisProvider === _providerRef.current, "thisProvider === _providerRef.current"); _setInstance(undefined); diff --git a/packages/fhevm-sdk/src/react/useFhevmDecrypt.ts b/packages/fhevm-sdk/src/react/useFhevmDecrypt.ts new file mode 100644 index 00000000..8c414716 --- /dev/null +++ b/packages/fhevm-sdk/src/react/useFhevmDecrypt.ts @@ -0,0 +1,110 @@ +import { useFhevmInstance } from "./useFhevmInstance"; +import type { Eip1193Provider } from "ethers"; + +/** + * Configuration for useFhevmDecrypt hook + */ +export interface UseFhevmDecryptConfig { + /** + * Ethereum provider (EIP-1193 compatible) or RPC URL + */ + provider: string | Eip1193Provider | undefined; + + /** + * Chain ID of the network + */ + chainId: number | undefined; + + /** + * Whether the hook is enabled (default: true) + */ + enabled?: boolean; + + /** + * Optional mock chain configurations for testing + */ + mockChains?: Readonly>; +} + +/** + * Return type for useFhevmDecrypt hook + */ +export interface UseFhevmDecryptResult { + /** + * Whether the decryption is ready to use + * + * Note: This hook provides the FHEVM instance for decryption. + * For full decryption functionality including signature management, + * use the useFHEDecrypt hook. + */ + isReady: boolean; + + /** + * Whether the FHEVM instance is loading + */ + isLoading: boolean; + + /** + * Error that occurred during initialization, if any + */ + error: Error | undefined; + + /** + * The FHEVM instance for advanced decryption operations + * + * Use this with useFHEDecrypt for full decryption functionality + */ + instance: ReturnType["instance"]; +} + +/** + * Hook for accessing FHEVM instance for decryption + * + * This is a Wagmi-style hook that provides the FHEVM instance. + * For full decryption functionality including signature management, + * use this in combination with useFHEDecrypt. + * + * @param config - Configuration options + * @returns FHEVM instance and status + * + * @example + * ```typescript + * import { useFhevmDecrypt, useFHEDecrypt } from '@fhevm-sdk/react'; + * import { useAccount, useSigner } from 'wagmi'; + * + * function DecryptComponent() { + * const { chainId } = useAccount(); + * const { data: signer } = useSigner(); + * const { instance, isReady } = useFhevmDecrypt({ + * provider: window.ethereum, + * chainId, + * }); + * + * // Use with useFHEDecrypt for full decryption + * const decryption = useFHEDecrypt({ + * instance, + * ethersSigner: signer, + * // ... other params + * }); + * + * return ( + *
+ * {isReady ? "Ready to decrypt" : "Loading..."} + *
+ * ); + * } + * ``` + */ +export function useFhevmDecrypt( + config: UseFhevmDecryptConfig +): UseFhevmDecryptResult { + const { instance, isReady, isLoading, error } = useFhevmInstance(config); + + return { + instance, + isReady, + isLoading, + error, + }; +} + diff --git a/packages/fhevm-sdk/src/react/useFhevmEncrypt.ts b/packages/fhevm-sdk/src/react/useFhevmEncrypt.ts new file mode 100644 index 00000000..c7e5ce2a --- /dev/null +++ b/packages/fhevm-sdk/src/react/useFhevmEncrypt.ts @@ -0,0 +1,158 @@ +import { useCallback, useMemo } from "react"; +import { useFhevmInstance } from "./useFhevmInstance"; +import type { Eip1193Provider } from "ethers"; +import type { RelayerEncryptedInput } from "@zama-fhe/relayer-sdk/web"; + +/** + * Configuration for useFhevmEncrypt hook + */ +export interface UseFhevmEncryptConfig { + /** + * Ethereum provider (EIP-1193 compatible) or RPC URL + */ + provider: string | Eip1193Provider | undefined; + + /** + * Chain ID of the network + */ + chainId: number | undefined; + + /** + * Whether the hook is enabled (default: true) + */ + enabled?: boolean; + + /** + * Optional mock chain configurations for testing + */ + mockChains?: Readonly>; +} + +/** + * Parameters for creating an encrypted input + */ +export interface CreateEncryptedInputParams { + /** + * The contract address that will receive the encrypted input + */ + contractAddress: string; + + /** + * The user address (typically the connected wallet address) + */ + userAddress: string; +} + +/** + * Return type for useFhevmEncrypt hook + */ +export interface UseFhevmEncryptResult { + /** + * Create an encrypted input builder + * + * @param params - Contract and user addresses + * @returns Encrypted input builder + * + * @example + * ```typescript + * const input = createEncryptedInput({ + * contractAddress: "0x1234...", + * userAddress: address + * }); + * input.add32(42); + * const encrypted = await input.encrypt(); + * ``` + */ + createEncryptedInput: ( + params: CreateEncryptedInputParams + ) => RelayerEncryptedInput | undefined; + + /** + * Whether the encryption is ready to use + */ + isReady: boolean; + + /** + * Whether the FHEVM instance is loading + */ + isLoading: boolean; + + /** + * Error that occurred during initialization, if any + */ + error: Error | undefined; +} + +/** + * Hook for encrypting data with FHEVM + * + * This is a Wagmi-style hook that provides a simple interface for + * creating encrypted inputs. + * + * @param config - Configuration options + * @returns Encryption utilities and status + * + * @example + * ```typescript + * import { useFhevmEncrypt } from '@fhevm-sdk/react'; + * import { useAccount } from 'wagmi'; + * + * function EncryptComponent() { + * const { address, chainId } = useAccount(); + * const { createEncryptedInput, isReady } = useFhevmEncrypt({ + * provider: window.ethereum, + * chainId, + * }); + * + * const handleEncrypt = async () => { + * if (!isReady || !address) return; + * + * const input = createEncryptedInput({ + * contractAddress: "0x1234...", + * userAddress: address, + * }); + * + * if (!input) return; + * + * input.add32(42); + * input.add64(1000n); + * const encrypted = await input.encrypt(); + * + * // Use encrypted.handles and encrypted.inputProof in your contract call + * }; + * + * return ( + * + * ); + * } + * ``` + */ +export function useFhevmEncrypt( + config: UseFhevmEncryptConfig +): UseFhevmEncryptResult { + const { instance, isReady, isLoading, error } = useFhevmInstance(config); + + const createEncryptedInput = useCallback( + (params: CreateEncryptedInputParams): RelayerEncryptedInput | undefined => { + if (!instance) { + return undefined; + } + + return instance.createEncryptedInput( + params.contractAddress, + params.userAddress + ) as RelayerEncryptedInput; + }, + [instance] + ); + + return { + createEncryptedInput, + isReady, + isLoading, + error, + }; +} + diff --git a/packages/fhevm-sdk/src/react/useFhevmInstance.ts b/packages/fhevm-sdk/src/react/useFhevmInstance.ts new file mode 100644 index 00000000..6f7830ba --- /dev/null +++ b/packages/fhevm-sdk/src/react/useFhevmInstance.ts @@ -0,0 +1,114 @@ +import { useMemo } from "react"; +import { useFhevm } from "./useFhevm"; +import type { FhevmInstance } from "../fhevmTypes"; +import type { Eip1193Provider } from "ethers"; + +/** + * Configuration for useFhevmInstance hook + */ +export interface UseFhevmInstanceConfig { + /** + * Ethereum provider (EIP-1193 compatible) or RPC URL + */ + provider: string | Eip1193Provider | undefined; + + /** + * Chain ID of the network + */ + chainId: number | undefined; + + /** + * Whether the hook is enabled (default: true) + */ + enabled?: boolean; + + /** + * Optional mock chain configurations for testing + * Maps chainId to RPC URL + */ + mockChains?: Readonly>; +} + +/** + * Return type for useFhevmInstance hook + */ +export interface UseFhevmInstanceResult { + /** + * The FHEVM instance, or undefined if not ready + */ + instance: FhevmInstance | undefined; + + /** + * Whether the instance is ready for use + */ + isReady: boolean; + + /** + * Whether the instance is currently loading + */ + isLoading: boolean; + + /** + * Error that occurred during initialization, if any + */ + error: Error | undefined; + + /** + * Refresh the instance (re-initialize) + */ + refresh: () => void; +} + +/** + * Hook to get an FHEVM instance + * + * This is a Wagmi-style hook that provides a simple interface for + * accessing the FHEVM instance. + * + * @param config - Configuration options + * @returns FHEVM instance and status + * + * @example + * ```typescript + * import { useFhevmInstance } from '@fhevm-sdk/react'; + * import { useAccount } from 'wagmi'; + * + * function MyComponent() { + * const { address, chainId } = useAccount(); + * const { instance, isReady, isLoading, error } = useFhevmInstance({ + * provider: window.ethereum, + * chainId, + * }); + * + * if (isLoading) return
Loading FHEVM...
; + * if (error) return
Error: {error.message}
; + * if (!isReady) return
FHEVM not ready
; + * + * return
FHEVM is ready!
; + * } + * ``` + */ +export function useFhevmInstance( + config: UseFhevmInstanceConfig +): UseFhevmInstanceResult { + const { provider, chainId, enabled = true, mockChains } = config; + + const { instance, status, error, refresh } = useFhevm({ + provider, + chainId, + enabled, + initialMockChains: mockChains, + }); + + const isReady = useMemo(() => status === "ready", [status]); + const isLoading = useMemo(() => status === "loading", [status]); + + return { + instance, + isReady, + isLoading, + error, + refresh, + }; +} + diff --git a/packages/fhevm-sdk/test/FhevmClient.test.ts b/packages/fhevm-sdk/test/FhevmClient.test.ts new file mode 100644 index 00000000..514d8df8 --- /dev/null +++ b/packages/fhevm-sdk/test/FhevmClient.test.ts @@ -0,0 +1,165 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { FhevmClient, createFhevmClient, FhevmError } from "../src/core"; + +describe("FhevmClient", () => { + describe("constructor", () => { + it("should create a client instance", () => { + const client = new FhevmClient({ + provider: "http://localhost:8545", + mockChains: { 31337: "http://localhost:8545" }, + }); + + expect(client).toBeDefined(); + expect(client).toBeInstanceOf(FhevmClient); + }); + + it("should initialize with idle status", () => { + const client = new FhevmClient({ + provider: "http://localhost:8545", + }); + + expect(client.getStatus()).toBe("idle"); + expect(client.isReady()).toBe(false); + }); + }); + + describe("getStatus", () => { + it("should return current status", () => { + const client = new FhevmClient({ + provider: "http://localhost:8545", + }); + + expect(client.getStatus()).toBe("idle"); + }); + }); + + describe("isReady", () => { + it("should return false when not initialized", () => { + const client = new FhevmClient({ + provider: "http://localhost:8545", + }); + + expect(client.isReady()).toBe(false); + }); + }); + + describe("getInstance", () => { + it("should return undefined when not initialized", () => { + const client = new FhevmClient({ + provider: "http://localhost:8545", + }); + + expect(client.getInstance()).toBeUndefined(); + }); + }); + + describe("createEncryptedInput", () => { + it("should throw error when not initialized", () => { + const client = new FhevmClient({ + provider: "http://localhost:8545", + }); + + expect(() => { + client.createEncryptedInput("0x1234567890123456789012345678901234567890", "0x1234567890123456789012345678901234567890"); + }).toThrow(FhevmError); + + try { + client.createEncryptedInput("0x1234567890123456789012345678901234567890", "0x1234567890123456789012345678901234567890"); + } catch (error) { + expect(error).toBeInstanceOf(FhevmError); + expect((error as FhevmError).code).toBe("NOT_INITIALIZED"); + } + }); + }); + + describe("abort", () => { + it("should not throw when called", () => { + const client = new FhevmClient({ + provider: "http://localhost:8545", + }); + + expect(() => client.abort()).not.toThrow(); + }); + }); + + describe("dispose", () => { + it("should reset client to idle state", () => { + const client = new FhevmClient({ + provider: "http://localhost:8545", + }); + + client.dispose(); + + expect(client.getStatus()).toBe("idle"); + expect(client.getInstance()).toBeUndefined(); + expect(client.isReady()).toBe(false); + }); + }); + + describe("FhevmError", () => { + it("should create error with code and message", () => { + const error = new FhevmError("TEST_ERROR", "Test error message"); + + expect(error).toBeInstanceOf(Error); + expect(error).toBeInstanceOf(FhevmError); + expect(error.code).toBe("TEST_ERROR"); + expect(error.message).toBe("Test error message"); + expect(error.name).toBe("FhevmError"); + }); + + it("should create error with suggestion", () => { + const error = new FhevmError( + "TEST_ERROR", + "Test error message", + "Try this solution" + ); + + expect(error.suggestion).toBe("Try this solution"); + }); + + it("should format error message with suggestion", () => { + const error = new FhevmError( + "TEST_ERROR", + "Test error message", + "Try this solution" + ); + + const formatted = error.toString(); + expect(formatted).toContain("FhevmError"); + expect(formatted).toContain("TEST_ERROR"); + expect(formatted).toContain("Test error message"); + expect(formatted).toContain("šŸ’” Suggestion"); + expect(formatted).toContain("Try this solution"); + }); + + it("should format error message without suggestion", () => { + const error = new FhevmError("TEST_ERROR", "Test error message"); + + const formatted = error.toString(); + expect(formatted).toContain("FhevmError"); + expect(formatted).toContain("TEST_ERROR"); + expect(formatted).toContain("Test error message"); + expect(formatted).not.toContain("šŸ’” Suggestion"); + }); + }); +}); + +describe("createFhevmClient", () => { + it("should be a function", () => { + expect(typeof createFhevmClient).toBe("function"); + }); + + it("should return a promise", () => { + const mockProvider = "http://localhost:8545"; + const result = createFhevmClient({ + provider: mockProvider, + mockChains: { 31337: "http://localhost:8545" }, + }); + + expect(result).toBeInstanceOf(Promise); + + // Clean up - abort the initialization + result.then(client => client.abort()).catch(() => {}); + }); +}); + diff --git a/packages/fhevm-sdk/test/errors.test.ts b/packages/fhevm-sdk/test/errors.test.ts new file mode 100644 index 00000000..038886ff --- /dev/null +++ b/packages/fhevm-sdk/test/errors.test.ts @@ -0,0 +1,162 @@ +import { describe, it, expect } from "vitest"; +import { + FhevmError, + ErrorCodes, + createError, + isFhevmError, + isErrorCode, + formatError, +} from "../src/core/errors"; + +describe("Error Utilities", () => { + describe("FhevmError", () => { + it("should create error with code and message", () => { + const error = new FhevmError("TEST_CODE", "Test message"); + expect(error.code).toBe("TEST_CODE"); + expect(error.message).toBe("Test message"); + expect(error.name).toBe("FhevmError"); + }); + + it("should create error with suggestion", () => { + const error = new FhevmError( + "TEST_CODE", + "Test message", + "Try this solution" + ); + expect(error.suggestion).toBe("Try this solution"); + }); + + it("should format error with toString", () => { + const error = new FhevmError("TEST_CODE", "Test message"); + const str = error.toString(); + expect(str).toContain("FhevmError"); + expect(str).toContain("TEST_CODE"); + expect(str).toContain("Test message"); + }); + + it("should include suggestion in toString", () => { + const error = new FhevmError( + "TEST_CODE", + "Test message", + "Try this solution" + ); + const str = error.toString(); + expect(str).toContain("šŸ’” Suggestion: Try this solution"); + }); + }); + + describe("ErrorCodes", () => { + it("should have NOT_INITIALIZED code", () => { + expect(ErrorCodes.NOT_INITIALIZED).toBe("NOT_INITIALIZED"); + }); + + it("should have INITIALIZATION_FAILED code", () => { + expect(ErrorCodes.INITIALIZATION_FAILED).toBe("INITIALIZATION_FAILED"); + }); + + it("should have INVALID_PROVIDER code", () => { + expect(ErrorCodes.INVALID_PROVIDER).toBe("INVALID_PROVIDER"); + }); + + it("should have ENCRYPTION_FAILED code", () => { + expect(ErrorCodes.ENCRYPTION_FAILED).toBe("ENCRYPTION_FAILED"); + }); + }); + + describe("createError", () => { + it("should create error with automatic suggestion", () => { + const error = createError( + ErrorCodes.NOT_INITIALIZED, + "Client not initialized" + ); + expect(error.code).toBe(ErrorCodes.NOT_INITIALIZED); + expect(error.message).toBe("Client not initialized"); + expect(error.suggestion).toBeDefined(); + expect(error.suggestion).toContain("initialize"); + }); + + it("should use custom suggestion when provided", () => { + const error = createError( + ErrorCodes.NOT_INITIALIZED, + "Client not initialized", + "Custom suggestion" + ); + expect(error.suggestion).toBe("Custom suggestion"); + }); + + it("should include cause when provided", () => { + const cause = new Error("Original error"); + const error = createError( + ErrorCodes.NETWORK_ERROR, + "Network failed", + undefined, + cause + ); + expect(error.cause).toBe(cause); + }); + }); + + describe("isFhevmError", () => { + it("should return true for FhevmError", () => { + const error = new FhevmError("TEST", "Test"); + expect(isFhevmError(error)).toBe(true); + }); + + it("should return false for regular Error", () => { + const error = new Error("Test"); + expect(isFhevmError(error)).toBe(false); + }); + + it("should return false for non-error values", () => { + expect(isFhevmError("string")).toBe(false); + expect(isFhevmError(123)).toBe(false); + expect(isFhevmError(null)).toBe(false); + expect(isFhevmError(undefined)).toBe(false); + }); + }); + + describe("isErrorCode", () => { + it("should return true for matching error code", () => { + const error = new FhevmError(ErrorCodes.NOT_INITIALIZED, "Test"); + expect(isErrorCode(error, ErrorCodes.NOT_INITIALIZED)).toBe(true); + }); + + it("should return false for non-matching error code", () => { + const error = new FhevmError(ErrorCodes.NOT_INITIALIZED, "Test"); + expect(isErrorCode(error, ErrorCodes.NETWORK_ERROR)).toBe(false); + }); + + it("should return false for regular Error", () => { + const error = new Error("Test"); + expect(isErrorCode(error, ErrorCodes.NOT_INITIALIZED)).toBe(false); + }); + + it("should return false for non-error values", () => { + expect(isErrorCode("string", ErrorCodes.NOT_INITIALIZED)).toBe(false); + }); + }); + + describe("formatError", () => { + it("should format FhevmError with toString", () => { + const error = new FhevmError("TEST", "Test message", "Suggestion"); + const formatted = formatError(error); + expect(formatted).toContain("FhevmError"); + expect(formatted).toContain("TEST"); + expect(formatted).toContain("Test message"); + expect(formatted).toContain("šŸ’” Suggestion"); + }); + + it("should format regular Error", () => { + const error = new Error("Test message"); + const formatted = formatError(error); + expect(formatted).toContain("Error"); + expect(formatted).toContain("Test message"); + }); + + it("should format non-error values", () => { + expect(formatError("string error")).toBe("string error"); + expect(formatError(123)).toBe("123"); + }); + }); +}); + diff --git a/packages/fhevm-sdk/test/presets.test.ts b/packages/fhevm-sdk/test/presets.test.ts new file mode 100644 index 00000000..4535164f --- /dev/null +++ b/packages/fhevm-sdk/test/presets.test.ts @@ -0,0 +1,98 @@ +import { describe, it, expect } from "vitest"; +import { + getNetworkPreset, + hasNetworkPreset, + getAvailableNetworks, + LOCALHOST_PRESET, + SEPOLIA_PRESET, +} from "../src/core/presets"; + +describe("Network Presets", () => { + describe("LOCALHOST_PRESET", () => { + it("should have correct configuration", () => { + expect(LOCALHOST_PRESET.name).toBe("localhost"); + expect(LOCALHOST_PRESET.chainId).toBe(31337); + expect(LOCALHOST_PRESET.rpcUrl).toBe("http://localhost:8545"); + expect(LOCALHOST_PRESET.mockChains).toEqual({ + 31337: "http://localhost:8545", + }); + }); + }); + + describe("SEPOLIA_PRESET", () => { + it("should have correct configuration", () => { + expect(SEPOLIA_PRESET.name).toBe("sepolia"); + expect(SEPOLIA_PRESET.chainId).toBe(11155111); + expect(SEPOLIA_PRESET.rpcUrl).toContain("sepolia"); + }); + }); + + describe("getNetworkPreset", () => { + it("should return localhost preset", () => { + const preset = getNetworkPreset("localhost"); + expect(preset).toEqual(LOCALHOST_PRESET); + }); + + it("should return sepolia preset", () => { + const preset = getNetworkPreset("sepolia"); + expect(preset).toEqual(SEPOLIA_PRESET); + }); + + it("should be case insensitive", () => { + const preset1 = getNetworkPreset("LOCALHOST"); + const preset2 = getNetworkPreset("Localhost"); + const preset3 = getNetworkPreset("localhost"); + + expect(preset1).toEqual(LOCALHOST_PRESET); + expect(preset2).toEqual(LOCALHOST_PRESET); + expect(preset3).toEqual(LOCALHOST_PRESET); + }); + + it("should throw error for unknown preset", () => { + expect(() => getNetworkPreset("unknown")).toThrow(); + expect(() => getNetworkPreset("unknown")).toThrow("not found"); + }); + + it("should list available presets in error message", () => { + try { + getNetworkPreset("unknown"); + } catch (error) { + expect((error as Error).message).toContain("localhost"); + expect((error as Error).message).toContain("sepolia"); + } + }); + }); + + describe("hasNetworkPreset", () => { + it("should return true for existing presets", () => { + expect(hasNetworkPreset("localhost")).toBe(true); + expect(hasNetworkPreset("sepolia")).toBe(true); + }); + + it("should return false for non-existing presets", () => { + expect(hasNetworkPreset("unknown")).toBe(false); + expect(hasNetworkPreset("mainnet")).toBe(false); + }); + + it("should be case insensitive", () => { + expect(hasNetworkPreset("LOCALHOST")).toBe(true); + expect(hasNetworkPreset("Localhost")).toBe(true); + expect(hasNetworkPreset("SEPOLIA")).toBe(true); + }); + }); + + describe("getAvailableNetworks", () => { + it("should return array of network names", () => { + const networks = getAvailableNetworks(); + expect(Array.isArray(networks)).toBe(true); + expect(networks.length).toBeGreaterThan(0); + }); + + it("should include localhost and sepolia", () => { + const networks = getAvailableNetworks(); + expect(networks).toContain("localhost"); + expect(networks).toContain("sepolia"); + }); + }); +}); + diff --git a/packages/nextjs/app/_components/SimplifiedFHECounterDemo.tsx b/packages/nextjs/app/_components/SimplifiedFHECounterDemo.tsx new file mode 100644 index 00000000..c8ec4efb --- /dev/null +++ b/packages/nextjs/app/_components/SimplifiedFHECounterDemo.tsx @@ -0,0 +1,229 @@ +"use client"; + +import { useMemo } from "react"; +import { useFhevmInstance, useFhevmEncrypt } from "@fhevm-sdk/react"; +import { useAccount } from "wagmi"; +import { RainbowKitCustomConnectButton } from "~~/components/helper/RainbowKitCustomConnectButton"; +import { useFHECounterWagmi } from "~~/hooks/fhecounter-example/useFHECounterWagmi"; + +/** + * Simplified FHECounter Demo using new Wagmi-style hooks + * + * This demonstrates the new simplified API: + * - useFhevmInstance: Get FHEVM instance with simple status checks + * - useFhevmEncrypt: Simplified encryption interface + * + * Compare this with FHECounterDemo.tsx to see the improvements! + */ +export const SimplifiedFHECounterDemo = () => { + const { isConnected, chain, address } = useAccount(); + + const chainId = chain?.id; + + ////////////////////////////////////////////////////////////////////////////// + // FHEVM instance - New simplified API + ////////////////////////////////////////////////////////////////////////////// + + // Create EIP-1193 provider from wagmi for FHEVM + const provider = useMemo(() => { + if (typeof window === "undefined") return undefined; + return (window as any).ethereum; + }, []); + + // Use the new useFhevmInstance hook - cleaner API! + const { instance, isReady, isLoading, error } = useFhevmInstance({ + provider, + chainId, + mockChains: { 31337: "http://localhost:8545" }, + enabled: true, + }); + + // Optional: Use useFhevmEncrypt for encryption-specific operations + const { createEncryptedInput } = useFhevmEncrypt({ + provider, + chainId, + mockChains: { 31337: "http://localhost:8545" }, + }); + + ////////////////////////////////////////////////////////////////////////////// + // FHECounter logic + ////////////////////////////////////////////////////////////////////////////// + + const fheCounter = useFHECounterWagmi({ + instance, + initialMockChains: { 31337: "http://localhost:8545" }, + }); + + ////////////////////////////////////////////////////////////////////////////// + // UI Styles + ////////////////////////////////////////////////////////////////////////////// + + const buttonClass = + "inline-flex items-center justify-center px-6 py-3 font-semibold shadow-lg " + + "transition-all duration-200 hover:scale-105 " + + "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-900 " + + "disabled:opacity-50 disabled:pointer-events-none disabled:cursor-not-allowed"; + + const primaryButtonClass = + buttonClass + + " bg-[#FFD208] text-[#2D2D2D] hover:bg-[#A38025] focus-visible:ring-[#2D2D2D] cursor-pointer"; + + const secondaryButtonClass = + buttonClass + + " bg-black text-[#F4F4F4] hover:bg-[#1F1F1F] focus-visible:ring-[#FFD208] cursor-pointer"; + + const titleClass = "font-bold text-gray-900 text-xl mb-4 border-b-1 border-gray-700 pb-2"; + const sectionClass = "bg-[#f4f4f4] shadow-lg p-6 mb-6 text-gray-900"; + + ////////////////////////////////////////////////////////////////////////////// + // UI Rendering + ////////////////////////////////////////////////////////////////////////////// + + if (!isConnected) { + return ( +
+
+
+
+ + āš ļø + +
+

Wallet Not Connected

+

Please connect your wallet to use the FHE Counter

+ +
+
+
+ ); + } + + return ( +
+ {/* Header */} +
+

+ šŸ” Simplified FHE Counter Demo +

+

+ Using the new Wagmi-style FHEVM SDK hooks +

+
+ + {/* FHEVM Status - New simplified status checks */} +
+

FHEVM Status (New API)

+
+
+
Loading
+
+ {isLoading ? "šŸ”„ Yes" : "āœ… No"} +
+
+
+
Ready
+
+ {isReady ? "āœ… Yes" : "ā³ No"} +
+
+
+
Error
+
+ {error ? `āŒ ${error.message}` : "āœ… None"} +
+
+
+
+ + {/* Counter Value */} +
+

Counter Value

+
+
+ {fheCounter.decryptedValue !== undefined ? fheCounter.decryptedValue.toString() : "?"} +
+
+ {fheCounter.isDecrypting ? "Decrypting..." : "Current Count"} +
+
+
+ + {/* Actions */} +
+

Actions

+
+ {/* Decrypt Button */} + + + {/* Increment Button */} + + + {/* Decrement Button */} + +
+
+ + {/* Debug Info */} +
+

Debug Info

+
+
+ Chain ID:{" "} + {chainId || "N/A"} +
+
+ Address:{" "} + {address || "N/A"} +
+
+ FHEVM Ready:{" "} + {isReady ? "Yes" : "No"} +
+
+ Encrypted Input Available:{" "} + + {createEncryptedInput && address ? "Yes" : "No"} + +
+
+
+ + {/* API Comparison Note */} +
+

šŸ’” New API Benefits

+
    +
  • + useFhevmInstance - Cleaner status + checks with isReady and{" "} + isLoading +
  • +
  • + useFhevmEncrypt - Simplified + encryption interface +
  • +
  • Wagmi-style API for familiar developer experience
  • +
  • Better TypeScript support and error handling
  • +
+
+
+ ); +}; +