Skip to content

DiceGameDapp Using CDN

A simple Dice Game DApp powered by FHEVM smart contracts with fully homomorphic encryption.

Here we will proceed with integrating the FHEVM smart contract & Fhevm.js with the React frontend. and determining the results of the game.

Prerequisites

  • Basic understanding of creating and initializing React-based projects
  • Node.js & npm/pnpm
  • Familiarity with Hardhat & Ethers.js
  • FHEVM SDK & Hardhat Plugin
  • Knowledge of wallet integration (MetaMask.js/Ethers.js) for connecting wallet extensions to DApps

Project Structure Root

This project uses the fhevm-hardhat-template as its foundation.

We have integrated a frontend into this template to create a complete DApp.

fhevm-dice-dapp-cdn/
├── contracts/                # Solidity contracts
│   └── FHEDiceGame.sol

├── deploy/                   # Deployment scripts
│   └── deploy.ts

├── frontend/                 # React frontend (dApp UI)
│   ├── node_modules/
│   ├── public/               # Static assets (favicon, index.html, etc.)
│   ├── src/
│   │   ├── components/       # React UI components
│   │   ├── hooks/            # Custom React hooks (e.g. useFHE)
│   │   ├── App.css
│   │   ├── App.js            # Main React app
│   │   └── index.js          # React entry point
│   ├── package.json
│   ├── package-lock.json
│   ├── pnpm-lock.yaml
│   └── README.md

├── tasks/                    # Hardhat tasks (custom CLI tasks)
│   └── FHEDiceGame.ts

├── test/                     # Hardhat/ethers test scripts
│    └── FHEDiceGame.ts

├── hardhat.config.ts         # Hardhat config
├── package.json              # Root package.json for Hardhat project
├── package-lock.json
├── tsconfig.json             # TypeScript config
└── README.md

Directory Overview

  • contracts/ - Contains all Solidity smart contracts (main game logic + encryption examples)
  • deploy/ - Scripts to deploy contracts onto testnets/mainnet
  • frontend/ - Full React-based dApp for interacting with the contracts
    • node_modules/ - Installed dependencies
    • public/ - Static files like favicon and index.html
    • src/ - Application source code
      • components/ - Reusable UI components (like Dice UI, results display, Playground)
      • hooks/ - Custom React hooks for wallet connection & FHE interactions
      • App.css - Main CSS
      • App.js - Root React component
      • index.js - Entry point
    • package.json - Frontend dependencies & scripts
    • package-lock.json / pnpm-lock.yaml - Locked dependency versions
  • tasks/ - Custom Hardhat CLI tasks
  • test/ - Tests for contracts (Hardhat + ethers.js)
  • hardhat.config.ts - Hardhat setup
  • README.md - Documentation for the entire project

Initiate a new project

Under the same root folder fhevm-hardhat-template Now proceed to initiate a new react project

npx create-react-app frontend
cd frontend

Now Install the required dependencies

npm install 

Using directly the library Setup the library @zama-fhe/relayer-sdk consists of multiple files, including WASM files and WebWorkers, which can make packaging these components correctly in your setup cumbersome. To simplify this process, especially if you're developing a dApp with server-side rendering (SSR), we recommend using our CDN. Include this line at the top of your project. Using UMD CDN

<script
  src="https://cdn.zama.ai/relayer-sdk-js/0.2.0/relayer-sdk-js.umd.cjs"
  type="text/javascript"
></script>

Update the project code

Under Public/index.HTML Replace with

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/dice-icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#ff6b6b" />
<meta name="description" content="FHEVM Dice Game" />
<meta name="keywords" content="dice game, blockchain, FHEVM, Zama, confidential" />
<meta name="author" content="FHEVM Dice Game" />
<meta http-equiv="Cross-Origin-Opener-Policy" content="same-origin" />
<meta http-equiv="Cross-Origin-Embedder-Policy" content="require-corp" />
<meta property="og:title" content="FHEVM Dice Game - Confidential Gaming" />
<meta property="og:description" content="A privacy-focused dice game built with Zama!" />
<meta property="og:type" content="website" />
<meta property="og:image" content="%PUBLIC_URL%/dice-preview.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="FHEVM Dice Game - Confidential Gaming" />
<meta name="twitter:description" content="dice game built with Zama FHEVM technology!" />
<meta name="twitter:image" content="%PUBLIC_URL%/dice-preview.png" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/dice-icon.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
 
<!-- Zama FHEVM Relayer SDK -->
<script src="https://cdn.zama.ai/relayer-sdk-js/0.2.0/relayer-sdk-js.umd.cjs" 
        type="text/javascript" crossorigin="anonymous"></script>
 
<!-- Dice Game Favicon and Icons -->
<link rel="shortcut icon" href="%PUBLIC_URL%/dice-icon.png" type="image/png" />
 
<!-- Preload important assets -->
<link rel="preload" href="%PUBLIC_URL%/dice-sounds.mp3" as="audio" />
 
<title>FHEVM Dice Game - Confidential Gaming DApp</title>
</head>
<body>
<noscript>
<div style="text-align: center; padding: 50px; font-family: Arial, sans-serif;">
<h2>🎲 FHEVM Dice Game</h2>
<p>You need to enable JavaScript to roll the dice and play this confidential game.</p>
<p>Please enable JavaScript in your browser settings to continue.</p>
</div>
</noscript>
<div id="root"></div>
<!--
Welcome to FHEVM Dice Game!
 
This is a confidential dice game built with Zama's FHEVM technology.
Features:
- Encrypted dice rolls for complete privacy
- Fair and verifiable randomness
- Blockchain-based smart contracts
- Real-time gameplay with instant results
 
🎲 Roll the dice and experience privacy-preserving gaming!
-->
</body>
</html>

Under src/App.js to

import React, { useState } from 'react';
import Header from './components/Header';
import WalletConnect from './components/WalletConnect';
 
 
 
import ContractInfo from './components/ContractInfo';
import Footer from './components/Footer';
import ToastContainer from './components/ToastContainer';
import StatusMessage from './components/StatusMessage';
 
import { useWallet } from './hooks/useWallet';
import { useToast } from './hooks/useToast';
import { useDiceGame } from './hooks/useDiceGame';
import PlayDice from './components/PlayDice';
import DiceResults from './components/DiceResults';
 
function App() {
  const [txStatus, setTxStatus] = useState('');
 
  // FHEDiceGame contract address - update to your deployed address
  const contractAddress = "0xA6915c97f44f1708e37dD871CCE991fD6D45E943";
 
  const { messages, showToast } = useToast();
  const { account, isConnected, connectWallet } = useWallet(showToast, contractAddress);
  const {
    loading,
    encryptedState,
    decryptedState,
    fairness,
    playDice,
    refreshEncryptedState,
    decryptLastDiceRoll,
    decryptPlayerGuess,
    decryptWinnerStatus
  } = useDiceGame(account, showToast, setTxStatus, contractAddress);
 
  return (
    <div className="app">
      <div className="container">
        <div className="app-content">
          <Header />
          <div className="section contract-section">
                <ContractInfo contractAddress={contractAddress} />
              </div>
          <ToastContainer messages={messages} />
 
          {/* Wallet Connection Section */}
          <div className="section wallet-section">
            <WalletConnect
              isConnected={isConnected}
              account={account}
              onConnect={connectWallet}
              txStatus={txStatus}
              loading={loading}
            />
 
            {isConnected && txStatus && (
              <StatusMessage type="info">
                <strong>Transaction in progress:</strong> {txStatus}
              </StatusMessage>
            )}
          </div>
 
          {isConnected && (
            <>
              <div className="section actions-section">
                <PlayDice loading={loading} onPlay={playDice} />
                <DiceResults
                  encrypted={encryptedState}
                  decrypted={decryptedState}
                  onDecryptRoll={decryptLastDiceRoll}
                  onDecryptGuess={decryptPlayerGuess}
                  onDecryptWinner={decryptWinnerStatus}
                  loading={loading}
                  fairness={fairness}
                />
              </div>
 
          
            </>
          )}
        </div>
      </div>
      <Footer />
    </div>
  );
}
 
