Implement timeout handling and commit resolution in Commit and Reveal components

- Added state management for opponent's move and commit time left in Commit component.
- Implemented automatic checks for opponent's move and commit timeout.
- Introduced timeout resolution functionality allowing players to claim victory if the opponent fails to commit in time.
- Updated UI to display timeout results and allow players to resolve timeouts.
- Enhanced Reveal component with timeout checks and resolution logic.
- Added necessary contract methods in config.json for commit time left and timeout resolution.
This commit is contained in:
averel10
2025-11-22 10:58:49 +01:00
parent d6ea65f0f7
commit 7f1143eb22
5 changed files with 745 additions and 297 deletions

View File

@@ -59,7 +59,7 @@
"type": "function" "type": "function"
} }
], ],
"GAME_CONTRACT_ADDRESS": "0xAA7057A0203539d9BE86EfB471B831Dd833a9e22", "GAME_CONTRACT_ADDRESS": "0x7C56Fd34374F0244402aDDf5E940490C1a53E92E",
"GAME_ABI": [ "GAME_ABI": [
{ {
"inputs": [], "inputs": [],
@@ -74,6 +74,19 @@
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{
"inputs": [],
"name": "COMMIT_TIMEOUT",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{ {
"inputs": [], "inputs": [],
"name": "REVEAL_TIMEOUT", "name": "REVEAL_TIMEOUT",
@@ -87,6 +100,25 @@
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{
"inputs": [
{
"internalType": "uint256",
"name": "gameId",
"type": "uint256"
}
],
"name": "commitTimeLeft",
"outputs": [
{
"internalType": "int256",
"name": "",
"type": "int256"
}
],
"stateMutability": "view",
"type": "function"
},
{ {
"inputs": [], "inputs": [],
"name": "getActiveGameIds", "name": "getActiveGameIds",
@@ -278,6 +310,19 @@
"stateMutability": "payable", "stateMutability": "payable",
"type": "function" "type": "function"
}, },
{
"inputs": [
{
"internalType": "uint256",
"name": "gameId",
"type": "uint256"
}
],
"name": "resolveTimeout",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{ {
"inputs": [ "inputs": [
{ {

View File

@@ -4,7 +4,8 @@ pragma solidity >=0.7.3;
contract Game { contract Game {
uint public constant BET_MIN = 1e16; // The minimum bet (1 BLD) uint public constant BET_MIN = 1e16; // The minimum bet (1 BLD)
uint public constant REVEAL_TIMEOUT = 10 minutes; // Max delay of revelation phase uint public constant REVEAL_TIMEOUT = 0.5 minutes; // Max delay of revelation phase
uint public constant COMMIT_TIMEOUT = 0.5 minutes; // Max delay of commit phase
enum Moves { enum Moves {
None, None,
@@ -17,7 +18,9 @@ contract Game {
None, None,
PlayerA, PlayerA,
PlayerB, PlayerB,
Draw Draw,
PlayerATimeout,
PlayerBTimeout
} // Possible outcomes } // Possible outcomes
struct Player { struct Player {
@@ -32,6 +35,7 @@ contract Game {
Player playerA; Player playerA;
Player playerB; Player playerB;
Outcomes outcome; Outcomes outcome;
uint firstCommit;
uint firstReveal; uint firstReveal;
uint initialBet; uint initialBet;
uint gameId; uint gameId;
@@ -50,11 +54,11 @@ contract Game {
// ------------------------- Registration ------------------------- // // ------------------------- Registration ------------------------- //
modifier validBet(uint gameId) { modifier validBet(uint gameId) {
require(msg.value >= BET_MIN, "Minimum bet not met"); require(msg.value == BET_MIN, "Minimum bet not met");
require( require(
games[gameId].initialBet == 0 || games[gameId].initialBet == 0 ||
msg.value >= games[gameId].initialBet, msg.value == games[gameId].initialBet,
"Bet value too low" "Bet value must match initial bet"
); );
_; _;
} }
@@ -125,8 +129,18 @@ contract Game {
function play(uint gameId, bytes32 encrMove) public isRegistered(gameId) returns (bool) { function play(uint gameId, bytes32 encrMove) public isRegistered(gameId) returns (bool) {
GameState storage game = games[gameId]; GameState storage game = games[gameId];
// Check if game is still active (commit phase not timed out)
require(game.isActive, "Game is no longer active");
require(game.firstCommit == 0 || block.timestamp <= game.firstCommit + COMMIT_TIMEOUT, "Commit phase timeout expired");
// Basic sanity checks with explicit errors to help debugging // Basic sanity checks with explicit errors to help debugging
require(encrMove != bytes32(0), "Encrypted move cannot be zero"); require(encrMove != bytes32(0), "Encrypted move cannot be zero");
// Track first commit timestamp
if (game.firstCommit == 0) {
game.firstCommit = block.timestamp;
}
// Ensure the caller hasn't already committed a move // Ensure the caller hasn't already committed a move
if (msg.sender == game.playerA.addr) { if (msg.sender == game.playerA.addr) {
require( require(
@@ -166,6 +180,10 @@ contract Game {
) public isRegistered(gameId) commitPhaseEnded(gameId) returns (Moves) { ) public isRegistered(gameId) commitPhaseEnded(gameId) returns (Moves) {
GameState storage game = games[gameId]; GameState storage game = games[gameId];
// Check if reveal phase timeout has expired
if( game.firstReveal != 0 ) {
require(block.timestamp <= game.firstReveal + REVEAL_TIMEOUT, "Reveal phase timeout expired");
}
bytes32 encrMove = keccak256(abi.encodePacked(clearMove)); // Hash of clear input (= "move-password") bytes32 encrMove = keccak256(abi.encodePacked(clearMove)); // Hash of clear input (= "move-password")
Moves move = Moves(getFirstChar(clearMove)); // Actual move (Rock / Paper / Scissors) Moves move = Moves(getFirstChar(clearMove)); // Actual move (Rock / Paper / Scissors)
@@ -286,6 +304,17 @@ contract Game {
} }
} }
// Pay to one player and slash the other (timeout resolution).
function payWithSlash(
address payable winner,
address payable loser,
uint betAmount
) private {
// Winner gets both bets
winner.transfer(betAmount * 2);
// Loser gets nothing (slashed)
}
// Reset a specific game. // Reset a specific game.
function resetGame(uint gameId) private { function resetGame(uint gameId) private {
GameState storage game = games[gameId]; GameState storage game = games[gameId];
@@ -326,11 +355,86 @@ contract Game {
GameState storage game = games[gameId]; GameState storage game = games[gameId];
if (game.firstReveal != 0) { if (game.firstReveal != 0) {
return int((game.firstReveal + REVEAL_TIMEOUT) - block.timestamp); uint deadline = game.firstReveal + REVEAL_TIMEOUT;
if (block.timestamp >= deadline) {
return 0;
}
return int(deadline - block.timestamp);
} }
return int(REVEAL_TIMEOUT); return int(REVEAL_TIMEOUT);
} }
// Return time left before the end of the commit phase.
function commitTimeLeft(uint gameId) public view returns (int) {
if (gameId == 0) return int(COMMIT_TIMEOUT);
GameState storage game = games[gameId];
if (game.firstCommit != 0) {
uint deadline = game.firstCommit + COMMIT_TIMEOUT;
if (block.timestamp >= deadline) {
return 0;
}
return int(deadline - block.timestamp);
}
return int(COMMIT_TIMEOUT);
}
// Resolve a game that has timed out. Caller must be the non-offending player.
// The offending player is slashed (loses their bet), winner gets both bets.
function resolveTimeout(uint gameId) public isRegistered(gameId) {
GameState storage game = games[gameId];
require(game.isActive, "Game is not active");
address caller = msg.sender;
address payable offender;
address payable winner = payable(caller);
bool commitPhaseTimedOut = game.firstCommit != 0 &&
block.timestamp > game.firstCommit + COMMIT_TIMEOUT && (game.playerA.encrMove == bytes32(0) || game.playerB.encrMove == bytes32(0));
bool revealPhaseTimedOut = game.firstReveal != 0 &&
block.timestamp > game.firstReveal + REVEAL_TIMEOUT;
if (commitPhaseTimedOut) {
// Commit phase timeout: player who didn't commit first is offender
require(
(caller == game.playerA.addr && game.playerB.encrMove == bytes32(0)) ||
(caller == game.playerB.addr && game.playerA.encrMove == bytes32(0)),
"Caller must be the non-offending player"
);
if (caller == game.playerA.addr) {
offender = game.playerB.addr;
game.outcome = Outcomes.PlayerBTimeout;
} else {
offender = game.playerA.addr;
game.outcome = Outcomes.PlayerATimeout;
}
} else if (revealPhaseTimedOut) {
// Reveal phase timeout: player who didn't reveal is offender
require(
(caller == game.playerA.addr && game.playerB.move == Moves.None) ||
(caller == game.playerB.addr && game.playerA.move == Moves.None),
"Caller must be the non-offending player"
);
if (caller == game.playerA.addr) {
offender = game.playerB.addr;
game.outcome = Outcomes.PlayerBTimeout;
} else {
offender = game.playerA.addr;
game.outcome = Outcomes.PlayerATimeout;
}
} else {
revert("No timeout has occurred");
}
// Reset game
resetGame(gameId);
// Pay winner and slash offender
payWithSlash(winner, offender, game.initialBet);
}
// ------------------------- Game Management ------------------------- // // ------------------------- Game Management ------------------------- //
// Get details of a specific game (for viewing any game) // Get details of a specific game (for viewing any game)

View File

@@ -45,9 +45,12 @@ export default function Commit({
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [playMove, setPlayMove] = useState<string>(""); const [playMove, setPlayMove] = useState<string>("");
const [selfPlayed, setSelfPlayed] = useState<string>(""); const [selfPlayed, setSelfPlayed] = useState<string>("");
const [opponentPlayed, setOpponentPlayed] = useState<string>("");
const [bothPlayed, setBothPlayed] = useState<string>(""); const [bothPlayed, setBothPlayed] = useState<string>("");
const [autoCheckInterval, setAutoCheckInterval] = useState<NodeJS.Timeout | null>(null); const [autoCheckInterval, setAutoCheckInterval] = useState<NodeJS.Timeout | null>(null);
const [moveSubmitted, setMoveSubmitted] = useState(false); const [moveSubmitted, setMoveSubmitted] = useState(false);
const [commitTimeLeft, setCommitTimeLeft] = useState<number>(0);
const [timeoutExpired, setTimeoutExpired] = useState(false);
// Update encrypted move when move or secret changes // Update encrypted move when move or secret changes
useEffect(() => { useEffect(() => {
@@ -82,6 +85,18 @@ export default function Commit({
checkSelfPlayed(); checkSelfPlayed();
const checkOpponentPlayed = async () => {
try {
const opponentKey = whoAmI === "player1" ? "playerB" : "playerA";
const encrMove = gameDetails[opponentKey].encrMove;
setOpponentPlayed(Number(encrMove) !== 0 ? "true" : "false");
} catch (err: any) {
console.error("Auto-check opponent played failed:", err.message);
}
};
checkOpponentPlayed();
// Check immediately on mount or when dependencies change // Check immediately on mount or when dependencies change
const checkBothPlayed = async () => { const checkBothPlayed = async () => {
try { try {
@@ -100,8 +115,27 @@ export default function Commit({
checkBothPlayed(); checkBothPlayed();
// Check commit timeout
const checkCommitTimeout = async () => {
try {
const timeLeft = await contract.methods.commitTimeLeft(gameDetails.returnGameId).call();
console.log("Commit time left:", timeLeft);
setCommitTimeLeft(Number(timeLeft));
if (Number(timeLeft) <= 0) {
setTimeoutExpired(true);
}
} catch (err: any) {
console.error("Commit timeout check failed:", err.message);
}
};
checkCommitTimeout();
// Set up interval to check every 2 seconds // Set up interval to check every 2 seconds
const interval = setInterval(checkBothPlayed, 2000); const interval = setInterval(() => {
checkBothPlayed();
checkCommitTimeout();
}, 2000);
setAutoCheckInterval(interval); setAutoCheckInterval(interval);
return () => { return () => {
@@ -151,99 +185,207 @@ export default function Commit({
setSelectedMove(move); setSelectedMove(move);
}; };
const handleResolveTimeout = async () => {
if (!contract || !web3 || !account) return;
setLoading(true);
try {
const tx = contract.methods.resolveTimeout(gameDetails?.returnGameId);
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()),
},
],
});
showToast("Timeout resolved: " + result, "success");
} catch (err: any) {
console.error("Timeout resolution failed:", err);
showToast("Timeout resolution failed: " + err.message, "error");
} finally {
setLoading(false);
}
};
// Check if current player can resolve timeout (they committed, opponent didn't, and game is still active)
const canResolveTimeout = timeoutExpired && selfPlayed === "true" && gameDetails?.isActive;
// Check if game is finished due to timeout
const isGameFinishedByTimeout = !gameDetails?.isActive && (Number(gameDetails?.outcome) === 4 || Number(gameDetails?.outcome) === 5);
// Determine if current player won/lost the timeout
const didIWinTimeout =
(whoAmI === "player2" && Number(gameDetails?.outcome) === 4) ||
(whoAmI === "player1" && Number(gameDetails?.outcome) === 5);
return ( return (
<div className="border p-6 rounded-lg bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-700 dark:to-slate-800"> <div className="border p-6 rounded-lg bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-700 dark:to-slate-800">
<h2 className="font-semibold text-lg mb-6 text-slate-900 dark:text-white">
Select Your Move
</h2>
{moveSubmitted || selfPlayed === "true" ? ( {/* Show timeout result after game is inactive */}
// Waiting animation after move is submitted {isGameFinishedByTimeout ? (
<div className="flex flex-col items-center justify-center py-16"> <div className="flex flex-col items-center justify-center py-16">
<div className="mb-6"> {didIWinTimeout ? (
<div className="w-16 h-16 border-4 border-slate-300 dark:border-slate-500 border-t-blue-500 rounded-full animate-spin"></div> <div className="mb-6 p-4 bg-green-50 dark:bg-green-900 border-2 border-green-400 dark:border-green-600 rounded-lg w-full">
</div> <p className="text-green-700 dark:text-green-200 font-semibold mb-3 text-center">
<p className="text-lg font-semibold text-slate-700 dark:text-slate-300 mb-2"> 🎉 Victory by Timeout!
Waiting for opponent... </p>
</p> <p className="text-sm text-green-600 dark:text-green-300 text-center">
<p className="text-sm text-slate-500 dark:text-slate-400"> Your opponent failed to commit in time. You claimed victory!
Your move has been submitted. Stand by while the other player commits. </p>
</p> </div>
) : (
<div className="mb-6 p-4 bg-red-50 dark:bg-red-900 border-2 border-red-400 dark:border-red-600 rounded-lg w-full">
<p className="text-red-700 dark:text-red-200 font-semibold mb-3 text-center">
Timeout Loss
</p>
<p className="text-sm text-red-600 dark:text-red-300 text-center">
You failed to commit in time. Your opponent claimed victory!
</p>
</div>
)}
</div> </div>
) : ( ) : (
<> <>
{/* Move Selection */} {/* Timeout Warning - Only show to eligible player */}
<div className="mb-8"> {timeoutExpired && canResolveTimeout ? (
<p className="text-sm text-slate-600 dark:text-slate-300 mb-4 font-medium"> <div className="flex flex-col items-center justify-center py-16">
Choose your move: <div className="mb-6 p-4 bg-red-50 dark:bg-red-900 border-2 border-red-400 dark:border-red-600 rounded-lg w-full">
</p> <p className="text-red-700 dark:text-red-200 font-semibold mb-3 text-center">
<div className="flex gap-4 justify-center"> Commit phase timeout expired!
{(["1", "2", "3"] as const).map((move) => ( </p>
<button <p className="text-sm text-red-600 dark:text-red-300 mb-4 text-center">
key={move} The opponent failed to commit in time. You can claim victory!
onClick={() => handleMoveSelect(move)} </p>
className={`flex flex-col items-center justify-center p-6 rounded-lg transition-all transform ${ <Button
selectedMove === move onClick={handleResolveTimeout}
? "bg-blue-500 text-white shadow-lg scale-110" disabled={loading || !account || !contract}
: "bg-white dark:bg-slate-600 text-slate-700 dark:text-slate-200 shadow hover:shadow-md hover:scale-105" variant="primary"
}`} className="w-full py-3 text-lg"
> >
<span className="text-5xl mb-2">{MOVES[move].icon}</span> {loading ? "Processing..." : "⚡ Resolve Timeout"}
<span className="font-semibold text-sm">{MOVES[move].name}</span> </Button>
</button> </div>
))}
</div> </div>
</div> ) : timeoutExpired && !canResolveTimeout ? (
// Show waiting message to opponent
{/* Secret Input */} <div className="flex flex-col items-center justify-center py-16">
<div className="mb-8 bg-white dark:bg-slate-700 p-4 rounded-lg"> <div className="mb-6">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-3"> <div className="w-16 h-16 border-4 border-slate-300 dark:border-slate-500 border-t-red-500 rounded-full animate-spin"></div>
Secret: </div>
</label> <p className="text-lg font-semibold text-slate-700 dark:text-slate-300 mb-2">
<div className="flex gap-2"> Opponent claiming victory...
<Input </p>
type="text" <p className="text-sm text-slate-500 dark:text-slate-400">
value={secret} The timeout has expired. Waiting for the opponent to resolve.
onChange={(e) => handleSecretChange(e.target.value)} </p>
placeholder="Your secret passphrase"
className="flex-1"
/>
<Button
onClick={regenerateSecret}
variant="secondary"
disabled={loading}
>
🔄 New
</Button>
</div> </div>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-2"> ) : (
Keep this secret safe! It's needed to reveal your move later. <>
</p> {/* Time Left Display */}
</div> {commitTimeLeft > 0 && opponentPlayed === "true" && (
<div className="mb-6 p-3 bg-blue-50 dark:bg-blue-900 border border-blue-200 dark:border-blue-700 rounded-lg">
<p className="text-sm text-blue-700 dark:text-blue-300 text-center font-semibold">
Time Left: {commitTimeLeft}s
</p>
</div>
)}
{/* Encrypted Move Display */} {moveSubmitted || selfPlayed === "true" ? (
<div className="mb-8 bg-blue-50 dark:bg-blue-900 p-4 rounded-lg"> // Waiting animation after move is submitted
<label className="block text-sm font-medium text-slate-700 dark:text-blue-200 mb-2"> <div className="flex flex-col items-center justify-center py-16">
Encrypted Move (to be sent): <div className="mb-6">
</label> <div className="w-16 h-16 border-4 border-slate-300 dark:border-slate-500 border-t-blue-500 rounded-full animate-spin"></div>
<div className="bg-white dark:bg-slate-700 p-3 rounded border border-blue-200 dark:border-blue-700 overflow-x-auto"> </div>
<code className="text-xs text-slate-600 dark:text-slate-300 font-mono break-all"> <p className="text-lg font-semibold text-slate-700 dark:text-slate-300 mb-2">
{playMove || "Select a move and enter a secret"} Waiting for opponent...
</code> </p>
</div> <p className="text-sm text-slate-500 dark:text-slate-400">
</div> Your move has been submitted. Stand by while the other player commits.
</p>
</div>
) : (
<>
{/* Move Selection */}
<div className="mb-8">
<p className="text-sm text-slate-600 dark:text-slate-300 mb-4 font-medium">
Choose your move:
</p>
<div className="flex gap-4 justify-center">
{(["1", "2", "3"] as const).map((move) => (
<button
key={move}
onClick={() => handleMoveSelect(move)}
className={`flex flex-col items-center justify-center p-6 rounded-lg transition-all transform ${
selectedMove === move
? "bg-blue-500 text-white shadow-lg scale-110"
: "bg-white dark:bg-slate-600 text-slate-700 dark:text-slate-200 shadow hover:shadow-md hover:scale-105"
}`}
>
<span className="text-5xl mb-2">{MOVES[move].icon}</span>
<span className="font-semibold text-sm">{MOVES[move].name}</span>
</button>
))}
</div>
</div>
{/* Action Buttons */} {/* Secret Input */}
<div className="flex flex-col gap-3"> <div className="mb-8 bg-white dark:bg-slate-700 p-4 rounded-lg">
<Button <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-3">
onClick={handlePlay} Secret:
disabled={loading || !account || !contract || !selectedMove || !secret} </label>
variant="primary" <div className="flex gap-2">
className="w-full py-3 text-lg" <Input
> type="text"
{loading ? "Submitting..." : "Submit Move"} value={secret}
</Button> onChange={(e) => handleSecretChange(e.target.value)}
</div> placeholder="Your secret passphrase"
className="flex-1"
/>
<Button
onClick={regenerateSecret}
variant="secondary"
disabled={loading}
>
🔄 New
</Button>
</div>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-2">
Keep this secret safe! It's needed to reveal your move later.
</p>
</div>
{/* Encrypted Move Display */}
<div className="mb-8 bg-blue-50 dark:bg-blue-900 p-4 rounded-lg">
<label className="block text-sm font-medium text-slate-700 dark:text-blue-200 mb-2">
Encrypted Move (to be sent):
</label>
<div className="bg-white dark:bg-slate-700 p-3 rounded border border-blue-200 dark:border-blue-700 overflow-x-auto">
<code className="text-xs text-slate-600 dark:text-slate-300 font-mono break-all">
{playMove || "Select a move and enter a secret"}
</code>
</div>
</div>
{/* Action Buttons */}
<div className="flex flex-col gap-3">
<Button
onClick={handlePlay}
disabled={loading || !account || !contract || !selectedMove || !secret}
variant="primary"
className="w-full py-3 text-lg"
>
{loading ? "Submitting..." : "Submit Move"}
</Button>
</div>
</>
)}
</>
)}
</> </>
)} )}
</div> </div>

View File

@@ -48,6 +48,8 @@ export default function Reveal({
const [opponentRevealed, setOpponentRevealed] = useState(false); const [opponentRevealed, setOpponentRevealed] = useState(false);
const [bothRevealed, setBothRevealed] = useState(false); const [bothRevealed, setBothRevealed] = useState(false);
const [outcome, setOutcome] = useState<number>(0); const [outcome, setOutcome] = useState<number>(0);
const [revealTimeLeft, setRevealTimeLeft] = useState<number>(0);
const [timeoutExpired, setTimeoutExpired] = useState(false);
const clearMove = selectedMove && secret ? `${selectedMove}-${secret}` : ""; const clearMove = selectedMove && secret ? `${selectedMove}-${secret}` : "";
@@ -77,6 +79,25 @@ export default function Reveal({
}; };
setStateFromGameDetails(); setStateFromGameDetails();
// Check reveal timeout
const checkRevealTimeout = async () => {
if (!contract || !gameDetails) return;
try {
const timeLeft = await contract.methods.revealTimeLeft(gameDetails.returnGameId).call();
setRevealTimeLeft(Number(timeLeft));
if (Number(timeLeft) <= 0) {
setTimeoutExpired(true);
}
} catch (err: any) {
console.error("Reveal timeout check failed:", err.message);
}
};
checkRevealTimeout();
const interval = setInterval(checkRevealTimeout, 2000);
return () => clearInterval(interval);
}, [gameDetails, contract, account, whoAmI]); }, [gameDetails, contract, account, whoAmI]);
const handleReveal = async () => { const handleReveal = async () => {
@@ -99,6 +120,7 @@ export default function Reveal({
}); });
showSuccessToast("Reveal tx sent: " + result); showSuccessToast("Reveal tx sent: " + result);
} catch (err: any) { } catch (err: any) {
console.error("Reveal failed:", err);
showErrorToast("Reveal failed: " + err.message); showErrorToast("Reveal failed: " + err.message);
} finally { } finally {
setLoading(false); setLoading(false);
@@ -132,230 +154,320 @@ export default function Reveal({
} }
}; };
const handleResolveTimeout = async () => {
if (!contract || !web3 || !account) return;
setLoading(true);
try {
const tx = contract.methods.resolveTimeout(gameDetails?.returnGameId);
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()),
},
],
});
showSuccessToast("Timeout resolved: " + result);
} catch (err: any) {
console.error(err);
showErrorToast("Timeout resolution failed: " + err.message);
} finally {
setLoading(false);
}
};
const outcomeData = OUTCOMES[outcome] || OUTCOMES[0]; const outcomeData = OUTCOMES[outcome] || OUTCOMES[0];
// Check if game is finished due to timeout
const isGameFinishedByTimeout = !gameDetails?.isActive && (Number(gameDetails?.outcome) === 4 || Number(gameDetails?.outcome) === 5);
// Determine if current player won/lost the timeout
const didIWinTimeout =
(whoAmI === "player2" && Number(gameDetails?.outcome) === 4) ||
(whoAmI === "player1" && Number(gameDetails?.outcome) === 5);
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Your Move Section - Hidden when both revealed */} {/* Show timeout result after game is inactive */}
{!bothRevealed && ( {isGameFinishedByTimeout && (
<div className="border p-6 rounded-lg bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-700 dark:to-slate-800"> <div className="flex flex-col items-center justify-center py-16">
<h2 className="font-semibold text-lg mb-4 text-slate-900 dark:text-white"> {didIWinTimeout ? (
Your Move <div className="mb-6 p-4 bg-green-50 dark:bg-green-900 border-2 border-green-400 dark:border-green-600 rounded-lg w-full">
</h2> <p className="text-green-700 dark:text-green-200 font-semibold mb-3 text-center">
{selectedMove ? ( 🎉 Victory by Timeout!
<div className="flex items-center justify-center gap-4"> </p>
<div className="flex flex-col items-center"> <p className="text-sm text-green-600 dark:text-green-300 text-center">
<span className="text-6xl mb-2">{MOVES[selectedMove].icon}</span> Your opponent failed to reveal in time. You claimed victory!
<span className="font-semibold text-lg"> </p>
{MOVES[selectedMove].name}
</span>
</div>
<div className="text-3xl text-slate-400"></div>
<div className="bg-white dark:bg-slate-600 p-4 rounded-lg">
<p className="text-xs text-slate-600 dark:text-slate-300 mb-1">
Clear Move:
</p>
<code className="text-sm font-mono text-slate-700 dark:text-slate-200">
{clearMove}
</code>
</div>
</div> </div>
) : ( ) : (
<p className="text-center text-slate-600 dark:text-slate-400"> <div className="mb-6 p-4 bg-red-50 dark:bg-red-900 border-2 border-red-400 dark:border-red-600 rounded-lg w-full">
No move selected yet <p className="text-red-700 dark:text-red-200 font-semibold mb-3 text-center">
</p> Timeout Loss
</p>
<p className="text-sm text-red-600 dark:text-red-300 text-center">
You failed to reveal in time. Your opponent claimed victory!
</p>
</div>
)} )}
</div> </div>
)} )}
{/* Game Status Section - Hidden when both revealed */} {!isGameFinishedByTimeout && (
{!bothRevealed && ( <>
<div className="grid grid-cols-3 gap-4"> {/* Your Move Section - Hidden when both revealed */}
<div {!bothRevealed && !timeoutExpired && (
className={`p-4 rounded-lg text-center ${ <div className="border p-6 rounded-lg bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-700 dark:to-slate-800">
selfRevealed <h2 className="font-semibold text-lg mb-4 text-slate-900 dark:text-white">
? "bg-green-50 dark:bg-green-900" Your Move
: "bg-slate-100 dark:bg-slate-700" </h2>
}`} {selectedMove ? (
> <div className="flex items-center justify-center gap-4">
<p className="text-2xl mb-1">{selfRevealed ? "✅" : "⏳"}</p> <div className="flex flex-col items-center">
<p className="text-xs font-semibold text-slate-600 dark:text-slate-300"> <span className="text-6xl mb-2">{MOVES[selectedMove].icon}</span>
Me <span className="font-semibold text-lg">
</p> {MOVES[selectedMove].name}
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1"> </span>
{selfRevealed ? "Revealed" : "Waiting"} </div>
</p> <div className="text-3xl text-slate-400"></div>
</div> <div className="bg-white dark:bg-slate-600 p-4 rounded-lg">
<div <p className="text-xs text-slate-600 dark:text-slate-300 mb-1">
className={`p-4 rounded-lg text-center ${ Clear Move:
opponentRevealed </p>
? "bg-green-50 dark:bg-green-900" <code className="text-sm font-mono text-slate-700 dark:text-slate-200">
: "bg-slate-100 dark:bg-slate-700" {clearMove}
}`} </code>
> </div>
<p className="text-2xl mb-1">{opponentRevealed ? "✅" : "⏳"}</p> </div>
<p className="text-xs font-semibold text-slate-600 dark:text-slate-300"> ) : (
Opponent <p className="text-center text-slate-600 dark:text-slate-400">
</p> No move selected yet
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1"> </p>
{opponentRevealed ? "Revealed" : "Waiting"} )}
</p> </div>
</div> )}
<div className="p-4 rounded-lg text-center bg-blue-50 dark:bg-blue-900"> {/* Timeout Warning */}
<p className="text-sm font-mono text-slate-600 dark:text-slate-300"> {timeoutExpired && (
<div className="mb-6 p-4 bg-red-50 dark:bg-red-900 border-2 border-red-400 dark:border-red-600 rounded-lg">
</p> <p className="text-red-700 dark:text-red-200 font-semibold mb-3">
<p className="text-xs font-semibold text-slate-600 dark:text-slate-300"> Reveal phase timeout expired!
Time Left </p>
</p> <p className="text-sm text-red-600 dark:text-red-300 mb-3">
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1"> {selfRevealed
{0} ? "The opponent failed to reveal in time. You can claim victory!"
</p> : "You failed to reveal in time. The opponent can claim victory!"}
</div> </p>
</div> {selfRevealed && (
)} <Button
onClick={handleResolveTimeout}
disabled={loading || !account || !contract}
variant="primary"
className="w-full py-3 text-lg"
>
{loading ? "Processing..." : "⚡ Resolve Timeout"}
</Button>)}
</div>
)}
{/* Game Status Section - Hidden when both revealed */}
{!bothRevealed && !timeoutExpired && (
<>
{/* Reveal Section - Hidden when both revealed */}
{!bothRevealed && (
<div className="border-2 border-blue-300 dark:border-blue-600 p-6 rounded-lg bg-blue-50 dark:bg-slate-700">
<h2 className="font-semibold text-lg mb-4 text-slate-900 dark:text-white">
Reveal Your Move
</h2>
<p className="text-sm text-slate-600 dark:text-slate-300 mb-4">
Submit your clear move and secret to the blockchain. This proves you
didn't cheat!
</p>
<Button
onClick={handleReveal}
disabled={loading || !account || !contract || !clearMove || selfRevealed}
variant="primary"
className="w-full py-3 text-lg"
>
{loading ? "Submitting..." : selfRevealed ? "✅ Revealed" : "Reveal Move"}
</Button>
</div>
)}
{/* Winner Section - Only show if both revealed */} <div className="grid grid-cols-3 gap-4">
{bothRevealed && ( <div
<div className="space-y-4"> className={`p-4 rounded-lg text-center ${
{/* Moves Comparison */} selfRevealed
<div className="border-2 border-slate-300 dark:border-slate-600 p-6 rounded-lg bg-slate-50 dark:bg-slate-800"> ? "bg-green-50 dark:bg-green-900"
<h2 className="font-semibold text-lg mb-4 text-slate-900 dark:text-white text-center"> : "bg-slate-100 dark:bg-slate-700"
Final Moves }`}
</h2> >
<div className="flex items-center justify-center gap-8"> <p className="text-2xl mb-1">{selfRevealed ? "✅" : "⏳"}</p>
{/* Your Move (always on left) */} <p className="text-xs font-semibold text-slate-600 dark:text-slate-300">
<div className="flex flex-col items-center"> Me
<span className="text-6xl mb-2"> </p>
{gameDetails && MOVES[String(whoAmI === "player1" ? gameDetails.playerA.move : gameDetails.playerB.move)]?.icon} <p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
</span> {selfRevealed ? "Revealed" : "Waiting"}
<span className="font-semibold text-slate-700 dark:text-slate-300"> </p>
You </div>
</span> <div
<span className="text-sm text-slate-500 dark:text-slate-400"> className={`p-4 rounded-lg text-center ${
{gameDetails && MOVES[String(whoAmI === "player1" ? gameDetails.playerA.move : gameDetails.playerB.move)]?.name} opponentRevealed
</span> ? "bg-green-50 dark:bg-green-900"
: "bg-slate-100 dark:bg-slate-700"
}`}
>
<p className="text-2xl mb-1">{opponentRevealed ? "✅" : "⏳"}</p>
<p className="text-xs font-semibold text-slate-600 dark:text-slate-300">
Opponent
</p>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
{opponentRevealed ? "Revealed" : "Waiting"}
</p>
</div>
<div className="p-4 rounded-lg text-center bg-blue-50 dark:bg-blue-900">
<p className="text-sm font-mono text-slate-600 dark:text-slate-300">
</p>
<p className="text-xs font-semibold text-slate-600 dark:text-slate-300">
Time Left
</p>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
{revealTimeLeft}s
</p>
</div>
</div>
</>
)}
{/* Reveal Section - Hidden when both revealed */}
{!bothRevealed && !timeoutExpired && (
<div className="border-2 border-blue-300 dark:border-blue-600 p-6 rounded-lg bg-blue-50 dark:bg-slate-700">
<h2 className="font-semibold text-lg mb-4 text-slate-900 dark:text-white">
Reveal Your Move
</h2>
<p className="text-sm text-slate-600 dark:text-slate-300 mb-4">
Submit your clear move and secret to the blockchain. This proves you
didn't cheat!
</p>
<Button
onClick={handleReveal}
disabled={loading || !account || !contract || !clearMove || selfRevealed}
variant="primary"
className="w-full py-3 text-lg"
>
{loading ? "Submitting..." : selfRevealed ? "✅ Revealed" : "Reveal Move"}
</Button>
</div>
)}
{/* Winner Section - Only show if both revealed */}
{bothRevealed && (
<div className="space-y-4">
{/* Moves Comparison */}
<div className="border-2 border-slate-300 dark:border-slate-600 p-6 rounded-lg bg-slate-50 dark:bg-slate-800">
<h2 className="font-semibold text-lg mb-4 text-slate-900 dark:text-white text-center">
Final Moves
</h2>
<div className="flex items-center justify-center gap-8">
{/* Your Move (always on left) */}
<div className="flex flex-col items-center">
<span className="text-6xl mb-2">
{gameDetails && MOVES[String(whoAmI === "player1" ? gameDetails.playerA.move : gameDetails.playerB.move)]?.icon}
</span>
<span className="font-semibold text-slate-700 dark:text-slate-300">
You
</span>
<span className="text-sm text-slate-500 dark:text-slate-400">
{gameDetails && MOVES[String(whoAmI === "player1" ? gameDetails.playerA.move : gameDetails.playerB.move)]?.name}
</span>
</div>
{/* VS */}
<div className="flex flex-col items-center">
<span className="text-4xl text-slate-400 dark:text-slate-500">
VS
</span>
</div>
{/* Opponent Move (always on right) */}
<div className="flex flex-col items-center">
<span className="text-6xl mb-2">
{gameDetails && MOVES[String(whoAmI === "player1" ? gameDetails.playerB.move : gameDetails.playerA.move)]?.icon}
</span>
<span className="font-semibold text-slate-700 dark:text-slate-300">
Opponent
</span>
<span className="text-sm text-slate-500 dark:text-slate-400">
{gameDetails && MOVES[String(whoAmI === "player1" ? gameDetails.playerB.move : gameDetails.playerA.move)]?.name}
</span>
</div>
</div>
</div> </div>
{/* VS */} {/* Outcome Section */}
<div className="flex flex-col items-center"> <div
<span className="text-4xl text-slate-400 dark:text-slate-500"> className={`border-2 p-6 rounded-lg text-center ${
VS outcomeData.color === "green"
</span> ? "border-green-400 bg-green-50 dark:bg-green-900 dark:border-green-600"
</div> : outcomeData.color === "red"
? "border-red-400 bg-red-50 dark:bg-red-900 dark:border-red-600"
: outcomeData.color === "yellow"
? "border-yellow-400 bg-yellow-50 dark:bg-yellow-900 dark:border-yellow-600"
: "border-slate-400 bg-slate-100 dark:bg-slate-700 dark:border-slate-600"
}`}
>
<p
className={`text-6xl mb-3 ${
outcomeData.color === "green"
? "text-green-600 dark:text-green-400"
: outcomeData.color === "red"
? "text-red-600 dark:text-red-400"
: outcomeData.color === "yellow"
? "text-yellow-600 dark:text-yellow-400"
: "text-slate-600 dark:text-slate-400"
}`}
>
{outcomeData.emoji}
</p>
<h3
className={`text-2xl font-bold ${
outcomeData.color === "green"
? "text-green-700 dark:text-green-300"
: outcomeData.color === "red"
? "text-red-700 dark:text-red-300"
: outcomeData.color === "yellow"
? "text-yellow-700 dark:text-yellow-300"
: "text-slate-700 dark:text-slate-300"
}`}
>
{outcomeData.name}
</h3>
{/* Opponent Move (always on right) */} {/* Display ETH winnings */}
<div className="flex flex-col items-center"> {gameDetails && outcome !== 0 && (
<span className="text-6xl mb-2"> <div className="mt-4 pt-4 border-t border-current border-opacity-20">
{gameDetails && MOVES[String(whoAmI === "player1" ? gameDetails.playerB.move : gameDetails.playerA.move)]?.icon} <p className="text-sm text-slate-600 dark:text-slate-300 mb-2">
</span> {outcome === 1 ? "You Won" : outcome === 3 ? "Draw" : "You Lost"}
<span className="font-semibold text-slate-700 dark:text-slate-300"> </p>
Opponent <p className="text-3xl font-bold">
</span> {outcome === 1 ? (
<span className="text-sm text-slate-500 dark:text-slate-400"> <>
{gameDetails && MOVES[String(whoAmI === "player1" ? gameDetails.playerB.move : gameDetails.playerA.move)]?.name} +{web3?.utils.fromWei(String(BigInt(gameDetails.initialBet) * BigInt(2)), "ether")} ETH
</span> </>
) : outcome === 3 ? (
<>
+{web3?.utils.fromWei(gameDetails.initialBet, "ether")} ETH
</>
) : (
<>
-{web3?.utils.fromWei(gameDetails.initialBet, "ether")} ETH
</>
)}
</p>
</div>
)}
{/* Show Claim Coins button only on win or draw and if game is active */}
{(outcome === 1 || outcome === 3) && gameDetails?.isActive && (
<Button
onClick={handleGetOutcome}
disabled={loading || !account || !contract}
variant="primary"
className="mt-4 w-full py-3 text-lg"
>
{loading ? "Processing..." : "💰 Claim Coins"}
</Button>
)}
</div> </div>
</div> </div>
</div> )}
</>
{/* Outcome Section */}
<div
className={`border-2 p-6 rounded-lg text-center ${
outcomeData.color === "green"
? "border-green-400 bg-green-50 dark:bg-green-900 dark:border-green-600"
: outcomeData.color === "red"
? "border-red-400 bg-red-50 dark:bg-red-900 dark:border-red-600"
: outcomeData.color === "yellow"
? "border-yellow-400 bg-yellow-50 dark:bg-yellow-900 dark:border-yellow-600"
: "border-slate-400 bg-slate-100 dark:bg-slate-700 dark:border-slate-600"
}`}
>
<p
className={`text-6xl mb-3 ${
outcomeData.color === "green"
? "text-green-600 dark:text-green-400"
: outcomeData.color === "red"
? "text-red-600 dark:text-red-400"
: outcomeData.color === "yellow"
? "text-yellow-600 dark:text-yellow-400"
: "text-slate-600 dark:text-slate-400"
}`}
>
{outcomeData.emoji}
</p>
<h3
className={`text-2xl font-bold ${
outcomeData.color === "green"
? "text-green-700 dark:text-green-300"
: outcomeData.color === "red"
? "text-red-700 dark:text-red-300"
: outcomeData.color === "yellow"
? "text-yellow-700 dark:text-yellow-300"
: "text-slate-700 dark:text-slate-300"
}`}
>
{outcomeData.name}
</h3>
{/* Display ETH winnings */}
{gameDetails && (
<div className="mt-4 pt-4 border-t border-current border-opacity-20">
<p className="text-sm text-slate-600 dark:text-slate-300 mb-2">
{outcome === 1 ? "You Won" : outcome === 3 ? "Draw" : "You Lost"}
</p>
<p className="text-3xl font-bold">
{outcome === 1 ? (
<>
+{web3?.utils.fromWei(String(BigInt(gameDetails.initialBet) * BigInt(2)), "ether")} ETH
</>
) : outcome === 3 ? (
<>
+{web3?.utils.fromWei(gameDetails.initialBet, "ether")} ETH
</>
) : (
<>
-{web3?.utils.fromWei(gameDetails.initialBet, "ether")} ETH
</>
)}
</p>
</div>
)}
{/* Show Claim Coins button only on win or draw and if game is active */}
{(outcome === 1 || outcome === 3) && gameDetails?.isActive && (
<Button
onClick={handleGetOutcome}
disabled={loading || !account || !contract}
variant="primary"
className="mt-4 w-full py-3 text-lg"
>
{loading ? "Processing..." : "💰 Claim Coins"}
</Button>
)}
</div>
</div>
)} )}
</div> </div>
); );

View File

@@ -59,7 +59,7 @@
"type": "function" "type": "function"
} }
], ],
"GAME_CONTRACT_ADDRESS": "0xAA7057A0203539d9BE86EfB471B831Dd833a9e22", "GAME_CONTRACT_ADDRESS": "0x7C56Fd34374F0244402aDDf5E940490C1a53E92E",
"GAME_ABI": [ "GAME_ABI": [
{ {
"inputs": [], "inputs": [],
@@ -74,6 +74,19 @@
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{
"inputs": [],
"name": "COMMIT_TIMEOUT",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{ {
"inputs": [], "inputs": [],
"name": "REVEAL_TIMEOUT", "name": "REVEAL_TIMEOUT",
@@ -87,6 +100,25 @@
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{
"inputs": [
{
"internalType": "uint256",
"name": "gameId",
"type": "uint256"
}
],
"name": "commitTimeLeft",
"outputs": [
{
"internalType": "int256",
"name": "",
"type": "int256"
}
],
"stateMutability": "view",
"type": "function"
},
{ {
"inputs": [], "inputs": [],
"name": "getActiveGameIds", "name": "getActiveGameIds",
@@ -278,6 +310,19 @@
"stateMutability": "payable", "stateMutability": "payable",
"type": "function" "type": "function"
}, },
{
"inputs": [
{
"internalType": "uint256",
"name": "gameId",
"type": "uint256"
}
],
"name": "resolveTimeout",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{ {
"inputs": [ "inputs": [
{ {