From fc721d809f35107e76fb60a30a52de4927ddeb4c Mon Sep 17 00:00:00 2001 From: Cedoor Date: Fri, 10 Oct 2025 14:35:19 +0100 Subject: [PATCH 1/2] refactor: re-organize client template --- pnpm-lock.yaml | 3 + templates/default/client/package.json | 1 + templates/default/client/src/App.tsx | 12 +- .../client/src/context/WizardContext.tsx | 175 ++++ .../default/client/src/pages/WizardRoutes.tsx | 96 ++ .../default/client/src/pages/WizardSDK.tsx | 981 ------------------ .../src/pages/components/EnvironmentError.tsx | 90 +- .../src/pages/components/ErrorDisplay.tsx | 39 + .../src/pages/components/SDKErrorDisplay.tsx | 28 + .../src/pages/components/StepIndicator.tsx | 48 + .../client/src/pages/steps/ActivateE3.tsx | 153 +++ .../client/src/pages/steps/ConnectWallet.tsx | 51 + .../client/src/pages/steps/EncryptSubmit.tsx | 134 +++ .../client/src/pages/steps/EnterInputs.tsx | 147 +++ .../src/pages/steps/RequestComputation.tsx | 233 +++++ .../client/src/pages/steps/Results.tsx | 91 ++ .../default/client/src/utils/env-config.ts | 51 +- .../default/client/src/utils/sdk-config.ts | 23 + 18 files changed, 1292 insertions(+), 1064 deletions(-) create mode 100644 templates/default/client/src/context/WizardContext.tsx create mode 100644 templates/default/client/src/pages/WizardRoutes.tsx delete mode 100644 templates/default/client/src/pages/WizardSDK.tsx create mode 100644 templates/default/client/src/pages/components/ErrorDisplay.tsx create mode 100644 templates/default/client/src/pages/components/SDKErrorDisplay.tsx create mode 100644 templates/default/client/src/pages/components/StepIndicator.tsx create mode 100644 templates/default/client/src/pages/steps/ActivateE3.tsx create mode 100644 templates/default/client/src/pages/steps/ConnectWallet.tsx create mode 100644 templates/default/client/src/pages/steps/EncryptSubmit.tsx create mode 100644 templates/default/client/src/pages/steps/EnterInputs.tsx create mode 100644 templates/default/client/src/pages/steps/RequestComputation.tsx create mode 100644 templates/default/client/src/pages/steps/Results.tsx create mode 100644 templates/default/client/src/utils/sdk-config.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 66b1e177f3..32efec9797 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -691,6 +691,9 @@ importers: react-dom: specifier: ^18.2.0 version: 18.3.1(react@18.3.1) + react-router-dom: + specifier: ^6.28.0 + version: 6.30.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) viem: specifier: 2.30.6 version: 2.30.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) diff --git a/templates/default/client/package.json b/templates/default/client/package.json index 5fc3aac870..a6bd0d2d98 100644 --- a/templates/default/client/package.json +++ b/templates/default/client/package.json @@ -23,6 +23,7 @@ "connectkit": "^1.9.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-router-dom": "^6.28.0", "viem": "2.30.6", "wagmi": "^2.14.16" }, diff --git a/templates/default/client/src/App.tsx b/templates/default/client/src/App.tsx index 67f183cf5c..8ddbefe852 100644 --- a/templates/default/client/src/App.tsx +++ b/templates/default/client/src/App.tsx @@ -5,14 +5,18 @@ // or FITNESS FOR A PARTICULAR PURPOSE. import React from 'react' -import WizardSDK from './pages/WizardSDK' +import { BrowserRouter } from 'react-router-dom' +import WizardRoutes from './pages/WizardRoutes' +import { WizardProvider } from './context/WizardContext' import './globals.css' const App: React.FC = () => { return ( -
- -
+ + + + + ) } diff --git a/templates/default/client/src/context/WizardContext.tsx b/templates/default/client/src/context/WizardContext.tsx new file mode 100644 index 0000000000..2a239998ec --- /dev/null +++ b/templates/default/client/src/context/WizardContext.tsx @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +import React, { createContext, useContext, useEffect, useMemo, useCallback, useState, ReactNode } from 'react' +import { useAccount } from 'wagmi' +import { useEnclaveSDK, UseEnclaveSDKReturn } from '@enclave-e3/react' +import { getEnclaveSDKConfig } from '@/utils/sdk-config' + +// ============================================================================ +// TYPES & ENUMS +// ============================================================================ + +export enum WizardStep { + CONNECT_WALLET = 1, + REQUEST_COMPUTATION = 2, + ACTIVATE_E3 = 3, + ENTER_INPUTS = 4, + ENCRYPT_SUBMIT = 5, + RESULTS = 6, +} + +export interface E3State { + id: bigint | null + isRequested: boolean + isCommitteePublished: boolean + isActivated: boolean + publicKey: `0x${string}` | null + expiresAt: bigint | null + plaintextOutput: string | null + hasPlaintextOutput: boolean +} + +interface WizardContextType { + currentStep: WizardStep + submittedInputs: { input1: string; input2: string } | null + lastTransactionHash: string | undefined + inputPublishError: string | null + inputPublishSuccess: boolean + result: number | null + e3State: E3State + + // Setters + setCurrentStep: (step: WizardStep) => void + setSubmittedInputs: (inputs: { input1: string; input2: string } | null) => void + setLastTransactionHash: (hash: string | undefined) => void + setInputPublishError: (error: string | null) => void + setInputPublishSuccess: (success: boolean) => void + setResult: (result: number | null) => void + setE3State: (state: E3State | ((prev: E3State) => E3State)) => void + + // Handlers + handleReset: () => void + handleTryAgain: () => void + + // SDK + sdk: UseEnclaveSDKReturn +} + +const WizardContext = createContext(undefined) + +export const useWizard = () => { + const context = useContext(WizardContext) + if (!context) { + throw new Error('useWizard must be used within a WizardProvider') + } + return context +} + +interface WizardProviderProps { + children: ReactNode +} + +/** + * WizardProvider component - Provides the WizardContext to the application + * + * This component is used to provide the WizardContext to the application, + * which is used to manage the wizard state and logic. + */ +export const WizardProvider: React.FC = ({ children }) => { + const { isConnected } = useAccount() + + // Memoize the SDK config to prevent unnecessary re-initializations. + const sdkConfig = useMemo(() => getEnclaveSDKConfig(), []) + const sdk = useEnclaveSDK(sdkConfig) + + const [currentStep, setCurrentStep] = useState(WizardStep.CONNECT_WALLET) + const [submittedInputs, setSubmittedInputs] = useState<{ input1: string; input2: string } | null>(null) + const [lastTransactionHash, setLastTransactionHash] = useState(undefined) + const [inputPublishError, setInputPublishError] = useState(null) + const [inputPublishSuccess, setInputPublishSuccess] = useState(false) + const [result, setResult] = useState(null) + const [e3State, setE3State] = useState({ + id: null, + isRequested: false, + isCommitteePublished: false, + isActivated: false, + publicKey: null, + expiresAt: null, + plaintextOutput: null, + hasPlaintextOutput: false, + }) + + // Auto-advance steps based on state. + useEffect(() => { + if (!isConnected) { + setCurrentStep(WizardStep.CONNECT_WALLET) + } else if (sdk.isInitialized && currentStep === WizardStep.CONNECT_WALLET) { + setCurrentStep(WizardStep.REQUEST_COMPUTATION) + } + }, [isConnected, sdk.isInitialized, currentStep]) + + const handleReset = useCallback(() => { + setCurrentStep(WizardStep.CONNECT_WALLET) + setSubmittedInputs(null) + setLastTransactionHash(undefined) + setInputPublishError(null) + setInputPublishSuccess(false) + setResult(null) + setE3State({ + id: null, + isRequested: false, + isCommitteePublished: false, + isActivated: false, + publicKey: null, + expiresAt: null, + plaintextOutput: null, + hasPlaintextOutput: false, + }) + }, []) + + const handleTryAgain = useCallback(() => { + setCurrentStep(WizardStep.ENTER_INPUTS) + setInputPublishError(null) + setInputPublishSuccess(false) + }, []) + + const contextValue: WizardContextType = useMemo( + () => ({ + currentStep, + submittedInputs, + lastTransactionHash, + inputPublishError, + inputPublishSuccess, + result, + e3State, + setCurrentStep, + setSubmittedInputs, + setLastTransactionHash, + setInputPublishError, + setInputPublishSuccess, + setResult, + setE3State, + handleReset, + handleTryAgain, + sdk, + }), + [ + currentStep, + submittedInputs, + lastTransactionHash, + inputPublishError, + inputPublishSuccess, + result, + e3State, + handleReset, + handleTryAgain, + sdk, + ], + ) + + return {children} +} diff --git a/templates/default/client/src/pages/WizardRoutes.tsx b/templates/default/client/src/pages/WizardRoutes.tsx new file mode 100644 index 0000000000..faed0fd7e7 --- /dev/null +++ b/templates/default/client/src/pages/WizardRoutes.tsx @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +import React, { useMemo } from 'react' +import { Routes, Route, Navigate } from 'react-router-dom' +import { + NumberSquareOneIcon, + NumberSquareTwoIcon, + NumberSquareThreeIcon, + NumberSquareFourIcon, + NumberSquareFiveIcon, + NumberSquareSixIcon, +} from '@phosphor-icons/react' + +// Step components +import ConnectWallet from './steps/ConnectWallet' +import RequestComputation from './steps/RequestComputation' +import ActivateE3 from './steps/ActivateE3' +import EnterInputs from './steps/EnterInputs' +import EncryptSubmit from './steps/EncryptSubmit' +import Results from './steps/Results' +import EnvironmentError from './components/EnvironmentError' +import SDKErrorDisplay from './components/SDKErrorDisplay' +import StepIndicator from './components/StepIndicator' +import { useWizard, WizardStep } from '../context/WizardContext' +import { MISSING_ENV_VARS } from '@/utils/env-config' + +interface StepConfig { + step: WizardStep + path: string + component: React.ComponentType + icon: React.ComponentType +} + +// Steps are defined below as an array of objects. +// Each entry specifies the wizard step, URL path, component, and icon for that step. +const STEPS: StepConfig[] = [ + { step: WizardStep.CONNECT_WALLET, path: '/step1', component: ConnectWallet, icon: NumberSquareOneIcon }, + { step: WizardStep.REQUEST_COMPUTATION, path: '/step2', component: RequestComputation, icon: NumberSquareTwoIcon }, + { step: WizardStep.ACTIVATE_E3, path: '/step3', component: ActivateE3, icon: NumberSquareThreeIcon }, + { step: WizardStep.ENTER_INPUTS, path: '/step4', component: EnterInputs, icon: NumberSquareFourIcon }, + { step: WizardStep.ENCRYPT_SUBMIT, path: '/step5', component: EncryptSubmit, icon: NumberSquareFiveIcon }, + { step: WizardStep.RESULTS, path: '/step6', component: Results, icon: NumberSquareSixIcon }, +] + +/** + * WizardRoutes component that manages the multi-step wizard flow for Enclave E3. + * Handles routing between wizard steps, displays step indicators, and manages + * error states for environment configuration and SDK errors. + * + * Dynamically sets up routes for each wizard step, only rendering the component + * for the current step and redirecting to the currentStep's route otherwise. + * This enforces linear navigation through the wizard. + */ +const WizardRoutes: React.FC = () => { + const { currentStep, sdk } = useWizard() + + // Early returns for error states. + if (MISSING_ENV_VARS.length > 0) { + return + } + + if (sdk.error) { + return + } + + // Memoize the current step path to avoid unnecessary recalculations. + const currentStepPath = useMemo(() => `/step${currentStep}`, [currentStep]) + + return ( +
+
+
+