export default App;

Under src/Index.js

iimport React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.js';
import './App.css';
 
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

Create Component on, as src/components

mkdir components
 
 
touch ContractInfo.js
touch Header.js
touch StatusMessage.js
touch DiceResults.js
touch PastRounds.js
touch ToastContainer.js
touch Footer.js
touch PlayDice.js
touch WalletConnect.js
 
 
mv ContractInfo.js Header.js StatusMessage.js DiceResults.js PastRounds.js ToastContainer.js Footer.js PlayDice.js WalletConnect.js components/

Replace the files with these

ContractInfo.js

const ContractInfo = ({ contractAddress }) => {
  return (
    <div className="card-secondary" style={{ padding: '8px' }}>
      <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
        {/* Contract Address */}
        <div style={{ 
          display: 'flex',
          justifyContent: 'space-between',
          alignItems: 'center',
          padding: '6px 10px',
          background: 'var(--bg-tertiary)',
          border: '1px solid var(--border-light)',
          borderRadius: '6px'
        }}>
<span style={{ color: 'var(--text-secondary)', fontWeight: '600', fontSize: '0.8rem' }}>
            Contract:
</span>
          <a
            href={`https://sepolia.etherscan.io/address/${contractAddress}`}
            target="_blank"
            rel="noopener noreferrer"
            style={{
              fontFamily: 'monospace',
              color: 'var(--primary-blue)',
              fontSize: '0.8rem',
              textDecoration: 'none',
              cursor: 'pointer',
              padding: '3px 6px',
              background: 'var(--bg-card)',
              border: '1px solid var(--border-light)',
              borderRadius: '4px',
              transition: 'all 0.2s ease'
            }}
            onMouseEnter={(e) => {
              e.target.style.background = 'var(--bg-tertiary)';
              e.target.style.transform = 'translateY(-1px)';
            }}
            onMouseLeave={(e) => {
              e.target.style.background = 'var(--bg-card)';
              e.target.style.transform = 'translateY(0)';
            }}
          >
            {contractAddress.slice(0, 8)}...{contractAddress.slice(-6)}
          </a>
        </div>
 
        {/* Network and Tech badges in one row */}
        <div style={{
          display: 'flex',
          justifyContent: 'space-between',
          alignItems: 'center',
          gap: '8px'
        }}>
          <span className="badge" style={{ 
            background: 'var(--bg-card)', 
            color: 'var(--text-primary)', 
            padding: '2px 6px', 
            fontSize: '0.7rem', 
            border: '1px solid var(--border-light)',
            borderRadius: '4px'
          }}>
            🌐 Sepolia
          </span>
          
          <div style={{ display: 'flex', gap: '4px' }}>
            <span className="badge" style={{ 
              background: 'var(--bg-card)', 
              border: '1px solid var(--border-light)', 
              fontSize: '0.65rem', 
              padding: '1px 5px',
              borderRadius: '4px'
            }}>🔒 FHEVM</span>
            <span className="badge" style={{ 
              background: 'var(--bg-card)', 
              border: '1px solid var(--border-light)', 
              fontSize: '0.65rem', 
              padding: '1px 5px',
              borderRadius: '4px'
            }}>🛡️ Private</span>
          </div>
        </div>
      </div>
    </div>
  );
};
 
export default ContractInfo;

Header.js

const Header = () => {
  return (
    <header className="header">
      <div className="header-content">
        <div style={{ 
          display: 'flex', 
          justifyContent: 'center', 
          gap: '10px', 
          marginBottom: '1px'
        }}>
        </div>
        <h1 className="title">
           Dice — Confidential Rolls
        </h1>
   
      </div>
    </header>
  );
};
 
export default Header;

StatusMessage.js

 
const StatusMessage = ({ children, type = 'info' }) => {
  return (
    <div className={`status-message status-${type}`}>
      {children}
    </div>
  );
};
 
export default StatusMessage;

