diff --git a/packages/fhevm-sdk/src/core/FhevmClient.ts b/packages/fhevm-sdk/src/core/FhevmClient.ts new file mode 100644 index 00000000..cb6691bc --- /dev/null +++ b/packages/fhevm-sdk/src/core/FhevmClient.ts @@ -0,0 +1,411 @@ +import { Eip1193Provider } from "ethers"; +import type { FhevmInstance } from "../fhevmTypes.js"; +import { createFhevmInstance } from "../internal/fhevm.js"; + +/** + * Status of the FHEVM client initialization process. + * @public + */ +export type FhevmClientStatus = "idle" | "loading" | "ready" | "error"; + +/** + * Configuration options for FhevmClient. + * @public + */ +export interface FhevmClientConfig { + /** Web3 provider (EIP-1193 or RPC URL) */ + provider: string | Eip1193Provider; + /** Blockchain chain ID */ + chainId?: number; + /** Mock chain RPC URLs for local development */ + mockChains?: Record; + /** Enable/disable client (default: true) */ + enabled?: boolean; +} + +/** + * Event handler for status changes. + * @public + */ +export type FhevmClientStatusHandler = (status: FhevmClientStatus) => void; + +/** + * Event handler for error events. + * @public + */ +export type FhevmClientErrorHandler = (error: Error) => void; + +/** + * FHEVM Client for managing FHEVM instance lifecycle. + * + * This class provides a framework-agnostic way to create and manage FHEVM instances. + * It handles initialization, cleanup, and status tracking without any React dependencies. + * + * @remarks + * The FhevmClient manages a single FHEVM instance at a time. When you change the provider + * or chain ID, the old instance is automatically cleaned up and a new one is created. + * + * @example + * Basic usage: + * ```typescript + * const client = new FhevmClient({ + * provider: window.ethereum, + * chainId: 11155111 + * }); + * + * client.on('statusChange', (status) => { + * console.log('Status:', status); + * }); + * + * try { + * const instance = await client.initialize(); + * console.log('FHEVM instance ready:', instance); + * } catch (error) { + * console.error('Failed to initialize:', error); + * } + * ``` + * + * @example + * With local development: + * ```typescript + * const client = new FhevmClient({ + * provider: 'http://localhost:8545', + * chainId: 31337, + * mockChains: { 31337: 'http://localhost:8545' } + * }); + * + * await client.initialize(); + * ``` + * + * @example + * Cleanup when done: + * ```typescript + * // When you're done with the client + * client.destroy(); + * ``` + * + * @public + */ +export class FhevmClient { + private instance: FhevmInstance | null = null; + private status: FhevmClientStatus = "idle"; + private error: Error | null = null; + private abortController: AbortController | null = null; + private config: FhevmClientConfig; + private statusHandlers: Set = new Set(); + private errorHandlers: Set = new Set(); + + /** + * Creates a new FhevmClient instance. + * + * @param config - Configuration options + * + * @example + * ```typescript + * const client = new FhevmClient({ + * provider: window.ethereum, + * chainId: 11155111, + * enabled: true + * }); + * ``` + */ + constructor(config: FhevmClientConfig) { + this.config = { ...config, enabled: config.enabled ?? true }; + } + + /** + * Initializes the FHEVM instance. + * + * @returns Promise resolving to the initialized FHEVM instance + * @throws {Error} If initialization fails or client is disabled + * + * @remarks + * This method is idempotent - calling it multiple times will cancel previous + * initialization attempts and start a new one. + * + * @example + * ```typescript + * try { + * const instance = await client.initialize(); + * // Use instance for encryption/decryption + * } catch (error) { + * console.error('Initialization failed:', error); + * } + * ``` + */ + async initialize(): Promise { + // Check if enabled + if (this.config.enabled === false) { + throw new Error("FhevmClient is disabled"); + } + + // Cancel any previous initialization + this.cancelCurrentOperation(); + + // Create new abort controller + this.abortController = new AbortController(); + const signal = this.abortController.signal; + + // Update status + this.setStatus("loading"); + this.error = null; + + try { + // Create FHEVM instance + const instance = await createFhevmInstance({ + provider: this.config.provider as any, + mockChains: this.config.mockChains, + signal, + onStatusChange: (s) => { + console.log(`[FhevmClient] createFhevmInstance status: ${s}`); + }, + }); + + // Check if aborted during creation + if (signal.aborted) { + throw new Error("Initialization was cancelled"); + } + + // Store instance + this.instance = instance; + this.setStatus("ready"); + + return instance; + } catch (error) { + // Don't set error state if we were cancelled + if (!signal.aborted) { + const err = error instanceof Error ? error : new Error(String(error)); + this.error = err; + this.setStatus("error"); + this.emitError(err); + } + throw error; + } + } + + /** + * Gets the current FHEVM instance. + * + * @returns The FHEVM instance, or null if not initialized + * + * @example + * ```typescript + * const instance = client.getInstance(); + * if (instance) { + * // Use instance + * } else { + * console.log('Not initialized yet'); + * } + * ``` + */ + getInstance(): FhevmInstance | null { + return this.instance; + } + + /** + * Gets the current initialization status. + * + * @returns The current status + * + * @example + * ```typescript + * const status = client.getStatus(); + * if (status === 'ready') { + * // Client is ready to use + * } + * ``` + */ + getStatus(): FhevmClientStatus { + return this.status; + } + + /** + * Gets the last error that occurred. + * + * @returns The error, or null if no error + * + * @example + * ```typescript + * const error = client.getError(); + * if (error) { + * console.error('Last error:', error.message); + * } + * ``` + */ + getError(): Error | null { + return this.error; + } + + /** + * Checks if the client is currently initializing. + * + * @returns True if loading, false otherwise + */ + isLoading(): boolean { + return this.status === "loading"; + } + + /** + * Checks if the client is ready to use. + * + * @returns True if ready, false otherwise + */ + isReady(): boolean { + return this.status === "ready" && this.instance !== null; + } + + /** + * Updates the client configuration and re-initializes if needed. + * + * @param config - New configuration (partial update) + * @returns Promise resolving to the new instance + * + * @remarks + * This will cancel any ongoing initialization and start a new one with + * the updated configuration. + * + * @example + * Switch to a different network: + * ```typescript + * await client.updateConfig({ + * chainId: 1, // Switch to mainnet + * provider: newProvider + * }); + * ``` + */ + async updateConfig(config: Partial): Promise { + this.config = { ...this.config, ...config }; + return this.initialize(); + } + + /** + * Refreshes the FHEVM instance by re-initializing. + * + * @returns Promise resolving to the new instance + * + * @remarks + * This is useful when you need to force a re-initialization without + * changing the configuration. + * + * @example + * ```typescript + * // Force refresh the instance + * await client.refresh(); + * ``` + */ + async refresh(): Promise { + return this.initialize(); + } + + /** + * Destroys the client and cleans up all resources. + * + * @remarks + * After calling this method, the client cannot be used anymore. + * You must create a new FhevmClient instance if you need one again. + * + * @example + * ```typescript + * // Clean up when done + * client.destroy(); + * ``` + */ + destroy(): void { + this.cancelCurrentOperation(); + this.instance = null; + this.error = null; + this.setStatus("idle"); + this.statusHandlers.clear(); + this.errorHandlers.clear(); + } + + /** + * Registers a status change event handler. + * + * @param event - The event type (currently only 'statusChange') + * @param handler - The handler function + * + * @example + * ```typescript + * client.on('statusChange', (status) => { + * console.log('Status changed:', status); + * }); + * ``` + */ + on(event: "statusChange", handler: FhevmClientStatusHandler): void; + on(event: "error", handler: FhevmClientErrorHandler): void; + on(event: string, handler: any): void { + if (event === "statusChange") { + this.statusHandlers.add(handler); + } else if (event === "error") { + this.errorHandlers.add(handler); + } + } + + /** + * Unregisters a status change event handler. + * + * @param event - The event type (currently only 'statusChange') + * @param handler - The handler function to remove + * + * @example + * ```typescript + * const handler = (status) => console.log(status); + * client.on('statusChange', handler); + * // Later... + * client.off('statusChange', handler); + * ``` + */ + off(event: "statusChange", handler: FhevmClientStatusHandler): void; + off(event: "error", handler: FhevmClientErrorHandler): void; + off(event: string, handler: any): void { + if (event === "statusChange") { + this.statusHandlers.delete(handler); + } else if (event === "error") { + this.errorHandlers.delete(handler); + } + } + + /** + * Cancels any current initialization operation. + * @private + */ + private cancelCurrentOperation(): void { + if (this.abortController) { + this.abortController.abort(); + this.abortController = null; + } + } + + /** + * Updates the status and notifies listeners. + * @private + */ + private setStatus(status: FhevmClientStatus): void { + if (this.status !== status) { + this.status = status; + this.statusHandlers.forEach((handler) => { + try { + handler(status); + } catch (error) { + console.error("Error in status change handler:", error); + } + }); + } + } + + /** + * Emits an error event to listeners. + * @private + */ + private emitError(error: Error): void { + this.errorHandlers.forEach((handler) => { + try { + handler(error); + } catch (err) { + console.error("Error in error handler:", err); + } + }); + } +} + diff --git a/packages/fhevm-sdk/src/core/index.ts b/packages/fhevm-sdk/src/core/index.ts index 6de1bba3..abfb6794 100644 --- a/packages/fhevm-sdk/src/core/index.ts +++ b/packages/fhevm-sdk/src/core/index.ts @@ -3,4 +3,4 @@ export * from "../internal/RelayerSDKLoader"; export * from "../internal/PublicKeyStorage"; export * from "../internal/fhevmTypes"; export * from "../internal/constants"; - +export * from "./FhevmClient"; diff --git a/packages/fhevm-sdk/src/react/useFhevm.tsx b/packages/fhevm-sdk/src/react/useFhevm.tsx index 4f31bdd6..67a21a77 100644 --- a/packages/fhevm-sdk/src/react/useFhevm.tsx +++ b/packages/fhevm-sdk/src/react/useFhevm.tsx @@ -1,128 +1,246 @@ import { useCallback, useEffect, useRef, useState } from "react"; -import type { FhevmInstance } from "../fhevmTypes.js"; -import { createFhevmInstance } from "../internal/fhevm.js"; import { ethers } from "ethers"; - -function _assert(condition: boolean, message?: string): asserts condition { - if (!condition) { - const m = message ? `Assertion failed: ${message}` : `Assertion failed.`; - throw new Error(m); - } -} - -export type FhevmGoState = "idle" | "loading" | "ready" | "error"; - +import type { FhevmInstance } from "../fhevmTypes.js"; +import { FhevmClient, type FhevmClientStatus } from "../core/FhevmClient.js"; + +/** + * Legacy type alias for backward compatibility + * @deprecated Use FhevmClientStatus from core instead + */ +export type FhevmGoState = FhevmClientStatus; + +/** + * React hook for managing FHEVM instances. + * + * This hook provides a React-friendly interface to the FhevmClient, + * handling lifecycle management, state updates, and cleanup automatically. + * + * @remarks + * The hook will automatically initialize the FHEVM instance when the provider + * and chainId are available (and `enabled` is true). It will clean up resources + * when the component unmounts or when dependencies change. + * + * **Note**: This hook now uses the refactored FhevmClient internally for better + * stability and testability while maintaining the same API. + * + * @param parameters - Configuration options + * @returns FHEVM instance state and control functions + * + * @example + * Basic usage: + * ```typescript + * function MyComponent() { + * const { instance, status, error, refresh } = useFhevm({ + * provider: window.ethereum, + * chainId: 11155111 + * }); + * + * if (status === 'loading') return
Loading...
; + * if (error) return
Error: {error.message}
; + * if (!instance) return
Not initialized
; + * + * return
Ready! Instance: {instance}
; + * } + * ``` + * + * @example + * With manual control: + * ```typescript + * function MyComponent() { + * const { instance, status, refresh } = useFhevm({ + * provider: window.ethereum, + * chainId: 11155111, + * enabled: false // Don't auto-initialize + * }); + * + * const handleInit = async () => { + * refresh(); // Manually trigger initialization + * }; + * + * return ; + * } + * ``` + * + * @example + * With local development: + * ```typescript + * function MyComponent() { + * const { instance, status } = useFhevm({ + * provider: window.ethereum, + * chainId: 31337, + * initialMockChains: { 31337: 'http://localhost:8545' } + * }); + * + * // Hook automatically detects local chain and uses mock mode + * return
Status: {status}
; + * } + * ``` + * + * @public + */ export function useFhevm(parameters: { + /** Web3 provider (EIP-1193 or RPC URL) */ provider: string | ethers.Eip1193Provider | undefined; + /** Blockchain chain ID */ chainId: number | undefined; + /** Enable auto-initialization (default: true) */ enabled?: boolean; + /** Mock chain RPC URLs for local development */ initialMockChains?: Readonly>; }): { + /** The initialized FHEVM instance, or undefined */ instance: FhevmInstance | undefined; + /** Function to manually refresh/re-initialize the instance */ refresh: () => void; + /** Last error that occurred, or undefined */ error: Error | undefined; + /** Current initialization status */ status: FhevmGoState; } { const { provider, chainId, initialMockChains, enabled = true } = parameters; - const [instance, _setInstance] = useState(undefined); - const [status, _setStatus] = useState("idle"); - const [error, _setError] = useState(undefined); - const [_isRunning, _setIsRunning] = useState(enabled); - const [_providerChanged, _setProviderChanged] = useState(0); - const _abortControllerRef = useRef(null); - const _providerRef = useRef(provider); - const _chainIdRef = useRef(chainId); - const _mockChainsRef = useRef | undefined>(initialMockChains as any); - + // State + const [instance, setInstance] = useState(undefined); + const [status, setStatus] = useState("idle"); + const [error, setError] = useState(undefined); + + // Ref to hold the client instance + const clientRef = useRef(null); + + // Track previous values to detect changes + const prevValuesRef = useRef<{ + provider: string | ethers.Eip1193Provider | undefined; + chainId: number | undefined; + enabled: boolean; + }>({ provider: undefined, chainId: undefined, enabled: false }); + + /** + * Manual refresh function + */ const refresh = useCallback(() => { - if (_abortControllerRef.current) { - _providerRef.current = undefined; - _chainIdRef.current = undefined; - - _abortControllerRef.current.abort(); - _abortControllerRef.current = null; - } - - _providerRef.current = provider; - _chainIdRef.current = chainId; - - _setInstance(undefined); - _setError(undefined); - _setStatus("idle"); - - if (provider !== undefined) { - _setProviderChanged(prev => prev + 1); + if (!clientRef.current || !enabled || !provider) { + return; } - }, [provider, chainId]); - - useEffect(() => { - refresh(); - }, [refresh]); - - useEffect(() => { - _setIsRunning(enabled); - }, [enabled]); + clientRef.current + .refresh() + .then((inst) => { + setInstance(inst); + setError(undefined); + }) + .catch((err) => { + // Ignore cancellation errors silently + if (err?.message?.includes("cancelled") || err?.message?.includes("Initialization was cancelled")) { + return; + } + setError(err instanceof Error ? err : new Error(String(err))); + }); + }, [enabled, provider]); + + /** + * Initialize or update the client when parameters change + */ useEffect(() => { - if (_isRunning === false) { - if (_abortControllerRef.current) { - _abortControllerRef.current.abort(); - _abortControllerRef.current = null; - } - _setInstance(undefined); - _setError(undefined); - _setStatus("idle"); + // Check if parameters changed + const prevValues = prevValuesRef.current; + const hasChanges = + prevValues.provider !== provider || + prevValues.chainId !== chainId || + prevValues.enabled !== enabled; + + if (!hasChanges) { return; } - if (_isRunning === true) { - if (_providerRef.current === undefined) { - _setInstance(undefined); - _setError(undefined); - _setStatus("idle"); - return; - } + // Update tracked values + prevValuesRef.current = { provider, chainId, enabled }; - if (!_abortControllerRef.current) { - _abortControllerRef.current = new AbortController(); + // If not enabled or missing provider, clean up + if (!enabled || !provider) { + if (clientRef.current) { + clientRef.current.destroy(); + clientRef.current = null; } + setInstance(undefined); + setStatus("idle"); + setError(undefined); + return; + } - _assert(!_abortControllerRef.current.signal.aborted, "!controllerRef.current.signal.aborted"); - - _setStatus("loading"); - _setError(undefined); - - const thisSignal = _abortControllerRef.current.signal; - const thisProvider = _providerRef.current; - const thisRpcUrlsByChainId = _mockChainsRef.current as any; - - createFhevmInstance({ - signal: thisSignal, - provider: thisProvider as any, - mockChains: thisRpcUrlsByChainId as any, - onStatusChange: s => console.log(`[useFhevm] createFhevmInstance status changed: ${s}`), - }) - .then(i => { - if (thisSignal.aborted) return; - _assert(thisProvider === _providerRef.current, "thisProvider === _providerRef.current"); - - _setInstance(i); - _setError(undefined); - _setStatus("ready"); + // Create or update client + if (!clientRef.current) { + // Create new client + const client = new FhevmClient({ + provider, + chainId, + mockChains: initialMockChains as Record, + enabled, + }); + + // Set up event listeners + client.on("statusChange", (s) => { + setStatus(s); + }); + + client.on("error", (err) => { + setError(err); + }); + + clientRef.current = client; + + // Initialize + client + .initialize() + .then((inst) => { + setInstance(inst); + setError(undefined); }) - .catch(e => { - if (thisSignal.aborted) return; - - _assert(thisProvider === _providerRef.current, "thisProvider === _providerRef.current"); - - _setInstance(undefined); - _setError(e as any); - _setStatus("error"); + .catch((err) => { + // Ignore cancellation errors silently + if (err?.message?.includes("cancelled") || err?.message?.includes("Initialization was cancelled")) { + return; + } + setError(err instanceof Error ? err : new Error(String(err))); + }); + } else { + // Update existing client config + clientRef.current + .updateConfig({ + provider, + chainId, + mockChains: initialMockChains as Record, + enabled, + }) + .then((inst) => { + setInstance(inst); + setError(undefined); + }) + .catch((err) => { + // Ignore cancellation errors silently + if (err?.message?.includes("cancelled") || err?.message?.includes("Initialization was cancelled")) { + return; + } + setError(err instanceof Error ? err : new Error(String(err))); }); } - }, [_isRunning, _providerChanged]); + }, [provider, chainId, enabled, initialMockChains]); - return { instance, refresh, error, status }; + /** + * Cleanup on unmount + */ + useEffect(() => { + return () => { + if (clientRef.current) { + clientRef.current.destroy(); + clientRef.current = null; + } + }; + }, []); + + return { + instance, + refresh, + error, + status, + }; } - diff --git a/packages/fhevm-sdk/test/FhevmClient.test.ts b/packages/fhevm-sdk/test/FhevmClient.test.ts new file mode 100644 index 00000000..26ebf9c2 --- /dev/null +++ b/packages/fhevm-sdk/test/FhevmClient.test.ts @@ -0,0 +1,337 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { FhevmClient } from "../src/core/FhevmClient"; + +// Mock the createFhevmInstance function +vi.mock("../src/internal/fhevm", () => ({ + createFhevmInstance: vi.fn(), +})); + +import { createFhevmInstance } from "../src/internal/fhevm"; + +describe("FhevmClient", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe("constructor", () => { + it("should create a client with default enabled=true", () => { + const client = new FhevmClient({ + provider: "http://localhost:8545", + chainId: 31337, + }); + + expect(client).toBeDefined(); + expect(client.getStatus()).toBe("idle"); + expect(client.getInstance()).toBeNull(); + expect(client.getError()).toBeNull(); + }); + + it("should accept disabled config", () => { + const client = new FhevmClient({ + provider: "http://localhost:8545", + chainId: 31337, + enabled: false, + }); + + expect(client).toBeDefined(); + expect(client.getStatus()).toBe("idle"); + }); + }); + + describe("initialize", () => { + it("should initialize successfully", async () => { + const mockInstance = { id: "mock-instance" }; + vi.mocked(createFhevmInstance).mockResolvedValue(mockInstance as any); + + const client = new FhevmClient({ + provider: "http://localhost:8545", + chainId: 31337, + }); + + const instance = await client.initialize(); + + expect(instance).toBe(mockInstance); + expect(client.getStatus()).toBe("ready"); + expect(client.getInstance()).toBe(mockInstance); + expect(client.getError()).toBeNull(); + expect(client.isReady()).toBe(true); + }); + + it("should handle initialization errors", async () => { + const mockError = new Error("Initialization failed"); + vi.mocked(createFhevmInstance).mockRejectedValue(mockError); + + const client = new FhevmClient({ + provider: "http://localhost:8545", + chainId: 31337, + }); + + await expect(client.initialize()).rejects.toThrow("Initialization failed"); + + expect(client.getStatus()).toBe("error"); + expect(client.getInstance()).toBeNull(); + expect(client.getError()).toBe(mockError); + expect(client.isReady()).toBe(false); + }); + + it("should throw error if disabled", async () => { + const client = new FhevmClient({ + provider: "http://localhost:8545", + chainId: 31337, + enabled: false, + }); + + await expect(client.initialize()).rejects.toThrow("FhevmClient is disabled"); + }); + + it("should cancel previous initialization when called multiple times", async () => { + let resolveCount = 0; + vi.mocked(createFhevmInstance).mockImplementation( + () => + new Promise((resolve) => { + setTimeout(() => { + resolveCount++; + resolve({ id: `instance-${resolveCount}` } as any); + }, 100); + }) + ); + + const client = new FhevmClient({ + provider: "http://localhost:8545", + chainId: 31337, + }); + + // Start first initialization + const promise1 = client.initialize(); + + // Start second initialization (should cancel first) + const promise2 = client.initialize(); + + // First should be cancelled + await expect(promise1).rejects.toThrow("cancelled"); + + // Second should succeed + const instance = await promise2; + expect(instance).toBeDefined(); + expect(client.isReady()).toBe(true); + }); + + it("should update status to loading during initialization", async () => { + const statusChanges: string[] = []; + + vi.mocked(createFhevmInstance).mockImplementation( + () => + new Promise((resolve) => { + setTimeout(() => { + resolve({ id: "mock-instance" } as any); + }, 50); + }) + ); + + const client = new FhevmClient({ + provider: "http://localhost:8545", + chainId: 31337, + }); + + client.on("statusChange", (status) => { + statusChanges.push(status); + }); + + await client.initialize(); + + expect(statusChanges).toContain("loading"); + expect(statusChanges).toContain("ready"); + expect(client.isLoading()).toBe(false); + }); + }); + + describe("event handlers", () => { + it("should emit statusChange events", async () => { + const mockInstance = { id: "mock-instance" }; + vi.mocked(createFhevmInstance).mockResolvedValue(mockInstance as any); + + const statusChanges: string[] = []; + const client = new FhevmClient({ + provider: "http://localhost:8545", + chainId: 31337, + }); + + client.on("statusChange", (status) => { + statusChanges.push(status); + }); + + await client.initialize(); + + expect(statusChanges).toEqual(["loading", "ready"]); + }); + + it("should emit error events", async () => { + const mockError = new Error("Test error"); + vi.mocked(createFhevmInstance).mockRejectedValue(mockError); + + const errors: Error[] = []; + const client = new FhevmClient({ + provider: "http://localhost:8545", + chainId: 31337, + }); + + client.on("error", (error) => { + errors.push(error); + }); + + await expect(client.initialize()).rejects.toThrow(); + + expect(errors).toHaveLength(1); + expect(errors[0]).toBe(mockError); + }); + + it("should allow removing event handlers", async () => { + const mockInstance = { id: "mock-instance" }; + vi.mocked(createFhevmInstance).mockResolvedValue(mockInstance as any); + + const statusChanges: string[] = []; + const client = new FhevmClient({ + provider: "http://localhost:8545", + chainId: 31337, + }); + + const handler = (status: string) => { + statusChanges.push(status); + }; + + client.on("statusChange", handler); + client.off("statusChange", handler); + + await client.initialize(); + + expect(statusChanges).toHaveLength(0); + }); + }); + + describe("updateConfig", () => { + it("should update config and re-initialize", async () => { + const mockInstance1 = { id: "instance-1" }; + const mockInstance2 = { id: "instance-2" }; + vi.mocked(createFhevmInstance) + .mockResolvedValueOnce(mockInstance1 as any) + .mockResolvedValueOnce(mockInstance2 as any); + + const client = new FhevmClient({ + provider: "http://localhost:8545", + chainId: 31337, + }); + + await client.initialize(); + expect(client.getInstance()).toBe(mockInstance1); + + await client.updateConfig({ + chainId: 11155111, + }); + + expect(client.getInstance()).toBe(mockInstance2); + }); + }); + + describe("refresh", () => { + it("should re-initialize with same config", async () => { + const mockInstance1 = { id: "instance-1" }; + const mockInstance2 = { id: "instance-2" }; + vi.mocked(createFhevmInstance) + .mockResolvedValueOnce(mockInstance1 as any) + .mockResolvedValueOnce(mockInstance2 as any); + + const client = new FhevmClient({ + provider: "http://localhost:8545", + chainId: 31337, + }); + + await client.initialize(); + expect(client.getInstance()).toBe(mockInstance1); + + await client.refresh(); + expect(client.getInstance()).toBe(mockInstance2); + }); + }); + + describe("destroy", () => { + it("should clean up all resources", async () => { + const mockInstance = { id: "mock-instance" }; + vi.mocked(createFhevmInstance).mockResolvedValue(mockInstance as any); + + const statusChanges: string[] = []; + const client = new FhevmClient({ + provider: "http://localhost:8545", + chainId: 31337, + }); + + client.on("statusChange", (status) => { + statusChanges.push(status); + }); + + await client.initialize(); + expect(client.getInstance()).toBe(mockInstance); + expect(client.getStatus()).toBe("ready"); + + client.destroy(); + + expect(client.getInstance()).toBeNull(); + expect(client.getStatus()).toBe("idle"); + expect(client.getError()).toBeNull(); + + // Event handlers should be cleared + const initialLength = statusChanges.length; + await client.initialize().catch(() => {}); + expect(statusChanges.length).toBe(initialLength); // No new events + }); + + it("should cancel ongoing initialization", async () => { + vi.mocked(createFhevmInstance).mockImplementation( + () => + new Promise((resolve) => { + setTimeout(() => { + resolve({ id: "instance" } as any); + }, 100); + }) + ); + + const client = new FhevmClient({ + provider: "http://localhost:8545", + chainId: 31337, + }); + + const promise = client.initialize(); + client.destroy(); + + await expect(promise).rejects.toThrow("cancelled"); + }); + }); + + describe("getters", () => { + it("should return correct status values", async () => { + const mockInstance = { id: "mock-instance" }; + vi.mocked(createFhevmInstance).mockResolvedValue(mockInstance as any); + + const client = new FhevmClient({ + provider: "http://localhost:8545", + chainId: 31337, + }); + + expect(client.getStatus()).toBe("idle"); + expect(client.isLoading()).toBe(false); + expect(client.isReady()).toBe(false); + + const promise = client.initialize(); + expect(client.isLoading()).toBe(true); + + await promise; + expect(client.getStatus()).toBe("ready"); + expect(client.isLoading()).toBe(false); + expect(client.isReady()).toBe(true); + }); + }); +}); +