diff --git a/packages/fhevm-sdk/src/core/DecryptionManager.ts b/packages/fhevm-sdk/src/core/DecryptionManager.ts new file mode 100644 index 00000000..465a4954 --- /dev/null +++ b/packages/fhevm-sdk/src/core/DecryptionManager.ts @@ -0,0 +1,259 @@ +import { FhevmInstance } from "../fhevmTypes.js"; +import { FhevmDecryptionSignature } from "../FhevmDecryptionSignature.js"; +import { GenericStringStorage } from "../storage/GenericStringStorage.js"; +import { ethers } from "ethers"; + +/** + * A single decryption request. + * @public + */ +export interface FHEDecryptRequest { + /** The encrypted handle to decrypt */ + handle: string; + /** The contract address that owns this handle */ + contractAddress: `0x${string}`; +} + +/** + * Result of a decryption operation, mapping handles to their decrypted values. + * @public + */ +export type DecryptResult = Record; + +/** + * Decryption manager for FHEVM operations. + * + * This class provides a framework-agnostic interface for decrypting FHE-encrypted + * data from smart contracts. It handles the EIP-712 signature creation and caching, + * making the decryption process seamless for developers. + * + * @remarks + * The DecryptionManager automatically manages decryption signatures and caches them + * for improved performance. Users only need to sign once per session for each set + * of contract addresses. + * + * @example + * Basic usage: + * ```typescript + * const manager = new DecryptionManager( + * fhevmInstance, + * signer, + * storage, + * 11155111 // Sepolia chain ID + * ); + * + * const results = await manager.decrypt([ + * { handle: '0x...', contractAddress: '0x...' } + * ]); + * + * console.log('Decrypted value:', results['0x...']); + * ``` + * + * @example + * With multiple handles: + * ```typescript + * const results = await manager.decrypt([ + * { handle: '0xhandle1', contractAddress: '0xcontract1' }, + * { handle: '0xhandle2', contractAddress: '0xcontract1' }, + * { handle: '0xhandle3', contractAddress: '0xcontract2' } + * ]); + * + * Object.entries(results).forEach(([handle, value]) => { + * console.log(`${handle}: ${value}`); + * }); + * ``` + * + * @public + */ +export class DecryptionManager { + /** + * Creates a new DecryptionManager instance. + * + * @param instance - The initialized FHEVM instance + * @param signer - The user's ethers signer (for EIP-712 signatures) + * @param signatureStorage - Storage for caching decryption signatures + * @param chainId - The blockchain chain ID + * + * @throws {TypeError} If any required parameter is missing + */ + constructor( + private readonly instance: FhevmInstance, + private readonly signer: ethers.JsonRpcSigner, + private readonly signatureStorage: GenericStringStorage, + private readonly chainId: number + ) { + if (!instance) { + throw new TypeError('DecryptionManager: instance is required'); + } + if (!signer) { + throw new TypeError('DecryptionManager: signer is required'); + } + if (!signatureStorage) { + throw new TypeError('DecryptionManager: signatureStorage is required'); + } + if (!chainId) { + throw new TypeError('DecryptionManager: chainId is required'); + } + } + + /** + * Decrypts multiple encrypted handles. + * + * @param requests - Array of decryption requests + * @returns Promise resolving to a mapping of handles to their decrypted values + * + * @throws {Error} If decryption fails or signature creation fails + * + * @remarks + * This method automatically handles: + * - Loading or creating EIP-712 signatures + * - Caching signatures for future use + * - Batching multiple decryption requests + * - Converting results to appropriate JavaScript types + * + * The first time you decrypt, the user will be prompted to sign an EIP-712 message. + * This signature is cached and reused for subsequent decryptions. + * + * @example + * Decrypt a single value: + * ```typescript + * const results = await manager.decrypt([ + * { handle: '0x123...', contractAddress: '0xabc...' } + * ]); + * + * const value = results['0x123...']; + * if (typeof value === 'bigint') { + * console.log('Count:', value.toString()); + * } + * ``` + * + * @example + * Decrypt multiple values: + * ```typescript + * const results = await manager.decrypt([ + * { handle: counterHandle, contractAddress: contractAddr }, + * { handle: balanceHandle, contractAddress: contractAddr } + * ]); + * + * console.log('Counter:', results[counterHandle]); + * console.log('Balance:', results[balanceHandle]); + * ``` + */ + async decrypt( + requests: FHEDecryptRequest[] + ): Promise { + if (!this.canDecrypt(requests)) { + throw new Error('DecryptionManager: Cannot decrypt - invalid parameters or empty requests'); + } + + try { + // Extract unique contract addresses + const uniqueAddresses = Array.from( + new Set(requests.map(r => r.contractAddress)) + ); + + // Load or create decryption signature + const signature = await this.loadOrCreateSignature(uniqueAddresses); + + if (!signature) { + throw new Error('Failed to create or load decryption signature'); + } + + // Convert requests to mutable format for FHEVM API + const mutableReqs = requests.map(r => ({ + handle: r.handle, + contractAddress: r.contractAddress + })); + + // Execute decryption + const results = await this.instance.userDecrypt( + mutableReqs, + signature.privateKey, + signature.publicKey, + signature.signature, + signature.contractAddresses, + signature.userAddress, + signature.startTimestamp, + signature.durationDays + ); + + return results; + } catch (error) { + throw new Error( + `Decryption failed: ${error instanceof Error ? error.message : String(error)}` + ); + } + } + + /** + * Checks if decryption is possible with the given requests. + * + * @param requests - Array of decryption requests to validate + * @returns True if decryption is possible, false otherwise + * + * @example + * ```typescript + * const requests = [ + * { handle: '0x...', contractAddress: '0x...' } + * ]; + * + * if (manager.canDecrypt(requests)) { + * const results = await manager.decrypt(requests); + * } else { + * console.log('Cannot decrypt: missing parameters or invalid requests'); + * } + * ``` + */ + canDecrypt(requests: FHEDecryptRequest[] | undefined): boolean { + return Boolean( + this.instance && + this.signer && + this.signatureStorage && + requests && + requests.length > 0 + ); + } + + /** + * Gets the chain ID this manager is configured for. + * + * @returns The chain ID + */ + getChainId(): number { + return this.chainId; + } + + /** + * Loads an existing decryption signature from storage or creates a new one. + * + * @param addresses - Contract addresses to include in the signature + * @returns Promise resolving to the decryption signature + * + * @throws {Error} If signature creation fails + * + * @remarks + * This method is called automatically by {@link decrypt} and handles: + * - Checking storage for existing valid signatures + * - Prompting the user to sign if no valid signature exists + * - Storing the new signature for future use + * + * @internal + */ + private async loadOrCreateSignature( + addresses: string[] + ): Promise { + try { + return await FhevmDecryptionSignature.loadOrSign( + this.instance, + addresses as `0x${string}`[], + this.signer, + this.signatureStorage + ); + } catch (error) { + throw new Error( + `Failed to load or create decryption signature: ${error instanceof Error ? error.message : String(error)}` + ); + } + } +} + diff --git a/packages/fhevm-sdk/src/core/EncryptionManager.ts b/packages/fhevm-sdk/src/core/EncryptionManager.ts new file mode 100644 index 00000000..38b32886 --- /dev/null +++ b/packages/fhevm-sdk/src/core/EncryptionManager.ts @@ -0,0 +1,323 @@ +import { FhevmInstance } from "../fhevmTypes.js"; +import { RelayerEncryptedInput } from "@zama-fhe/relayer-sdk/web"; + +/** + * Result of an encryption operation. + * @public + */ +export interface EncryptResult { + /** Array of encrypted value handles */ + handles: Uint8Array[]; + /** Zero-knowledge proof of correct encryption */ + inputProof: Uint8Array; +} + +/** + * Encryption manager for FHEVM operations. + * + * This class provides a framework-agnostic interface for encrypting data + * using Fully Homomorphic Encryption (FHE) before sending it to smart contracts. + * + * @example + * Basic usage: + * ```typescript + * const manager = new EncryptionManager( + * fhevmInstance, + * '0x1234...', // contract address + * '0x5678...' // user address + * ); + * + * const result = await manager.encrypt((builder) => { + * builder.add32(42); + * builder.add64(BigInt(1000)); + * }); + * ``` + * + * @example + * With contract interaction: + * ```typescript + * const encrypted = await manager.encrypt((builder) => { + * builder.add32(secretValue); + * }); + * + * const params = EncryptionManager.buildParamsFromAbi( + * encrypted, + * contractAbi, + * 'setEncryptedValue' + * ); + * + * await contract.setEncryptedValue(...params); + * ``` + * + * @public + */ +export class EncryptionManager { + /** + * Creates a new EncryptionManager instance. + * + * @param instance - The initialized FHEVM instance + * @param contractAddress - The target smart contract address + * @param userAddress - The user's wallet address + * + * @throws {TypeError} If any required parameter is missing + */ + constructor( + private readonly instance: FhevmInstance, + private readonly contractAddress: string, + private readonly userAddress: string + ) { + if (!instance) { + throw new TypeError('EncryptionManager: instance is required'); + } + if (!contractAddress) { + throw new TypeError('EncryptionManager: contractAddress is required'); + } + if (!userAddress) { + throw new TypeError('EncryptionManager: userAddress is required'); + } + } + + /** + * Encrypts data using the provided builder function. + * + * @param buildFn - Function that receives a builder and adds encrypted values + * @returns Promise resolving to encryption result with handles and proof + * + * @throws {Error} If encryption fails + * + * @example + * Encrypt a single value: + * ```typescript + * const result = await manager.encrypt((builder) => { + * builder.add32(42); + * }); + * ``` + * + * @example + * Encrypt multiple values: + * ```typescript + * const result = await manager.encrypt((builder) => { + * builder.add32(42); + * builder.addBool(true); + * builder.add64(BigInt(1000)); + * }); + * ``` + */ + async encrypt( + buildFn: (builder: RelayerEncryptedInput) => void + ): Promise { + try { + // Create encrypted input builder + const input = this.instance.createEncryptedInput( + this.contractAddress, + this.userAddress + ) as RelayerEncryptedInput; + + // Allow caller to add values to encrypt + buildFn(input); + + // Execute encryption + const encrypted = await input.encrypt(); + + return encrypted; + } catch (error) { + throw new Error( + `Encryption failed: ${error instanceof Error ? error.message : String(error)}` + ); + } + } + + /** + * Checks if encryption is possible with current configuration. + * + * @returns True if all required parameters are available + * + * @example + * ```typescript + * if (manager.canEncrypt()) { + * await manager.encrypt((builder) => builder.add32(42)); + * } else { + * console.log('Cannot encrypt: missing parameters'); + * } + * ``` + */ + canEncrypt(): boolean { + return Boolean( + this.instance && + this.contractAddress && + this.userAddress + ); + } + + /** + * Gets the contract address this manager is configured for. + * + * @returns The contract address + */ + getContractAddress(): string { + return this.contractAddress; + } + + /** + * Gets the user address this manager is configured for. + * + * @returns The user address + */ + getUserAddress(): string { + return this.userAddress; + } + + /** + * Maps Solidity internal type names to RelayerEncryptedInput builder method names. + * + * @param internalType - The Solidity internal type (e.g., 'externalEuint32') + * @returns The corresponding builder method name (e.g., 'add32') + * + * @remarks + * This is useful for dynamically building encrypted inputs based on ABI definitions. + * Falls back to 'add64' for unknown types. + * + * @example + * ```typescript + * const method = EncryptionManager.getEncryptionMethod('externalEuint32'); + * // Returns 'add32' + * + * const result = await manager.encrypt((builder) => { + * (builder as any)[method](value); + * }); + * ``` + */ + static getEncryptionMethod(internalType: string): string { + const methodMap: Record = { + 'externalEbool': 'addBool', + 'externalEuint8': 'add8', + 'externalEuint16': 'add16', + 'externalEuint32': 'add32', + 'externalEuint64': 'add64', + 'externalEuint128': 'add128', + 'externalEuint256': 'add256', + 'externalEaddress': 'addAddress', + }; + + const method = methodMap[internalType]; + + if (!method) { + console.warn( + `EncryptionManager: Unknown internal type '${internalType}', defaulting to 'add64'` + ); + return 'add64'; + } + + return method; + } + + /** + * Converts a Uint8Array or hex string to a 0x-prefixed hex string. + * + * @param value - The value to convert (Uint8Array or string) + * @returns A 0x-prefixed hex string + * + * @example + * From Uint8Array: + * ```typescript + * const hex = EncryptionManager.toHex(new Uint8Array([1, 2, 3])); + * // Returns '0x010203' + * ``` + * + * @example + * From string: + * ```typescript + * const hex = EncryptionManager.toHex('010203'); + * // Returns '0x010203' + * ``` + * + * @example + * Already prefixed: + * ```typescript + * const hex = EncryptionManager.toHex('0x010203'); + * // Returns '0x010203' + * ``` + */ + static toHex(value: Uint8Array | string): `0x${string}` { + if (typeof value === 'string') { + return (value.startsWith('0x') ? value : `0x${value}`) as `0x${string}`; + } + + // Convert Uint8Array to hex + return ('0x' + Buffer.from(value).toString('hex')) as `0x${string}`; + } + + /** + * Builds contract function parameters from encryption result and ABI. + * + * @param enc - The encryption result containing handles and proof + * @param abi - The contract ABI array + * @param functionName - The name of the function to call + * @returns Array of parameters ready to pass to the contract function + * + * @throws {Error} If function is not found in ABI + * + * @remarks + * This helper automatically maps encryption outputs to the correct parameter types + * based on the function's ABI definition. Typically, the first parameter is the + * encrypted handle(s) and the second is the input proof. + * + * @example + * ```typescript + * const encrypted = await manager.encrypt((builder) => builder.add32(42)); + * + * const params = EncryptionManager.buildParamsFromAbi( + * encrypted, + * contractAbi, + * 'setEncryptedValue' + * ); + * + * // Use with ethers.js + * await contract.setEncryptedValue(...params); + * ``` + */ + static buildParamsFromAbi( + enc: EncryptResult, + abi: any[], + functionName: string + ): any[] { + // Find the function in ABI + const fn = abi.find( + (item: any) => item.type === 'function' && item.name === functionName + ); + + if (!fn) { + throw new Error(`Function '${functionName}' not found in contract ABI`); + } + + // Map each input parameter + return fn.inputs.map((input: any, index: number) => { + // First param is typically the handle, rest is proof + const raw = index === 0 ? enc.handles[0] : enc.inputProof; + + // Convert based on Solidity type + switch (input.type) { + case 'bytes32': + case 'bytes': + return EncryptionManager.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( + `EncryptionManager: Unknown ABI parameter type '${input.type}' for function '${functionName}', converting to hex` + ); + return EncryptionManager.toHex(raw); + } + }); + } +} + diff --git a/packages/fhevm-sdk/src/core/index.ts b/packages/fhevm-sdk/src/core/index.ts index 6de1bba3..5cbb29fc 100644 --- a/packages/fhevm-sdk/src/core/index.ts +++ b/packages/fhevm-sdk/src/core/index.ts @@ -1,6 +1,8 @@ -export * from "../internal/fhevm"; -export * from "../internal/RelayerSDKLoader"; -export * from "../internal/PublicKeyStorage"; -export * from "../internal/fhevmTypes"; -export * from "../internal/constants"; - +export * from "../internal/fhevm"; +export * from "../internal/RelayerSDKLoader"; +export * from "../internal/PublicKeyStorage"; +export * from "../internal/fhevmTypes"; +export * from "../internal/constants"; +export * from "./EncryptionManager"; +export * from "./DecryptionManager"; + diff --git a/packages/fhevm-sdk/src/react/useFHEDecrypt.ts b/packages/fhevm-sdk/src/react/useFHEDecrypt.ts index 41a0d982..d9e684af 100644 --- a/packages/fhevm-sdk/src/react/useFHEDecrypt.ts +++ b/packages/fhevm-sdk/src/react/useFHEDecrypt.ts @@ -1,129 +1,262 @@ "use client"; import { useCallback, useMemo, useRef, useState } from "react"; -import { FhevmDecryptionSignature } from "../FhevmDecryptionSignature.js"; +import { DecryptionManager, FHEDecryptRequest, DecryptResult } from "../core/DecryptionManager.js"; import { GenericStringStorage } from "../storage/GenericStringStorage.js"; import { FhevmInstance } from "../fhevmTypes.js"; import { ethers } from "ethers"; -export type FHEDecryptRequest = { handle: string; contractAddress: `0x${string}` }; +// Re-export types for backward compatibility +export type { FHEDecryptRequest, DecryptResult }; +/** + * React hook for FHE decryption operations. + * + * This hook provides a React-friendly interface to decrypt FHE-encrypted data + * from smart contracts. It manages the DecryptionManager lifecycle, handles + * loading states, and provides user-friendly status messages. + * + * @param params - Configuration parameters + * @param params.instance - The FHEVM instance (from useFhevm hook) + * @param params.ethersSigner - The user's ethers signer + * @param params.fhevmDecryptionSignatureStorage - Storage for caching signatures + * @param params.chainId - The blockchain chain ID + * @param params.requests - Array of handles to decrypt + * + * @returns Object containing decryption status, methods, and results + * + * @remarks + * This hook automatically manages: + * - EIP-712 signature creation and caching + * - Loading states during decryption + * - Error handling and user feedback + * - Preventing duplicate decryption requests + * + * The hook uses internal state tracking to ensure decryptions are not + * triggered multiple times simultaneously. + * + * @example + * Basic usage: + * ```tsx + * import { useFHEDecrypt } from '@fhevm-sdk'; + * + * function MyComponent() { + * const { instance } = useFhevm({ provider, chainId }); + * const { ethersSigner } = useEthersSigner(); + * const { storage } = useInMemoryStorage(); + * + * const requests = useMemo(() => [ + * { handle: counterHandle, contractAddress: '0x...' } + * ], [counterHandle]); + * + * const { + * canDecrypt, + * decrypt, + * isDecrypting, + * results, + * message, + * error + * } = useFHEDecrypt({ + * instance, + * ethersSigner, + * fhevmDecryptionSignatureStorage: storage, + * chainId: 11155111, + * requests + * }); + * + * return ( + *
+ * + * {message &&

{message}

} + * {error &&

{error}

} + * {results[counterHandle] && ( + *

Value: {results[counterHandle].toString()}

+ * )} + *
+ * ); + * } + * ``` + * + * @example + * With automatic updates: + * ```tsx + * const [handle, setHandle] = useState(); + * + * const requests = useMemo(() => { + * if (!handle) return undefined; + * return [{ handle, contractAddress }]; + * }, [handle, contractAddress]); + * + * const { decrypt, results, isDecrypting } = useFHEDecrypt({ + * instance, + * ethersSigner, + * fhevmDecryptionSignatureStorage: storage, + * chainId, + * requests + * }); + * + * // Decrypt automatically when handle changes + * useEffect(() => { + * if (handle && !isDecrypting) { + * decrypt(); + * } + * }, [handle, decrypt, isDecrypting]); + * ``` + * + * @public + */ export const useFHEDecrypt = (params: { + /** The FHEVM instance */ instance: FhevmInstance | undefined; + /** The user's ethers signer */ ethersSigner: ethers.JsonRpcSigner | undefined; + /** Storage for caching decryption signatures */ fhevmDecryptionSignatureStorage: GenericStringStorage; + /** The blockchain chain ID */ chainId: number | undefined; + /** Array of handles to decrypt */ requests: readonly FHEDecryptRequest[] | undefined; }) => { const { instance, ethersSigner, fhevmDecryptionSignatureStorage, chainId, requests } = params; + // React state for UI feedback const [isDecrypting, setIsDecrypting] = useState(false); - const [message, setMessage] = useState(""); - const [results, setResults] = useState>({}); + const [message, setMessage] = useState(''); + const [results, setResults] = useState({}); const [error, setError] = useState(null); - const isDecryptingRef = useRef(isDecrypting); - const lastReqKeyRef = useRef(""); + // Internal state tracking + const isDecryptingRef = useRef(false); + const lastReqKeyRef = useRef(''); + // Create a stable key for the requests to detect changes const requestsKey = useMemo(() => { - if (!requests || requests.length === 0) return ""; + if (!requests || requests.length === 0) return ''; const sorted = [...requests].sort((a, b) => - (a.handle + a.contractAddress).localeCompare(b.handle + b.contractAddress), + (a.handle + a.contractAddress).localeCompare(b.handle + b.contractAddress) ); return JSON.stringify(sorted); }, [requests]); + // Create and memoize DecryptionManager instance + const manager = useMemo(() => { + if (!instance || !ethersSigner || !chainId || !fhevmDecryptionSignatureStorage) { + return null; + } + + try { + return new DecryptionManager( + instance, + ethersSigner, + fhevmDecryptionSignatureStorage, + chainId + ); + } catch (error) { + console.error('useFHEDecrypt: Failed to create DecryptionManager:', error); + return null; + } + }, [instance, ethersSigner, fhevmDecryptionSignatureStorage, chainId]); + + // Check if decryption is possible const canDecrypt = useMemo(() => { - return Boolean(instance && ethersSigner && requests && requests.length > 0 && !isDecrypting); - }, [instance, ethersSigner, requests, isDecrypting]); + return Boolean( + manager?.canDecrypt(requests as any) && + !isDecrypting && + requests && + requests.length > 0 + ); + }, [manager, requests, isDecrypting]); - const decrypt = useCallback(() => { - if (isDecryptingRef.current) return; - if (!instance || !ethersSigner || !requests || requests.length === 0) return; + // Memoized decryption function + const decrypt = useCallback(async () => { + // Prevent concurrent decryption attempts + if (isDecryptingRef.current) { + console.warn('useFHEDecrypt: Decryption already in progress'); + return; + } + if (!manager || !requests || requests.length === 0) { + console.warn('useFHEDecrypt: Cannot decrypt - manager or requests not available'); + return; + } + + // Capture current state for staleness checks const thisChainId = chainId; const thisSigner = ethersSigner; - const thisRequests = requests; + const thisRequestsKey = requestsKey; - // Capture the current requests key to avoid false "stale" detection on first run + // Update the last request key to avoid false "stale" detection lastReqKeyRef.current = requestsKey; + // Set decrypting state isDecryptingRef.current = true; setIsDecrypting(true); - setMessage("Start decrypt"); + setMessage('Starting decryption...'); setError(null); - const run = async () => { + try { + // Helper function to check if state has changed const isStale = () => - 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( - instance, - uniqueAddresses as `0x${string}`[], - ethersSigner, - fhevmDecryptionSignatureStorage, - ); - - if (!sig) { - setMessage("Unable to build FHEVM decryption signature"); - setError("SIGNATURE_ERROR: Failed to create decryption signature"); - return; - } - - 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"); - return; - } - - setMessage("FHEVM userDecrypt completed!"); - - if (isStale()) { - setMessage("Ignore FHEVM decryption"); - 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"); - } finally { - isDecryptingRef.current = false; - setIsDecrypting(false); - lastReqKeyRef.current = requestsKey; + thisChainId !== chainId || + thisSigner !== ethersSigner || + thisRequestsKey !== lastReqKeyRef.current; + + // Check staleness before starting + if (isStale()) { + setMessage('Decryption cancelled: parameters changed'); + return; } - }; - run(); - }, [instance, ethersSigner, fhevmDecryptionSignatureStorage, chainId, requests, requestsKey]); + setMessage('Loading decryption signature...'); + + // Execute decryption + const decryptedResults = await manager.decrypt(requests as FHEDecryptRequest[]); + + // Check staleness after decryption + if (isStale()) { + setMessage('Decryption completed but results discarded: parameters changed'); + return; + } + + // Update results + setResults(decryptedResults); + setMessage('Decryption completed successfully!'); + setError(null); + } catch (err) { + // Handle errors + const errorObj = err as Error; + const errorMessage = errorObj.message || String(err); + + console.error('useFHEDecrypt: Decryption failed:', errorObj); + + setError(errorMessage); + setMessage('Decryption failed'); + setResults({}); + } finally { + // Reset decrypting state + isDecryptingRef.current = false; + setIsDecrypting(false); + } + }, [manager, requests, chainId, ethersSigner, requestsKey]); - return { canDecrypt, decrypt, isDecrypting, message, results, error, setMessage, setError } as const; -}; \ No newline at end of file + return { + /** Whether decryption is currently possible */ + canDecrypt, + /** Function to trigger decryption */ + decrypt, + /** Whether decryption is currently in progress */ + isDecrypting, + /** Status message for user feedback */ + message, + /** Decryption results mapping handles to values */ + results, + /** Error message if decryption failed */ + error, + /** Function to manually set status message */ + setMessage, + /** Function to manually set error message */ + setError, + } as const; +}; diff --git a/packages/fhevm-sdk/src/react/useFHEEncryption.ts b/packages/fhevm-sdk/src/react/useFHEEncryption.ts index 01fbfc03..09bc7338 100644 --- a/packages/fhevm-sdk/src/react/useFHEEncryption.ts +++ b/packages/fhevm-sdk/src/react/useFHEEncryption.ts @@ -1,101 +1,181 @@ "use client"; -import { useCallback, useMemo } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { FhevmInstance } from "../fhevmTypes.js"; +import { EncryptionManager, EncryptResult } from "../core/EncryptionManager.js"; import { RelayerEncryptedInput } from "@zama-fhe/relayer-sdk/web"; import { ethers } from "ethers"; -export type EncryptResult = { - handles: Uint8Array[]; - inputProof: Uint8Array; -}; +/** + * Re-export utility functions from EncryptionManager for backward compatibility. + * @deprecated Import directly from EncryptionManager instead + */ +export const getEncryptionMethod = EncryptionManager.getEncryptionMethod; -// 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; - } -}; +/** + * Re-export toHex utility function for backward compatibility. + * @deprecated Import directly from EncryptionManager instead + */ +export const toHex = EncryptionManager.toHex; -// 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}`; -}; +/** + * Re-export buildParamsFromAbi utility function for backward compatibility. + * @deprecated Import directly from EncryptionManager instead + */ +export const buildParamsFromAbi = EncryptionManager.buildParamsFromAbi; -// 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 types +export type { EncryptResult }; +/** + * React hook for FHE encryption operations. + * + * This hook provides a React-friendly interface to encrypt data using + * Fully Homomorphic Encryption before sending it to smart contracts. + * It manages the EncryptionManager lifecycle and provides memoized functions. + * + * @param params - Configuration parameters + * @param params.instance - The FHEVM instance (from useFhevm hook) + * @param params.ethersSigner - The user's ethers signer + * @param params.contractAddress - The target contract address + * + * @returns Object containing encryption status and methods + * + * @example + * Basic usage: + * ```tsx + * import { useFHEEncryption } from '@fhevm-sdk'; + * + * function MyComponent() { + * const { instance } = useFhevm({ provider, chainId }); + * const { ethersSigner } = useEthersSigner(); + * + * const { canEncrypt, encryptWith } = useFHEEncryption({ + * instance, + * ethersSigner, + * contractAddress: '0x1234...' + * }); + * + * const handleEncrypt = async () => { + * const result = await encryptWith((builder) => { + * builder.add32(42); + * }); + * console.log('Encrypted:', result); + * }; + * + * return ( + * + * ); + * } + * ``` + * + * @example + * With contract interaction: + * ```tsx + * const { canEncrypt, encryptWith } = useFHEEncryption({ + * instance, + * ethersSigner, + * contractAddress + * }); + * + * const handleIncrement = async () => { + * const encrypted = await encryptWith((builder) => { + * builder.add32(1); + * }); + * + * const params = buildParamsFromAbi(encrypted, contractAbi, 'increment'); + * await contract.increment(...params); + * }; + * ``` + * + * @public + */ export const useFHEEncryption = (params: { + /** The FHEVM instance */ instance: FhevmInstance | undefined; + /** The user's ethers signer */ ethersSigner: ethers.JsonRpcSigner | undefined; + /** The target contract address */ contractAddress: `0x${string}` | undefined; }) => { const { instance, ethersSigner, contractAddress } = params; + // Track user address from signer + const [userAddress, setUserAddress] = useState(); + + // Fetch user address when signer changes + useEffect(() => { + if (!ethersSigner) { + setUserAddress(undefined); + return; + } + + let cancelled = false; + + ethersSigner + .getAddress() + .then((address) => { + if (!cancelled) { + setUserAddress(address); + } + }) + .catch((error) => { + console.error('useFHEEncryption: Failed to get user address:', error); + if (!cancelled) { + setUserAddress(undefined); + } + }); + + return () => { + cancelled = true; + }; + }, [ethersSigner]); + + // Create and memoize EncryptionManager instance + const manager = useMemo(() => { + if (!instance || !contractAddress || !userAddress) { + return null; + } + + try { + return new EncryptionManager(instance, contractAddress, userAddress); + } catch (error) { + console.error('useFHEEncryption: Failed to create EncryptionManager:', error); + return null; + } + }, [instance, contractAddress, userAddress]); + + // Check if encryption is possible const canEncrypt = useMemo( - () => Boolean(instance && ethersSigner && contractAddress), - [instance, ethersSigner, contractAddress], + () => Boolean(manager?.canEncrypt()), + [manager] ); + // Memoized encryption function const encryptWith = useCallback( - async (buildFn: (builder: RelayerEncryptedInput) => void): Promise => { - 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; + async ( + buildFn: (builder: RelayerEncryptedInput) => void + ): Promise => { + if (!manager) { + console.warn('useFHEEncryption: EncryptionManager not available'); + return undefined; + } + + try { + const result = await manager.encrypt(buildFn); + return result; + } catch (error) { + console.error('useFHEEncryption: Encryption failed:', error); + throw error; + } }, - [instance, ethersSigner, contractAddress], + [manager] ); return { canEncrypt, encryptWith, } as const; -}; \ No newline at end of file +};