diff --git a/packages/enclave-react/README.md b/packages/enclave-react/README.md index c91318cb06..fcc2cb56a8 100644 --- a/packages/enclave-react/README.md +++ b/packages/enclave-react/README.md @@ -122,7 +122,6 @@ function MyComponent() { - `isInitialized`: Boolean indicating if SDK is ready - `error`: Error message if initialization failed - `requestE3`: Function to request E3 computation -- `activateE3`: Function to activate E3 environment - `publishInput`: Function to publish encrypted inputs - `onEnclaveEvent`: Function to subscribe to events - `off`: Function to unsubscribe from events diff --git a/templates/default/client/src/context/WizardContext.tsx b/templates/default/client/src/context/WizardContext.tsx index 86361284bb..1aebb24cf3 100644 --- a/templates/default/client/src/context/WizardContext.tsx +++ b/templates/default/client/src/context/WizardContext.tsx @@ -17,17 +17,16 @@ import { getEnclaveSDKConfig } from '@/utils/sdk-config' export enum WizardStep { CONNECT_WALLET = 1, REQUEST_COMPUTATION = 2, - ACTIVATE_E3 = 3, - ENTER_INPUTS = 4, - ENCRYPT_SUBMIT = 5, - RESULTS = 6, + ENTER_INPUTS = 3, + ENCRYPT_SUBMIT = 4, + RESULTS = 5, } export interface E3State { id: bigint | null isRequested: boolean isCommitteePublished: boolean - isActivated: boolean + isCiphertextPublished: boolean publicKey: `0x${string}` | null expiresAt: bigint | null plaintextOutput: string | null @@ -38,7 +37,7 @@ const INITIAL_E3_STATE: E3State = { id: null, isRequested: false, isCommitteePublished: false, - isActivated: false, + isCiphertextPublished: false, publicKey: null, expiresAt: null, plaintextOutput: null, diff --git a/templates/default/client/src/pages/WizardRoutes.tsx b/templates/default/client/src/pages/WizardRoutes.tsx index fa94cdb3a2..e570fd440c 100644 --- a/templates/default/client/src/pages/WizardRoutes.tsx +++ b/templates/default/client/src/pages/WizardRoutes.tsx @@ -12,13 +12,11 @@ import { 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' @@ -41,10 +39,9 @@ interface StepConfig { 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 }, + { step: WizardStep.ENTER_INPUTS, path: '/step3', component: EnterInputs, icon: NumberSquareThreeIcon }, + { step: WizardStep.ENCRYPT_SUBMIT, path: '/step4', component: EncryptSubmit, icon: NumberSquareFourIcon }, + { step: WizardStep.RESULTS, path: '/step5', component: Results, icon: NumberSquareFiveIcon }, ] /** diff --git a/templates/default/client/src/pages/components/ErrorDisplay.tsx b/templates/default/client/src/pages/components/ErrorDisplay.tsx index 60070d8153..656d52873e 100644 --- a/templates/default/client/src/pages/components/ErrorDisplay.tsx +++ b/templates/default/client/src/pages/components/ErrorDisplay.tsx @@ -20,7 +20,7 @@ const ErrorDisplay: React.FC = ({ error, showDetails, onToggl const technicalMessage = error.message || JSON.stringify(error, null, 2) return ( -
+

Error: {userMessage}

diff --git a/templates/default/client/src/pages/steps/ActivateE3.tsx b/templates/default/client/src/pages/steps/ActivateE3.tsx deleted file mode 100644 index bd7277b046..0000000000 --- a/templates/default/client/src/pages/steps/ActivateE3.tsx +++ /dev/null @@ -1,153 +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 } 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) - 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/EncryptSubmit.tsx b/templates/default/client/src/pages/steps/EncryptSubmit.tsx index cce62ef1f6..b629e4e79a 100644 --- a/templates/default/client/src/pages/steps/EncryptSubmit.tsx +++ b/templates/default/client/src/pages/steps/EncryptSubmit.tsx @@ -5,7 +5,7 @@ // or FITNESS FOR A PARTICULAR PURPOSE. import React, { useState, useEffect } from 'react' -import { LockIcon, CheckCircleIcon } from '@phosphor-icons/react' +import { LockIcon, CheckCircleIcon, WarningCircleIcon } from '@phosphor-icons/react' import CardContent from '../components/CardContent' import Spinner from '../components/Spinner' import ErrorDisplay from '../components/ErrorDisplay' @@ -13,22 +13,34 @@ import { useWizard, WizardStep } from '../../context/WizardContext' import { decodePlaintextOutput } from '@enclave-e3/sdk' /** - * EncryptSubmit component - Fifth step in the Enclave wizard flow + * EncryptSubmit component - Fourth 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 { e3State, setE3State, setResult, setCurrentStep, inputPublishError, inputPublishSuccess, handleTryAgain, handleReset, sdk } = + useWizard() const { isInitialized, onEnclaveEvent, off, EnclaveEventType } = sdk const [showErrorDetails, setShowErrorDetails] = useState(false) + const [isExpired, setIsExpired] = useState(false) // Set up event listeners for this step useEffect(() => { if (!isInitialized) return + const handleCiphertextOutput = (event: any) => { + const { e3Id } = event.data + setE3State((prev) => { + if (prev.id !== null && e3Id === prev.id) { + return { ...prev, isCiphertextPublished: true } + } + return prev + }) + } + const handlePlaintextOutput = (event: any) => { const { e3Id, plaintextOutput } = event.data setE3State((prev) => { @@ -45,13 +57,31 @@ const EncryptSubmit: React.FC = () => { }) } + onEnclaveEvent(EnclaveEventType.CIPHERTEXT_OUTPUT_PUBLISHED, handleCiphertextOutput) onEnclaveEvent(EnclaveEventType.PLAINTEXT_OUTPUT_PUBLISHED, handlePlaintextOutput) return () => { + off(EnclaveEventType.CIPHERTEXT_OUTPUT_PUBLISHED, handleCiphertextOutput) off(EnclaveEventType.PLAINTEXT_OUTPUT_PUBLISHED, handlePlaintextOutput) } }, [isInitialized, onEnclaveEvent, off, EnclaveEventType, setE3State, setResult]) + // Check for E3 expiration + useEffect(() => { + if (!e3State.expiresAt || e3State.hasPlaintextOutput) return + + const checkExpiration = () => { + const nowSeconds = BigInt(Math.floor(Date.now() / 1000)) + if (nowSeconds > e3State.expiresAt!) { + setIsExpired(true) + } + } + + checkExpiration() + const interval = setInterval(checkExpiration, 5000) + return () => clearInterval(interval) + }, [e3State.expiresAt, e3State.hasPlaintextOutput]) + // Auto-advance to results when output is available useEffect(() => { if (e3State.hasPlaintextOutput) { @@ -59,17 +89,46 @@ const EncryptSubmit: React.FC = () => { } }, [e3State.hasPlaintextOutput, setCurrentStep]) + // Progress steps for the computing phase + const progressSteps = [ + { label: 'Inputs submitted', done: inputPublishSuccess }, + { label: 'FHE computation complete', done: e3State.isCiphertextPublished }, + { label: 'Committee decryption', done: e3State.hasPlaintextOutput }, + ] + return (
-

Step 5: Encrypting & Submitting

+

Step 4: Encrypting & Submitting

Secure Process Execution

- {!inputPublishError && !inputPublishSuccess && ( + {isExpired && !e3State.hasPlaintextOutput && ( +
+
+ +
+
+

+ E3 Input Window Expired +
+ The input deadline for this computation has passed. The computation may not have received enough inputs to produce a + result. +

+
+ +
+ )} + + {!isExpired && !inputPublishError && !inputPublishSuccess && (
@@ -102,27 +161,39 @@ const EncryptSubmit: React.FC = () => {
)} - {inputPublishSuccess && ( + {!isExpired && 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. -

+ + {/* Progress tracker */} +
+
    + {progressSteps.map((step, i) => ( +
  • + {step.done ? : } + {step.label} +
  • + ))} +
-
-
- + + {!e3State.isCiphertextPublished && ( +
+

+ The Compute Provider is executing the FHE computation over your encrypted inputs... +

-

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

-
+ )} + + {e3State.isCiphertextPublished && !e3State.hasPlaintextOutput && ( +
+

+ Ciphertext output published. Waiting for the Ciphernode Committee to collectively decrypt the result... +

+
+ )}
)}
diff --git a/templates/default/client/src/pages/steps/EnterInputs.tsx b/templates/default/client/src/pages/steps/EnterInputs.tsx index f65b575007..feffed2754 100644 --- a/templates/default/client/src/pages/steps/EnterInputs.tsx +++ b/templates/default/client/src/pages/steps/EnterInputs.tsx @@ -7,11 +7,14 @@ import React, { useState } from 'react' import { NumberSquareOneIcon } from '@phosphor-icons/react' import { hexToBytes } from 'viem' +import { useAccount, useWalletClient } from 'wagmi' import CardContent from '../components/CardContent' import { useWizard, WizardStep } from '../../context/WizardContext' +import { publishInput } from '../../utils/input' +import { getContractAddresses } from '../../utils/env-config' /** - * EnterInputs component - Fourth step in the Enclave wizard flow + * EnterInputs component - Third 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 @@ -22,13 +25,13 @@ const EnterInputs: React.FC = () => { const [input2, setInput2] = useState('') const { e3State, setCurrentStep, setLastTransactionHash, setInputPublishError, setInputPublishSuccess, setSubmittedInputs, sdk } = useWizard() - const { publishInput } = sdk + const { address } = useAccount() + const { data: walletClient } = useWalletClient() + const contracts = getContractAddresses() 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') + if (!input1 || !input2 || e3State.publicKey === null || e3State.id === null || !walletClient || !address) { return } @@ -55,14 +58,13 @@ const EnterInputs: React.FC = () => { throw new Error('Failed to encrypt inputs') } + const toHex = (bytes: Uint8Array): `0x${string}` => `0x${Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('')}` + // Publish first input - await publishInput(e3State.id, `0x${Array.from(encryptedInput1, (b) => b.toString(16).padStart(2, '0')).join('')}` as `0x${string}`) + await publishInput(walletClient, e3State.id, toHex(encryptedInput1), address, contracts.e3Program) // 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}`, - ) + const hash2 = await publishInput(walletClient, e3State.id, toHex(encryptedInput2), address, contracts.e3Program) setLastTransactionHash(hash2) setInputPublishSuccess(true) @@ -78,7 +80,7 @@ const EnterInputs: React.FC = () => {
-

Step 4: Enter Your Numbers

+

Step 3: Enter Your Numbers

Homomorphic Encrypted Computation

@@ -134,10 +136,14 @@ const EnterInputs: React.FC = () => { diff --git a/templates/default/client/src/pages/steps/RequestComputation.tsx b/templates/default/client/src/pages/steps/RequestComputation.tsx index eabfb108e0..6f93add909 100644 --- a/templates/default/client/src/pages/steps/RequestComputation.tsx +++ b/templates/default/client/src/pages/steps/RequestComputation.tsx @@ -42,30 +42,27 @@ const RequestComputation: React.FC = () => { if (!isInitialized) return const handleE3Requested = (event: any) => { - const e3Id = event.data.e3Id + const { e3Id, e3 } = event.data setE3State((prev) => ({ ...prev, id: e3Id, isRequested: true, + expiresAt: e3.inputWindow?.[1] ?? null, })) } 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}`, - } + setE3State((prev) => { + if (prev.id !== null && e3Id === prev.id) { + return { + ...prev, + isCommitteePublished: true, + publicKey: publicKey as `0x${string}`, } - return prev - }) - }, 2000) + } + return prev + }) } onEnclaveEvent(EnclaveEventType.E3_REQUESTED, handleE3Requested) @@ -80,12 +77,11 @@ const RequestComputation: React.FC = () => { // Auto-advance to next step when committee publishes useEffect(() => { if (e3State.isCommitteePublished && e3State.publicKey) { - setCurrentStep(WizardStep.ACTIVATE_E3) + setCurrentStep(WizardStep.ENTER_INPUTS) } }, [e3State.isCommitteePublished, e3State.publicKey, setCurrentStep]) const handleRequestComputation = async () => { - console.log('handleRequestComputation') setIsRequesting(true) setRequestError(null) setRequestSuccess(false) @@ -95,7 +91,7 @@ const RequestComputation: React.FC = () => { id: null, isRequested: false, isCommitteePublished: false, - isActivated: false, + isCiphertextPublished: false, publicKey: null, expiresAt: null, plaintextOutput: null, @@ -110,19 +106,24 @@ const RequestComputation: React.FC = () => { const committeeSize = DEFAULT_E3_CONFIG.committeeSize const publicClient = sdk.sdk.getPublicClient() - const inputWindow = await calculateInputWindow(publicClient, 60) // 1 minute + const inputWindow = await calculateInputWindow(publicClient, 600) // 10 min const thresholdBfvParams = await sdk.getThresholdBfvParamsSet() const e3ProgramParams = encodeBfvParams(thresholdBfvParams) const computeProviderParams = encodeComputeProviderParams(DEFAULT_COMPUTE_PROVIDER_PARAMS) - console.log('requestE3') - const hash = await requestE3({ + const requestParams = { committeeSize, inputWindow, e3Program: contracts.e3Program, e3ProgramParams, computeProviderParams, - }) + } + + const fee = await sdk.sdk.getE3Quote(requestParams) + const approveTx = await sdk.sdk.approveFeeToken(fee) + await publicClient.waitForTransactionReceipt({ hash: approveTx }) + + const hash = await requestE3(requestParams) setLocalTransactionHash(hash) setLastTransactionHash(hash) @@ -150,7 +151,7 @@ const RequestComputation: React.FC = () => {

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

@@ -172,7 +173,7 @@ const RequestComputation: React.FC = () => {
Public Key: {e3State.publicKey.slice(0, 20)}...{e3State.publicKey.slice(-10)}
- Ready to activate E3 environment. + Ready for encrypted input.

) : ( @@ -224,7 +225,7 @@ const RequestComputation: React.FC = () => { ? 'Submitting to Blockchain...' : e3State.isRequested ? e3State.isCommitteePublished - ? 'Committee Ready - Proceeding to Activation!' + ? 'Committee Ready - Proceeding to Input!' : 'Waiting for Committee...' : 'Request E3 Computation (0.001 ETH)'} diff --git a/templates/default/client/src/pages/steps/Results.tsx b/templates/default/client/src/pages/steps/Results.tsx index ec873365c1..d8ed6baaea 100644 --- a/templates/default/client/src/pages/steps/Results.tsx +++ b/templates/default/client/src/pages/steps/Results.tsx @@ -4,31 +4,48 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -import React from 'react' -import { CheckCircleIcon } from '@phosphor-icons/react' +import React, { useState } from 'react' +import { CheckCircleIcon, CopyIcon, CheckIcon } from '@phosphor-icons/react' import CardContent from '../components/CardContent' import { useWizard } from '../../context/WizardContext' /** - * Results component - Sixth step in the Enclave wizard flow + * Results component - Fifth 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 [copiedField, setCopiedField] = useState(null) - const onReset = () => { - handleReset() + const copyToClipboard = async (text: string, field: string) => { + try { + await navigator.clipboard.writeText(text) + setCopiedField(field) + setTimeout(() => setCopiedField(null), 2000) + } catch { + // Fallback for environments without clipboard API + } } + const renderCopyButton = (text: string, field: string) => ( + + ) + return (
-

Step 6: Results

+

Step 5: Results

Computation Complete!

@@ -38,31 +55,36 @@ const Results: React.FC = () => { Your Encrypted Computation:

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

- {result !== null &&

✅ Computed securely using FHE with distributed key decryption!

} + {result !== null &&

Computed securely using FHE with distributed key decryption.

}
-

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

+ E3 ID: {String(e3State.id)} + {e3State.id !== null && renderCopyButton(String(e3State.id), 'e3id')}

{lastTransactionHash && (
-

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

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

)} {e3State.plaintextOutput && (
-

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

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

)} @@ -70,7 +92,7 @@ const Results: React.FC = () => {

- 🔒 Cryptographic Guarantees: Your inputs remained encrypted throughout the entire process. The Ciphernode + 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.

@@ -78,7 +100,7 @@ const Results: React.FC = () => {