DiceResults.js

 
const DiceResults = ({
  encrypted,
  decrypted,
  onDecryptRoll,
  onDecryptGuess,
  onDecryptWinner,
  loading,
  fairness
}) => {
  const handleDecryptAll = async () => {
    try {
      await onDecryptRoll();
      await onDecryptGuess();
      await onDecryptWinner();
    } catch (err) {
      // swallow; individual handlers already toast errors
    }
  };
  return (
    <div className="card" style={{ maxWidth: '90rem', margin: '6.6px auto 0' }}>
      <div style={{
        textAlign: 'center',
        marginBottom: '24px'
      }}>
        <h3 style={{ 
          fontSize: '1.5rem', 
          color: 'var(--text-primary)', 
          fontWeight: 700,
          marginBottom: '8px'
        }}>
          🔐 Encrypted Results
        </h3>
        <p style={{ 
          color: 'var(--text-secondary)', 
          fontSize: '0.9rem',
          margin: 0
        }}>
          Decrypt your results to see the outcome
        </p>
      </div>
 
      <div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
        <div style={{ 
          padding: '20px',
          background: 'var(--bg-card)',
          border: '1px solid var(--border-light)',
          borderRadius: '16px',
          boxShadow: '0 2px 8px rgba(0, 0, 0, 0.06)'
        }}>
          <div style={{ 
            display: 'flex', 
            justifyContent: 'space-between', 
            alignItems: 'center',
            gap: '20px'
          }}>
            <div style={{ flex: 1 }}>
              <div style={{ 
                fontSize: '0.9rem', 
                color: 'var(--text-secondary)',
                fontWeight: '600',
                marginBottom: '12px',
                display: 'flex',
                alignItems: 'center',
                gap: '8px'
              }}>
                🎲 Last Dice Roll (encrypted)
              </div>
              <div style={{ 
                fontFamily: 'monospace', 
                fontSize: '0.8rem',
                color: 'var(--text-primary)',
                background: 'var(--bg-tertiary)',
                padding: '12px 16px',
                borderRadius: '8px',
                border: '1px solid var(--border-light)',
                wordBreak: 'break-all',
                lineHeight: '1.4'
              }}>
                {encrypted.lastDiceRoll}
              </div>
              {decrypted.lastDiceRoll != null && (
                <div style={{ 
                  marginTop: '10px',
                  textAlign: 'left',
                  padding: '8px 12px',
                  background: 'var(--success-green)',
                  color: 'var(--text-white)',
                  borderRadius: '8px',
                  fontWeight: '600',
                  fontSize: '1rem',
                }}>
                  ➡️ {decrypted.lastDiceRoll}
                </div>
              )}
            </div>
            <div style={{ display: 'flex', alignItems: 'center' }}>
              <button
                onClick={onDecryptRoll}
                disabled={loading}
                className={`btn ${loading ? 'btn-loading' : 'btn-secondary'}`}
                style={{ 
                  fontSize: '0.9rem',
                  padding: '10px 20px',
                  minWidth: '120px'
                }}
              >
                🔓 Decrypt
              </button>
            </div>
          </div>
        </div>
 
        <div style={{ 
          padding: '20px',
          background: 'var(--bg-card)',
          border: '1px solid var(--border-light)',
          borderRadius: '16px',
          boxShadow: '0 2px 8px rgba(0, 0, 0, 0.06)'
        }}>
          <div style={{ 
            display: 'flex', 
            justifyContent: 'space-between', 
            alignItems: 'center',
            gap: '20px'
          }}>
            <div style={{ flex: 1 }}>
              <div style={{ 
                fontSize: '0.9rem', 
                color: 'var(--text-secondary)',
                fontWeight: '600',
                marginBottom: '12px',
                display: 'flex',
                alignItems: 'center',
                gap: '8px'
              }}>
                🎯 Your Guess (encrypted)
              </div>
              <div style={{ 
                fontFamily: 'monospace', 
                fontSize: '0.8rem',
                color: 'var(--text-primary)',
                background: 'var(--bg-tertiary)',
                padding: '12px 16px',
                borderRadius: '8px',
                border: '1px solid var(--border-light)',
                wordBreak: 'break-all',
                lineHeight: '1.4'
              }}>
                {encrypted.playerGuess}
              </div>
              {decrypted.playerGuess != null && (
                <div style={{ 
                  marginTop: '10px',
                  textAlign: 'left',
                  padding: '8px 12px',
                  background: 'var(--success-green)',
                  color: 'var(--text-white)',
                  borderRadius: '8px',
                  fontWeight: '600',
                  fontSize: '1rem',
                }}>
                  ➡️ {decrypted.playerGuess}
                </div>
              )}
            </div>
            <div style={{ display: 'flex', alignItems: 'center' }}>
              <button
                onClick={onDecryptGuess}
                disabled={loading}
                className={`btn ${loading ? 'btn-loading' : 'btn-secondary'}`}
                style={{ 
                  fontSize: '0.9rem',
                  padding: '10px 20px',
                  minWidth: '120px'
                }}
              >
                🔓 Decrypt
              </button>
            </div>
          </div>
        </div>
 
        <div style={{ 
          padding: '20px',
          background: 'var(--bg-card)',
          border: '1px solid var(--border-light)',
          borderRadius: '16px',
          boxShadow: '0 2px 8px rgba(0, 0, 0, 0.06)'
        }}>
          <div style={{ 
            display: 'flex', 
            justifyContent: 'space-between', 
            alignItems: 'center',
            gap: '20px'
          }}>
            <div style={{ flex: 1 }}>
              <div style={{ 
                fontSize: '0.9rem', 
                color: 'var(--text-secondary)',
                fontWeight: '600',
                marginBottom: '12px',
                display: 'flex',
                alignItems: 'center',
                gap: '8px'
              }}>
                🏆 Winner Status (encrypted)
              </div>
              <div style={{ 
                fontFamily: 'monospace', 
                fontSize: '0.8rem',
                color: 'var(--text-primary)',
                background: 'var(--bg-tertiary)',
                padding: '12px 16px',
                borderRadius: '8px',
                border: '1px solid var(--border-light)',
                wordBreak: 'break-all',
                lineHeight: '1.4'
              }}>
                {encrypted.winnerStatus}
              </div>
              {decrypted.winnerStatus != null && (
                <div style={{ 
                  marginTop: '10px',
                  textAlign: 'left',
                  padding: '8px 12px',
                  background: decrypted.winnerStatus ? 'var(--success-green)' : 'var(--error-red)',
                  color: 'var(--text-white)',
                  borderRadius: '8px',
                  fontWeight: '600',
                  fontSize: '1rem',
                }}>
                  ➡️ {decrypted.winnerStatus ? '🎉 Winner!' : '❌ Not winner'}
                </div>
              )}
            </div>
            <div style={{ display: 'flex', alignItems: 'center' }}>
              <button
                onClick={onDecryptWinner}
                disabled={loading}
                className={`btn ${loading ? 'btn-loading' : 'btn-secondary'}`}
                style={{ 
                  fontSize: '0.9rem',
                  padding: '10px 20px',
                  minWidth: '120px'
                }}
              >
                🔓 Decrypt
              </button>
            </div>
          </div>
        </div>
        
        <div style={{ display: 'flex', justifyContent: 'center', marginTop: '12px' }}>
          <button
            onClick={handleDecryptAll}
            disabled={loading}
            className={`btn ${loading ? 'btn-loading' : 'btn-primary'}`}
            style={{ minWidth: '160px', height: '40px', fontSize: '0.95rem', fontWeight: 600 }}
          >
            {loading ? 'Decrypting...' : 'Decrypt All'}
          </button>
        </div>
      </div>
      
      {fairness && (
        <div style={{ 
          padding: '16px',
          background: 'var(--bg-tertiary)',
          border: '1px solid var(--border-light)',
          borderRadius: '12px',
          marginTop: '20px'
        }}>
          <div style={{ fontWeight: 600, marginBottom: '8px', color: 'var(--text-primary)' }}>✅ Fairness Proof</div>
          <div style={{ fontSize: '0.9rem', color: 'var(--text-secondary)', marginBottom: '8px' }}>
            Seed used: <span style={{ color: 'var(--text-primary)' }}>{fairness.seed}</span> • Guess: <span style={{ color: 'var(--text-primary)' }}>{fairness.guess}</span>
          </div>
          <div style={{ fontFamily: 'monospace', fontSize: '0.8rem', background: 'var(--bg-card)', padding: '8px 12px', border: '1px solid var(--border-light)', borderRadius: '6px', wordBreak: 'break-all', marginBottom: '8px' }}>
            commitment (sha256(seed)) = {fairness.commitment}
          </div>
          {fairness.txHash && (
            <a href={`https://sepolia.etherscan.io/tx/${fairness.txHash}`} target="_blank" rel="noopener noreferrer" style={{ fontSize: '0.85rem' }}>
              View transaction ↗
            </a>
          )}
        </div>
      )}
    </div>
  );
};
export default DiceResults;

PastRounds.js

