Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 29 additions & 44 deletions packages/erc7984example/app/_components/ERC7984Demo.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"use client";

import { useMemo, useState, useEffect } from "react";
import { useEffect, useMemo, useState } from "react";
import { FHEBenchmark } from "./FHEBenchmark";
import { ethers } from "ethers";
import { useFhevm } from "fhevm-sdk";
import { useAccount } from "wagmi";
import { ethers } from "ethers";
import { RainbowKitCustomConnectButton } from "~~/components/helper/RainbowKitCustomConnectButton";
import { useERC7984Wagmi } from "~~/hooks/erc7984/useERC7984Wagmi";
import { useDeployedContractInfo } from "~~/hooks/helper";
Expand Down Expand Up @@ -101,11 +102,7 @@ export const ERC7984Demo = () => {

setClaimStatus("checking");
try {
const contract = new ethers.Contract(
airdropContract.address,
airdropContract.abi,
erc7984.ethersSigner
);
const contract = new ethers.Contract(airdropContract.address, airdropContract.abi, erc7984.ethersSigner);

const claimed = await contract.hasClaimed(address, erc7984.contractAddress);
setAlreadyClaimed(claimed);
Expand All @@ -128,11 +125,7 @@ export const ERC7984Demo = () => {
setClaimStatus("claiming");

try {
const contract = new ethers.Contract(
airdropContract.address,
airdropContract.abi,
erc7984.ethersSigner
);
const contract = new ethers.Contract(airdropContract.address, airdropContract.abi, erc7984.ethersSigner);

notification.info("Claiming tokens...");

Expand Down Expand Up @@ -198,19 +191,13 @@ export const ERC7984Demo = () => {
"disabled:opacity-40 disabled:pointer-events-none disabled:cursor-not-allowed";

// Primary (accent) button
const primaryButtonClass =
buttonClass +
" text-[#2D2D2D] cursor-pointer";
const primaryButtonClass = buttonClass + " text-[#2D2D2D] cursor-pointer";

// Secondary button
const secondaryButtonClass =
buttonClass +
" !bg-[#2D2D2D] text-[#F4F4F4] hover:!bg-[#A38025] cursor-pointer";
const secondaryButtonClass = buttonClass + " !bg-[#2D2D2D] text-[#F4F4F4] hover:!bg-[#A38025] cursor-pointer";

// Success/confirmed state
const successButtonClass =
buttonClass +
" !bg-[#A38025] text-[#F4F4F4] hover:!bg-[#2D2D2D]";
const successButtonClass = buttonClass + " !bg-[#A38025] text-[#F4F4F4] hover:!bg-[#2D2D2D]";

const titleClass = "font-semibold text-[#2D2D2D] text-2xl mb-4 pb-3 border-b border-[#2D2D2D]";
const sectionClass = "glass-card-strong p-8 mb-6 text-[#2D2D2D] relative z-10";
Expand All @@ -220,12 +207,12 @@ export const ERC7984Demo = () => {
<div className="max-w-6xl mx-auto p-6 min-h-[60vh] flex items-center justify-center relative z-10">
<div className="glass-card-strong p-12 text-center max-w-md">
<div className="mb-6">
<span className="inline-flex items-center justify-center w-20 h-20 bg-[#FFD208] text-5xl">
⚠️
</span>
<span className="inline-flex items-center justify-center w-20 h-20 bg-[#FFD208] text-5xl">⚠️</span>
</div>
<h2 className="text-3xl font-bold text-[#2D2D2D] mb-3">Wallet not connected</h2>
<p className="text-[#2D2D2D]/80 mb-8 text-lg">Connect your wallet to use the ERC7984 confidential token demo.</p>
<p className="text-[#2D2D2D]/80 mb-8 text-lg">
Connect your wallet to use the ERC7984 confidential token demo.
</p>
<div className="flex items-center justify-center">
<RainbowKitCustomConnectButton />
</div>
Expand All @@ -239,7 +226,9 @@ export const ERC7984Demo = () => {
{/* Header */}
<div className="text-center mb-10">
<h1 className="text-5xl font-bold mb-4 text-[#2D2D2D] tracking-tight">ERC7984 Confidential Token Demo</h1>
<p className="text-xl text-[#2D2D2D]/70">Interact with the Fully Homomorphic Encryption confidential token contract</p>
<p className="text-xl text-[#2D2D2D]/70">
Interact with the Fully Homomorphic Encryption confidential token contract
</p>
</div>

{/* Balance Handle Display */}
Expand Down Expand Up @@ -272,17 +261,15 @@ export const ERC7984Demo = () => {
onClick={handleClaim}
disabled={!address || claimStatus === "claiming" || claimStatus === "checking" || alreadyClaimed}
>
{!address ? (
"Connect Wallet"
) : claimStatus === "checking" ? (
"⏳ Checking..."
) : claimStatus === "claiming" ? (
"⏳ Claiming Tokens..."
) : alreadyClaimed ? (
"✅ Already Claimed"
) : (
"💧 Get Free Tokens"
)}
{!address
? "Connect Wallet"
: claimStatus === "checking"
? "⏳ Checking..."
: claimStatus === "claiming"
? "⏳ Claiming Tokens..."
: alreadyClaimed
? "✅ Already Claimed"
: "💧 Get Free Tokens"}
</button>
</div>
</div>
Expand Down Expand Up @@ -382,6 +369,9 @@ export const ERC7984Demo = () => {
</div>
</div>
</div>

{/* FHE Performance Benchmark */}
<FHEBenchmark instance={fhevmInstance} fhevmStatus={fhevmStatus} />
</div>
);
};
Expand Down Expand Up @@ -433,9 +423,7 @@ function printPropertyTruncated(name: string, value: unknown) {

// Truncate long strings
const shouldTruncate = displayValue.length > 12;
const truncatedValue = shouldTruncate
? `${displayValue.slice(0, 6)}...${displayValue.slice(-4)}`
: displayValue;
const truncatedValue = shouldTruncate ? `${displayValue.slice(0, 6)}...${displayValue.slice(-4)}` : displayValue;

return (
<div className="flex flex-col gap-2 py-3 px-4 glass-card w-full group relative">
Expand All @@ -461,14 +449,11 @@ function printBooleanProperty(name: string, value: boolean) {
<span className="text-[#2D2D2D]/70 font-medium text-xs">{name}</span>
<span
className={`font-mono text-sm font-bold px-3 py-1.5 text-center ${
value
? "text-[#F4F4F4] bg-[#A38025]"
: "text-[#F4F4F4] bg-[#2D2D2D]"
value ? "text-[#F4F4F4] bg-[#A38025]" : "text-[#F4F4F4] bg-[#2D2D2D]"
}`}
>
{value ? "✓ true" : "✗ false"}
</span>
</div>
);
}

225 changes: 225 additions & 0 deletions packages/erc7984example/app/_components/FHEBenchmark.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
"use client";

import { useState } from "react";
import { FhevmInstance } from "fhevm-sdk";
import { useAccount } from "wagmi";
import { useDeployedContractInfo } from "~~/hooks/helper";
import { useWagmiEthers } from "~~/hooks/wagmi/useWagmiEthers";
import type { AllowedChainIds } from "~~/utils/helper/networks";

type BenchmarkResult = {
operation: string;
duration: number;
timestamp: string;
};

type FHEBenchmarkProps = {
instance: FhevmInstance | undefined;
fhevmStatus: string;
};

export const FHEBenchmark = ({ instance, fhevmStatus }: FHEBenchmarkProps) => {
const { chain } = useAccount();
const chainId = chain?.id;

const initialMockChains = { 31337: "http://localhost:8545" };

const [results, setResults] = useState<BenchmarkResult[]>([]);
const [isRunning, setIsRunning] = useState(false);
const [threadInfo, setThreadInfo] = useState<string>("Click 'Check Threading' to analyze");
const [statusMessage, setStatusMessage] = useState<string>("");

const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

// Get ethers signer and contract info
const { ethersSigner } = useWagmiEthers(initialMockChains);
const allowedChainId = typeof chainId === "number" ? (chainId as AllowedChainIds) : undefined;
const { data: erc7984 } = useDeployedContractInfo({ contractName: "ERC7984Example", chainId: allowedChainId });

const addResult = (operation: string, duration: number) => {
setResults(prev => [
...prev,
{
operation,
duration,
timestamp: new Date().toISOString(),
},
]);
};

const checkThreading = () => {
try {
const info: string[] = [];

// Check for crossOriginIsolated (required for SharedArrayBuffer/multi-threading)
info.push(`crossOriginIsolated: ${window.crossOriginIsolated}`);

// Check for SharedArrayBuffer support
info.push(`SharedArrayBuffer: ${typeof SharedArrayBuffer !== "undefined"}`);

// Check navigator.hardwareConcurrency
info.push(`CPU cores: ${navigator.hardwareConcurrency || "unknown"}`);

// Check if relayerSDK is loaded
const w = window as any;
info.push(`relayerSDK loaded: ${!!w.relayerSDK}`);

setThreadInfo(info.join(" | "));
} catch (e) {
setThreadInfo(`Error: ${e}`);
}
};

const waitWithCountdown = async (seconds: number, message: string) => {
for (let i = seconds; i > 0; i--) {
setStatusMessage(`${message} (${i}s remaining)`);
await delay(1000);
}
setStatusMessage("");
};

const runEncryptionBenchmark = async () => {
if (!instance || !ethersSigner || !erc7984?.address) {
alert("Instance, signer, or contract not ready");
return;
}

setIsRunning(true);
setResults([]); // Clear previous results
const userAddress = await ethersSigner.getAddress();
const totalRuns = 3; // Reduced to 3 runs to minimize rate limit risk

try {
// Warm-up run
setStatusMessage("Running warm-up encryption...");
console.log("[Benchmark] Warm-up encryption...");
const warmupStart = performance.now();
const warmupInput = instance.createEncryptedInput(erc7984.address, userAddress);
(warmupInput as any).add64(BigInt(100));
await (warmupInput as any).encrypt();
const warmupEnd = performance.now();
addResult("Warm-up (euint64)", warmupEnd - warmupStart);

// Wait before next request
await waitWithCountdown(10, "Rate limit cooldown");

// Run benchmark encryptions with delays
for (let i = 0; i < totalRuns; i++) {
setStatusMessage(`Running encryption #${i + 1}...`);
console.log(`[Benchmark] Encryption #${i + 1}...`);
const startN = performance.now();
const inputN = instance.createEncryptedInput(erc7984.address, userAddress);
(inputN as any).add64(BigInt(i * 1000 + 12345));
await (inputN as any).encrypt();
const endN = performance.now();
addResult(`Encrypt euint64 #${i + 1}`, endN - startN);

// Wait between requests (except after the last one)
if (i < totalRuns - 1) {
await waitWithCountdown(10, "Rate limit cooldown");
}
}

setStatusMessage("Benchmark complete!");
console.log("[Benchmark] Encryption complete!");
await delay(2000);
setStatusMessage("");
} catch (e) {
console.error("[Benchmark] Encryption error:", e);
addResult("Encryption ERROR", -1);
setStatusMessage("Error occurred during benchmark");
} finally {
setIsRunning(false);
}
};

const clearResults = () => {
setResults([]);
};

const isReady = instance && ethersSigner && erc7984?.address;
const validResults = results.filter(r => r.duration > 0 && !r.operation.includes("Warm-up"));
const avgDuration =
validResults.length > 0 ? validResults.reduce((a, b) => a + b.duration, 0) / validResults.length : 0;

return (
<div className="p-4 border rounded-lg bg-base-200 space-y-4 w-full max-w-2xl">
<h2 className="text-xl font-bold">FHE Performance Benchmark</h2>

{/* Threading Info */}
<div className="p-3 bg-base-300 rounded">
<div className="font-semibold mb-2">Threading Status:</div>
<div className="text-sm font-mono break-all">{threadInfo}</div>
<button className="btn btn-sm btn-secondary mt-2" onClick={checkThreading}>
Check Threading
</button>
</div>

{/* Status */}
<div className="text-sm space-y-1">
<div>
FHEVM Status: <span className="font-mono">{fhevmStatus}</span>
</div>
<div>
Ready: <span className={isReady ? "text-success" : "text-error"}>{isReady ? "Yes" : "No"}</span>
{!isReady && " - Connect wallet and wait for FHEVM instance"}
</div>
</div>

{/* Rate Limit Warning */}
<div className="p-3 bg-warning/20 border border-warning rounded text-sm">
<span className="font-semibold">Rate Limit Warning:</span> The FHE encryption service has strict rate limits.
More than 5 requests in 10 seconds will result in a 1-hour ban. This benchmark includes 10-second delays between
operations to stay safe.
</div>

{/* Actions */}
<div className="flex gap-2 flex-wrap">
<button className="btn btn-primary" onClick={runEncryptionBenchmark} disabled={!isReady || isRunning}>
{isRunning ? "Running..." : "Run Encryption Benchmark"}
</button>
<button className="btn btn-ghost" onClick={clearResults} disabled={results.length === 0}>
Clear Results
</button>
</div>

{/* Status Message */}
{statusMessage && (
<div className="p-3 bg-info/20 border border-info rounded text-sm font-mono">{statusMessage}</div>
)}

{/* Results Table */}
{results.length > 0 && (
<div className="overflow-x-auto">
<table className="table table-sm">
<thead>
<tr>
<th>Operation</th>
<th>Duration (ms)</th>
<th>Time</th>
</tr>
</thead>
<tbody>
{results.map((r, i) => (
<tr key={i} className={r.operation.includes("Warm-up") ? "opacity-50" : ""}>
<td>{r.operation}</td>
<td className={r.duration < 0 ? "text-error" : ""}>
{r.duration < 0 ? "ERROR" : r.duration.toFixed(2)}
</td>
<td className="text-xs opacity-70">{r.timestamp.split("T")[1].split(".")[0]}</td>
</tr>
))}
</tbody>
</table>
</div>
)}

{/* Summary */}
{validResults.length > 0 && (
<div className="text-sm text-base-content/70">
Average (excluding warm-up): <strong>{avgDuration.toFixed(2)} ms</strong> across {validResults.length} runs
</div>
)}
</div>
);
};
Loading
Loading