init contract frontent

This commit is contained in:
SamKry
2025-11-18 17:58:57 +01:00
parent bd2ff2808e
commit 4ec03d7cdb
6 changed files with 1135 additions and 4 deletions

View File

@@ -0,0 +1,53 @@
interface CommitProps {
playMove: string;
setPlayMove: (v: string) => void;
handlePlay: () => void;
loading: boolean;
account: string;
contract: any;
bothPlayed: string;
handleBothPlayed: () => void;
revealTimeLeft: string;
handleRevealTimeLeft: () => void;
}
export default function Commit({
playMove,
setPlayMove,
handlePlay,
loading,
account,
contract,
bothPlayed,
handleBothPlayed,
revealTimeLeft,
handleRevealTimeLeft,
}: Readonly<CommitProps>) {
return (
<div className="border p-4 rounded-lg">
<h2 className="font-semibold mb-2">play(bytes32 encrMove)</h2>
<input
type="text"
placeholder="Encrypted Move (bytes32)"
value={playMove}
onChange={(e) => setPlayMove(e.target.value)}
className="border px-2 py-1 mr-2 rounded"
/>
<button
onClick={handlePlay}
disabled={loading || !account || !contract}
className="bg-blue-500 text-white px-4 py-2 rounded"
>
Play
</button>
<div className="mt-4 space-y-2">
<button onClick={handleBothPlayed} className="bg-gray-200 px-2 py-1 rounded">bothPlayed</button>
<span className="ml-2 text-xs">{bothPlayed}</span>
<br />
<button onClick={handleRevealTimeLeft} className="bg-gray-200 px-2 py-1 rounded">revealTimeLeft</button>
<span className="ml-2 text-xs">{revealTimeLeft}</span>
</div>
</div>
);
}

View File