const PastRounds = ({ pastRounds, account, onClaimPastPrize, loading }) => {
  if (!pastRounds || pastRounds.length === 0) return null;
 
  return (
    <div className="card" style={{ maxWidth: '600px', margin: '20px auto 0' }}>
      <h3 style={{ marginBottom: '16px', fontSize: '1.1rem', color: 'var(--text-primary)' }}>📜 Past Rounds</h3>
 
      <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
        {pastRounds.map((round) => {
          const isWinner = round.winner.toLowerCase() === account.toLowerCase();
          const canClaim = isWinner && !round.claimed;
 
          return (
            <div
              key={round.index}
              style={{
                padding: '12px',
                border: '1px solid var(--border-color)',
                borderRadius: '8px',
                backgroundColor: 'var(--bg-secondary)'
              }}
            >
              <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
                <span style={{ fontWeight: 'bold', color: 'var(--text-primary)' }}>
                  Round #{round.index + 1}
                </span>
                <span style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>
                  {new Date(round.drawTime * 1000).toLocaleDateString()}
                </span>
              </div>
 
              <div style={{ marginBottom: '8px' }}>
                <span style={{ color: 'var(--text-secondary)' }}>Winner: </span>
                <span style={{ fontFamily: 'monospace', fontSize: '0.9rem' }}>
                  {round.winner.slice(0, 6)}...{round.winner.slice(-4)}
                </span>
              </div>
 
              <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
                <span style={{ color: 'var(--text-secondary)' }}>
                  Prize: {round.prize} ETH
                </span>
 
                {isWinner ? (
                  round.claimed ? (
                    <span style={{ color: 'var(--success-color)', fontSize: '0.9rem' }}>✅ Claimed</span>
                  ) : (
                    <button
                      onClick={() => onClaimPastPrize(round.index)}
                      disabled={loading}
                      className="btn btn-success"
                      style={{ fontSize: '0.8rem', padding: '4px 8px' }}
                    >
                      {loading ? '💰 Claiming...' : '💰 Claim Prize'}
                    </button>
                  )
                ) : (
                  <span style={{ color: 'var(--text-secondary)', fontSize: '0.9rem' }}>
                    Better luck next time! 🎯
                  </span>
                )}
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
};
 
export default PastRounds;

ToastContainer.js

const ToastContainer = ({ messages }) => {
  return (
    <div className="toast-container">
      {messages.map(msg => (
        <div key={msg.id} className={`toast toast-${msg.type}`}>
          {msg.message}
        </div>
      ))}
    </div>
  );
};
 
export default ToastContainer;

Footer.js

const Footer = () => {
  return (
    <footer className="footer" style={{ padding: '8px 0', margin: '8px 0 0' }}>
      <div className="footer-content" style={{ 
        justifyContent: 'center', 
        alignItems: 'center',
        gap: '12px',
        padding: '0'
      }}>
        <div className="footer-text">
          <p style={{
            fontSize: '0.75rem',
            color: 'var(--text-secondary)',
            margin: 0,
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            gap: '6px'
          }}>
            <span>🎲 Dice</span>
            <span>•</span>
            <span>FHEVM</span>
          </p>
        </div>
        <div className="footer-links" style={{ 
          gap: '8px',
          display: 'flex',
          alignItems: 'center'
        }}>
          <a
            href="https://github.com/chimmykk"
            target="_blank"
            rel="noopener noreferrer"
            className="footer-link"
            style={{ fontSize: '0.75rem' }}
          >
            GitHub
          </a>
         
        </div>
      </div>
    </footer>
  );
};
 
export default Footer;

PlayDice.js

import { useEffect, useRef, useState } from 'react';
 
const faces = [1, 2, 3, 4, 5, 6];
 
const PlayDice = ({ loading, onPlay }) => {
  const [seed, setSeed] = useState('');
  const [guess, setGuess] = useState(1);
  const [capturing, setCapturing] = useState(false);
  const [captureProgress, setCaptureProgress] = useState(0);
  const entropyRef = useRef([]);
  const areaRef = useRef(null);
  const [rolling, setRolling] = useState(false);
  const [currentFace, setCurrentFace] = useState(1);
  const [entropyReady, setEntropyReady] = useState(false);
  const [userPicked, setUserPicked] = useState(false);
 
  // Simple 32-bit hash of a string
  const hash32 = (str) => {
    let h = 2166136261 >>> 0; // FNV-1a
    for (let i = 0; i < str.length; i++) {
      h ^= str.charCodeAt(i);
      h = Math.imul(h, 16777619);
    }
    return (h >>> 0) % 0xffffffff;
  };
 
  // Start 2s mouse entropy capture
  const startEntropyCapture = () => {
    if (capturing) return;
    entropyRef.current = [];
    setCaptureProgress(0);
    setCapturing(true);
    setEntropyReady(false);
 
    const onMove = (e) => {
      const rect = areaRef.current?.getBoundingClientRect();
      const x = rect ? e.clientX - rect.left : e.clientX;
      const y = rect ? e.clientY - rect.top : e.clientY;
      entropyRef.current.push(`${x},${y},${e.timeStamp}`);
    };
 
    const area = areaRef.current || window;
    area.addEventListener('mousemove', onMove);
 
    const start = Date.now();
    const interval = setInterval(() => {
      const elapsed = Date.now() - start;
      setCaptureProgress(Math.min(100, Math.round((elapsed / 2000) * 100)));
    }, 100);
 
    setTimeout(() => {
      area.removeEventListener('mousemove', onMove);
      clearInterval(interval);
      setCapturing(false);
      const data = entropyRef.current.join('|');
      const h = hash32(data);
      // Map to a reasonably large uint range for seed
      const seedNum = h % 1000000007; // prime mod
      setSeed(String(seedNum));
      setEntropyReady(true);
    }, 2000);
  };
 
  const disabled = loading || !seed || Number(guess) < 1 || Number(guess) > 6;
 
  return (
    <div className="card" style={{ maxWidth: '600px', margin: '0 auto' }}>
      <div style={{
        textAlign: 'center',
        marginBottom: '24px'
      }}>
        <h3 style={{ 
          fontSize: '1.5rem', 
          color: 'var(--text-primary)', 
          fontWeight: 700, 
          marginBottom: '8px'
        }}>
          🎲 Roll the Dice
        </h3>
        <p style={{ 
          color: 'var(--text-secondary)', 
          fontSize: '1rem',
          margin: 0
        }}>
          Choose your number or let fate decide
        </p>
      </div>
 
      <div style={{ display: 'flex', flexDirection: 'column', gap: '24px', alignItems: 'center' }}>
        <div 
          className="dice-stage" 
          onMouseEnter={() => { if (!capturing && !entropyReady) startEntropyCapture(); }}
        >
          <div
            className={`dice-cube ${rolling ? 'dice-rolling' : ''}`}
            style={{
              transform: rolling ? undefined : (
                currentFace === 1 ? 'rotateY(0deg)' :
                currentFace === 2 ? 'rotateY(-90deg)' :
                currentFace === 3 ? 'rotateY(-180deg)' :
                currentFace === 4 ? 'rotateY(90deg)' :
                currentFace === 5 ? 'rotateX(-90deg)' :
                'rotateX(90deg)'
              )
            }}
          >
            <div className="dice-face face-1">⚀</div>
            <div className="dice-face face-2">⚁</div>
            <div className="dice-face face-3">⚂</div>
            <div className="dice-face face-4">⚃</div>
            <div className="dice-face face-5">⚄</div>
            <div className="dice-face face-6">⚅</div>
          </div>
        </div>
 
        {/* Guess selector: click chips to choose face */}
        <div style={{ 
          display: 'flex', 
          gap: '12px', 
          flexWrap: 'wrap',
          justifyContent: 'center'
        }}>
          {faces.map((f) => (
            <button
              key={f}
              onClick={() => { setGuess(f); setCurrentFace(f); setUserPicked(true); }}
              className={`btn ${guess === f ? 'btn-primary' : 'btn-secondary'}`}
              style={{ 
                padding: '12px 16px',
                fontSize: '2rem', // Increased from 1.2rem to 2rem
                minWidth: '60px'
              }}
              disabled={loading}
            >
              {['⚀','⚁','⚂','⚃','⚄','⚅'][f-1]}
            </button>
          ))}
        </div>
 
        {/* Entropy indicator */}
        <div style={{ 
          width: '100%', 
          maxWidth: '520px'
        }}>
          {!entropyReady && (
            <div style={{
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
              gap: '8px',
              padding: '12px 16px',
              background: 'var(--bg-tertiary)',
              border: '1px solid var(--border-light)',
              borderRadius: '8px',
              fontSize: '14px',
              color: 'var(--text-secondary)',
              marginBottom: '12px'
            }}>
              <div style={{
                width: '12px',
                height: '12px',
                border: '2px solid var(--primary-blue)',
                borderTop: '2px solid transparent',
                borderRadius: '50%',
                animation: 'spin 1s linear infinite'
              }}></div>
              <span>Move your mouse to generate entropy ({captureProgress}%)</span>
            </div>
          )}
          <button
            onClick={async () => {
              // If user didn't pick, choose random face now
              const chosen = userPicked ? guess : (Math.floor(Math.random() * 6) + 1);
              if (!userPicked) {
                setGuess(chosen);
                setCurrentFace(chosen);
              }
              // ensure some extra movement during roll
              setRolling(true);
              const timer = setInterval(() => setCurrentFace(prev => (prev % 6) + 1), 50);
              await new Promise(r => setTimeout(r, 800));
              clearInterval(timer);
              setRolling(false);
              // Ensure the final face is set correctly
              setCurrentFace(chosen);
              onPlay(Number(seed || Date.now()), Number(chosen));
            }}
            disabled={loading || !entropyReady}
            className={`btn ${loading ? 'btn-loading' : 'btn-primary'}`}
            style={{ 
              width: '100%',
              height: '50px',
              fontSize: '1rem',
              fontWeight: '600'
            }}
          >
            {loading ? (
              'Encrypting & Playing...'
            ) : entropyReady ? (
              userPicked ? 'Roll Your Pick' : 'Roll Dice (Random)'
            ) : (
              'Move mouse to unlock'
            )}
          </button>
        </div>
      </div>
 
      {/* Hidden entropy capture area to track movement across the card */}
      <div ref={areaRef} style={{ width: '100%', height: 1 }} />
    </div>
  );
};
 
export default PlayDice;

WalletConnect.js

import React from 'react';
 
const WalletConnect = ({ isConnected, account, onConnect, txStatus, pastRounds, onClaimPastPrize, loading }) => {
  const [showPastRounds, setShowPastRounds] = React.useState(false);
 
  if (isConnected) {
    return (
      <div>
        {/* Connected Account */}
        <div className="card" style={{ padding: '10px' }}>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
            <div>
              <div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>Connected Account</div>
              <div style={{ fontWeight: 'bold', fontSize: '0.95rem' }}>
                {account.slice(0, 6)}...{account.slice(-4)}
              </div>
            </div>
            <span className="badge" style={{ background: 'var(--success-color)', fontSize: '0.7rem', padding: '2px 8px' }}>
              ✅ Connected
            </span>
          </div>
        </div>
 
        {/* Past Rounds Section */}
        {pastRounds && pastRounds.length > 0 && (
          <div className="card" style={{ marginTop: '12px', padding: '12px' }}>
            <div
              style={{
                display: 'flex',
                justifyContent: 'space-between',
                alignItems: 'center',
                cursor: 'pointer',
                padding: '8px'
              }}
              onClick={() => setShowPastRounds(!showPastRounds)}
            >
              <div style={{ fontSize: '0.8rem', fontWeight: 'bold', color: 'var(--text-primary)' }}>
                📜 Past Rounds ({pastRounds.length})
              </div>
              <span style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>
                {showPastRounds ? '▼' : '▶'}
              </span>
            </div>
 
            {showPastRounds && (
              <div style={{ padding: '0 8px 8px 8px' }}>
                <div style={{ display: 'flex', flexDirection: 'column', gap: '8px', maxHeight: '200px', overflowY: 'auto' }}>
                  {pastRounds.map((round) => {
                    const isWinner = round.winner.toLowerCase() === account.toLowerCase();
                    const canClaim = isWinner && !round.claimed;
 
                    return (
                      <div
                        key={round.index}
                        style={{
                          padding: '8px',
                          border: '1px solid var(--border-color)',
                          borderRadius: '6px',
                          backgroundColor: 'var(--bg-secondary)',
                          fontSize: '0.8rem'
                        }}
                      >
                        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4px' }}>
                          <span style={{ fontWeight: 'bold', color: 'var(--text-primary)' }}>
                            Round #{round.index + 1}
                          </span>
                          <span style={{ color: 'var(--text-secondary)' }}>
                            {new Date(round.drawTime * 1000).toLocaleDateString()}
                          </span>
                        </div>
 
                        <div style={{ marginBottom: '4px' }}>
                          <span style={{ color: 'var(--text-secondary)' }}>Winner: </span>
                          <span style={{ fontFamily: 'monospace' }}>
                            {round.winner.slice(0, 4)}...{round.winner.slice(-4)}
                          </span>
                        </div>
 
                        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
                          <span style={{ color: 'var(--text-secondary)' }}>
                            Prize: {round.prize} ETH
                          </span>
 
                          {isWinner ? (
                            round.claimed ? (
                              <span style={{ color: 'var(--success-color)', fontSize: '0.75rem' }}>✅ Claimed</span>
                            ) : (
                              <button
                                onClick={() => onClaimPastPrize(round.index)}
                                disabled={loading}
                                className="btn btn-success"
                                style={{ fontSize: '0.7rem', padding: '2px 6px' }}
                              >
                                {loading ? '💰 ...' : '💰 Claim'}
                              </button>
                            )
                          ) : (
                            <span style={{ color: 'var(--text-secondary)', fontSize: '0.75rem' }}>
                              Better luck!
                            </span>
                          )}
                        </div>
                      </div>
                    );
                  })}
                </div>
              </div>
            )}
          </div>
        )}
      </div>
    );
  }
 
  return (
    <div className="card-priority" style={{ 
      maxWidth: '500px', 
      margin: '0 auto', 
      textAlign: 'center'
    }}>
      <div style={{
        textAlign: 'center',
        marginBottom: '24px'
      }}>
        <h2 style={{ 
          fontSize: '1.75rem', 
          marginBottom: '12px', 
          color: 'var(--text-primary)',
          fontWeight: 700
        }}>
          Connect Your Wallet
        </h2>
        <p style={{ 
          color: 'var(--text-secondary)', 
          marginBottom: '0', 
          fontSize: '1rem',
          lineHeight: '1.6'
        }}>
          Connect your MetaMask wallet to experience confidential dice rolling
        </p>
      </div>
 
      {/* App Description */}
      <div className="card-secondary" style={{
        borderRadius: '12px',
        padding: '20px',
        marginBottom: '24px',
        textAlign: 'left'
      }}>
        <h3 style={{ 
          fontSize: '1rem', 
          marginBottom: '12px', 
          color: 'var(--text-primary)',
          fontWeight: 700,
          display: 'flex',
          alignItems: 'center',
          gap: '8px'
        }}>
          What is Confidential Dice?
        </h3>
        <p style={{ 
          fontSize: '0.9rem', 
          color: 'var(--text-secondary)', 
          marginBottom: '12px',
          lineHeight: '1.6'
        }}>
          A privacy-focused dice game built with Zama FHEVM technology where:
        </p>
        <ul style={{ 
          fontSize: '0.9rem', 
          color: 'var(--text-secondary)', 
          paddingLeft: '20px', 
          marginBottom: '0',
          lineHeight: '1.6'
        }}>
          <li style={{ marginBottom: '6px' }}>🎲 Roll dice with encrypted results</li>
          <li style={{ marginBottom: '6px' }}>🔒 Your moves stay completely private</li>
   
        </ul>
      </div>
 
      <button
        onClick={onConnect}
        disabled={txStatus === 'Connecting...'}
        className={`btn ${txStatus === 'Connecting...' ? 'btn-loading' : 'btn-primary'}`}
        style={{ 
          minWidth: '200px',
          height: '50px',
          fontSize: '1rem',
          fontWeight: '600'
        }}
      >
        {txStatus === 'Connecting...' ? 'Connecting...' : 'Connect MetaMask'}
      </button>
    </div>
  );
};
 
export default WalletConnect;

Create hooks folder and files.

 
mkdir hooks
 
 
touch useDiceGame.js
touch useToast.js
touch useWallet.js
 
mv useDiceGame.js useToast.js useWallet.js hooks/

Now replace the files with this snippets useDiceGame.js

import { useEffect, useState } from 'react';
import { BrowserProvider, Contract, parseEther, hexlify } from 'ethers';
 
// Hook for interacting with FHEDiceGame contract
export const useDiceGame = (account, showToast, setTxStatus, contractAddress) => {
  const [contract, setContract] = useState(null);
  const [loading, setLoading] = useState(false);
  const [entryFeeWei, setEntryFeeWei] = useState(null);
  const [encryptedState, setEncryptedState] = useState({
    lastDiceRoll: '0x',
    playerGuess: '0x',
    winnerStatus: '0x'
  });
  const [decryptedState, setDecryptedState] = useState({
    lastDiceRoll: null,
    playerGuess: null,
    winnerStatus: null
  });
  const [fairness, setFairness] = useState(null);
 
  const abi = [
    "function ENTRY_FEE() view returns (uint256)",
    "function owner() view returns (address)",
    "function getLastDiceRoll() view returns (bytes32)",
    "function getPlayerGuess() view returns (bytes32)",
    "function getWinnerStatus() view returns (bytes32)",
    "function playDice(bytes32,bytes,bytes32,bytes) payable",
    "function withdraw()"
  ];
 
  useEffect(() => {
    if (!account || !window.ethereum) return;
    const init = async () => {
      try {
        const provider = new BrowserProvider(window.ethereum);
        const signer = await provider.getSigner();
        const c = new Contract(contractAddress, abi, signer);
        setContract(c);
        try {
          const fee = await c.ENTRY_FEE();
          setEntryFeeWei(fee);
        } catch {}
        await refreshEncryptedState(c);
      } catch (err) {
        console.error('Error initializing FHEDiceGame contract:', err);
      }
    };
    init();
  }, [account, contractAddress]);
 
  const refreshEncryptedState = async (c) => {
    try {
      const [lastDiceRoll, playerGuess, winnerStatus] = await Promise.all([
        (c || contract).getLastDiceRoll(),
        (c || contract).getPlayerGuess(),
        (c || contract).getWinnerStatus()
      ]);
      setEncryptedState({
        lastDiceRoll,
        playerGuess,
        winnerStatus
      });
    } catch (err) {
      console.error('Failed to load encrypted state:', err);
    }
  };
 
  const playDice = async (seedNumber, guessNumber) => {
    if (!contract) return;
    if (
      typeof seedNumber !== 'number' || Number.isNaN(seedNumber) ||
      typeof guessNumber !== 'number' || Number.isNaN(guessNumber) ||
      guessNumber < 1 || guessNumber > 6
    ) {
      showToast('Enter a valid seed and a guess between 1-6', 'warning');
      return;
    }
 
    setLoading(true);
    setTxStatus('Encrypting seed and guess...');
    try {
      // Initialize FHEVM Relayer SDK
      await window.relayerSDK.initSDK();
      const config = { ...window.relayerSDK.SepoliaConfig, network: window.ethereum };
      const fhevm = await window.relayerSDK.createInstance(config);
 
      if (!window.relayerSDK) throw new Error('Relayer SDK not loaded');
 
      // Prepare encrypted inputs (handles + proofs) using add32 flow
      const seedInput = await fhevm.createEncryptedInput(contractAddress, account);
      await seedInput.add32(seedNumber);
      const seedEnc = await seedInput.encrypt();
 
      const guessInput = await fhevm.createEncryptedInput(contractAddress, account);
      await guessInput.add32(guessNumber);
      const guessEnc = await guessInput.encrypt();
 
      // Extract handles and proofs in a robust way
      const firstSeed = (seedEnc.handles && seedEnc.handles[0]) || seedEnc.handle || seedEnc.inputHandle;
      const firstGuess = (guessEnc.handles && guessEnc.handles[0]) || guessEnc.handle || guessEnc.inputHandle;
      let seedHandle = typeof firstSeed === 'object' && firstSeed?.handle ? firstSeed.handle : firstSeed;
      let guessHandle = typeof firstGuess === 'object' && firstGuess?.handle ? firstGuess.handle : firstGuess;
      let seedProof = seedEnc.inputProof || seedEnc.proof;
      let guessProof = guessEnc.inputProof || guessEnc.proof;
 
      // Normalize to hex strings (0x...)
      const toHex = (v) => {
        if (!v) return v;
        if (typeof v === 'string') return v.startsWith('0x') ? v : `0x${v}`;
        try { return hexlify(v); } catch { return v; }
      };
      const padToBytes32 = (hex) => {
        if (!hex) return hex;
        let h = hex.toLowerCase();
        if (!h.startsWith('0x')) h = `0x${h}`;
        h = h.replace(/^0x/, '');
        if (h.length > 64) h = h.slice(h.length - 64); // keep last 32 bytes
        return '0x' + h.padStart(64, '0');
      };
      seedHandle = toHex(seedHandle);
      guessHandle = toHex(guessHandle);
      seedProof = toHex(seedProof);
      guessProof = toHex(guessProof);
 
      // Ensure handles are exactly bytes32
      seedHandle = padToBytes32(seedHandle);
      guessHandle = padToBytes32(guessHandle);
 
      if (!seedHandle || !seedProof || !guessHandle || !guessProof) {
        showToast('Encryption output missing handle or proof', 'error');
        setLoading(false);
        setTxStatus('');
        return;
      }
 
      // Compute fairness commitment: sha256(seed)
      const buffer = new TextEncoder().encode(String(seedNumber));
      const digest = await window.crypto.subtle.digest('SHA-256', buffer);
      const bytes = Array.from(new Uint8Array(digest));
      const commitment = '0x' + bytes.map(b => b.toString(16).padStart(2, '0')).join('');
 
      setTxStatus('Submitting playDice transaction...');
      const tx = await contract.playDice(
        seedHandle,
        seedProof,
        guessHandle,
        guessProof,
        { value: entryFeeWei ?? parseEther('0.0002') }
      );
      await tx.wait();
 
      // Record fairness information
      setFairness({ seed: seedNumber, guess: guessNumber, commitment, txHash: tx.hash });
 
      showToast('Dice played successfully! 🎲', 'success');
      await refreshEncryptedState();
    } catch (err) {
      console.error('playDice failed:', err);
      showToast('playDice failed: ' + (err?.message || String(err)), 'error');
    } finally {
      setLoading(false);
      setTxStatus('');
    }
  };
 
  const decryptLastDiceRoll = async () => {
    if (!encryptedState.lastDiceRoll || encryptedState.lastDiceRoll === '0x') return;
    setTxStatus('Decrypting last dice roll...');
    try {
      const value = await userDecryptValue(encryptedState.lastDiceRoll, 'uint32');
      setDecryptedState(prev => ({ ...prev, lastDiceRoll: Number(value) }));
      showToast('Decrypted last roll ✅', 'success');
    } catch (err) {
      console.error('Decrypt last roll failed:', err);
      showToast('Decrypt failed: ' + (err?.message || String(err)), 'error');
    } finally {
      setTxStatus('');
    }
  };
 
  const decryptPlayerGuess = async () => {
    if (!encryptedState.playerGuess || encryptedState.playerGuess === '0x') return;
    setTxStatus('Decrypting your guess...');
    try {
      const value = await userDecryptValue(encryptedState.playerGuess, 'uint32');
      setDecryptedState(prev => ({ ...prev, playerGuess: Number(value) }));
      showToast('Decrypted your guess ✅', 'success');
    } catch (err) {
      console.error('Decrypt guess failed:', err);
      showToast('Decrypt failed: ' + (err?.message || String(err)), 'error');
    } finally {
      setTxStatus('');
    }
  };
 
  const decryptWinnerStatus = async () => {
    if (!encryptedState.winnerStatus || encryptedState.winnerStatus === '0x') return;
    setTxStatus('Decrypting winner status...');
    try {
      const value = await userDecryptValue(encryptedState.winnerStatus, 'bool');
      const isWinner = Boolean(value);
      setDecryptedState(prev => ({ ...prev, winnerStatus: isWinner }));
      showToast(isWinner ? '🎉 You won!' : 'Not a winner this time', 'info');
    } catch (err) {
      console.error('Decrypt winner failed:', err);
      showToast('Decrypt failed: ' + (err?.message || String(err)), 'error');
    } finally {
      setTxStatus('');
    }
  };
 
  // Robust user decryption helper using EIP-712 flow
  const userDecryptValue = async (ciphertextHandle, valueType) => {
    await window.relayerSDK.initSDK();
    const config = { ...window.relayerSDK.SepoliaConfig, network: window.ethereum };
    const instance = await window.relayerSDK.createInstance(config);
 
    const provider = new BrowserProvider(window.ethereum);
    const signer = await provider.getSigner();
    const signerAddress = await signer.getAddress();
 
    const keypair = instance.generateKeypair();
    const handleContractPairs = [
      { handle: ciphertextHandle, contractAddress }
    ];
    const startTimeStamp = Math.floor(Date.now() / 1000).toString();
    const durationDays = '10';
    const contractAddresses = [contractAddress];
 
    const eip712 = instance.createEIP712(
      keypair.publicKey,
      contractAddresses,
      startTimeStamp,
      durationDays
    );
 
    const signature = await signer.signTypedData(
      eip712.domain,
      { UserDecryptRequestVerification: eip712.types.UserDecryptRequestVerification },
      eip712.message
    );
 
    const result = await instance.userDecrypt(
      handleContractPairs,
      keypair.privateKey,
      keypair.publicKey,
      signature.replace('0x', ''),
      contractAddresses,
      signerAddress,
      startTimeStamp,
      durationDays
    );
 
    const raw = result[ciphertextHandle];
    if (valueType === 'bool') {
      return raw === true || raw === 'true' || raw === 1 || raw === '1';
    }
    // Default to uint32 number
    return Number(raw);
  };
 
  return {
    contract,
    loading,
    encryptedState,
    decryptedState,
    fairness,
    playDice,
    refreshEncryptedState,
    decryptLastDiceRoll,
    decryptPlayerGuess,
    decryptWinnerStatus
  };
};

useToast.js

import { useState } from 'react';
 
export const useToast = () => {
  const [messages, setMessages] = useState([]);
 
  const showToast = (message, type = 'info') => {
    const id = Date.now();
    setMessages(prev => [...prev, { id, message, type }]);
    setTimeout(() => {
      setMessages(prev => prev.filter(msg => msg.id !== id));
    }, 5000);
  };
 
  return { messages, showToast };
};

useWallet.js

import { useState } from 'react';
import { BrowserProvider, Contract } from 'ethers';
 
export const useWallet = (showToast, contractAddress) => {
const [account, setAccount] = useState('');
const [isConnected, setIsConnected] = useState(false);
 
// Contract ABI for Dice Game
const contractABI = [
"function playDice(uint256 seed, uint8 guess) payable",
"function getLastDiceRoll() view returns (bytes)",
"function getPlayerGuess() view returns (bytes)", 
"function getWinnerStatus() view returns (bytes)",
"function decryptLastDiceRoll() view returns (uint8)",
"function decryptPlayerGuess() view returns (uint8)",
"function decryptWinnerStatus() view returns (bool)",
"function getGameFee() view returns (uint256)",
"function getContractBalance() view returns (uint256)"
];
 
// Switch to Sepolia network
const switchToSepolia = async () => {
try {
  await window.ethereum.request({
    method: 'wallet_switchEthereumChain',
    params: [{ chainId: '0xaa36a7' }],
  });
} catch (error) {
  if (error.code === 4902) {
    try {
      await window.ethereum.request({
        method: 'wallet_addEthereumChain',
        params: [{
          chainId: '0xaa36a7',
          chainName: 'Sepolia Testnet',
          nativeCurrency: { name: 'SepoliaETH', symbol: 'ETH', decimals: 18 },
          rpcUrls: ['https://sepolia.infura.io/v3/54d227d3f34347b5b4ba31bbfdb83093'],
          blockExplorerUrls: ['https://sepolia.etherscan.io/']
        }]
      });
    } catch (addError) {
      console.error("Error adding Sepolia network:", addError);
    }
  } else {
    console.error("Error switching to Sepolia:", error);
  }
}
};
 
// Connect to MetaMask
const connectWallet = async () => {
if (!window.ethereum) {
  showToast('Please install MetaMask browser extension', 'error');
  return;
}
 
try {
  showToast('Connecting...', 'info');
 
  await switchToSepolia();
 
  const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
  setAccount(accounts[0]);
  setIsConnected(true);
 
  showToast('Wallet connected successfully!', 'success');
 
} catch (error) {
  console.error("Error connecting wallet:", error);
  showToast('Connection failed: ' + error.message, 'error');
}
};
 
return { account, isConnected, connectWallet };
};

CSS

under src/app.css

/* Clean Modern Design System */
:root {
  /* Clean Color Palette */
  --primary-blue: #3b82f6;
  --primary-blue-hover: #2563eb;
  --secondary-gray: #6b7280;
  --success-green: #10b981;
  --warning-orange: #f59e0b;
  --error-red: #ef4444;
  
  /* Neutral Colors */
  --bg-primary: #ffffff;
  --bg-secondary: #f8fafc;
  --bg-tertiary: #f1f5f9;
  --bg-card: #ffffff;
  --bg-card-hover: #f8fafc;
  
  /* Text Colors */
  --text-primary: #1f2937;
  --text-secondary: #6b7280;
  --text-muted: #9ca3af;
  --text-white: #ffffff;
  
  /* Border Colors */
  --border-light: #e5e7eb;
  --border-medium: #d1d5db;
  --border-dark: #9ca3af;
  
  /* Spacing Scale */
  --space-xs: 0.25rem;
  --space-sm: 0.5rem;
  --space-md: 1rem;
  --space-lg: 1.5rem;
  --space-xl: 2rem;
  --space-2xl: 3rem;
  
  /* Typography */
  --font-xs: 0.75rem;
  --font-sm: 0.875rem;
  --font-base: 1rem;
  --font-lg: 1.125rem;
  --font-xl: 1.25rem;
  --font-2xl: 1.5rem;
  --font-3xl: 1.875rem;
  --font-4xl: 2.25rem;
  
  /* Shadows */
  --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
  --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
  --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
  --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
  
  /* Border Radius */
  --radius-sm: 0.375rem;
  --radius-md: 0.5rem;
  --radius-lg: 0.75rem;
  --radius-xl: 1rem;
  --radius-2xl: 1.5rem;
  
  /* Animation Durations */
  --duration-fast: 0.15s;
  --duration-normal: 0.3s;
  --duration-slow: 0.5s;
}
 
/* Global Reset & Base Styles */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
 
html {
  scroll-behavior: smooth;
}
 
body {
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  background: var(--bg-primary);
  color: var(--text-primary);
  line-height: 1.6;
  overflow-x: hidden;
}
 
/* Main App Container */
.app {
  min-height: 100vh;
  background: var(--bg-primary);
}
 
.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 var(--space-lg);
  position: relative;
  z-index: 1;
}
 
.app-content {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  padding: var(--space-sm) 0;
  min-height: 100vh;
}
 
/* Section Styling */
.section {
  display: flex;
  flex-direction: column;
  gap: var(--space-lg);
}
 
.wallet-section {
  margin-bottom: var(--space-xs);
}
 
.actions-section {
  margin: var(--space-md) 0;
  gap: var(--space-xl);
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: start;
}
 
.contract-section {
  margin-top: var(--space-sm);
}
 
/* Header Styles */
.header {
  text-align: center;
  padding: var(--space-md) 0 var(--space-xs);
  position: relative;
}
 
.header-content {
  position: relative;
  z-index: 2;
}
 
.title {
  font-size: var(--font-3xl);
  font-weight: 800;
  color: var(--text-primary);
  margin-bottom: var(--space-xs);
  letter-spacing: -0.025em;
}
 
.subtitle {
  font-size: var(--font-base);
  color: var(--text-secondary);
  max-width: 600px;
  margin: 0 auto var(--space-sm);
  line-height: 1.5;
}
 
.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  background: var(--bg-tertiary);
  color: var(--text-secondary);
  padding: var(--space-sm) var(--space-md);
  border-radius: var(--radius-2xl);
  font-size: var(--font-sm);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  border: 1px solid var(--border-light);
}
 
/* Card Components */
.card {
  background: var(--bg-card);
  border: 1px solid var(--border-light);
  border-radius: var(--radius-xl);
  padding: var(--space-xl);
  box-shadow: var(--shadow-md);
  position: relative;
  transition: all var(--duration-normal) ease;
}
 
.card:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-lg);
  border-color: var(--border-medium);
}
 
 
.card-secondary {
  background: var(--bg-secondary);
  border: 1px solid var(--border-light);
  opacity: 0.95;
}
 
/* 3D Dice */
.dice-stage {
  display: flex;
  justify-content: center;
  align-items: center;
  perspective: 1000px;
  height: 180px;
  margin: var(--space-xl) 0;
  position: relative;
}
 
.dice-cube {
  width: 100px;
  height: 100px;
  position: relative;
  transform-style: preserve-3d;
  transition: transform 1s cubic-bezier(0.25, 0.46, 0.45, 0.94);
  cursor: pointer;
}
 
.dice-cube:hover {
  transform: scale(1.05);
}
 
.dice-face {
  position: absolute;
  width: 100px;
  height: 100px;
  background: #ffffff;
  border-radius: 12px;
  border: 2px solid var(--border-light);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 2.5rem;
  color: var(--text-primary);
  box-shadow: 
    inset 0 0 10px rgba(0, 0, 0, 0.05),
    0 4px 8px rgba(0, 0, 0, 0.1);
  transition: all var(--duration-normal) ease;
}
 
.dice-face:hover {
  box-shadow: 
    inset 0 0 15px rgba(0, 0, 0, 0.1),
    0 6px 12px rgba(0, 0, 0, 0.15);
}
 
/* Face positions */
.face-1 { transform: rotateY(0deg) translateZ(50px); }
.face-2 { transform: rotateY(90deg) translateZ(50px); }
.face-3 { transform: rotateY(180deg) translateZ(50px); }
.face-4 { transform: rotateY(-90deg) translateZ(50px); }
.face-5 { transform: rotateX(90deg) translateZ(50px); }
.face-6 { transform: rotateX(-90deg) translateZ(50px); }
 
.dice-rolling {
  animation: diceRoll 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94) infinite;
}
 