Enclave E3

+

Confidential computation with Enclave Encrypted Execution Environments.

+
+ + + +
+ + } /> + {STEPS.map(({ step, path, component: Component }) => ( + : } /> + ))} + +
+
+
+ ) +} + +export default WizardRoutes diff --git a/templates/default/client/src/pages/WizardSDK.tsx b/templates/default/client/src/pages/WizardSDK.tsx deleted file mode 100644 index 06d2914a26..0000000000 --- a/templates/default/client/src/pages/WizardSDK.tsx +++ /dev/null @@ -1,981 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. - -import React, { useState, useEffect, useMemo } from 'react' -import { useAccount } from 'wagmi' -import { ConnectKitButton } from 'connectkit' -import { hexToBytes } from 'viem' - -// Components -import CardContent from './components/CardContent' -import EnvironmentError from './components/EnvironmentError' -import Spinner from './components/Spinner' - -// SDK and utilities -import { useEnclaveSDK } from '@enclave-e3/react' -import { - encodeBfvParams, - encodeComputeProviderParams, - calculateStartWindow, - decodePlaintextOutput, - DEFAULT_COMPUTE_PROVIDER_PARAMS, - DEFAULT_E3_CONFIG, - FheProtocol, -} from '@enclave-e3/sdk' -import { HAS_MISSING_ENV_VARS, MISSING_ENV_VARS, getContractAddresses } from '@/utils/env-config' -import { formatContractError } from '@/utils/error-formatting' - -// Icons -import { - Wallet, - Calculator, - Lock, - CheckCircle, - NumberSquareOne, - NumberSquareTwo, - NumberSquareThree, - NumberSquareFour, - NumberSquareFive, - NumberSquareSix, -} from '@phosphor-icons/react' - -// ============================================================================ -// TYPES & ENUMS -// ============================================================================ - -enum WizardStep { - CONNECT_WALLET = 1, - REQUEST_COMPUTATION = 2, - ACTIVATE_E3 = 3, - ENTER_INPUTS = 4, - ENCRYPT_SUBMIT = 5, - RESULTS = 6, -} - -interface E3State { - id: bigint | null - isRequested: boolean - isCommitteePublished: boolean - isActivated: boolean - publicKey: `0x${string}` | null - expiresAt: bigint | null - plaintextOutput: string | null - hasPlaintextOutput: boolean -} - -// ============================================================================ -// ERROR DISPLAY COMPONENT -// ============================================================================ - -interface ErrorDisplayProps { - error: any - showDetails: boolean - onToggleDetails: () => void -} - -const ErrorDisplay: React.FC = ({ error, showDetails, onToggleDetails }) => { - if (!error) return null - - const userMessage = formatContractError(error) - const technicalMessage = error.message || JSON.stringify(error, null, 2) - - return ( -
-

- Error: {userMessage} -

- {userMessage !== technicalMessage && ( - - )} - {showDetails && userMessage !== technicalMessage && ( -
{technicalMessage}
- )} -
- ) -} - -// ============================================================================ -// STEP COMPONENTS -// ============================================================================ - -const ConnectWalletStep: React.FC = () => ( - -
-
- -
-

Step 1: Connect Your Wallet

-
-

Welcome to Enclave

-

- Enclave is a protocol for Encrypted Execution Environments (E3) that enables secure computations on private data using fully - homomorphic encryption (FHE), zero-knowledge proofs, and distributed key cryptography. Connect your wallet to experience - privacy-preserving computation. -

-
-

- How it works: You'll request an E3 computation → Ciphernode committee is selected → Committee publishes shared - public key → You encrypt and submit inputs → Secure computation executes → Only verified outputs are decrypted by the committee. -