@@ -0,0 +1,122 @@
interface LobbyProps {
registerGameId: string;
setRegisterGameId: (v: string) => void;
registerBet: string;
setRegisterBet: (v: string) => void;
handleRegister: () => void;
loading: boolean;
account: string;
contract: any;
betMin: string;
handleGetBetMin: () => void;
activeGameIds: string;
handleGetActiveGameIds: () => void;
contractBalance: string;
handleGetContractBalance: () => void;
gameDetailsId: string;
setGameDetailsId: (v: string) => void;
gameDetails: any;
handleGetGameDetails: () => void;
myActiveGameId: string;
handleGetMyActiveGameId: () => void;
pastGameIndex: string;
setPastGameIndex: (v: string) => void;
pastGame: any;
handleGetPastGame: () => void;
pastGamesCount: string;
handleGetPastGamesCount: () => void;
whoAmI: string;
handleWhoAmI: () => void;
}
export default function Lobby({
registerGameId,
setRegisterGameId,
registerBet,
setRegisterBet,
handleRegister,
loading,
account,
contract,
betMin,
handleGetBetMin,
activeGameIds,
handleGetActiveGameIds,
contractBalance,
handleGetContractBalance,
gameDetailsId,
setGameDetailsId,
gameDetails,
handleGetGameDetails,
myActiveGameId,
handleGetMyActiveGameId,
pastGameIndex,
setPastGameIndex,
pastGame,
handleGetPastGame,
pastGamesCount,
handleGetPastGamesCount,
whoAmI,
handleWhoAmI,
}: Readonly<LobbyProps>) {
return (
<div className="border p-4 rounded-lg">
<h2 className="font-semibold mb-2">register(uint gameId) (payable)</h2>
<input
type="text"
placeholder="Game ID (0 = auto)"
value={registerGameId}
onChange={(e) => setRegisterGameId(e.target.value)}
className="border px-2 py-1 mr-2 rounded"
/>
<input
type="number"
min="0.01"
step="0.01"
placeholder="Bet in ETH (default 0.01)"
value={registerBet}
onChange={(e) => setRegisterBet(e.target.value)}
className="border px-2 py-1 mr-2 rounded"
/>
<div className="text-xs text-slate-500 mt-1">
Enter amount in ETH (e.g., 0.01 for 0.01 ETH). Entering 1 means 1 full
ETH.
</div>
<button
onClick={handleRegister}
disabled={loading || !account || !contract}
className="bg-blue-500 text-white px-4 py-2 rounded"
>
Register
</button>
<div className="mt-4 space-y-2">
<button onClick={handleGetBetMin} className="bg-gray-200 px-2 py-1 rounded">BET_MIN</button>
<span className="ml-2 text-xs">{betMin}</span>
<br />
<button onClick={handleGetActiveGameIds} className="bg-gray-200 px-2 py-1 rounded">getActiveGameIds</button>
<span className="ml-2 text-xs">{activeGameIds}</span>
<br />
<button onClick={handleGetContractBalance} className="bg-gray-200 px-2 py-1 rounded">getContractBalance</button>
<span className="ml-2 text-xs">{contractBalance}</span>
<br />
<input type="text" placeholder="Game ID" value={gameDetailsId} onChange={e => setGameDetailsId(e.target.value)} className="border px-2 py-1 mr-2 rounded" />
<button onClick={handleGetGameDetails} className="bg-gray-200 px-2 py-1 rounded">getGameDetails</button>
<span className="ml-2 text-xs">{gameDetails && <pre className="inline whitespace-pre-wrap">{JSON.stringify(gameDetails, null, 2)}</pre>}</span>
<br />
<button onClick={handleGetMyActiveGameId} className="bg-gray-200 px-2 py-1 rounded">getMyActiveGameId</button>
<span className="ml-2 text-xs">{myActiveGameId}</span>
<br />
<input type="text" placeholder="Past Game Index" value={pastGameIndex} onChange={e => setPastGameIndex(e.target.value)} className="border px-2 py-1 mr-2 rounded" />
<button onClick={handleGetPastGame} className="bg-gray-200 px-2 py-1 rounded">getPastGame</button>
<span className="ml-2 text-xs">{pastGame && <pre className="inline whitespace-pre-wrap">{JSON.stringify(pastGame, null, 2)}</pre>}</span>
<br />
<button onClick={handleGetPastGamesCount} className="bg-gray-200 px-2 py-1 rounded">getPastGamesCount</button>
<span className="ml-2 text-xs">{pastGamesCount}</span>
<br />
<button onClick={handleWhoAmI} className="bg-gray-200 px-2 py-1 rounded">whoAmI</button>
<span className="ml-2 text-xs">{whoAmI}</span>
</div>
</div>
);
}

View File

@@ -0,0 +1,67 @@
interface RevealProps {
revealMove: string;
setRevealMove: (v: string) => void;
handleReveal: () => void;
loading: boolean;
account: string;
contract: any;
bothRevealed: string;
handleBothRevealed: () => void;
playerARevealed: string;
handlePlayerARevealed: () => void;
playerBRevealed: string;
handlePlayerBRevealed: () => void;
revealTimeLeft: string;
handleRevealTimeLeft: () => void;
}
export default function Reveal({
revealMove,
setRevealMove,
handleReveal,
loading,
account,
contract,
bothRevealed,
handleBothRevealed,
playerARevealed,
handlePlayerARevealed,
playerBRevealed,
handlePlayerBRevealed,
revealTimeLeft,
handleRevealTimeLeft,
}: Readonly<RevealProps>) {
return (
<div className="border p-4 rounded-lg">
<h2 className="font-semibold mb-2">reveal(string clearMove)</h2>
<input
type="text"
placeholder="Clear Move (e.g. 1-password)"
value={revealMove}
onChange={(e) => setRevealMove(e.target.value)}
className="border px-2 py-1 mr-2 rounded"
/>
<button
onClick={handleReveal}
disabled={loading || !account || !contract}
className="bg-blue-500 text-white px-4 py-2 rounded"
>
Reveal
</button>
<div className="mt-4 space-y-2">
<button onClick={handleBothRevealed} className="bg-gray-200 px-2 py-1 rounded">bothRevealed</button>
<span className="ml-2 text-xs">{bothRevealed}</span>
<br />
<button onClick={handlePlayerARevealed} className="bg-gray-200 px-2 py-1 rounded">playerARevealed</button>
<span className="ml-2 text-xs">{playerARevealed}</span>
<br />
<button onClick={handlePlayerBRevealed} className="bg-gray-200 px-2 py-1 rounded">playerBRevealed</button>
<span className="ml-2 text-xs">{playerBRevealed}</span>
<br />
<button onClick={handleRevealTimeLeft} className="bg-gray-200 px-2 py-1 rounded">revealTimeLeft</button>
<span className="ml-2 text-xs">{revealTimeLeft}</span>
</div>
</div>
);
}