@keyframes diceRoll {
  0% { transform: rotateX(0deg) rotateY(0deg) rotateZ(0deg); }
  25% { transform: rotateX(90deg) rotateY(90deg) rotateZ(45deg); }
  50% { transform: rotateX(180deg) rotateY(180deg) rotateZ(90deg); }
  75% { transform: rotateX(270deg) rotateY(270deg) rotateZ(135deg); }
  100% { transform: rotateX(360deg) rotateY(360deg) rotateZ(180deg); }
}
 
/* Buttons */
.btn {
  position: relative;
  background: var(--primary-blue);
  color: var(--text-white);
  border: none;
  padding: var(--space-md) var(--space-xl);
  border-radius: var(--radius-lg);
  font-size: var(--font-base);
  font-weight: 600;
  cursor: pointer;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  transition: all var(--duration-normal) ease;
  overflow: hidden;
  box-shadow: var(--shadow-sm);
}
 
.btn:hover:not(:disabled) {
  background: var(--primary-blue-hover);
  transform: translateY(-1px);
  box-shadow: var(--shadow-md);
}
 
.btn:active {
  transform: translateY(0);
  box-shadow: var(--shadow-sm);
}
 
.btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
  transform: none;
}
 
.btn-primary {
  background: var(--primary-blue);
}
 
