Files
2025-12-27 21:15:45 -08:00

475 lines
17 KiB
TypeScript

"use client";
import { useMemo, useState, useEffect } from "react";
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";
import type { AllowedChainIds } from "~~/utils/helper/networks";
import { notification } from "~~/utils/helper/notification";
/*
* Main ERC7984 React component for interacting with confidential tokens
* - "Decrypt" button: allows you to decrypt the current balance handle
* - "Transfer" button: allows you to transfer tokens using FHE operations
*/
export const ERC7984Demo = () => {
const { isConnected, chain, address } = useAccount();
const chainId = chain?.id;
//////////////////////////////////////////////////////////////////////////////
// FHEVM instance
//////////////////////////////////////////////////////////////////////////////
// Create EIP-1193 provider from wagmi for FHEVM
const provider = useMemo(() => {
if (typeof window === "undefined") return undefined;
// Get the wallet provider from window.ethereum
return (window as any).ethereum;
}, []);
const initialMockChains = { 31337: "http://localhost:8545" };
// State to control fhevm initialization - wait for mount and provider
const [isMounted, setIsMounted] = useState(false);
const [fhevmEnabled, setFhevmEnabled] = useState(false);
// Set mounted on client side only
useEffect(() => {
setIsMounted(true);
}, []);
// Enable FHE ONLY after mounted and when provider and chainId are available
useEffect(() => {
if (!isMounted) return;
const shouldEnable = Boolean(provider && chainId);
if (shouldEnable && !fhevmEnabled) {
// Small delay to ensure everything is stable
const timeoutId = setTimeout(() => {
setFhevmEnabled(true);
}, 500);
return () => clearTimeout(timeoutId);
} else if (!shouldEnable && fhevmEnabled) {
setFhevmEnabled(false);
}
}, [isMounted, provider, chainId, fhevmEnabled]);
const {
instance: fhevmInstance,
status: fhevmStatus,
error: fhevmError,
} = useFhevm({
provider,
chainId,
initialMockChains,
enabled: fhevmEnabled,
});
//////////////////////////////////////////////////////////////////////////////
// useERC7984 is a custom hook containing all the ERC7984 logic, including
// - calling the ERC7984 contract
// - encrypting FHE inputs
// - decrypting FHE handles
//////////////////////////////////////////////////////////////////////////////
const erc7984 = useERC7984Wagmi({
instance: fhevmInstance,
initialMockChains,
});
//////////////////////////////////////////////////////////////////////////////
// Airdrop/Faucet state and logic
//////////////////////////////////////////////////////////////////////////////
const allowedChainId = typeof chainId === "number" ? (chainId as AllowedChainIds) : undefined;
const { data: airdropContract } = useDeployedContractInfo({ contractName: "Airdrop", chainId: allowedChainId });
const [claimStatus, setClaimStatus] = useState<"idle" | "checking" | "claiming" | "claimed" | "error">("idle");
const [alreadyClaimed, setAlreadyClaimed] = useState<boolean>(false);
// Check if user has already claimed
useEffect(() => {
async function checkClaimStatus() {
if (!address || !erc7984.ethersSigner || !airdropContract || !erc7984.contractAddress) return;
setClaimStatus("checking");
try {
const contract = new ethers.Contract(
airdropContract.address,
airdropContract.abi,
erc7984.ethersSigner
);
const claimed = await contract.hasClaimed(address, erc7984.contractAddress);
setAlreadyClaimed(claimed);
setClaimStatus(claimed ? "claimed" : "idle");
} catch (error) {
console.error("Error checking claim status:", error);
setClaimStatus("idle");
}
}
checkClaimStatus();
}, [address, erc7984.ethersSigner, airdropContract, erc7984.contractAddress]);
const handleClaim = async () => {
if (!erc7984.ethersSigner || !address || !airdropContract || !erc7984.contractAddress) {
notification.error("Please connect your wallet first");
return;
}
setClaimStatus("claiming");
try {
const contract = new ethers.Contract(
airdropContract.address,
airdropContract.abi,
erc7984.ethersSigner
);
notification.info("Claiming tokens...");
const tx = await contract.claim(erc7984.contractAddress);
notification.info("Transaction submitted. Waiting for confirmation...");
const receipt = await tx.wait();
if (receipt.status === 1) {
notification.success("Successfully claimed tokens!");
setClaimStatus("claimed");
setAlreadyClaimed(true);
// Refresh balance after claim
erc7984.refreshBalanceHandle();
} else {
throw new Error("Transaction failed");
}
} catch (error: any) {
console.error("Claim error:", error);
let errorMessage = "Failed to claim tokens";
if (error.message?.includes("AlreadyClaimed")) {
errorMessage = "You have already claimed tokens";
setAlreadyClaimed(true);
setClaimStatus("claimed");
} else if (error.message?.includes("user rejected")) {
errorMessage = "Transaction cancelled";
setClaimStatus("idle");
} else {
setClaimStatus("error");
}
notification.error(errorMessage);
setTimeout(() => {
setClaimStatus(alreadyClaimed ? "claimed" : "idle");
}, 2000);
}
};
//////////////////////////////////////////////////////////////////////////////
// Transfer state
//////////////////////////////////////////////////////////////////////////////
const [transferTo, setTransferTo] = useState<string>("");
const [transferAmount, setTransferAmount] = useState<string>("1");
//////////////////////////////////////////////////////////////////////////////
// UI Stuff:
// --------
// A basic page containing
// - A bunch of debug values allowing you to better visualize the React state
// - 1x "Decrypt" button (to decrypt the latest balance handle)
// - Transfer form with amount and recipient
//////////////////////////////////////////////////////////////////////////////
const buttonClass =
"glass-button inline-flex items-center justify-center px-6 py-3 font-semibold " +
"transition-all duration-300 " +
"focus-visible:outline-none " +
"disabled:opacity-40 disabled:pointer-events-none disabled:cursor-not-allowed";
// Primary (accent) button
const primaryButtonClass =
buttonClass +
" text-[#2D2D2D] cursor-pointer";
// Secondary button
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 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";
if (!isConnected) {
return (
<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>
</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>
<div className="flex items-center justify-center">
<RainbowKitCustomConnectButton />
</div>
</div>
</div>
);
}
return (
<div className="max-w-6xl mx-auto p-6 space-y-6 relative z-10">
{/* 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>
</div>
{/* Balance Handle Display */}
<div className={sectionClass}>
<h3 className={titleClass}>💰 Confidential Balance</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{/* Column 1 - Encrypted Handle */}
<div className="space-y-3">
<h4 className="text-sm font-semibold text-[#2D2D2D]/60 mb-2">Encrypted Handle</h4>
{printPropertyTruncated("Handle", erc7984.handle || "No handle available")}
</div>
{/* Column 2 - Decrypted Value */}
<div className="space-y-3">
<h4 className="text-sm font-semibold text-[#2D2D2D]/60 mb-2">Decrypted Value</h4>
{printProperty("Balance", erc7984.isDecrypted ? erc7984.clear : "Not decrypted yet")}
</div>
{/* Column 3 - Your Address */}
<div className="space-y-3">
<h4 className="text-sm font-semibold text-[#2D2D2D]/60 mb-2">Your Address</h4>
{printPropertyTruncated("Address", erc7984.address || "N/A")}
</div>
{/* Column 4 - Faucet */}
<div className="flex flex-col justify-center">
<h4 className="text-sm font-semibold text-[#2D2D2D]/60 mb-2">🚰 Faucet</h4>
<button
className={alreadyClaimed ? successButtonClass + " w-full" : primaryButtonClass + " w-full"}
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"
)}
</button>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="grid grid-cols-1 gap-4 text-black">
<button
className={erc7984.isDecrypted ? successButtonClass : primaryButtonClass}
disabled={!erc7984.canDecrypt}
onClick={erc7984.decryptBalanceHandle}
>
{erc7984.canDecrypt
? "🔓 Decrypt Balance"
: erc7984.isDecrypted
? `✅ Decrypted: ${erc7984.clear}`
: erc7984.isDecrypting
? "⏳ Decrypting..."
: "❌ Nothing to decrypt"}
</button>
</div>
{/* Transfer Section */}
<div className={sectionClass}>
<h3 className={titleClass}>📤 Transfer Confidential Tokens</h3>
<div className="space-y-5">
<div>
<label htmlFor="transferTo" className="block text-sm font-semibold text-[#2D2D2D] mb-2">
Recipient Address
</label>
<input
id="transferTo"
type="text"
placeholder="0x..."
value={transferTo}
onChange={e => setTransferTo(e.target.value)}
className="glass-input w-full px-4 py-3 text-[#2D2D2D] placeholder:text-[#2D2D2D]/40"
/>
</div>
<div>
<label htmlFor="transferAmount" className="block text-sm font-semibold text-[#2D2D2D] mb-2">
Amount
</label>
<input
id="transferAmount"
type="number"
placeholder="1"
value={transferAmount}
onChange={e => setTransferAmount(e.target.value)}
className="glass-input w-full px-4 py-3 text-[#2D2D2D] placeholder:text-[#2D2D2D]/40"
/>
</div>
<button
className={secondaryButtonClass + " w-full"}
disabled={!erc7984.canTransfer || !transferTo || !transferAmount}
onClick={() => erc7984.transferTokens(transferTo, Number(transferAmount))}
>
{erc7984.canTransfer && transferTo && transferAmount
? `🔄 Transfer ${transferAmount} tokens`
: erc7984.isProcessing
? "⏳ Processing transfer..."
: "❌ Cannot transfer"}
</button>
</div>
</div>
{/* Messages */}
{erc7984.message && (
<div className={sectionClass}>
<h3 className={titleClass}>💬 Messages</h3>
<div className="glass-card p-5">
<p className="text-[#2D2D2D] font-medium">{erc7984.message}</p>
</div>
</div>
)}
{/* Status Cards */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className={sectionClass}>
<h3 className={titleClass}>🔧 FHEVM Instance</h3>
<div className="space-y-3">
{printProperty("Instance Status", fhevmInstance ? "✅ Connected" : "❌ Disconnected")}
{printProperty("Status", fhevmStatus)}
{printProperty("Error", fhevmError ?? "No errors")}
</div>
</div>
<div className={sectionClass}>
<h3 className={titleClass}>📊 Token Status</h3>
<div className="space-y-3">
{printProperty("Refreshing", erc7984.isRefreshing)}
{printProperty("Decrypting", erc7984.isDecrypting)}
{printProperty("Processing", erc7984.isProcessing)}
{printProperty("Can Get Balance", erc7984.canGetBalance)}
{printProperty("Can Decrypt", erc7984.canDecrypt)}
{printProperty("Can Transfer", erc7984.canTransfer)}
</div>
</div>
</div>
</div>
);
};
function printProperty(name: string, value: unknown) {
let displayValue: string;
if (typeof value === "boolean") {
return printBooleanProperty(name, value);
} else if (typeof value === "string" || typeof value === "number") {
displayValue = String(value);
} else if (typeof value === "bigint") {
displayValue = String(value);
} else if (value === null) {
displayValue = "null";
} else if (value === undefined) {
displayValue = "undefined";
} else if (value instanceof Error) {
displayValue = value.message;
} else {
displayValue = JSON.stringify(value);
}
return (
<div className="flex flex-col gap-2 py-3 px-4 glass-card w-full">
<span className="text-[#2D2D2D]/70 font-medium text-xs">{name}</span>
<span className="font-mono text-sm font-bold text-[#2D2D2D] bg-[#E8E8E8] px-3 py-1.5 text-center">
{displayValue}
</span>
</div>
);
}
function printPropertyTruncated(name: string, value: unknown) {
let displayValue: string;
if (typeof value === "string" || typeof value === "number") {
displayValue = String(value);
} else if (typeof value === "bigint") {
displayValue = String(value);
} else if (value === null) {
displayValue = "null";
} else if (value === undefined) {
displayValue = "undefined";
} else if (value instanceof Error) {
displayValue = value.message;
} else {
displayValue = JSON.stringify(value);
}
// Truncate long strings
const shouldTruncate = displayValue.length > 12;
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">
<span className="text-[#2D2D2D]/70 font-medium text-xs">{name}</span>
<span className="font-mono text-sm font-bold text-[#2D2D2D] bg-[#E8E8E8] px-3 py-1.5 text-center cursor-help">
{truncatedValue}
</span>
{/* Tooltip on hover */}
{shouldTruncate && (
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-2 bg-[#2D2D2D] text-[#F4F4F4] text-xs opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50 max-w-xs break-all">
<div className="font-mono">{displayValue}</div>
<div className="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-[#2D2D2D]"></div>
</div>
)}
</div>
);
}
function printBooleanProperty(name: string, value: boolean) {
return (
<div className="flex flex-col gap-2 py-3 px-4 glass-card w-full">
<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 ? "✓ true" : "✗ false"}
</span>
</div>
);
}