View File

@@ -1,11 +1,520 @@
"use client";
import { useEffect, useState } from "react";
import Web3 from "web3";
import Lobby from "./Lobby";
import Commit from "./Commit";
import Reveal from "./Reveal";
export default function Clash() {
const [config, setConfig] = useState<Config | null>(null);
const [web3, setWeb3] = useState<Web3 | null>(null);
const [contract, setContract] = useState<any>(null);
const [account, setAccount] = useState<string>("");
const [status, setStatus] = useState<string>("");
const [loading, setLoading] = useState(false);
// Inputs for contract functions
const [phase, setPhase] = useState<"lobby" | "commit" | "reveal">("lobby");
const [registerGameId, setRegisterGameId] = useState<string>("0");
const [registerBet, setRegisterBet] = useState<string>("");
const [playMove, setPlayMove] = useState<string>("");
const [revealMove, setRevealMove] = useState<string>("");
// State for read-only contract calls
const [betMin, setBetMin] = useState<string>("");
const [activeGameIds, setActiveGameIds] = useState<string>("");
const [contractBalance, setContractBalance] = useState<string>("");
const [gameDetailsId, setGameDetailsId] = useState<string>("");
const [gameDetails, setGameDetails] = useState<any>(null);
const [myActiveGameId, setMyActiveGameId] = useState<string>("");
const [pastGameIndex, setPastGameIndex] = useState<string>("");
const [pastGame, setPastGame] = useState<any>(null);
const [pastGamesCount, setPastGamesCount] = useState<string>("");
const [whoAmI, setWhoAmI] = useState<string>("");
const [bothPlayed, setBothPlayed] = useState<string>("");
const [revealTimeLeft, setRevealTimeLeft] = useState<string>("");
const [bothRevealed, setBothRevealed] = useState<string>("");
const [playerARevealed, setPlayerARevealed] = useState<string>("");
const [playerBRevealed, setPlayerBRevealed] = useState<string>("");
// Reveal phase read-only handlers
const handleBothRevealed = async () => {
if (!contract) return;
setLoading(true);
try {
const res = await contract.methods.bothRevealed().call();
setBothRevealed(res ? "true" : "false");
} catch (err: any) {
setStatus("Failed to fetch bothRevealed: " + err.message);
} finally {
setLoading(false);
}
};
const handlePlayerARevealed = async () => {
if (!contract) return;
setLoading(true);
try {
const res = await contract.methods.playerARevealed().call();
setPlayerARevealed(res ? "true" : "false");
} catch (err: any) {
setStatus("Failed to fetch playerARevealed: " + err.message);
} finally {
setLoading(false);
}
};
const handlePlayerBRevealed = async () => {
if (!contract) return;
setLoading(true);
try {
const res = await contract.methods.playerBRevealed().call();
setPlayerBRevealed(res ? "true" : "false");
} catch (err: any) {
setStatus("Failed to fetch playerBRevealed: " + err.message);
const [bothRevealed, setBothRevealed] = useState<string>("");
const [playerARevealed, setPlayerARevealed] = useState<string>("");
const [playerBRevealed, setPlayerBRevealed] = useState<string>("");
setLoading(false);
}
};
// Commit phase read-only handlers
const handleBothPlayed = async () => {
if (!contract) return;
setLoading(true);
try {
const res = await contract.methods.bothPlayed().call();
setBothPlayed(res ? "true" : "false");
} catch (err: any) {
setStatus("Failed to fetch bothPlayed: " + err.message);
} finally {
setLoading(false);
}
};
const handleRevealTimeLeft = async () => {
if (!contract) return;
setLoading(true);
try {
const res = await contract.methods.revealTimeLeft().call();
setRevealTimeLeft(res.toString());
} catch (err: any) {
setStatus("Failed to fetch revealTimeLeft: " + err.message);
} finally {
setLoading(false);
}
};
// Read-only contract function handlers
const handleGetBetMin = async () => {
if (!contract || !web3) return;
setLoading(true);
try {
const res = await contract.methods.BET_MIN().call();
setBetMin(web3.utils.fromWei(res, "ether") + " ETH");
} catch (err: any) {
setStatus("Failed to fetch BET_MIN: " + err.message);
} finally {
setLoading(false);
}
};
const handleGetActiveGameIds = async () => {
if (!contract) return;
setLoading(true);
try {
const res = await contract.methods.getActiveGameIds().call();
setActiveGameIds(res.join(", "));
} catch (err: any) {
setStatus("Failed to fetch active game IDs: " + err.message);
} finally {
setLoading(false);
}
};
const handleGetContractBalance = async () => {
if (!contract || !web3) return;
setLoading(true);
try {
const res = await contract.methods.getContractBalance().call();
setContractBalance(web3.utils.fromWei(res, "ether") + " ETH");
} catch (err: any) {
setStatus("Failed to fetch contract balance: " + err.message);
} finally {
setLoading(false);
}
};
const handleGetGameDetails = async () => {
if (!contract) return;
setLoading(true);
try {
const res = await contract.methods
.getGameDetails(Number(gameDetailsId))
.call();
setGameDetails(res);
} catch (err: any) {
setStatus("Failed to fetch game details: " + err.message);
} finally {
setLoading(false);
}
};
const handleGetMyActiveGameId = async () => {
if (!contract || !account) return;
setLoading(true);
try {
const res = await contract.methods
.getMyActiveGameId()
.call({ from: account });
setMyActiveGameId(res.toString());
} catch (err: any) {
setStatus("Failed to fetch my active game ID: " + err.message);
} finally {
setLoading(false);
}
};
const handleGetPastGame = async () => {
// Reveal phase read-only handlers
const handleBothRevealed = async () => {
if (!contract) return;
setLoading(true);
try {
const res = await contract.methods.bothRevealed().call();
setBothRevealed(res ? "true" : "false");
} catch (err: any) {
setStatus("Failed to fetch bothRevealed: " + err.message);
} finally {
setLoading(false);
}
};
const handlePlayerARevealed = async () => {
if (!contract) return;
setLoading(true);
try {
const res = await contract.methods.playerARevealed().call();
setPlayerARevealed(res ? "true" : "false");
} catch (err: any) {
setStatus("Failed to fetch playerARevealed: " + err.message);
} finally {
setLoading(false);
}
};
const handlePlayerBRevealed = async () => {
if (!contract) return;
setLoading(true);
try {
const res = await contract.methods.playerBRevealed().call();
setPlayerBRevealed(res ? "true" : "false");
} catch (err: any) {
setStatus("Failed to fetch playerBRevealed: " + err.message);
} finally {
setLoading(false);
}
};
if (!contract) return;
setLoading(true);
try {
const res = await contract.methods
.getPastGame(Number(pastGameIndex))
.call();
setPastGame(res);
} catch (err: any) {
setStatus("Failed to fetch past game: " + err.message);
} finally {
setLoading(false);
}
};
const handleGetPastGamesCount = async () => {
if (!contract) return;
setLoading(true);
try {
const res = await contract.methods.getPastGamesCount().call();
setPastGamesCount(res.toString());
} catch (err: any) {
setStatus("Failed to fetch past games count: " + err.message);
} finally {
setLoading(false);
}
};
const handleWhoAmI = async () => {
if (!contract || !account) return;
setLoading(true);
try {
const res = await contract.methods.whoAmI().call({ from: account });
setWhoAmI(res.toString());
} catch (err: any) {
setStatus("Failed to fetch whoAmI: " + err.message);
} finally {
setLoading(false);
}
};
// (Removed unused inputs and state)
// Load config and contract
useEffect(() => {
const loadConfig = async () => {
try {
const response = await fetch("/crypto_clash/config.json");
const data = await response.json();
setConfig(data);
const web3Instance = new Web3(data.API_URL);
setWeb3(web3Instance);
const contractInstance = new web3Instance.eth.Contract(
data.GAME_ABI,
data.GAME_CONTRACT_ADDRESS
);
setContract(contractInstance);
// Get account
if (globalThis.window !== undefined && (globalThis as any).ethereum) {
try {
const accounts = await (globalThis as any).ethereum.request({
method: "eth_requestAccounts",
});
setAccount(accounts[0]);
} catch (err: any) {
setStatus(
"MetaMask not available or user denied access: " + err.message
);
}
}
} catch (err: any) {
setStatus("Failed to load config: " + err.message);
}
};
loadConfig();
}, []);
// (Removed unused helpers)
// Contract function handlers
const handleRegister = async () => {
if (!contract || !web3 || !account) return;
setLoading(true);
setStatus("");
try {
console.log(registerBet)
const bet = web3.utils.toWei(registerBet || "0.01", "ether");
console.log(bet)
console.log(web3.utils.toHex(bet))
const tx = contract.methods.register(Number(registerGameId || 0));
const gas = await tx.estimateGas({ from: account, value: bet });
const result = await (globalThis as any).ethereum.request({
method: "eth_sendTransaction",
params: [
{
from: account,
to: config?.GAME_CONTRACT_ADDRESS,
data: tx.encodeABI(),
value: web3.utils.numberToHex(bet),
gas: web3.utils.toHex(gas),
chainId: web3.utils.toHex(await web3.eth.net.getId()),
},
],
});
setStatus("Register tx sent: " + result);
} catch (err: any) {
setStatus("Register failed: " + err.message);
} finally {
setLoading(false);
}
};
const handlePlay = async () => {
if (!contract || !web3 || !account) return;
setLoading(true);
setStatus("");
try {
// playMove should be a hex string (bytes32)
const tx = contract.methods.play(playMove);
const gas = await tx.estimateGas({ from: account });
const result = await (globalThis as any).ethereum.request({
method: "eth_sendTransaction",
params: [
{
from: account,
to: config?.GAME_CONTRACT_ADDRESS,
data: tx.encodeABI(),
gas: web3.utils.toHex(gas),
chainId: web3.utils.toHex(await web3.eth.net.getId()),
},
],
});
setStatus("Play tx sent: " + result);
} catch (err: any) {
setStatus("Play failed: " + err.message);
} finally {
setLoading(false);
}
};
const handleReveal = async () => {
if (!contract || !web3 || !account) return;
setLoading(true);
setStatus("");
try {
const tx = contract.methods.reveal(revealMove);
const gas = await tx.estimateGas({ from: account });
const result = await (globalThis as any).ethereum.request({
method: "eth_sendTransaction",
params: [
{
from: account,
to: config?.GAME_CONTRACT_ADDRESS,
data: tx.encodeABI(),
gas: web3.utils.toHex(gas),
chainId: web3.utils.toHex(await web3.eth.net.getId()),
},
],
});
setStatus("Reveal tx sent: " + result);
} catch (err: any) {
setStatus("Reveal failed: " + err.message);
} finally {
setLoading(false);
}
};
return (
<div>
<h1>Clash Page</h1>
<p>This is the Clash page content.</p>
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-slate-900 dark:to-slate-800 font-sans">
<main className="w-full max-w-3xl mx-auto py-12 px-6">
<div className="bg-white dark:bg-slate-800 rounded-lg shadow-lg p-8">
<h1 className="text-4xl font-bold text-center mb-2 text-slate-900 dark:text-white">
Crypto Clash
</h1>
<p className="text-center text-slate-600 dark:text-slate-300 mb-8">
{phase === "lobby" && "Register for a game to start."}
{phase === "commit" && "Commit your move."}
{phase === "reveal" && "Reveal your move."}
</p>
<div className="mb-8 p-4 bg-slate-100 dark:bg-slate-700 rounded-lg">
<p className="text-sm text-slate-600 dark:text-slate-300">
<span className="font-semibold">Connected Account:</span>{" "}
{account
? `${account.slice(0, 6)}...${account.slice(-4)}`
: "Not connected"}
</p>
<p className="text-sm text-slate-600 dark:text-slate-300 mt-2">
<span className="font-semibold">Game Contract Address:</span>{" "}
{config?.GAME_CONTRACT_ADDRESS}
</p>
</div>
<div className="flex justify-center mb-6 space-x-4">
<button
onClick={() => setPhase("lobby")}
className={`px-4 py-2 rounded ${
phase === "lobby"
? "bg-blue-600 text-white"
: "bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200"
}`}
>
Lobby
</button>
<button
onClick={() => setPhase("commit")}
className={`px-4 py-2 rounded ${
phase === "commit"
? "bg-blue-600 text-white"
: "bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200"
}`}
>
Commit
</button>
<button
onClick={() => setPhase("reveal")}
className={`px-4 py-2 rounded ${
phase === "reveal"
? "bg-blue-600 text-white"
: "bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200"
}`}
>
Reveal
</button>
</div>
<div className="space-y-6">
{phase === "lobby" && (
<Lobby
registerGameId={registerGameId}
setRegisterGameId={setRegisterGameId}
registerBet={registerBet}
setRegisterBet={setRegisterBet}
handleRegister={handleRegister}
loading={loading}
account={account}
contract={contract}
betMin={betMin}
handleGetBetMin={handleGetBetMin}
activeGameIds={activeGameIds}
handleGetActiveGameIds={handleGetActiveGameIds}
contractBalance={contractBalance}
handleGetContractBalance={handleGetContractBalance}
gameDetailsId={gameDetailsId}
setGameDetailsId={setGameDetailsId}
gameDetails={gameDetails}
handleGetGameDetails={handleGetGameDetails}
myActiveGameId={myActiveGameId}
handleGetMyActiveGameId={handleGetMyActiveGameId}
pastGameIndex={pastGameIndex}
setPastGameIndex={setPastGameIndex}
pastGame={pastGame}
handleGetPastGame={handleGetPastGame}
pastGamesCount={pastGamesCount}
handleGetPastGamesCount={handleGetPastGamesCount}
whoAmI={whoAmI}
handleWhoAmI={handleWhoAmI}
/>
)}
{phase === "commit" && (
<Commit
playMove={playMove}
setPlayMove={setPlayMove}
handlePlay={handlePlay}
loading={loading}
account={account}
contract={contract}
bothPlayed={bothPlayed}
handleBothPlayed={handleBothPlayed}
revealTimeLeft={revealTimeLeft}
handleRevealTimeLeft={handleRevealTimeLeft}
/>
)}
{phase === "reveal" && (
<Reveal
revealMove={revealMove}
setRevealMove={setRevealMove}
handleReveal={handleReveal}
loading={loading}
account={account}
contract={contract}
bothRevealed={bothRevealed}
handleBothRevealed={handleBothRevealed}
playerARevealed={playerARevealed}
handlePlayerARevealed={handlePlayerARevealed}
playerBRevealed={playerBRevealed}
handlePlayerBRevealed={handlePlayerBRevealed}
revealTimeLeft={revealTimeLeft}
handleRevealTimeLeft={handleRevealTimeLeft}
/>
)}
</div>
{status && (
<div
className={`mt-6 p-4 rounded-lg ${
status.includes("tx sent")
? "bg-green-50 dark:bg-green-900 text-green-800 dark:text-green-200"
: "bg-red-50 dark:bg-red-900 text-red-800 dark:text-red-200"
}`}
>
<p className="text-sm break-words">{status}</p>
</div>
)}
<div className="mt-8 p-4 bg-yellow-50 dark:bg-yellow-900 rounded-lg text-sm text-yellow-800 dark:text-yellow-200">
<p className="font-semibold mb-2"> Note:</p>
<ul className="list-disc list-inside space-y-1">
<li>
MetaMask or a compatible Web3 wallet is required for write
operations
</li>
<li>
Use bytes32 for encrypted move (see contract docs for details)
</li>
<li>ETH values are in Ether (not Wei)</li>
</ul>
</div>
</div>
</main>
</div>
);
}