.btn-primary:hover:not(:disabled) {
  background: var(--primary-blue-hover);
}
 
.btn-secondary {
  background: var(--secondary-gray);
  color: var(--text-white);
}
 
.btn-secondary:hover:not(:disabled) {
  background: #4b5563;
}
 
.btn-success {
  background: var(--success-green);
  color: var(--text-white);
}
 
.btn-success:hover:not(:disabled) {
  background: #059669;
}
 
.btn-loading {
  position: relative;
  pointer-events: none;
}
 
.btn-loading::after {
  content: '';
  position: absolute;
  width: 16px;
  height: 16px;
  border: 2px solid transparent;
  border-top: 2px solid currentColor;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}
 
@keyframes spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}
 
/* Form Elements */
.form-group {
  margin-bottom: var(--space-xl);
}
 
.form-label {
  display: block;
  margin-bottom: var(--space-sm);
  color: var(--text-primary);
  font-weight: 600;
  font-size: var(--font-sm);
}
 
.form-input {
  width: 100%;
  padding: var(--space-md) var(--space-lg);
  background: var(--bg-card);
  border: 1px solid var(--border-light);
  border-radius: var(--radius-lg);
  color: var(--text-primary);
  font-size: var(--font-base);
  transition: all var(--duration-normal) ease;
}
 
