diff --git a/packages/fhevm-sdk/package.json b/packages/fhevm-sdk/package.json index 47445861..62471f64 100644 --- a/packages/fhevm-sdk/package.json +++ b/packages/fhevm-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@fhevm-sdk", - "version": "0.1.0", + "version": "0.2.0", "private": true, "type": "module", "main": "dist/index.js", @@ -15,18 +15,63 @@ "types": "./src/core/index.ts", "default": "./dist/core/index.js" }, + "./core/config": { + "types": "./src/core/config.ts", + "default": "./dist/core/config.js" + }, + "./core/client": { + "types": "./src/core/FhevmClient.ts", + "default": "./dist/core/FhevmClient.js" + }, "./storage": { "types": "./src/storage/index.ts", "default": "./dist/storage/index.js" }, "./types": { - "types": "./src/fhevmTypes.ts", - "default": "./dist/fhevmTypes.js" + "types": "./src/types/index.ts", + "default": "./dist/types/index.js" + }, + "./utils": { + "types": "./src/utils/index.ts", + "default": "./dist/utils/index.js" + }, + "./utils/errors": { + "types": "./src/utils/errors.ts", + "default": "./dist/utils/errors.js" + }, + "./utils/encryption": { + "types": "./src/utils/encryption.ts", + "default": "./dist/utils/encryption.js" + }, + "./utils/decryption": { + "types": "./src/utils/decryption.ts", + "default": "./dist/utils/decryption.js" }, "./react": { "types": "./src/react/index.ts", "default": "./dist/react/index.js" - } + }, + "./react/useFhevm": { + "types": "./src/react/useFhevm.tsx", + "default": "./dist/react/useFhevm.js" + }, + "./react/useFHEEncryption": { + "types": "./src/react/useFHEEncryption.ts", + "default": "./dist/react/useFHEEncryption.js" + }, + "./react/useFHEDecrypt": { + "types": "./src/react/useFHEDecrypt.ts", + "default": "./dist/react/useFHEDecrypt.js" + }, + "./version": { + "types": "./src/version.ts", + "default": "./dist/version.js" + }, + "./constants": { + "types": "./src/internal/constants.ts", + "default": "./dist/internal/constants.js" + }, + "./package.json": "./package.json" }, "scripts": { "build": "tsc -p tsconfig.json", diff --git a/packages/fhevm-sdk/src/FhevmDecryptionSignature.ts b/packages/fhevm-sdk/src/FhevmDecryptionSignature.ts index 24575014..9f18941c 100644 --- a/packages/fhevm-sdk/src/FhevmDecryptionSignature.ts +++ b/packages/fhevm-sdk/src/FhevmDecryptionSignature.ts @@ -1,5 +1,7 @@ import { GenericStringStorage } from "./storage/GenericStringStorage"; import { EIP712Type, FhevmDecryptionSignatureType, FhevmInstance } from "./fhevmTypes"; +import { DEFAULT_SIGNATURE_DURATION_DAYS } from "./internal/constants"; +import { FhevmSignatureError } from "./utils/errors"; import { ethers } from "ethers"; function _timestampNow(): number { @@ -236,7 +238,7 @@ export class FhevmDecryptionSignature { try { const userAddress = (await signer.getAddress()) as `0x${string}`; const startTimestamp = _timestampNow(); - const durationDays = 365; + const durationDays = DEFAULT_SIGNATURE_DURATION_DAYS; const eip712 = (instance as any).createEIP712(publicKey, contractAddresses, startTimestamp, durationDays); const signature = await (signer as any).signTypedData( eip712.domain, diff --git a/packages/fhevm-sdk/src/core/FhevmClient.ts b/packages/fhevm-sdk/src/core/FhevmClient.ts new file mode 100644 index 00000000..202ef0b6 --- /dev/null +++ b/packages/fhevm-sdk/src/core/FhevmClient.ts @@ -0,0 +1,365 @@ +/** + * Framework-agnostic FHEVM client with performance optimizations + */ + +import { FhevmInstance } from "../fhevmTypes"; +import { createFhevmInstance } from "../internal/fhevm"; +import { FhevmStatus } from "../types/index"; +import { validateFhevmConfig } from "./config"; +import { FhevmInitializationError } from "../utils/errors"; + +export interface FhevmClientOptions { + provider: any; + chainId?: number; + mockChains?: Record; + onStatusChange?: (status: FhevmStatus) => void; + /** + * Enable performance monitoring + * @default false + */ + enablePerformanceMonitoring?: boolean; + /** + * Cache timeout in milliseconds + * Set to 0 to disable caching + * @default 300000 (5 minutes) + */ + cacheTimeout?: number; +} + +/** + * Performance metrics for FHEVM operations + */ +export interface PerformanceMetrics { + initializationTime?: number; + lastInitialization?: Date; + totalInitializations: number; + cacheHits: number; + cacheMisses: number; +} + +/** + * Core FHEVM client (framework-agnostic) + * + * This class provides the core FHEVM functionality without any framework dependencies. + * It can be used directly in Node.js, browser, or wrapped by framework-specific adapters. + * + * @example + * ```typescript + * // Create client + * const client = new FhevmClient({ + * provider: window.ethereum, + * chainId: 31337, + * mockChains: { 31337: "http://localhost:8545" } + * }); + * + * // Initialize + * const instance = await client.initialize(); + * + * // Use instance for encryption/decryption + * console.log('Status:', client.getStatus()); + * + * // Cleanup + * client.dispose(); + * ``` + */ +export class FhevmClient { + private instance?: FhevmInstance; + private status: FhevmStatus = "idle"; + private error?: Error; + private abortController?: AbortController; + private options: FhevmClientOptions; + + // Performance optimization fields + private initializationPromise?: Promise; + private cacheTimestamp?: number; + private metrics: PerformanceMetrics = { + totalInitializations: 0, + cacheHits: 0, + cacheMisses: 0, + }; + + /** + * Create a new FHEVM client + * + * @param options - Client configuration options + * @throws {FhevmConfigurationError} If configuration is invalid + */ + constructor(options: FhevmClientOptions) { + // Validate configuration on construction + validateFhevmConfig({ + provider: options.provider, + chainId: options.chainId, + mockChains: options.mockChains, + }); + + // Set default options + this.options = { + enablePerformanceMonitoring: false, + cacheTimeout: 300000, // 5 minutes + ...options, + }; + } + + /** + * Initialize FHEVM instance with caching and deduplication + * + * This method creates and initializes the FHEVM instance. It should be called + * once before using any encryption or decryption features. + * + * Features: + * - Automatic caching (configurable timeout) + * - Request deduplication (prevents concurrent initializations) + * - Performance monitoring (optional) + * + * @returns Promise resolving to the initialized FHEVM instance + * @throws {FhevmInitializationError} If initialization fails + * + * @example + * ```typescript + * try { + * const instance = await client.initialize(); + * console.log('Initialized successfully'); + * } catch (error) { + * console.error('Initialization failed:', error); + * } + * ``` + */ + async initialize(): Promise { + // 1. Check cache validity + if (this.instance && this.status === "ready" && this.isCacheValid()) { + if (this.options.enablePerformanceMonitoring) { + this.metrics.cacheHits++; + console.log("[FhevmClient] Cache hit, returning cached instance"); + } + return this.instance; + } + + // 2. Deduplicate concurrent requests + if (this.initializationPromise) { + if (this.options.enablePerformanceMonitoring) { + console.log("[FhevmClient] Initialization in progress, waiting..."); + } + return this.initializationPromise; + } + + // 3. Start new initialization + this.metrics.cacheMisses++; + this.initializationPromise = this.performInitialization(); + + try { + const instance = await this.initializationPromise; + return instance; + } finally { + this.initializationPromise = undefined; + } + } + + /** + * Perform the actual initialization + * @private + */ + private async performInitialization(): Promise { + const startTime = this.options.enablePerformanceMonitoring ? performance.now() : 0; + + // Cleanup any previous initialization attempt + if (this.abortController) { + this.abortController.abort(); + } + + this.setStatus("loading"); + this.abortController = new AbortController(); + this.error = undefined; + + try { + const instance = await createFhevmInstance({ + signal: this.abortController.signal, + provider: this.options.provider, + mockChains: this.options.mockChains, + onStatusChange: (s) => { + if (this.options.enablePerformanceMonitoring) { + console.log(`[FhevmClient] Status: ${s}`); + } + }, + }); + + // Check if aborted during initialization + if (this.abortController.signal.aborted) { + throw new FhevmInitializationError("Initialization was aborted"); + } + + this.instance = instance; + this.cacheTimestamp = Date.now(); + this.metrics.totalInitializations++; + this.setStatus("ready"); + + // Record performance metrics + if (this.options.enablePerformanceMonitoring) { + const endTime = performance.now(); + this.metrics.initializationTime = endTime - startTime; + this.metrics.lastInitialization = new Date(); + console.log( + `[FhevmClient] Initialization completed in ${this.metrics.initializationTime.toFixed(2)}ms` + ); + } + + return instance; + } catch (error) { + const wrappedError = new FhevmInitializationError( + "Failed to initialize FHEVM instance", + error as Error + ); + this.error = wrappedError; + this.setStatus("error"); + throw wrappedError; + } + } + + /** + * Check if cached instance is still valid + * @private + */ + private isCacheValid(): boolean { + if (!this.cacheTimestamp || !this.options.cacheTimeout) { + return false; + } + + const elapsed = Date.now() - this.cacheTimestamp; + return elapsed < this.options.cacheTimeout; + } + + /** + * Get current FHEVM instance + * + * Returns the instance if initialized, undefined otherwise. + * Use `initialize()` first to create the instance. + * + * @returns The FHEVM instance or undefined + */ + getInstance(): FhevmInstance | undefined { + return this.instance; + } + + /** + * Get current client status + * + * @returns Current status: "idle" | "loading" | "ready" | "error" + */ + getStatus(): FhevmStatus { + return this.status; + } + + /** + * Get error if any + * + * @returns Error instance or undefined + */ + getError(): Error | undefined { + return this.error; + } + + /** + * Check if client is ready to use + * + * @returns True if instance is initialized and ready + */ + isReady(): boolean { + return this.status === "ready" && this.instance !== undefined; + } + + /** + * Dispose and cleanup resources + * + * Call this method when you're done using the client to clean up resources. + * After disposal, you need to call `initialize()` again to use the client. + * + * @example + * ```typescript + * // When component unmounts or app closes + * client.dispose(); + * ``` + */ + dispose(): void { + if (this.abortController) { + this.abortController.abort(); + this.abortController = undefined; + } + this.instance = undefined; + this.error = undefined; + this.cacheTimestamp = undefined; + this.initializationPromise = undefined; + this.setStatus("idle"); + } + + /** + * Clear cached instance and force re-initialization on next call + * + * @example + * ```typescript + * client.clearCache(); + * const instance = await client.initialize(); // Will create new instance + * ``` + */ + clearCache(): void { + this.cacheTimestamp = undefined; + if (this.options.enablePerformanceMonitoring) { + console.log("[FhevmClient] Cache cleared"); + } + } + + /** + * Get performance metrics + * + * @returns Current performance metrics + * + * @example + * ```typescript + * const metrics = client.getMetrics(); + * console.log(`Cache hit rate: ${metrics.cacheHits / (metrics.cacheHits + metrics.cacheMisses)}`); + * ``` + */ + getMetrics(): Readonly { + return { ...this.metrics }; + } + + /** + * Reset performance metrics + */ + resetMetrics(): void { + this.metrics = { + totalInitializations: 0, + cacheHits: 0, + cacheMisses: 0, + }; + } + + /** + * Update status and notify listeners + */ + private setStatus(status: FhevmStatus): void { + this.status = status; + this.options.onStatusChange?.(status); + } + + /** + * Re-initialize the client + * + * This is a convenience method that disposes the current instance + * and initializes a new one. + * + * @returns Promise resolving to the new FHEVM instance + */ + async reinitialize(): Promise { + this.dispose(); + return this.initialize(); + } + + /** + * Get client configuration + * + * @returns Current client options + */ + getOptions(): Readonly { + return { ...this.options }; + } +} + diff --git a/packages/fhevm-sdk/src/core/config.ts b/packages/fhevm-sdk/src/core/config.ts new file mode 100644 index 00000000..3af58d9f --- /dev/null +++ b/packages/fhevm-sdk/src/core/config.ts @@ -0,0 +1,135 @@ +/** + * Configuration validation and utilities + */ + +import { FhevmConfig } from "../types/index"; +import { FhevmConfigurationError } from "../utils/errors"; + +/** + * Validate FHEVM configuration + * + * @param config - Configuration to validate + * @throws {FhevmConfigurationError} If configuration is invalid + * + * @example + * ```typescript + * try { + * validateFhevmConfig({ provider: window.ethereum, chainId: 31337 }); + * } catch (error) { + * console.error('Invalid config:', error.message); + * } + * ``` + */ +export function validateFhevmConfig(config: Partial): void { + if (!config.provider) { + throw new FhevmConfigurationError("Provider is required"); + } + + if (config.chainId !== undefined) { + if (typeof config.chainId !== "number") { + throw new FhevmConfigurationError( + `Invalid chainId type: expected number, got ${typeof config.chainId}` + ); + } + + if (config.chainId < 0) { + throw new FhevmConfigurationError( + `Invalid chainId: ${config.chainId} (must be non-negative)` + ); + } + } + + if (config.mockChains !== undefined) { + if (typeof config.mockChains !== "object" || config.mockChains === null) { + throw new FhevmConfigurationError( + "Invalid mockChains: must be an object" + ); + } + + // Validate each mock chain entry + for (const [chainId, rpcUrl] of Object.entries(config.mockChains)) { + const numChainId = Number(chainId); + if (isNaN(numChainId) || numChainId < 0) { + throw new FhevmConfigurationError( + `Invalid mock chain ID: ${chainId} (must be a non-negative number)` + ); + } + + if (typeof rpcUrl !== "string" || rpcUrl.length === 0) { + throw new FhevmConfigurationError( + `Invalid RPC URL for chain ${chainId}: must be a non-empty string` + ); + } + } + } +} + +/** + * Merge configuration with defaults + * + * @param config - Partial configuration + * @returns Complete configuration with defaults applied + * + * @example + * ```typescript + * const config = mergeFhevmConfig({ + * provider: window.ethereum, + * chainId: 31337 + * }); + * // Returns: { provider, chainId: 31337, enabled: true, mockChains: undefined } + * ``` + */ +export function mergeFhevmConfig( + config: Partial +): FhevmConfig { + return { + provider: config.provider!, + chainId: config.chainId, + enabled: config.enabled ?? true, + mockChains: config.mockChains, + }; +} + +/** + * Check if configuration has changed + * + * @param oldConfig - Previous configuration + * @param newConfig - New configuration + * @returns True if configuration has changed + */ +export function hasConfigChanged( + oldConfig: Partial | undefined, + newConfig: Partial +): boolean { + if (!oldConfig) return true; + + // Check provider (by reference) + if (oldConfig.provider !== newConfig.provider) return true; + + // Check chainId + if (oldConfig.chainId !== newConfig.chainId) return true; + + // Check enabled + if (oldConfig.enabled !== newConfig.enabled) return true; + + // Check mockChains (shallow comparison) + const oldChains = oldConfig.mockChains; + const newChains = newConfig.mockChains; + + if (oldChains === newChains) return false; + if (!oldChains || !newChains) return true; + + const oldKeys = Object.keys(oldChains); + const newKeys = Object.keys(newChains); + + if (oldKeys.length !== newKeys.length) return true; + + for (const key of oldKeys) { + if (oldChains[Number(key)] !== newChains[Number(key)]) { + return true; + } + } + + return false; +} + diff --git a/packages/fhevm-sdk/src/core/index.ts b/packages/fhevm-sdk/src/core/index.ts index 6de1bba3..cee61ae8 100644 --- a/packages/fhevm-sdk/src/core/index.ts +++ b/packages/fhevm-sdk/src/core/index.ts @@ -4,3 +4,9 @@ export * from "../internal/PublicKeyStorage"; export * from "../internal/fhevmTypes"; export * from "../internal/constants"; +// Configuration utilities +export * from "./config"; + +// Core FHEVM client +export * from "./FhevmClient"; + diff --git a/packages/fhevm-sdk/src/index.ts b/packages/fhevm-sdk/src/index.ts index 7741fe87..13927820 100644 --- a/packages/fhevm-sdk/src/index.ts +++ b/packages/fhevm-sdk/src/index.ts @@ -1,6 +1,18 @@ export * from "./core/index"; export * from "./storage/index"; -export * from "./fhevmTypes"; + +// Type definitions (centralized) +export * from "./types/index"; + +// Configuration constants +export * from "./internal/constants"; + +// Utility functions (errors, encryption, decryption) +export * from "./utils/index"; + +// SDK version and metadata +export * from "./version"; + export * from "./FhevmDecryptionSignature"; export * from "./react/index"; diff --git a/packages/fhevm-sdk/src/internal/constants.ts b/packages/fhevm-sdk/src/internal/constants.ts index cd1442c0..fb5948e3 100644 --- a/packages/fhevm-sdk/src/internal/constants.ts +++ b/packages/fhevm-sdk/src/internal/constants.ts @@ -1,2 +1,49 @@ +/** + * FHEVM SDK Configuration Constants + */ + +/** + * Relayer SDK CDN URL + */ export const SDK_CDN_URL = "https://cdn.zama.ai/relayer-sdk-js/0.2.0/relayer-sdk-js.umd.cjs"; + +/** + * Default decryption signature duration (365 days / 1 year) + */ +export const DEFAULT_SIGNATURE_DURATION_DAYS = 365; + +/** + * FHEVM instance initialization timeout (30 seconds) + */ +export const FHEVM_INIT_TIMEOUT_MS = 30000; + +/** + * IndexedDB database name for FHEVM storage + */ +export const FHEVM_STORAGE_DB_NAME = "fhevm-sdk-storage"; + +/** + * Storage key prefix for decryption signatures + */ +export const DECRYPTION_SIGNATURE_STORE_KEY = "fhevm-decryption-signatures"; + +/** + * Default polling interval for status checks (ms) + */ +export const DEFAULT_POLLING_INTERVAL_MS = 1000; + +/** + * Default local chain ID (Hardhat) + */ +export const DEFAULT_LOCAL_CHAIN_ID = 31337; + +/** + * Default local RPC URL + */ +export const DEFAULT_LOCAL_RPC_URL = "http://localhost:8545"; + +/** + * Gateway chain ID for mock FHEVM + */ +export const MOCK_GATEWAY_CHAIN_ID = 55815; diff --git a/packages/fhevm-sdk/src/internal/fhevm.ts b/packages/fhevm-sdk/src/internal/fhevm.ts index 5c8da7a6..5d20cd6e 100644 --- a/packages/fhevm-sdk/src/internal/fhevm.ts +++ b/packages/fhevm-sdk/src/internal/fhevm.ts @@ -8,6 +8,7 @@ import type { import { isFhevmWindowType, RelayerSDKLoader } from "./RelayerSDKLoader"; import { publicKeyStorageGet, publicKeyStorageSet } from "./PublicKeyStorage"; import { FhevmInstance, FhevmInstanceConfig } from "../fhevmTypes"; +import { DEFAULT_LOCAL_CHAIN_ID, DEFAULT_LOCAL_RPC_URL } from "./constants"; export class FhevmReactError extends Error { code: string; @@ -189,7 +190,7 @@ async function resolve( let rpcUrl = typeof providerOrUrl === "string" ? providerOrUrl : undefined; const _mockChains: Record = { - 31337: "http://localhost:8545", + [DEFAULT_LOCAL_CHAIN_ID]: DEFAULT_LOCAL_RPC_URL, ...(mockChains ?? {}), }; diff --git a/packages/fhevm-sdk/src/internal/mock/fhevmMock.ts b/packages/fhevm-sdk/src/internal/mock/fhevmMock.ts index 75dd0b3a..c4865643 100644 --- a/packages/fhevm-sdk/src/internal/mock/fhevmMock.ts +++ b/packages/fhevm-sdk/src/internal/mock/fhevmMock.ts @@ -9,6 +9,7 @@ import { JsonRpcProvider } from "ethers"; import { MockFhevmInstance } from "@fhevm/mock-utils"; import { FhevmInstance } from "../../fhevmTypes"; +import { MOCK_GATEWAY_CHAIN_ID } from "../constants"; export const fhevmMockCreateInstance = async (parameters: { rpcUrl: string; @@ -24,7 +25,7 @@ export const fhevmMockCreateInstance = async (parameters: { //aclContractAddress: "0x50157CFfD6bBFA2DECe204a89ec419c23ef5755D", aclContractAddress: parameters.metadata.ACLAddress, chainId: parameters.chainId, - gatewayChainId: 55815, + gatewayChainId: MOCK_GATEWAY_CHAIN_ID, // inputVerifierContractAddress: "0x901F8942346f7AB3a01F6D7613119Bca447Bb030", // kmsContractAddress: "0x1364cBBf2cDF5032C47d8226a6f6FBD2AFCDacAC", inputVerifierContractAddress: parameters.metadata.InputVerifierAddress, diff --git a/packages/fhevm-sdk/src/react/index.ts b/packages/fhevm-sdk/src/react/index.ts index f96544e7..e7d6bd06 100644 --- a/packages/fhevm-sdk/src/react/index.ts +++ b/packages/fhevm-sdk/src/react/index.ts @@ -3,3 +3,6 @@ export * from "./useFHEEncryption"; export * from "./useFHEDecrypt"; export * from "./useInMemoryStorage"; +// Performance optimization utilities +export * from "./useFhevmCache"; + diff --git a/packages/fhevm-sdk/src/react/useFHEDecrypt.ts b/packages/fhevm-sdk/src/react/useFHEDecrypt.ts index 41a0d982..2515ec7f 100644 --- a/packages/fhevm-sdk/src/react/useFHEDecrypt.ts +++ b/packages/fhevm-sdk/src/react/useFHEDecrypt.ts @@ -5,9 +5,41 @@ import { FhevmDecryptionSignature } from "../FhevmDecryptionSignature.js"; import { GenericStringStorage } from "../storage/GenericStringStorage.js"; import { FhevmInstance } from "../fhevmTypes.js"; import { ethers } from "ethers"; - -export type FHEDecryptRequest = { handle: string; contractAddress: `0x${string}` }; - +import { + type FHEDecryptRequest, + type DecryptedValue, + createRequestsKey, + canDecrypt as canDecryptUtil, +} from "../utils/decryption.js"; +import { useStableObject, useComputedValue } from "./useFhevmCache.js"; + +// Re-export types for convenience +export type { FHEDecryptRequest, DecryptedValue }; + +/** + * React Hook for FHE decryption + * + * Provides utilities to decrypt encrypted values from FHEVM contracts. + * + * @param params - Hook configuration + * @returns Decryption utilities, status, and results + * + * @example + * ```typescript + * const { canDecrypt, decrypt, results, isDecrypting, error } = useFHEDecrypt({ + * instance: fhevmInstance, + * ethersSigner: signer, + * fhevmDecryptionSignatureStorage: storage, + * chainId: 31337, + * requests: [{ handle: "0x123", contractAddress: "0xabc" }] + * }); + * + * if (canDecrypt) { + * await decrypt(); + * console.log(results); + * } + * ``` + */ export const useFHEDecrypt = (params: { instance: FhevmInstance | undefined; ethersSigner: ethers.JsonRpcSigner | undefined; @@ -19,111 +51,140 @@ export const useFHEDecrypt = (params: { const [isDecrypting, setIsDecrypting] = useState(false); const [message, setMessage] = useState(""); - const [results, setResults] = useState>({}); + const [results, setResults] = useState>({}); const [error, setError] = useState(null); - const isDecryptingRef = useRef(isDecrypting); + const isDecryptingRef = useRef(false); const lastReqKeyRef = useRef(""); - const requestsKey = useMemo(() => { - if (!requests || requests.length === 0) return ""; - const sorted = [...requests].sort((a, b) => - (a.handle + a.contractAddress).localeCompare(b.handle + b.contractAddress), - ); - return JSON.stringify(sorted); - }, [requests]); + // Stabilize requests object reference (avoid re-renders when content is same) + const stableRequests = useStableObject(requests as any); + // Create unique key for current requests with caching + const requestsKey = useComputedValue( + () => createRequestsKey(stableRequests || []), + [stableRequests], + { timeout: 60000 } // Cache for 1 minute + ); + + // Check if decryption is available const canDecrypt = useMemo(() => { - return Boolean(instance && ethersSigner && requests && requests.length > 0 && !isDecrypting); + return canDecryptUtil(instance, ethersSigner, requests) && !isDecrypting; }, [instance, ethersSigner, requests, isDecrypting]); const decrypt = useCallback(() => { - if (isDecryptingRef.current) return; - if (!instance || !ethersSigner || !requests || requests.length === 0) return; - + // Prevent concurrent decryption + if (isDecryptingRef.current) { + console.warn("[useFHEDecrypt] Decryption already in progress"); + return; + } + + if (!instance || !ethersSigner || !requests || requests.length === 0) { + console.warn("[useFHEDecrypt] Missing required parameters"); + return; + } + + // Capture current values for staleness check const thisChainId = chainId; const thisSigner = ethersSigner; const thisRequests = requests; - // Capture the current requests key to avoid false "stale" detection on first run + // Update refs lastReqKeyRef.current = requestsKey; - isDecryptingRef.current = true; + + // Reset state setIsDecrypting(true); - setMessage("Start decrypt"); + setMessage("Starting decryption..."); setError(null); - const run = async () => { + const performDecryption = async () => { + // Check if request is stale const isStale = () => - thisChainId !== chainId || thisSigner !== ethersSigner || requestsKey !== lastReqKeyRef.current; + thisChainId !== chainId || + thisSigner !== ethersSigner || + requestsKey !== lastReqKeyRef.current; try { - const uniqueAddresses = Array.from(new Set(thisRequests.map(r => r.contractAddress))); - const sig: FhevmDecryptionSignature | null = await FhevmDecryptionSignature.loadOrSign( + // Get unique contract addresses + const uniqueAddresses = Array.from( + new Set(thisRequests.map((r) => r.contractAddress)) + ); + + // Load or create decryption signature + setMessage("Loading decryption signature..."); + const sig = await FhevmDecryptionSignature.loadOrSign( instance, uniqueAddresses as `0x${string}`[], ethersSigner, - fhevmDecryptionSignatureStorage, + fhevmDecryptionSignatureStorage ); if (!sig) { - setMessage("Unable to build FHEVM decryption signature"); - setError("SIGNATURE_ERROR: Failed to create decryption signature"); - return; + throw new Error("Failed to create decryption signature"); } + // Check staleness before proceeding if (isStale()) { - setMessage("Ignore FHEVM decryption"); - return; - } - - setMessage("Call FHEVM userDecrypt..."); - - const mutableReqs = thisRequests.map(r => ({ handle: r.handle, contractAddress: r.contractAddress })); - let res: Record = {}; - try { - res = await instance.userDecrypt( - mutableReqs, - sig.privateKey, - sig.publicKey, - sig.signature, - sig.contractAddresses, - sig.userAddress, - sig.startTimestamp, - sig.durationDays, - ); - } catch (e) { - const err = e as unknown as { name?: string; message?: string }; - const code = err && typeof err === "object" && "name" in (err as any) ? (err as any).name : "DECRYPT_ERROR"; - const msg = err && typeof err === "object" && "message" in (err as any) ? (err as any).message : "Decryption failed"; - setError(`${code}: ${msg}`); - setMessage("FHEVM userDecrypt failed"); + setMessage("Request cancelled (parameters changed)"); return; } - setMessage("FHEVM userDecrypt completed!"); + // Perform decryption + setMessage("Decrypting values..."); + const mutableReqs = thisRequests.map((r) => ({ + handle: r.handle, + contractAddress: r.contractAddress, + })); + + const decryptedResults = await instance.userDecrypt( + mutableReqs, + sig.privateKey, + sig.publicKey, + sig.signature, + sig.contractAddresses, + sig.userAddress, + sig.startTimestamp, + sig.durationDays + ); + // Final staleness check if (isStale()) { - setMessage("Ignore FHEVM decryption"); + setMessage("Request cancelled (parameters changed)"); return; } - setResults(res); - } catch (e) { - const err = e as unknown as { name?: string; message?: string }; - const code = err && typeof err === "object" && "name" in (err as any) ? (err as any).name : "UNKNOWN_ERROR"; - const msg = err && typeof err === "object" && "message" in (err as any) ? (err as any).message : "Unknown error"; - setError(`${code}: ${msg}`); - setMessage("FHEVM decryption errored"); + // Update results + setResults(decryptedResults); + setMessage("Decryption completed successfully!"); + console.log("[useFHEDecrypt] Decryption successful:", decryptedResults); + } catch (err) { + // Format error message + const error = err as Error; + const errorMessage = `${error.name || "DecryptionError"}: ${error.message || "Unknown error"}`; + + setError(errorMessage); + setMessage("Decryption failed"); + console.error("[useFHEDecrypt] Decryption failed:", error); } finally { + // Cleanup isDecryptingRef.current = false; setIsDecrypting(false); lastReqKeyRef.current = requestsKey; } }; - run(); + performDecryption(); }, [instance, ethersSigner, fhevmDecryptionSignatureStorage, chainId, requests, requestsKey]); - return { canDecrypt, decrypt, isDecrypting, message, results, error, setMessage, setError } as const; + return { + canDecrypt, + decrypt, + isDecrypting, + message, + results, + error, + setMessage, + setError, + } as const; }; \ No newline at end of file diff --git a/packages/fhevm-sdk/src/react/useFHEEncryption.ts b/packages/fhevm-sdk/src/react/useFHEEncryption.ts index 01fbfc03..991489a0 100644 --- a/packages/fhevm-sdk/src/react/useFHEEncryption.ts +++ b/packages/fhevm-sdk/src/react/useFHEEncryption.ts @@ -1,74 +1,45 @@ "use client"; -import { useCallback, useMemo } from "react"; +import { useCallback, useMemo, useRef } from "react"; import { FhevmInstance } from "../fhevmTypes.js"; import { RelayerEncryptedInput } from "@zama-fhe/relayer-sdk/web"; import { ethers } from "ethers"; +import { + type EncryptResult, + getEncryptionMethod, + toHex, + buildParamsFromAbi, + encryptInput, +} from "../utils/encryption.js"; +import { useStableCallback } from "./useFhevmCache.js"; -export type EncryptResult = { - handles: Uint8Array[]; - inputProof: Uint8Array; -}; - -// Map external encrypted integer type to RelayerEncryptedInput builder method -export const getEncryptionMethod = (internalType: string) => { - switch (internalType) { - case "externalEbool": - return "addBool" as const; - case "externalEuint8": - return "add8" as const; - case "externalEuint16": - return "add16" as const; - case "externalEuint32": - return "add32" as const; - case "externalEuint64": - return "add64" as const; - case "externalEuint128": - return "add128" as const; - case "externalEuint256": - return "add256" as const; - case "externalEaddress": - return "addAddress" as const; - default: - console.warn(`Unknown internalType: ${internalType}, defaulting to add64`); - return "add64" as const; - } -}; - -// Convert Uint8Array or hex-like string to 0x-prefixed hex string -export const toHex = (value: Uint8Array | string): `0x${string}` => { - if (typeof value === "string") { - return (value.startsWith("0x") ? value : `0x${value}`) as `0x${string}`; - } - // value is Uint8Array - return ("0x" + Buffer.from(value).toString("hex")) as `0x${string}`; -}; - -// Build contract params from EncryptResult and ABI for a given function -export const buildParamsFromAbi = (enc: EncryptResult, abi: any[], functionName: string): any[] => { - const fn = abi.find((item: any) => item.type === "function" && item.name === functionName); - if (!fn) throw new Error(`Function ABI not found for ${functionName}`); - - return fn.inputs.map((input: any, index: number) => { - const raw = index === 0 ? enc.handles[0] : enc.inputProof; - switch (input.type) { - case "bytes32": - case "bytes": - return toHex(raw); - case "uint256": - return BigInt(raw as unknown as string); - case "address": - case "string": - return raw as unknown as string; - case "bool": - return Boolean(raw); - default: - console.warn(`Unknown ABI param type ${input.type}; passing as hex`); - return toHex(raw); - } - }); -}; +// Re-export commonly used types and utilities for convenience +export type { EncryptResult }; +export { getEncryptionMethod, toHex, buildParamsFromAbi }; +/** + * React Hook for FHE encryption + * + * Provides utilities to encrypt values for FHEVM contracts. + * + * @param params - Hook configuration + * @returns Encryption utilities and status + * + * @example + * ```typescript + * const { canEncrypt, encryptWith } = useFHEEncryption({ + * instance: fhevmInstance, + * ethersSigner: signer, + * contractAddress: "0x123..." + * }); + * + * if (canEncrypt) { + * const result = await encryptWith((input) => { + * input.add32(42); + * }); + * } + * ``` + */ export const useFHEEncryption = (params: { instance: FhevmInstance | undefined; ethersSigner: ethers.JsonRpcSigner | undefined; @@ -76,20 +47,34 @@ export const useFHEEncryption = (params: { }) => { const { instance, ethersSigner, contractAddress } = params; + // Cache user address to avoid repeated calls + const userAddressRef = useRef(undefined); + const canEncrypt = useMemo( () => Boolean(instance && ethersSigner && contractAddress), [instance, ethersSigner, contractAddress], ); - const encryptWith = useCallback( + // Use stable callback to prevent unnecessary re-renders + const encryptWith = useStableCallback( async (buildFn: (builder: RelayerEncryptedInput) => void): Promise => { - if (!instance || !ethersSigner || !contractAddress) return undefined; + if (!instance || !ethersSigner || !contractAddress) { + return undefined; + } - const userAddress = await ethersSigner.getAddress(); - const input = instance.createEncryptedInput(contractAddress, userAddress) as RelayerEncryptedInput; - buildFn(input); - const enc = await input.encrypt(); - return enc; + try { + // Cache user address + if (!userAddressRef.current) { + userAddressRef.current = await ethersSigner.getAddress(); + } + + return await encryptInput(instance, contractAddress, userAddressRef.current as `0x${string}`, buildFn); + } catch (error) { + console.error("[useFHEEncryption] Encryption failed:", error); + // Clear cache on error + userAddressRef.current = undefined; + throw error; + } }, [instance, ethersSigner, contractAddress], ); diff --git a/packages/fhevm-sdk/src/react/useFhevm.tsx b/packages/fhevm-sdk/src/react/useFhevm.tsx index 4f31bdd6..d939496b 100644 --- a/packages/fhevm-sdk/src/react/useFhevm.tsx +++ b/packages/fhevm-sdk/src/react/useFhevm.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import type { FhevmInstance } from "../fhevmTypes.js"; import { createFhevmInstance } from "../internal/fhevm.js"; +import { validateFhevmConfig } from "../core/config.js"; import { ethers } from "ethers"; function _assert(condition: boolean, message?: string): asserts condition { @@ -90,6 +91,20 @@ export function useFhevm(parameters: { _assert(!_abortControllerRef.current.signal.aborted, "!controllerRef.current.signal.aborted"); + // Validate configuration before creating instance + try { + validateFhevmConfig({ + provider: _providerRef.current, + chainId: _chainIdRef.current, + mockChains: _mockChainsRef.current, + enabled: _isRunning, + }); + } catch (error) { + _setError(error as Error); + _setStatus("error"); + return; + } + _setStatus("loading"); _setError(undefined); diff --git a/packages/fhevm-sdk/src/react/useFhevmCache.ts b/packages/fhevm-sdk/src/react/useFhevmCache.ts new file mode 100644 index 00000000..dde31550 --- /dev/null +++ b/packages/fhevm-sdk/src/react/useFhevmCache.ts @@ -0,0 +1,235 @@ +/** + * Caching utilities for FHEVM React Hooks + * + * Provides memoization and caching strategies to improve Hook performance + */ + +import { useMemo, useRef } from "react"; + +/** + * Cache configuration options + */ +export interface CacheOptions { + /** + * Cache timeout in milliseconds + * @default 300000 (5 minutes) + */ + timeout?: number; + /** + * Enable debug logging + * @default false + */ + debug?: boolean; +} + +/** + * Cache entry structure + */ +interface CacheEntry { + value: T; + timestamp: number; +} + +/** + * Hook for memoizing values with timeout-based cache invalidation + * + * @param value - Value to cache + * @param dependencies - Dependencies array (like useMemo) + * @param options - Cache configuration + * @returns Cached value + * + * @example + * ```typescript + * const cachedInstance = useCachedValue( + * instance, + * [provider, chainId], + * { timeout: 60000 } // 1 minute cache + * ); + * ``` + */ +export function useCachedValue( + value: T, + dependencies: React.DependencyList, + options: CacheOptions = {} +): T { + const { timeout = 300000, debug = false } = options; + + const cacheRef = useRef | null>(null); + + return useMemo(() => { + const now = Date.now(); + + // Check if cache is valid + if (cacheRef.current) { + const elapsed = now - cacheRef.current.timestamp; + + if (elapsed < timeout) { + if (debug) { + console.log("[useCachedValue] Cache hit", { elapsed, timeout }); + } + return cacheRef.current.value; + } + } + + // Cache miss or expired + if (debug) { + console.log("[useCachedValue] Cache miss or expired"); + } + + cacheRef.current = { + value, + timestamp: now, + }; + + return value; + }, dependencies); +} + +/** + * Hook for stable callback references with dependency tracking + * + * Similar to useCallback but with better memoization for complex dependencies + * + * @param callback - Callback function + * @param dependencies - Dependencies array + * @returns Memoized callback + * + * @example + * ```typescript + * const stableEncrypt = useStableCallback( + * (data) => encryptWith((input) => input.add32(data)), + * [encryptWith] + * ); + * ``` + */ +export function useStableCallback any>( + callback: T, + dependencies: React.DependencyList +): T { + const callbackRef = useRef(callback); + const depsRef = useRef(dependencies); + + // Update ref if dependencies changed + const depsChanged = useMemo(() => { + if (depsRef.current.length !== dependencies.length) { + return true; + } + + return dependencies.some((dep, index) => dep !== depsRef.current[index]); + }, dependencies); + + if (depsChanged) { + callbackRef.current = callback; + depsRef.current = dependencies; + } + + // Return stable reference + const stableCallback = useRef(((...args: any[]) => { + return callbackRef.current(...args); + }) as T); + + return stableCallback.current; +} + +/** + * Hook for debouncing values + * + * @param value - Value to debounce + * @param delay - Debounce delay in milliseconds + * @returns Debounced value + * + * @example + * ```typescript + * const debouncedChainId = useDebouncedValue(chainId, 500); + * ``` + */ +export function useDebouncedValue(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = React.useState(value); + const timeoutRef = useRef(undefined); + + React.useEffect(() => { + // Clear previous timeout + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + // Set new timeout + timeoutRef.current = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + // Cleanup + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, [value, delay]); + + return debouncedValue; +} + +/** + * Hook for creating a stable object reference based on JSON serialization + * + * Useful for objects that should be compared by value, not reference + * + * @param obj - Object to stabilize + * @returns Stable object reference + * + * @example + * ```typescript + * const stableMockChains = useStableObject({ 31337: "http://localhost:8545" }); + * // Reference only changes if content changes + * ``` + */ +export function useStableObject(obj: T | undefined): T | undefined { + const objRef = useRef(obj); + const jsonRef = useRef(obj ? JSON.stringify(obj) : ""); + + return useMemo(() => { + if (!obj) { + return undefined; + } + + const currentJson = JSON.stringify(obj); + + if (currentJson === jsonRef.current) { + return objRef.current; + } + + jsonRef.current = currentJson; + objRef.current = obj; + return obj; + }, [obj]); +} + +/** + * Hook for computing derived values with caching + * + * @param compute - Computation function + * @param dependencies - Dependencies array + * @param options - Cache options + * @returns Computed value + * + * @example + * ```typescript + * const contractAddresses = useComputedValue( + * () => requests.map(r => r.contractAddress), + * [requests], + * { timeout: 60000 } + * ); + * ``` + */ +export function useComputedValue( + compute: () => T, + dependencies: React.DependencyList, + options: CacheOptions = {} +): T { + const computed = useMemo(compute, dependencies); + return useCachedValue(computed, dependencies, options); +} + +// Re-export React for convenience +import * as React from "react"; + diff --git a/packages/fhevm-sdk/src/types/index.ts b/packages/fhevm-sdk/src/types/index.ts new file mode 100644 index 00000000..2b498eae --- /dev/null +++ b/packages/fhevm-sdk/src/types/index.ts @@ -0,0 +1,24 @@ +/** + * Core type definitions for FHEVM SDK + */ + +// Re-export existing types +export * from "../fhevmTypes"; +export * from "../internal/fhevmTypes"; + +// Add new common types +export type FhevmStatus = "idle" | "loading" | "ready" | "error"; + +export interface FhevmConfig { + provider: string | any; // Will be refined later + chainId?: number; + enabled?: boolean; + mockChains?: Readonly>; +} + +export interface FhevmErrorType { + code: string; + message: string; + cause?: Error; +} + diff --git a/packages/fhevm-sdk/src/utils/decryption.ts b/packages/fhevm-sdk/src/utils/decryption.ts new file mode 100644 index 00000000..a216203c --- /dev/null +++ b/packages/fhevm-sdk/src/utils/decryption.ts @@ -0,0 +1,204 @@ +/** + * Decryption utilities for FHEVM with performance optimizations + * + * These are internal utility functions used by React hooks. + * For actual decryption, use the useFHEDecrypt hook. + */ + +import { FhevmDecryptionError } from "./errors"; + +/** + * Decryption request + */ +export interface FHEDecryptRequest { + handle: string; + contractAddress: `0x${string}`; +} + +/** + * Decryption result type + */ +export type DecryptedValue = string | bigint | boolean; + +/** + * Decryption performance metrics + */ +export interface DecryptionMetrics { + /** Total time spent on decryption (ms) */ + decryptionTime: number; + /** Number of values decrypted */ + valueCount: number; + /** Time spent on signature creation (ms) */ + signatureTime?: number; + /** Timestamp of the operation */ + timestamp: Date; +} + +/** + * Decryption options + */ +export interface DecryptionOptions { + /** Enable performance monitoring */ + enableMetrics?: boolean; + /** Timeout for decryption operation (ms) */ + timeout?: number; +} + +/** + * Sort and create unique key for decryption requests + * + * This is used to detect when requests have changed. + * + * @param requests - Array of decryption requests + * @returns Unique string key for the requests + * + * @example + * ```typescript + * const key1 = createRequestsKey([{ handle: "0x1", contractAddress: "0xa" }]); + * const key2 = createRequestsKey([{ handle: "0x1", contractAddress: "0xa" }]); + * console.log(key1 === key2); // true + * ``` + */ +export function createRequestsKey(requests: readonly FHEDecryptRequest[]): string { + if (!requests || requests.length === 0) return ""; + + const sorted = [...requests].sort((a, b) => + (a.handle + a.contractAddress).localeCompare(b.handle + b.contractAddress) + ); + + return JSON.stringify(sorted); +} + +/** + * Group decryption requests by contract address for efficient processing + * + * @param requests - Array of decryption requests + * @returns Map of contract address to requests + * + * @example + * ```typescript + * const grouped = groupRequestsByContract([ + * { handle: "0x1", contractAddress: "0xa" }, + * { handle: "0x2", contractAddress: "0xa" }, + * { handle: "0x3", contractAddress: "0xb" } + * ]); + * // Returns: Map { "0xa" => [req1, req2], "0xb" => [req3] } + * ``` + */ +export function groupRequestsByContract( + requests: readonly FHEDecryptRequest[] +): Map<`0x${string}`, FHEDecryptRequest[]> { + const grouped = new Map<`0x${string}`, FHEDecryptRequest[]>(); + + for (const req of requests) { + const existing = grouped.get(req.contractAddress) || []; + existing.push(req); + grouped.set(req.contractAddress, existing); + } + + return grouped; +} + +/** + * Get unique contract addresses from requests + * + * @param requests - Array of decryption requests + * @returns Array of unique contract addresses + * + * @example + * ```typescript + * const addresses = getUniqueContractAddresses([ + * { handle: "0x1", contractAddress: "0xa" }, + * { handle: "0x2", contractAddress: "0xa" }, + * { handle: "0x3", contractAddress: "0xb" } + * ]); + * // Returns: ["0xa", "0xb"] + * ``` + */ +export function getUniqueContractAddresses( + requests: readonly FHEDecryptRequest[] +): `0x${string}`[] { + const addresses = new Set<`0x${string}`>(); + + for (const req of requests) { + addresses.add(req.contractAddress); + } + + return Array.from(addresses); +} + +/** + * Validate decryption requests + * + * @param requests - Array of decryption requests + * @throws {FhevmDecryptionError} If requests are invalid + * + * @example + * ```typescript + * validateDecryptRequests([ + * { handle: "0x1", contractAddress: "0xa" } + * ]); // OK + * + * validateDecryptRequests([ + * { handle: "", contractAddress: "0xa" } + * ]); // Throws error + * ``` + */ +export function validateDecryptRequests( + requests: readonly FHEDecryptRequest[] +): void { + if (!requests || requests.length === 0) { + throw new FhevmDecryptionError("No decryption requests provided"); + } + + for (let i = 0; i < requests.length; i++) { + const req = requests[i]; + + if (!req.handle || typeof req.handle !== "string") { + throw new FhevmDecryptionError( + `Invalid handle at index ${i}: ${req.handle}` + ); + } + + if (!req.contractAddress || typeof req.contractAddress !== "string") { + throw new FhevmDecryptionError( + `Invalid contract address at index ${i}: ${req.contractAddress}` + ); + } + + if (!req.contractAddress.startsWith("0x")) { + throw new FhevmDecryptionError( + `Contract address must start with 0x at index ${i}: ${req.contractAddress}` + ); + } + } +} + +/** + * Check if decryption is available + * + * @param instance - FHEVM instance + * @param signer - Ethers signer + * @param requests - Decryption requests + * @returns True if decryption is available + * + * @example + * ```typescript + * if (canDecrypt(instance, signer, requests)) { + * // Ready to decrypt + * } + * ``` + */ +export function canDecrypt( + instance: any | undefined, + signer: any | undefined, + requests: readonly FHEDecryptRequest[] | undefined +): boolean { + return Boolean( + instance && + signer && + requests && + requests.length > 0 + ); +} + diff --git a/packages/fhevm-sdk/src/utils/encryption.ts b/packages/fhevm-sdk/src/utils/encryption.ts new file mode 100644 index 00000000..1ef07df6 --- /dev/null +++ b/packages/fhevm-sdk/src/utils/encryption.ts @@ -0,0 +1,341 @@ +/** + * Encryption utilities for FHEVM with performance optimizations + */ + +import { FhevmInstance } from "../fhevmTypes"; +import { RelayerEncryptedInput } from "@zama-fhe/relayer-sdk/web"; +import { FhevmEncryptionError } from "./errors"; + +/** + * Result of encryption operation + */ +export type EncryptResult = { + handles: Uint8Array[]; + inputProof: Uint8Array; +}; + +/** + * Encryption performance metrics + */ +export interface EncryptionMetrics { + /** Total time spent on encryption (ms) */ + encryptionTime: number; + /** Number of values encrypted */ + valueCount: number; + /** Timestamp of the operation */ + timestamp: Date; +} + +/** + * Encryption options + */ +export interface EncryptionOptions { + /** Enable performance monitoring */ + enableMetrics?: boolean; + /** Timeout for encryption operation (ms) */ + timeout?: number; +} + +/** + * Map external encrypted integer type to RelayerEncryptedInput builder method + * + * @param internalType - The internal type string (e.g., "externalEuint32") + * @returns The corresponding encryption method name + * + * @example + * ```typescript + * const method = getEncryptionMethod("externalEuint32"); + * // Returns: "add32" + * ``` + */ +export function getEncryptionMethod(internalType: string): + | "addBool" + | "add8" + | "add16" + | "add32" + | "add64" + | "add128" + | "add256" + | "addAddress" { + switch (internalType) { + case "externalEbool": + return "addBool"; + case "externalEuint8": + return "add8"; + case "externalEuint16": + return "add16"; + case "externalEuint32": + return "add32"; + case "externalEuint64": + return "add64"; + case "externalEuint128": + return "add128"; + case "externalEuint256": + return "add256"; + case "externalEaddress": + return "addAddress"; + default: + console.warn(`Unknown internalType: ${internalType}, defaulting to add64`); + return "add64"; + } +} + +/** + * Convert Uint8Array or hex-like string to 0x-prefixed hex string + * + * @param value - Uint8Array or hex string to convert + * @returns 0x-prefixed hex string + * + * @example + * ```typescript + * toHex(new Uint8Array([0, 1, 2])); // "0x000102" + * toHex("123abc"); // "0x123abc" + * toHex("0x123abc"); // "0x123abc" + * ``` + */ +export function toHex(value: Uint8Array | string): `0x${string}` { + if (typeof value === "string") { + return (value.startsWith("0x") ? value : `0x${value}`) as `0x${string}`; + } + // value is Uint8Array + return ("0x" + Buffer.from(value).toString("hex")) as `0x${string}`; +} + +/** + * Build contract parameters from encryption result and ABI + * + * @param enc - The encryption result + * @param abi - Contract ABI + * @param functionName - Name of the function to call + * @returns Array of properly formatted parameters + * + * @throws Error if function ABI not found + * + * @example + * ```typescript + * const params = buildParamsFromAbi(encResult, contractAbi, "transfer"); + * // Returns: ["0x...", "0x..."] ready for contract call + * ``` + */ +export function buildParamsFromAbi( + enc: EncryptResult, + abi: any[], + functionName: string +): any[] { + const fn = abi.find( + (item: any) => item.type === "function" && item.name === functionName + ); + + if (!fn) { + throw new Error(`Function ABI not found for ${functionName}`); + } + + return fn.inputs.map((input: any, index: number) => { + const raw = index === 0 ? enc.handles[0] : enc.inputProof; + + switch (input.type) { + case "bytes32": + case "bytes": + return toHex(raw); + case "uint256": + return BigInt(raw as unknown as string); + case "address": + case "string": + return raw as unknown as string; + case "bool": + return Boolean(raw); + default: + console.warn(`Unknown ABI param type ${input.type}; passing as hex`); + return toHex(raw); + } + }); +} + +/** + * Create encrypted input for contract with performance monitoring + * + * @param instance - FHEVM instance + * @param contractAddress - Contract address + * @param userAddress - User address + * @param buildFn - Function to build encrypted input + * @param options - Encryption options + * @returns Promise resolving to encryption result + * + * @throws {FhevmEncryptionError} If encryption fails or times out + * + * @example + * ```typescript + * const result = await encryptInput( + * fhevmInstance, + * "0x123...", + * "0xabc...", + * (input) => { + * input.add32(42); + * }, + * { enableMetrics: true, timeout: 10000 } + * ); + * ``` + */ +export async function encryptInput( + instance: FhevmInstance, + contractAddress: string, + userAddress: string, + buildFn: (builder: RelayerEncryptedInput) => void, + options: EncryptionOptions = {} +): Promise { + const { enableMetrics = false, timeout = 30000 } = options; + const startTime = enableMetrics ? performance.now() : 0; + + try { + const input = instance.createEncryptedInput( + contractAddress, + userAddress + ) as RelayerEncryptedInput; + + buildFn(input); + + // Add timeout support + const encryptPromise = input.encrypt(); + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => reject(new Error("Encryption timeout")), timeout); + }); + + const enc = await Promise.race([encryptPromise, timeoutPromise]); + + // Log metrics if enabled + if (enableMetrics) { + const endTime = performance.now(); + const metrics: EncryptionMetrics = { + encryptionTime: endTime - startTime, + valueCount: enc.handles.length, + timestamp: new Date(), + }; + console.log("[Encryption Metrics]", metrics); + } + + return enc; + } catch (error) { + throw new FhevmEncryptionError( + "Encryption failed", + error as Error + ); + } +} + +/** + * Batch encryption request + */ +export interface BatchEncryptRequest { + /** Build function for this encryption */ + buildFn: (builder: RelayerEncryptedInput) => void; + /** Optional identifier for this request */ + id?: string; +} + +/** + * Batch encryption result + */ +export interface BatchEncryptResult { + /** Request identifier (if provided) */ + id?: string; + /** Encryption result */ + result: EncryptResult; + /** Time taken for this encryption (ms) */ + duration: number; +} + +/** + * Batch encrypt multiple values efficiently + * + * @param instance - FHEVM instance + * @param contractAddress - Contract address + * @param userAddress - User address + * @param requests - Array of encryption requests + * @param options - Encryption options + * @returns Promise resolving to array of results + * + * @example + * ```typescript + * const results = await batchEncrypt( + * instance, + * contractAddress, + * userAddress, + * [ + * { id: "value1", buildFn: (input) => input.add32(42) }, + * { id: "value2", buildFn: (input) => input.add32(100) } + * ], + * { enableMetrics: true } + * ); + * ``` + */ +export async function batchEncrypt( + instance: FhevmInstance, + contractAddress: string, + userAddress: string, + requests: BatchEncryptRequest[], + options: EncryptionOptions = {} +): Promise { + const { enableMetrics = false } = options; + + if (enableMetrics) { + console.log(`[Batch Encryption] Starting ${requests.length} encryptions`); + } + + const startTime = enableMetrics ? performance.now() : 0; + + try { + const results = await Promise.all( + requests.map(async (req, index) => { + const reqStartTime = performance.now(); + + const result = await encryptInput( + instance, + contractAddress, + userAddress, + req.buildFn, + { ...options, enableMetrics: false } // Disable per-request metrics + ); + + const reqEndTime = performance.now(); + + return { + id: req.id || `request-${index}`, + result, + duration: reqEndTime - reqStartTime, + }; + }) + ); + + if (enableMetrics) { + const endTime = performance.now(); + const totalTime = endTime - startTime; + const avgTime = totalTime / requests.length; + + console.log(`[Batch Encryption] Completed ${requests.length} encryptions in ${totalTime.toFixed(2)}ms (avg: ${avgTime.toFixed(2)}ms)`); + } + + return results; + } catch (error) { + throw new FhevmEncryptionError( + "Batch encryption failed", + error as Error + ); + } +} + +/** + * Check if encryption is available + * + * @param instance - FHEVM instance + * @param contractAddress - Contract address + * @param userAddress - User address + * @returns True if all required parameters are available + */ +export function canEncrypt( + instance: FhevmInstance | undefined, + contractAddress: string | undefined, + userAddress: string | undefined +): boolean { + return Boolean(instance && contractAddress && userAddress); +} + diff --git a/packages/fhevm-sdk/src/utils/errors.ts b/packages/fhevm-sdk/src/utils/errors.ts new file mode 100644 index 00000000..5c4c982c --- /dev/null +++ b/packages/fhevm-sdk/src/utils/errors.ts @@ -0,0 +1,114 @@ +/** + * Error handling utilities for FHEVM SDK + */ + +/** + * Base error class for FHEVM SDK + */ +export class FhevmError extends Error { + constructor( + public code: string, + message: string, + public cause?: Error + ) { + super(message); + this.name = "FhevmError"; + } +} + +/** + * Error thrown during FHEVM instance initialization + */ +export class FhevmInitializationError extends FhevmError { + constructor(message: string, cause?: Error) { + super("FHEVM_INIT_ERROR", message, cause); + this.name = "FhevmInitializationError"; + } +} + +/** + * Error thrown during encryption operations + */ +export class FhevmEncryptionError extends FhevmError { + constructor(message: string, cause?: Error) { + super("FHEVM_ENCRYPTION_ERROR", message, cause); + this.name = "FhevmEncryptionError"; + } +} + +/** + * Error thrown during decryption operations + */ +export class FhevmDecryptionError extends FhevmError { + constructor(message: string, cause?: Error) { + super("FHEVM_DECRYPTION_ERROR", message, cause); + this.name = "FhevmDecryptionError"; + } +} + +/** + * Error thrown during signature operations + */ +export class FhevmSignatureError extends FhevmError { + constructor(message: string, cause?: Error) { + super("FHEVM_SIGNATURE_ERROR", message, cause); + this.name = "FhevmSignatureError"; + } +} + +/** + * Error thrown for invalid configuration + */ +export class FhevmConfigurationError extends FhevmError { + constructor(message: string, cause?: Error) { + super("FHEVM_CONFIG_ERROR", message, cause); + this.name = "FhevmConfigurationError"; + } +} + +/** + * Format error for user display + * + * @param error - The error to format + * @returns Formatted error message + * + * @example + * ```typescript + * try { + * await client.initialize(); + * } catch (error) { + * console.error(formatFhevmError(error)); + * } + * ``` + */ +export function formatFhevmError(error: unknown): string { + if (error instanceof FhevmError) { + return `[${error.code}] ${error.message}`; + } + if (error instanceof Error) { + return error.message; + } + return String(error); +} + +/** + * Check if an error is a FHEVM error + */ +export function isFhevmError(error: unknown): error is FhevmError { + return error instanceof FhevmError; +} + +/** + * Wrap an unknown error in a FhevmError + */ +export function wrapError(error: unknown, defaultMessage: string): FhevmError { + if (error instanceof FhevmError) { + return error; + } + + const cause = error instanceof Error ? error : undefined; + const message = error instanceof Error ? error.message : defaultMessage; + + return new FhevmError("UNKNOWN_ERROR", message, cause); +} + diff --git a/packages/fhevm-sdk/src/utils/index.ts b/packages/fhevm-sdk/src/utils/index.ts new file mode 100644 index 00000000..20e0c89f --- /dev/null +++ b/packages/fhevm-sdk/src/utils/index.ts @@ -0,0 +1,7 @@ +/** + * Utility functions for FHEVM SDK + */ + +// Only export errors here, encryption and decryption are internal +export * from "./errors"; + diff --git a/packages/fhevm-sdk/src/version.ts b/packages/fhevm-sdk/src/version.ts new file mode 100644 index 00000000..5240180f --- /dev/null +++ b/packages/fhevm-sdk/src/version.ts @@ -0,0 +1,57 @@ +/** + * SDK version and metadata + */ + +export const SDK_VERSION = "0.2.0"; +export const SDK_NAME = "@fhevm-sdk"; +export const SDK_REPOSITORY = "https://github.com/zama-ai/fhevm-react-template"; +export const SDK_AUTHOR = "Zama"; +export const SDK_LICENSE = "BSD-3-Clause-Clear"; + +/** + * SDK information object + */ +export interface SDKInfo { + name: string; + version: string; + repository: string; + author: string; + license: string; + buildDate: string; +} + +/** + * Get SDK version information + * + * @returns SDK metadata object + * + * @example + * ```typescript + * import { getSDKInfo } from '@fhevm-sdk'; + * + * const info = getSDKInfo(); + * console.log(`Using ${info.name} v${info.version}`); + * ``` + */ +export function getSDKInfo(): SDKInfo { + return { + name: SDK_NAME, + version: SDK_VERSION, + repository: SDK_REPOSITORY, + author: SDK_AUTHOR, + license: SDK_LICENSE, + buildDate: new Date().toISOString(), + }; +} + +/** + * Log SDK version on import (useful for debugging) + * Can be disabled by setting FHEVM_SDK_QUIET environment variable + */ +if (typeof console !== "undefined" && typeof process !== "undefined") { + const isQuiet = process.env?.FHEVM_SDK_QUIET === "true"; + if (!isQuiet) { + console.log(`[FHEVM SDK] v${SDK_VERSION} loaded`); + } +} +