-
-
-
- -
-
-
-) - -interface RequestComputationStepProps { - e3State: E3State - isRequesting: boolean - transactionHash: string | undefined - error: any - isSuccess: boolean - onRequestComputation: () => Promise -} - -const RequestComputationStep: React.FC = ({ - e3State, - isRequesting, - transactionHash, - error, - isSuccess, - onRequestComputation, -}) => ( - -
-
- -
-

Step 2: Request Computation

-
-

Request Encrypted Execution Environment

-

- Request an E3 computation from Enclave's decentralized network. This initiates the selection of a Ciphernode Committee through - cryptographic sortition, who will generate shared keys for securing your computation without any single point of trust. -

-
-

- Process: Request E3 → Committee Selection via Sortition → Key Generation → Ready for Activation -

-
- - {/* E3 State Progress */} - {e3State.id !== null && ( -
-
-

- ✅ E3 ID: {String(e3State.id)} -
- Status: Computation requested -

-
- - {e3State.isCommitteePublished && e3State.publicKey ? ( -
-

- 🔑 Committee Published Public Key! -
- Public Key: {e3State.publicKey.slice(0, 20)}...{e3State.publicKey.slice(-10)} -
- Ready to activate E3 environment. -

-
- ) : ( -
-
- -

- ⏳ Waiting for committee to publish public key... -
- The computation committee is being selected and will provide the public key shortly. -

-
-
- )} -
- )} - - {error && {}} />} - - {isSuccess && transactionHash && ( -
-

- ✅ Transaction Successful! -
- Hash: {transactionHash.slice(0, 10)}...{transactionHash.slice(-8)} -

-
- )} -
- - {isRequesting && ( -
- -
- )} - - -
-
-) - -interface ActivateE3StepProps { - e3State: E3State - isRequesting: boolean - transactionHash: string | undefined - error: any - isSuccess: boolean - onActivateE3: () => Promise -} - -const ActivateE3Step: React.FC = ({ e3State, isRequesting, transactionHash, error, isSuccess, onActivateE3 }) => ( - -
-
- -
-

Step 3: Activate E3

-
-

Activate Encrypted Execution Environment

-

- Activate the E3 using the Ciphernode Committee's shared public key. This distributed key ensures no single node can decrypt your - inputs or intermediate states - only the verified final output can be collectively decrypted by the committee. -

- - {e3State.isActivated && e3State.expiresAt && ( -
-

- ✅ E3 Environment Activated! -
- Expires At: {new Date(Number(e3State.expiresAt) * 1000).toLocaleString()} -

-
- )} - - {error && {}} />} - - {isSuccess && transactionHash && ( -
-

- ✅ Transaction Successful! -
- Hash: {transactionHash.slice(0, 10)}...{transactionHash.slice(-8)} -

-
- )} -
- - {isRequesting && ( -
- -
- )} - - -
-
-) - -interface EnterInputsStepProps { - e3State: E3State - input1: string - input2: string - onInput1Change: (value: string) => void - onInput2Change: (value: string) => void - onSubmit: (e: React.FormEvent) => void -} - -const EnterInputsStep: React.FC = ({ e3State, input1, input2, onInput1Change, onInput2Change, onSubmit }) => ( - -
-
- -
-

Step 4: Enter Your Numbers

-
-

Homomorphic Encrypted Computation

-

- Enter two numbers for a privacy-preserving addition using fully homomorphic encryption (FHE). Your inputs will be encrypted - locally and remain encrypted throughout the entire computation process, with only the final result being decrypted. -

-
-

- Privacy Guarantee: FHE allows computation on encrypted data. Your numbers remain private throughout the process - - inputs, intermediate states, and execution are all encrypted. -

-
- -
-
- - onInput1Change(e.target.value)} - className='w-full rounded-md border border-slate-300 px-3 py-2 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-enclave-500' - placeholder='Enter first number' - required - /> -
-
- - onInput2Change(e.target.value)} - className='w-full rounded-md border border-slate-300 px-3 py-2 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-enclave-500' - placeholder='Enter second number' - required - /> -
-
- - {input1 && input2 && ( -
-

- Ready to compute: {input1} + {input2} = ? -

-
- )} -
- - -
-
-) - -interface EncryptSubmitStepProps { - inputPublishError: string | null - inputPublishSuccess: boolean - showErrorDetails: boolean - onToggleErrorDetails: () => void - onTryAgain: () => void -} - -const EncryptSubmitStep: React.FC = ({ - inputPublishError, - inputPublishSuccess, - showErrorDetails, - onToggleErrorDetails, - onTryAgain, -}) => ( - -
-
- -
-

Step 5: Encrypting & Submitting

-
-

Secure Process Execution

- - {!inputPublishError && !inputPublishSuccess && ( -
-
- -
-

- Your inputs are being encrypted to the committee's public key and submitted to the E3. The Compute Provider will execute the - FHE computation over your encrypted data... -

-
-

- Process: Encrypt to Key → Submit to E3 → FHE Computation → Ciphertext Output -

-
-
- )} - - {inputPublishError && ( -
- - -
- )} - - {inputPublishSuccess && ( -
-
- -
-
-

- ✅ Inputs Successfully Submitted! -
- Your encrypted inputs have been published to the E3. The Compute Provider is executing the FHE computation and will publish - the ciphertext output for committee decryption. -

-
-
-
- -
-

- Computing... Waiting for the Ciphernode Committee to collectively decrypt the verified output. -

-
-
- )} -
-
-
-) - -interface ResultsStepProps { - input1: string - input2: string - result: number | null - e3State: E3State - transactionHash: string | undefined - onReset: () => void -} - -const ResultsStep: React.FC = ({ input1, input2, result, e3State, transactionHash, onReset }) => ( - -
-
- -
-

Step 6: Results

-
-

Computation Complete!

- -
-
-

- Your Encrypted Computation: -

-

- {input1} + {input2} = {result !== null ? result : 'Computing...'} -

- {result !== null &&

✅ Computed securely using FHE with distributed key decryption!

} -
-
- -
-
-

- E3 ID: {String(e3State.id)} -

-
- {transactionHash && ( -
-

- Transaction: {transactionHash.slice(0, 10)}...{transactionHash.slice(-8)} -

-
- )} - {e3State.plaintextOutput && ( -
-

- Raw Output: {e3State.plaintextOutput.slice(0, 20)}... -

-
- )} -
- -
-

- 🔒 Cryptographic Guarantees: Your inputs remained encrypted throughout the entire process. The Ciphernode - Committee used distributed key cryptography to decrypt only the verified output, ensuring data privacy, data integrity, and - correct execution. -

-
-
- - -
-
-) - -// ============================================================================ -// MAIN WIZARD COMPONENT -// ============================================================================ - -const WizardSDK: React.FC = () => { - const { isConnected } = useAccount() - - if (HAS_MISSING_ENV_VARS) { - return - } - const contracts = getContractAddresses() - const sdkConfig = useMemo( - () => ({ - autoConnect: true, - contracts: { - enclave: contracts.enclave, - ciphernodeRegistry: contracts.ciphernodeRegistry, - }, - protocol: FheProtocol.BFV, - }), - [contracts.enclave, contracts.ciphernodeRegistry], - ) - - const { - isInitialized, - error: sdkError, - requestE3, - activateE3, - publishInput, - onEnclaveEvent, - off, - EnclaveEventType, - RegistryEventType, - sdk, - } = useEnclaveSDK(sdkConfig) - - // Component state - const [currentStep, setCurrentStep] = useState(WizardStep.CONNECT_WALLET) - const [input1, setInput1] = useState('') - const [input2, setInput2] = useState('') - const [lastTransactionHash, setLastTransactionHash] = useState() - const [inputPublishError, setInputPublishError] = useState(null) - const [inputPublishSuccess, setInputPublishSuccess] = useState(false) - const [requestError, setRequestError] = useState(null) - const [showErrorDetails, setShowErrorDetails] = useState(false) - const [isRequesting, setIsRequesting] = useState(false) - const [requestSuccess, setRequestSuccess] = useState(false) - const [result, setResult] = useState(null) - - // E3 state tracking - const [e3State, setE3State] = useState({ - id: null, - isRequested: false, - isCommitteePublished: false, - isActivated: false, - publicKey: null, - expiresAt: null, - plaintextOutput: null, - hasPlaintextOutput: false, - }) - - // Set up event listeners - useEffect(() => { - console.log('running use effect') - if (!isInitialized) { - console.log('refusing to run because sdk is not initialized!') - return - } - - const handleE3Requested = (event: any) => { - const e3Id = event.data.e3Id - setE3State((prev) => ({ - ...prev, - id: e3Id, - isRequested: true, - })) - } - - const handleCommitteePublished = (event: any) => { - const { e3Id, publicKey } = event.data - - // I added a 2 second delay to show the waiting state, its too fast on anvil - setTimeout(() => { - setE3State((prev) => { - if (prev.id !== null && e3Id === prev.id) { - return { - ...prev, - isCommitteePublished: true, - publicKey: publicKey as `0x${string}`, - } - } - return prev - }) - }, 2000) - } - - const handleE3Activated = (event: any) => { - const { e3Id, expiration } = event.data - setE3State((prev) => { - if (prev.id !== null && e3Id === prev.id) { - return { - ...prev, - isActivated: true, - expiresAt: expiration || null, - } - } - return prev - }) - } - - const handlePlaintextOutput = (event: any) => { - const { e3Id, plaintextOutput } = event.data - setE3State((prev) => { - if (prev.id !== null && e3Id === prev.id) { - const decodedResult = decodePlaintextOutput(plaintextOutput) - setResult(decodedResult) - return { - ...prev, - plaintextOutput: plaintextOutput as string, - hasPlaintextOutput: true, - } - } - return prev - }) - } - - // Set up event listeners - console.log('Setting up listeners...') - onEnclaveEvent(EnclaveEventType.E3_REQUESTED, handleE3Requested) - onEnclaveEvent(RegistryEventType.COMMITTEE_PUBLISHED, handleCommitteePublished) - onEnclaveEvent(EnclaveEventType.E3_ACTIVATED, handleE3Activated) - onEnclaveEvent(EnclaveEventType.PLAINTEXT_OUTPUT_PUBLISHED, handlePlaintextOutput) - - // Cleanup - return () => { - console.log('Cleaning up listeners...') - off(EnclaveEventType.E3_REQUESTED, handleE3Requested) - off(RegistryEventType.COMMITTEE_PUBLISHED, handleCommitteePublished) - off(EnclaveEventType.E3_ACTIVATED, handleE3Activated) - off(EnclaveEventType.PLAINTEXT_OUTPUT_PUBLISHED, handlePlaintextOutput) - } - }, [isInitialized, onEnclaveEvent, off, EnclaveEventType, RegistryEventType]) - - // Auto-advance steps based on state - useEffect(() => { - if (!isConnected && currentStep > WizardStep.CONNECT_WALLET) { - setCurrentStep(WizardStep.CONNECT_WALLET) - } else if (isConnected && isInitialized && currentStep === WizardStep.CONNECT_WALLET) { - setCurrentStep(WizardStep.REQUEST_COMPUTATION) - } else if (e3State.isCommitteePublished && currentStep === WizardStep.REQUEST_COMPUTATION) { - setCurrentStep(WizardStep.ACTIVATE_E3) - } else if (e3State.isActivated && currentStep === WizardStep.ACTIVATE_E3) { - setCurrentStep(WizardStep.ENTER_INPUTS) - } else if (e3State.hasPlaintextOutput && currentStep < WizardStep.RESULTS) { - setCurrentStep(WizardStep.RESULTS) - } - }, [isConnected, isInitialized, currentStep, e3State]) - - const handleRequestComputation = async () => { - console.log('handleRequestComputation') - setIsRequesting(true) - setRequestError(null) - setRequestSuccess(false) - - // Reset E3 state - setE3State({ - id: null, - isRequested: false, - isCommitteePublished: false, - isActivated: false, - publicKey: null, - expiresAt: null, - plaintextOutput: null, - hasPlaintextOutput: false, - }) - - try { - const threshold: [number, number] = [DEFAULT_E3_CONFIG.threshold_min, DEFAULT_E3_CONFIG.threshold_max] - const startWindow = calculateStartWindow(60) // 1 minute - const duration = BigInt(60) // 1 minute - const e3ProgramParams = encodeBfvParams() - const computeProviderParams = encodeComputeProviderParams(DEFAULT_COMPUTE_PROVIDER_PARAMS) - - console.log('requestE3') - const hash = await requestE3({ - filter: contracts.filterRegistry, - threshold, - startWindow, - duration, - e3Program: contracts.e3Program, - e3ProgramParams, - computeProviderParams, - value: BigInt('1000000000000000'), // 0.001 ETH - }) - - setLastTransactionHash(hash) - setRequestSuccess(true) - } catch (error) { - setRequestError(error) - console.error('Error requesting computation:', error) - } finally { - setIsRequesting(false) - } - } - - const handleActivateE3 = async () => { - console.log('handleActivateE3') - - if (e3State.id === null || e3State.publicKey === null) { - console.log('refusing to run handler because id or publicKey is null') - return - } - setIsRequesting(true) - setRequestError(null) - - try { - const hash = await activateE3(e3State.id, e3State.publicKey) - setLastTransactionHash(hash) - setRequestSuccess(true) - } catch (error) { - setRequestError(error) - console.error('Error activating E3:', error) - } finally { - setIsRequesting(false) - } - } - - const handleInputSubmit = async (e: React.FormEvent) => { - e.preventDefault() - console.log('handleInputSubmit') - if (!input1 || !input2 || e3State.publicKey === null || e3State.id === null) { - console.log('Refusing to submit input because input is empty or publickey is null or is is null') - return - } - - setCurrentStep(WizardStep.ENCRYPT_SUBMIT) - setInputPublishError(null) - setInputPublishSuccess(false) - - try { - // Parse inputs - const num1 = BigInt(input1) - const num2 = BigInt(input2) - - // Convert hex public key to bytes - const publicKeyBytes = hexToBytes(e3State.publicKey) - - // Encrypt both inputs - const encryptedInput1 = await sdk?.encryptNumber(num1, publicKeyBytes) - const encryptedInput2 = await sdk?.encryptNumber(num2, publicKeyBytes) - - if (!encryptedInput1 || !encryptedInput2) { - throw new Error('Failed to encrypt inputs') - } - - // Publish first input - await publishInput(e3State.id, `0x${Array.from(encryptedInput1, (b) => b.toString(16).padStart(2, '0')).join('')}` as `0x${string}`) - - // Publish second input - const hash2 = await publishInput( - e3State.id, - `0x${Array.from(encryptedInput2, (b) => b.toString(16).padStart(2, '0')).join('')}` as `0x${string}`, - ) - - setLastTransactionHash(hash2) - setInputPublishSuccess(true) - } catch (error) { - setInputPublishError(error instanceof Error ? error.message : 'Failed to encrypt and publish inputs') - console.error('Error encrypting/publishing inputs:', error) - } - } - - const handleReset = () => { - console.log('handleReset') - setCurrentStep(WizardStep.CONNECT_WALLET) - setInput1('') - setInput2('') - setLastTransactionHash(undefined) - setInputPublishError(null) - setInputPublishSuccess(false) - setRequestError(null) - setIsRequesting(false) - setRequestSuccess(false) - setResult(null) - setE3State({ - id: null, - isRequested: false, - isCommitteePublished: false, - isActivated: false, - publicKey: null, - expiresAt: null, - plaintextOutput: null, - hasPlaintextOutput: false, - }) - } - - const handleTryAgain = () => { - console.log('HandleTryAgain') - setCurrentStep(WizardStep.ENTER_INPUTS) - setInputPublishError(null) - setInputPublishSuccess(false) - } - - const getStepIcon = (step: WizardStep) => { - const iconProps = { size: 24, className: currentStep >= step ? 'text-enclave-500' : 'text-slate-400' } - switch (step) { - case WizardStep.CONNECT_WALLET: - return - case WizardStep.REQUEST_COMPUTATION: - return - case WizardStep.ACTIVATE_E3: - return - case WizardStep.ENTER_INPUTS: - return - case WizardStep.ENCRYPT_SUBMIT: - return - case WizardStep.RESULTS: - return - } - } - - const renderStepIndicator = () => ( -
-
- {[1, 2, 3, 4, 5, 6].map((step) => ( -
-
= step ? 'border-enclave-400 bg-enclave-100 text-enclave-600' : 'border-slate-300 bg-slate-100 text-slate-400' - }`} - > - {getStepIcon(step as WizardStep)} -
- {step < 6 && ( -
step ? 'bg-enclave-400' : 'bg-slate-300'}`} /> - )} -
- ))} -
-
- ) - - const renderStepContent = () => { - switch (currentStep) { - case WizardStep.CONNECT_WALLET: - return - case WizardStep.REQUEST_COMPUTATION: - return ( - - ) - case WizardStep.ACTIVATE_E3: - return ( - - ) - case WizardStep.ENTER_INPUTS: - return ( - - ) - case WizardStep.ENCRYPT_SUBMIT: - return ( - setShowErrorDetails(!showErrorDetails)} - onTryAgain={handleTryAgain} - /> - ) - case WizardStep.RESULTS: - return ( - - ) - default: - return null - } - } - - if (sdkError) { - return ( -
-
-
-
-
-

SDK Error

-
{sdkError}
-
-
-
-
-
- ) - } - - return ( -
-
-
-

Enclave E3

-

Confidential computation with Enclave Encrypted Execution Environments

-
- - {renderStepIndicator()} - -
{renderStepContent()}
-
-
- ) -} - -export default WizardSDK diff --git a/templates/default/client/src/pages/components/EnvironmentError.tsx b/templates/default/client/src/pages/components/EnvironmentError.tsx index bf5bd6f6f3..9b792b7860 100644 --- a/templates/default/client/src/pages/components/EnvironmentError.tsx +++ b/templates/default/client/src/pages/components/EnvironmentError.tsx @@ -5,62 +5,60 @@ // or FITNESS FOR A PARTICULAR PURPOSE. import React from 'react' -import { Warning } from '@phosphor-icons/react' +import { WarningIcon } from '@phosphor-icons/react' interface EnvironmentErrorProps { - missingVars: string[] + missingVars: string[] } const EnvironmentError: React.FC = ({ missingVars }) => { - return ( -
-
-
-
- -
+ return ( +
+
+
+
+ +
-

- Environment Configuration Required -

+

Environment Configuration Required

-

- The following environment variables need to be configured before you can use the encrypted computation features: -

+

+ The following environment variables need to be configured before you can use the encrypted computation features: +

-
-
    - {missingVars.map((varName) => ( -
  • - - {varName} - -
  • - ))} -
-
+
+
    + {missingVars.map((varName) => ( +
  • + {varName} +
  • + ))} +
+
-
-

How to configure:

-
    -
  1. Create a .env file in the client directory
  2. -
  3. Add the missing environment variables with their appropriate values
  4. -
  5. Restart the development server
  6. -
-
+
+

How to configure:

+
    +
  1. + Create a .env file in the client directory +
  2. +
  3. Add the missing environment variables with their appropriate values
  4. +
  5. Restart the development server
  6. +
+
-
- -
-
-
+
+ +
- ) +
+
+ ) } -export default EnvironmentError \ No newline at end of file +export default EnvironmentError diff --git a/templates/default/client/src/pages/components/ErrorDisplay.tsx b/templates/default/client/src/pages/components/ErrorDisplay.tsx new file mode 100644 index 0000000000..60070d8153 --- /dev/null +++ b/templates/default/client/src/pages/components/ErrorDisplay.tsx @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +import React from 'react' +import { formatContractError } from '@/utils/error-formatting' + +interface ErrorDisplayProps { + error: any + showDetails: boolean + onToggleDetails: () => void +} + +const ErrorDisplay: React.FC = ({ error, showDetails, onToggleDetails }) => { + if (!error) return null + + const userMessage = formatContractError(error) + const technicalMessage = error.message || JSON.stringify(error, null, 2) + + return ( +
+

+ Error: {userMessage} +

+ {userMessage !== technicalMessage && ( + + )} + {showDetails && userMessage !== technicalMessage && ( +
{technicalMessage}
+ )} +
+ ) +} + +export default ErrorDisplay diff --git a/templates/default/client/src/pages/components/SDKErrorDisplay.tsx b/templates/default/client/src/pages/components/SDKErrorDisplay.tsx new file mode 100644 index 0000000000..2cc9098f1b --- /dev/null +++ b/templates/default/client/src/pages/components/SDKErrorDisplay.tsx @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +import React from 'react' + +interface SDKErrorDisplayProps { + error: string +} + +const SDKErrorDisplay: React.FC = ({ error }) => ( +
+
+
+
+
+

SDK Error

+
{error}
+
+
+
+
+
+) + +export default SDKErrorDisplay diff --git a/templates/default/client/src/pages/components/StepIndicator.tsx b/templates/default/client/src/pages/components/StepIndicator.tsx new file mode 100644 index 0000000000..0a696a3033 --- /dev/null +++ b/templates/default/client/src/pages/components/StepIndicator.tsx @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +import React from 'react' +import { WizardStep } from '../../context/WizardContext' + +interface StepConfig { + step: WizardStep + icon: React.ComponentType +} + +interface StepIndicatorProps { + currentStep: WizardStep + steps: StepConfig[] +} + +const StepIndicator: React.FC = React.memo(({ currentStep, steps }) => { + return ( +
+
+ {steps.map(({ step, icon: IconComponent }, index) => { + const isActive = currentStep >= step + const isCompleted = currentStep > step + + return ( +
+
+ +
+ {index < steps.length - 1 && ( +
+ )} +
+ ) + })} +
+
+ ) +}) + +export default StepIndicator diff --git a/templates/default/client/src/pages/steps/ActivateE3.tsx b/templates/default/client/src/pages/steps/ActivateE3.tsx new file mode 100644 index 0000000000..f71da5d0ec --- /dev/null +++ b/templates/default/client/src/pages/steps/ActivateE3.tsx @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +import React, { useState, useEffect } from 'react' +import { LockIcon } from '@phosphor-icons/react' +import CardContent from '../components/CardContent' +import Spinner from '../components/Spinner' +import ErrorDisplay from '../components/ErrorDisplay' +import { useWizard, WizardStep } from '../../context/WizardContext' + +/** + * ActivateE3 component - Third step in the Enclave wizard flow + * + * This component handles the activation of the E3 using the Ciphernode Committee's + * shared public key. It provides feedback on the activation process and displays + * the status of the activation. + */ +const ActivateE3: React.FC = () => { + const { e3State, setE3State, setLastTransactionHash, setCurrentStep, sdk } = useWizard() + const { isInitialized, activateE3, onEnclaveEvent, off, EnclaveEventType } = sdk + + const [isRequesting, setIsRequesting] = useState(false) + const [requestError, setRequestError] = useState(null) + const [requestSuccess, setRequestSuccess] = useState(false) + const [lastTransactionHash, setLocalTransactionHash] = useState() + const [showErrorDetails, setShowErrorDetails] = useState(false) + + // Set up event listeners for this step + useEffect(() => { + if (!isInitialized) return + + const handleE3Activated = (event: any) => { + const { e3Id, expiration } = event.data + setE3State((prev) => { + if (prev.id !== null && e3Id === prev.id) { + return { + ...prev, + isActivated: true, + expiresAt: expiration || null, + } + } + return prev + }) + } + + onEnclaveEvent(EnclaveEventType.E3_ACTIVATED, handleE3Activated) + + return () => { + off(EnclaveEventType.E3_ACTIVATED, handleE3Activated) + } + }, [isInitialized, onEnclaveEvent, off, EnclaveEventType, setE3State]) + + // Auto-advance to next step when E3 is activated + useEffect(() => { + if (e3State.isActivated) { + setCurrentStep(WizardStep.ENTER_INPUTS) + } + }, [e3State.isActivated, setCurrentStep]) + + const handleActivateE3 = async () => { + console.log('handleActivateE3') + + if (e3State.id === null || e3State.publicKey === null) { + console.log('refusing to run handler because id or publicKey is null') + return + } + setIsRequesting(true) + setRequestError(null) + + try { + const hash = await activateE3(e3State.id, e3State.publicKey) + setLocalTransactionHash(hash) + setLastTransactionHash(hash) + setRequestSuccess(true) + } catch (error) { + setRequestError(error) + console.error('Error activating E3:', error) + } finally { + setIsRequesting(false) + } + } + + return ( + +
+
+ +
+

Step 3: Activate E3

+
+

Activate Encrypted Execution Environment

+

+ Activate the E3 using the Ciphernode Committee's shared public key. This distributed key ensures no single node can decrypt your + inputs or intermediate states - only the verified final output can be collectively decrypted by the committee. +

+ + {e3State.isActivated && e3State.expiresAt && ( +
+

+ ✅ E3 Environment Activated! +
+ Expires At: {new Date(Number(e3State.expiresAt) * 1000).toLocaleString()} +

+
+ )} + + {requestError && ( + setShowErrorDetails(!showErrorDetails)} + /> + )} + + {requestSuccess && lastTransactionHash && ( +
+

+ ✅ Transaction Successful! +
+ Hash: {lastTransactionHash.slice(0, 10)}...{lastTransactionHash.slice(-8)} +

+
+ )} +
+ + {isRequesting && ( +
+ +
+ )} + + +
+
+ ) +} + +export default ActivateE3 diff --git a/templates/default/client/src/pages/steps/ConnectWallet.tsx b/templates/default/client/src/pages/steps/ConnectWallet.tsx new file mode 100644 index 0000000000..4dc6b796e8 --- /dev/null +++ b/templates/default/client/src/pages/steps/ConnectWallet.tsx @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +import React from 'react' +import { ConnectKitButton } from 'connectkit' +import { WalletIcon } from '@phosphor-icons/react' +import CardContent from '../components/CardContent' + +/** + * ConnectWallet component - First step in the Enclave wizard flow + * + * This component introduces users to the Enclave protocol and provides the initial + * wallet connection interface. It explains the E3 (Encrypted Execution Environment) + * concept and guides users through the secure computation workflow using FHE, + * zero-knowledge proofs, and distributed key cryptography. + */ +const ConnectWallet: React.FC = () => { + return ( + +
+
+ +
+

Step 1: Connect Your Wallet

+
+

Welcome to Enclave

+

+ Enclave is a protocol for Encrypted Execution Environments (E3) that enables secure computations on private data using fully + homomorphic encryption (FHE), zero-knowledge proofs, and distributed key cryptography. Connect your wallet to experience + privacy-preserving computation. +

+
+

+ How it works: You'll request an E3 computation → Ciphernode committee is selected → Committee publishes + shared public key → You encrypt and submit inputs → Secure computation executes → Only verified outputs are decrypted by the + committee. +

+
+
+
+ +
+
+
+ ) +} + +export default ConnectWallet diff --git a/templates/default/client/src/pages/steps/EncryptSubmit.tsx b/templates/default/client/src/pages/steps/EncryptSubmit.tsx new file mode 100644 index 0000000000..cce62ef1f6 --- /dev/null +++ b/templates/default/client/src/pages/steps/EncryptSubmit.tsx @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +import React, { useState, useEffect } from 'react' +import { LockIcon, CheckCircleIcon } from '@phosphor-icons/react' +import CardContent from '../components/CardContent' +import Spinner from '../components/Spinner' +import ErrorDisplay from '../components/ErrorDisplay' +import { useWizard, WizardStep } from '../../context/WizardContext' +import { decodePlaintextOutput } from '@enclave-e3/sdk' + +/** + * EncryptSubmit component - Fifth step in the Enclave wizard flow + * + * This component handles the encryption and submission of user inputs to the E3. + * It provides feedback on the encryption process and displays the status of the + * submission to the E3. + */ +const EncryptSubmit: React.FC = () => { + const { e3State, setE3State, setResult, setCurrentStep, inputPublishError, inputPublishSuccess, handleTryAgain, sdk } = useWizard() + const { isInitialized, onEnclaveEvent, off, EnclaveEventType } = sdk + + const [showErrorDetails, setShowErrorDetails] = useState(false) + + // Set up event listeners for this step + useEffect(() => { + if (!isInitialized) return + + const handlePlaintextOutput = (event: any) => { + const { e3Id, plaintextOutput } = event.data + setE3State((prev) => { + if (prev.id !== null && e3Id === prev.id) { + const decodedResult = decodePlaintextOutput(plaintextOutput) + setResult(decodedResult) + return { + ...prev, + plaintextOutput: plaintextOutput as string, + hasPlaintextOutput: true, + } + } + return prev + }) + } + + onEnclaveEvent(EnclaveEventType.PLAINTEXT_OUTPUT_PUBLISHED, handlePlaintextOutput) + + return () => { + off(EnclaveEventType.PLAINTEXT_OUTPUT_PUBLISHED, handlePlaintextOutput) + } + }, [isInitialized, onEnclaveEvent, off, EnclaveEventType, setE3State, setResult]) + + // Auto-advance to results when output is available + useEffect(() => { + if (e3State.hasPlaintextOutput) { + setCurrentStep(WizardStep.RESULTS) + } + }, [e3State.hasPlaintextOutput, setCurrentStep]) + + return ( + +
+
+ +
+

Step 5: Encrypting & Submitting

+
+

Secure Process Execution

+ + {!inputPublishError && !inputPublishSuccess && ( +
+
+ +
+

+ Your inputs are being encrypted to the committee's public key and submitted to the E3. The Compute Provider will execute the + FHE computation over your encrypted data... +

+
+

+ Process: Encrypt to Key → Submit to E3 → FHE Computation → Ciphertext Output +

+
+
+ )} + + {inputPublishError && ( +
+ setShowErrorDetails(!showErrorDetails)} + /> + +
+ )} + + {inputPublishSuccess && ( +
+
+ +
+
+

+ ✅ Inputs Successfully Submitted! +
+ Your encrypted inputs have been published to the E3. The Compute Provider is executing the FHE computation and will + publish the ciphertext output for committee decryption. +

+
+
+
+ +
+

+ Computing... Waiting for the Ciphernode Committee to collectively decrypt the verified output. +

+
+
+ )} +
+
+
+ ) +} + +export default EncryptSubmit diff --git a/templates/default/client/src/pages/steps/EnterInputs.tsx b/templates/default/client/src/pages/steps/EnterInputs.tsx new file mode 100644 index 0000000000..f65b575007 --- /dev/null +++ b/templates/default/client/src/pages/steps/EnterInputs.tsx @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +import React, { useState } from 'react' +import { NumberSquareOneIcon } from '@phosphor-icons/react' +import { hexToBytes } from 'viem' +import CardContent from '../components/CardContent' +import { useWizard, WizardStep } from '../../context/WizardContext' + +/** + * EnterInputs component - Fourth step in the Enclave wizard flow + * + * This component handles the input of two numbers for a privacy-preserving addition + * using fully homomorphic encryption (FHE). It provides feedback on the input process + * and displays the status of the input submission. + */ +const EnterInputs: React.FC = () => { + const [input1, setInput1] = useState('') + const [input2, setInput2] = useState('') + const { e3State, setCurrentStep, setLastTransactionHash, setInputPublishError, setInputPublishSuccess, setSubmittedInputs, sdk } = + useWizard() + const { publishInput } = sdk + + const handleInputSubmit = async (e: React.FormEvent) => { + e.preventDefault() + console.log('handleInputSubmit') + if (!input1 || !input2 || e3State.publicKey === null || e3State.id === null) { + console.log('Refusing to submit input because input is empty or publickey is null or is is null') + return + } + + setCurrentStep(WizardStep.ENCRYPT_SUBMIT) + setInputPublishError(null) + setInputPublishSuccess(false) + + try { + // Store the inputs in context for the Results component + setSubmittedInputs({ input1, input2 }) + + // Parse inputs + const num1 = BigInt(input1) + const num2 = BigInt(input2) + + // Convert hex public key to bytes + const publicKeyBytes = hexToBytes(e3State.publicKey) + + // Encrypt both inputs + const encryptedInput1 = await sdk.sdk?.encryptNumber(num1, publicKeyBytes) + const encryptedInput2 = await sdk.sdk?.encryptNumber(num2, publicKeyBytes) + + if (!encryptedInput1 || !encryptedInput2) { + throw new Error('Failed to encrypt inputs') + } + + // Publish first input + await publishInput(e3State.id, `0x${Array.from(encryptedInput1, (b) => b.toString(16).padStart(2, '0')).join('')}` as `0x${string}`) + + // Publish second input + const hash2 = await publishInput( + e3State.id, + `0x${Array.from(encryptedInput2, (b: any) => b.toString(16).padStart(2, '0')).join('')}` as `0x${string}`, + ) + + setLastTransactionHash(hash2) + setInputPublishSuccess(true) + } catch (error) { + setInputPublishError(error instanceof Error ? error.message : 'Failed to encrypt and publish inputs') + console.error('Error encrypting/publishing inputs:', error) + } + } + + return ( + +
+
+ +
+

Step 4: Enter Your Numbers

+
+

Homomorphic Encrypted Computation

+

+ Enter two numbers for a privacy-preserving addition using fully homomorphic encryption (FHE). Your inputs will be encrypted + locally and remain encrypted throughout the entire computation process, with only the final result being decrypted. +

+
+

+ Privacy Guarantee: FHE allows computation on encrypted data. Your numbers remain private throughout the + process - inputs, intermediate states, and execution are all encrypted. +

+
+ +
+
+ + setInput1(e.target.value)} + className='w-full rounded-md border border-slate-300 px-3 py-2 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-enclave-500' + placeholder='Enter first number' + required + /> +
+
+ + setInput2(e.target.value)} + className='w-full rounded-md border border-slate-300 px-3 py-2 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-enclave-500' + placeholder='Enter second number' + required + /> +
+
+ + {input1 && input2 && ( +
+

+ Ready to compute: {input1} + {input2} = ? +

+
+ )} +
+ + +
+
+ ) +} + +export default EnterInputs diff --git a/templates/default/client/src/pages/steps/RequestComputation.tsx b/templates/default/client/src/pages/steps/RequestComputation.tsx new file mode 100644 index 0000000000..0916fe0bd1 --- /dev/null +++ b/templates/default/client/src/pages/steps/RequestComputation.tsx @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +import React, { useState, useEffect } from 'react' +import { CalculatorIcon } from '@phosphor-icons/react' +import CardContent from '../components/CardContent' +import Spinner from '../components/Spinner' +import ErrorDisplay from '../components/ErrorDisplay' +import { useWizard, WizardStep } from '../../context/WizardContext' +import { + encodeBfvParams, + encodeComputeProviderParams, + calculateStartWindow, + DEFAULT_COMPUTE_PROVIDER_PARAMS, + DEFAULT_E3_CONFIG, +} from '@enclave-e3/sdk' +import { getContractAddresses } from '@/utils/env-config' + +/** + * RequestComputation component - Second step in the Enclave wizard flow + * + * This component handles the request for an E3 computation from the Enclave network. + * It provides feedback on the request process and displays the status of the request. + */ +const RequestComputation: React.FC = () => { + const { e3State, setE3State, setLastTransactionHash, setCurrentStep, sdk } = useWizard() + const { isInitialized, requestE3, onEnclaveEvent, off, EnclaveEventType, RegistryEventType } = sdk + + const contracts = getContractAddresses() + + const [isRequesting, setIsRequesting] = useState(false) + const [requestError, setRequestError] = useState(null) + const [requestSuccess, setRequestSuccess] = useState(false) + const [lastTransactionHash, setLocalTransactionHash] = useState() + const [showErrorDetails, setShowErrorDetails] = useState(false) + + // Set up event listeners for this step + useEffect(() => { + if (!isInitialized) return + + const handleE3Requested = (event: any) => { + const e3Id = event.data.e3Id + setE3State((prev) => ({ + ...prev, + id: e3Id, + isRequested: true, + })) + } + + const handleCommitteePublished = (event: any) => { + const { e3Id, publicKey } = event.data + + // Add a 2 second delay to show the waiting state + setTimeout(() => { + setE3State((prev) => { + if (prev.id !== null && e3Id === prev.id) { + return { + ...prev, + isCommitteePublished: true, + publicKey: publicKey as `0x${string}`, + } + } + return prev + }) + }, 2000) + } + + onEnclaveEvent(EnclaveEventType.E3_REQUESTED, handleE3Requested) + onEnclaveEvent(RegistryEventType.COMMITTEE_PUBLISHED, handleCommitteePublished) + + return () => { + off(EnclaveEventType.E3_REQUESTED, handleE3Requested) + off(RegistryEventType.COMMITTEE_PUBLISHED, handleCommitteePublished) + } + }, [isInitialized, onEnclaveEvent, off, EnclaveEventType, RegistryEventType]) + + // Auto-advance to next step when committee publishes + useEffect(() => { + if (e3State.isCommitteePublished && e3State.publicKey) { + setCurrentStep(WizardStep.ACTIVATE_E3) + } + }, [e3State.isCommitteePublished, e3State.publicKey, setCurrentStep]) + + const handleRequestComputation = async () => { + console.log('handleRequestComputation') + setIsRequesting(true) + setRequestError(null) + setRequestSuccess(false) + + // Reset E3 state + setE3State({ + id: null, + isRequested: false, + isCommitteePublished: false, + isActivated: false, + publicKey: null, + expiresAt: null, + plaintextOutput: null, + hasPlaintextOutput: false, + }) + + try { + const threshold: [number, number] = [DEFAULT_E3_CONFIG.threshold_min, DEFAULT_E3_CONFIG.threshold_max] + const startWindow = calculateStartWindow(60) // 1 minute + const duration = BigInt(60) // 1 minute + const e3ProgramParams = encodeBfvParams() + const computeProviderParams = encodeComputeProviderParams(DEFAULT_COMPUTE_PROVIDER_PARAMS) + + console.log('requestE3') + const hash = await requestE3({ + filter: contracts.filterRegistry, + threshold, + startWindow, + duration, + e3Program: contracts.e3Program, + e3ProgramParams, + computeProviderParams, + value: BigInt('1000000000000000'), // 0.001 ETH + }) + + setLocalTransactionHash(hash) + setLastTransactionHash(hash) + setRequestSuccess(true) + } catch (error) { + setRequestError(error) + console.error('Error requesting computation:', error) + } finally { + setIsRequesting(false) + } + } + + return ( + +
+
+ +
+

Step 2: Request Computation

+
+

Request Encrypted Execution Environment

+

+ Request an E3 computation from Enclave's decentralized network. This initiates the selection of a Ciphernode Committee through + cryptographic sortition, who will generate shared keys for securing your computation without any single point of trust. +

+
+

+ Process: Request E3 → Committee Selection via Sortition → Key Generation → Ready for Activation +

+
+ + {/* E3 State Progress */} + {e3State.id !== null && ( +
+
+

+ ✅ E3 ID: {String(e3State.id)} +
+ Status: Computation requested +

+
+ + {e3State.isCommitteePublished && e3State.publicKey ? ( +
+

+ 🔑 Committee Published Public Key! +
+ Public Key: {e3State.publicKey.slice(0, 20)}...{e3State.publicKey.slice(-10)} +
+ Ready to activate E3 environment. +

+
+ ) : ( +
+
+ +

+ ⏳ Waiting for committee to publish public key... +
+ The computation committee is being selected and will provide the public key shortly. +

+
+
+ )} +
+ )} + + {requestError && ( + setShowErrorDetails(!showErrorDetails)} + /> + )} + + {requestSuccess && lastTransactionHash && ( +
+

+ ✅ Transaction Successful! +
+ Hash: {lastTransactionHash.slice(0, 10)}...{lastTransactionHash.slice(-8)} +

+
+ )} +
+ + {isRequesting && ( +
+ +
+ )} + + +
+
+ ) +} + +export default RequestComputation diff --git a/templates/default/client/src/pages/steps/Results.tsx b/templates/default/client/src/pages/steps/Results.tsx new file mode 100644 index 0000000000..ec873365c1 --- /dev/null +++ b/templates/default/client/src/pages/steps/Results.tsx @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +import React from 'react' +import { CheckCircleIcon } from '@phosphor-icons/react' +import CardContent from '../components/CardContent' +import { useWizard } from '../../context/WizardContext' + +/** + * Results component - Sixth step in the Enclave wizard flow + * + * This component displays the results of the computation, including the encrypted + * computation, the E3 ID, the transaction hash, and the raw output. + */ +const Results: React.FC = () => { + const { submittedInputs, result, e3State, lastTransactionHash, handleReset } = useWizard() + + const onReset = () => { + handleReset() + } + + return ( + +
+
+ +
+

Step 6: Results

+
+

Computation Complete!

+ +
+
+

+ Your Encrypted Computation: +

+

+ {submittedInputs + ? `${submittedInputs.input1} + ${submittedInputs.input2} = ${result !== null ? result : 'Computing...'}` + : 'Computing...'} +

+ {result !== null &&

✅ Computed securely using FHE with distributed key decryption!

} +
+
+ +
+
+

+ E3 ID: {String(e3State.id)} +

+
+ {lastTransactionHash && ( +
+

+ Transaction: {lastTransactionHash.slice(0, 10)}...{lastTransactionHash.slice(-8)} +

+
+ )} + {e3State.plaintextOutput && ( +
+

+ Raw Output: {e3State.plaintextOutput.slice(0, 20)}... +

+
+ )} +
+ +
+

+ 🔒 Cryptographic Guarantees: Your inputs remained encrypted throughout the entire process. The Ciphernode + Committee used distributed key cryptography to decrypt only the verified output, ensuring data privacy, data integrity, and + correct execution. +

+
+
+ + +
+
+ ) +} + +export default Results diff --git a/templates/default/client/src/utils/env-config.ts b/templates/default/client/src/utils/env-config.ts index 464edc1064..a2cf0408fc 100644 --- a/templates/default/client/src/utils/env-config.ts +++ b/templates/default/client/src/utils/env-config.ts @@ -10,40 +10,25 @@ export const REGISTRY_ADDRESS = import.meta.env.VITE_REGISTRY_ADDRESS export const FILTER_REGISTRY_ADDRESS = import.meta.env.VITE_FILTER_REGISTRY_ADDRESS export const RPC_URL = import.meta.env.VITE_RPC_URL || 'http://localhost:8545' -const requiredEnvVars = { - VITE_ENCLAVE_ADDRESS: ENCLAVE_ADDRESS, - VITE_E3_PROGRAM_ADDRESS: E3_PROGRAM_ADDRESS, - VITE_REGISTRY_ADDRESS: REGISTRY_ADDRESS, - VITE_FILTER_REGISTRY_ADDRESS: FILTER_REGISTRY_ADDRESS, -} - -export const MISSING_ENV_VARS = Object.entries(requiredEnvVars) - .filter(([, value]) => !value) - .map(([key]) => key) - -export const HAS_MISSING_ENV_VARS = MISSING_ENV_VARS.length > 0 - -/** - * Validate environment variables and throw an error if any are missing - */ -export function validateEnvVars(): void { - if (HAS_MISSING_ENV_VARS) { - throw new Error( - `Missing required environment variables: ${MISSING_ENV_VARS.join(', ')}\n` + - 'Please check your .env file and ensure all required variables are set.' - ) - } -} +// Get the missing environment variables. +// This is used to check if the environment variables are set. +export const MISSING_ENV_VARS = Object.entries({ + VITE_ENCLAVE_ADDRESS: ENCLAVE_ADDRESS, + VITE_E3_PROGRAM_ADDRESS: E3_PROGRAM_ADDRESS, + VITE_REGISTRY_ADDRESS: REGISTRY_ADDRESS, + VITE_FILTER_REGISTRY_ADDRESS: FILTER_REGISTRY_ADDRESS, +}) + .filter(([, value]) => !value) + .map(([key]) => key) /** - * Get validated contract addresses + * Get validated contract addresses. */ export function getContractAddresses() { - validateEnvVars() - return { - enclave: ENCLAVE_ADDRESS as `0x${string}`, - ciphernodeRegistry: REGISTRY_ADDRESS as `0x${string}`, - filterRegistry: FILTER_REGISTRY_ADDRESS as `0x${string}`, - e3Program: E3_PROGRAM_ADDRESS as `0x${string}`, - } -} \ No newline at end of file + return { + enclave: ENCLAVE_ADDRESS as `0x${string}`, + ciphernodeRegistry: REGISTRY_ADDRESS as `0x${string}`, + filterRegistry: FILTER_REGISTRY_ADDRESS as `0x${string}`, + e3Program: E3_PROGRAM_ADDRESS as `0x${string}`, + } +} diff --git a/templates/default/client/src/utils/sdk-config.ts b/templates/default/client/src/utils/sdk-config.ts new file mode 100644 index 0000000000..0318a930b1 --- /dev/null +++ b/templates/default/client/src/utils/sdk-config.ts @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +import { FheProtocol } from '@enclave-e3/sdk' +import { getContractAddresses } from './env-config' + +/** + * Get the Enclave SDK configuration. + */ +export function getEnclaveSDKConfig() { + const contracts = getContractAddresses() + return { + autoConnect: true, + contracts: { + enclave: contracts.enclave, + ciphernodeRegistry: contracts.ciphernodeRegistry, + }, + protocol: FheProtocol.BFV, + } +} From 7b5135473963a129169b60d08e97a1e6be0acdd7 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Fri, 10 Oct 2025 16:03:17 +0100 Subject: [PATCH 2/2] refactor: move hook before returns --- templates/default/client/src/pages/WizardRoutes.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/default/client/src/pages/WizardRoutes.tsx b/templates/default/client/src/pages/WizardRoutes.tsx index faed0fd7e7..6dd8075f2e 100644 --- a/templates/default/client/src/pages/WizardRoutes.tsx +++ b/templates/default/client/src/pages/WizardRoutes.tsx @@ -58,6 +58,9 @@ const STEPS: StepConfig[] = [ const WizardRoutes: React.FC = () => { const { currentStep, sdk } = useWizard() + // Memoize the current step path to avoid unnecessary recalculations. + const currentStepPath = useMemo(() => `/step${currentStep}`, [currentStep]) + // Early returns for error states. if (MISSING_ENV_VARS.length > 0) { return @@ -67,9 +70,6 @@ const WizardRoutes: React.FC = () => { return } - // Memoize the current step path to avoid unnecessary recalculations. - const currentStepPath = useMemo(() => `/step${currentStep}`, [currentStep]) - return (