.form-input:focus {
  outline: none;
  border-color: var(--primary-blue);
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
 
.form-input::placeholder {
  color: var(--text-muted);
}
 
/* Status Messages */
.status-message {
  padding: var(--space-lg) var(--space-xl);
  border-radius: var(--radius-lg);
  margin-bottom: var(--space-lg);
  display: flex;
  align-items: center;
  gap: var(--space-md);
  font-weight: 600;
  font-size: var(--font-base);
  border: 1px solid;
}
 
.status-success {
  background: rgba(16, 185, 129, 0.1);
  color: var(--success-green);
  border-color: var(--success-green);
}
 
.status-error {
  background: rgba(239, 68, 68, 0.1);
  color: var(--error-red);
  border-color: var(--error-red);
}
 
.status-info {
  background: rgba(59, 130, 246, 0.1);
  color: var(--primary-blue);
  border-color: var(--primary-blue);
}
 
/* Toast Notifications */
.toast-container {
  position: fixed;
  top: var(--space-xl);
  right: var(--space-xl);
  z-index: 1000;
  max-width: 400px;
}
 
.toast {
  background: var(--bg-card);
  color: var(--text-primary);
  padding: var(--space-lg) var(--space-xl);
  border-radius: var(--radius-lg);
  margin-bottom: var(--space-md);
  box-shadow: var(--shadow-xl);
  border: 1px solid var(--border-light);
  display: flex;
  align-items: center;
  gap: var(--space-md);
  font-weight: 600;
  position: relative;
  overflow: hidden;
}
 
.toast-success {
  border-color: var(--success-green);
}
 
.toast-error {
  border-color: var(--error-red);
}
 
.toast-info {
  border-color: var(--primary-blue);
}
 
/* Animations */
@keyframes slideInUp {
  from {
    transform: translateY(20px);
    opacity: 0;
  }
  to {
    transform: translateY(0);
    opacity: 1;
  }
}
 
@keyframes fadeInUp {
  from {
    transform: translateY(10px);
    opacity: 0;
  }
  to {
    transform: translateY(0);
    opacity: 1;
  }
}
 
/* Footer */
.footer {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  padding: var(--space-lg) 0;
  background: var(--bg-card);
  border-top: 1px solid var(--border-light);
  z-index: 10;
}
 
.footer-content {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 var(--space-lg);
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-lg);
}
 
.footer-text p {
  font-size: var(--font-sm);
  color: var(--text-secondary);
  margin: 0;
}
 
.footer-links {
  display: flex;
  gap: var(--space-lg);
  align-items: center;
}
 
.footer-link {
  color: var(--text-secondary);
  text-decoration: none;
  font-size: var(--font-sm);
  font-weight: 600;
  padding: var(--space-sm) var(--space-md);
  border-radius: var(--radius-md);
  transition: all var(--duration-normal) ease;
  border: 1px solid transparent;
}
 
.footer-link:hover {
  color: var(--primary-blue);
  border-color: var(--border-light);
  background: var(--bg-tertiary);
  transform: translateY(-1px);
}
 
/* Responsive Design */
@media (max-width: 1024px) {
  .actions-section {
    grid-template-columns: 1fr;
  }
}
 
@media (max-width: 768px) {
  .container {
    padding: 0 var(--space-md);
  }
  
  .app-content {
    gap: var(--space-lg);
    padding: var(--space-lg) 0;
  }
  
  .title {
    font-size: var(--font-3xl);
  }
  
  .card {
    padding: var(--space-lg);
  }
  
  .dice-cube {
    width: 80px;
    height: 80px;
  }
  
  .dice-face {
    width: 80px;
    height: 80px;
    font-size: 2rem;
  }
  
  .face-1, .face-2, .face-3, .face-4, .face-5, .face-6 {
    transform: rotateY(0deg) translateZ(40px);
  }
  
  .footer-content {
    flex-direction: column;
    text-align: center;
    gap: var(--space-md);
  }
}
 
@media (max-width: 480px) {
  .title {
    font-size: var(--font-2xl);
  }
  
  .subtitle {
    font-size: var(--font-base);
  }
  
  .card {
    padding: var(--space-md);
  }
  
  .btn {
    padding: var(--space-sm) var(--space-lg);
    font-size: var(--font-sm);
  }
}

Run the frontend DApp:

npm run start

If you have follow along all way to this,

You have successfully completed the journey of Deploying a Dapps using FHEVM powered by Zama

Give a try to the below question on what you have understand!


DApp Development Mastery

Test your understanding of building FHEVM-powered DApps!

Question 1 of 5Frontend Architecture

What are the key components in a FHEVM DApp frontend?

Resources