mirror of
https://github.com/averel10/crypto_clash.git
synced 2026-03-12 19:08:11 +01:00
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:
47
config.json
47
config.json
@@ -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": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,11 +185,115 @@ 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
|
{/* Show timeout result after game is inactive */}
|
||||||
</h2>
|
{isGameFinishedByTimeout ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-16">
|
||||||
|
{didIWinTimeout ? (
|
||||||
|
<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">
|
||||||
|
<p className="text-green-700 dark:text-green-200 font-semibold mb-3 text-center">
|
||||||
|
🎉 Victory by Timeout!
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-green-600 dark:text-green-300 text-center">
|
||||||
|
Your opponent failed to commit in time. You claimed victory!
|
||||||
|
</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>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Timeout Warning - Only show to eligible player */}
|
||||||
|
{timeoutExpired && canResolveTimeout ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-16">
|
||||||
|
<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">
|
||||||
|
⏱️ Commit phase timeout expired!
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-red-600 dark:text-red-300 mb-4 text-center">
|
||||||
|
The opponent failed to commit in time. You can claim victory!
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
onClick={handleResolveTimeout}
|
||||||
|
disabled={loading || !account || !contract}
|
||||||
|
variant="primary"
|
||||||
|
className="w-full py-3 text-lg"
|
||||||
|
>
|
||||||
|
{loading ? "Processing..." : "⚡ Resolve Timeout"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : timeoutExpired && !canResolveTimeout ? (
|
||||||
|
// Show waiting message to opponent
|
||||||
|
<div className="flex flex-col items-center justify-center py-16">
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="w-16 h-16 border-4 border-slate-300 dark:border-slate-500 border-t-red-500 rounded-full animate-spin"></div>
|
||||||
|
</div>
|
||||||
|
<p className="text-lg font-semibold text-slate-700 dark:text-slate-300 mb-2">
|
||||||
|
Opponent claiming victory...
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-slate-500 dark:text-slate-400">
|
||||||
|
The timeout has expired. Waiting for the opponent to resolve.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Time Left Display */}
|
||||||
|
{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>
|
||||||
|
)}
|
||||||
|
|
||||||
{moveSubmitted || selfPlayed === "true" ? (
|
{moveSubmitted || selfPlayed === "true" ? (
|
||||||
// Waiting animation after move is submitted
|
// Waiting animation after move is submitted
|
||||||
@@ -246,6 +384,10 @@ export default function Commit({
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,12 +154,74 @@ 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">
|
||||||
|
{/* Show timeout result after game is inactive */}
|
||||||
|
{isGameFinishedByTimeout && (
|
||||||
|
<div className="flex flex-col items-center justify-center py-16">
|
||||||
|
{didIWinTimeout ? (
|
||||||
|
<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">
|
||||||
|
<p className="text-green-700 dark:text-green-200 font-semibold mb-3 text-center">
|
||||||
|
🎉 Victory by Timeout!
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-green-600 dark:text-green-300 text-center">
|
||||||
|
Your opponent failed to reveal in time. You claimed victory!
|
||||||
|
</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 reveal in time. Your opponent claimed victory!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isGameFinishedByTimeout && (
|
||||||
|
<>
|
||||||
{/* Your Move Section - Hidden when both revealed */}
|
{/* Your Move Section - Hidden when both revealed */}
|
||||||
{!bothRevealed && (
|
{!bothRevealed && !timeoutExpired && (
|
||||||
<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-4 text-slate-900 dark:text-white">
|
<h2 className="font-semibold text-lg mb-4 text-slate-900 dark:text-white">
|
||||||
Your Move
|
Your Move
|
||||||
@@ -167,9 +251,34 @@ export default function Reveal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{/* Timeout Warning */}
|
||||||
|
{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 className="text-red-700 dark:text-red-200 font-semibold mb-3">
|
||||||
|
⏱️ Reveal phase timeout expired!
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-red-600 dark:text-red-300 mb-3">
|
||||||
|
{selfRevealed
|
||||||
|
? "The opponent failed to reveal in time. You can claim victory!"
|
||||||
|
: "You failed to reveal in time. The opponent can claim victory!"}
|
||||||
|
</p>
|
||||||
|
{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 */}
|
{/* Game Status Section - Hidden when both revealed */}
|
||||||
{!bothRevealed && (
|
{!bothRevealed && !timeoutExpired && (
|
||||||
|
<>
|
||||||
|
|
||||||
|
|
||||||
<div className="grid grid-cols-3 gap-4">
|
<div className="grid grid-cols-3 gap-4">
|
||||||
<div
|
<div
|
||||||
className={`p-4 rounded-lg text-center ${
|
className={`p-4 rounded-lg text-center ${
|
||||||
@@ -209,14 +318,15 @@ export default function Reveal({
|
|||||||
Time Left
|
Time Left
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
|
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
|
||||||
{0}
|
{revealTimeLeft}s
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Reveal Section - Hidden when both revealed */}
|
{/* Reveal Section - Hidden when both revealed */}
|
||||||
{!bothRevealed && (
|
{!bothRevealed && !timeoutExpired && (
|
||||||
<div className="border-2 border-blue-300 dark:border-blue-600 p-6 rounded-lg bg-blue-50 dark:bg-slate-700">
|
<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">
|
<h2 className="font-semibold text-lg mb-4 text-slate-900 dark:text-white">
|
||||||
Reveal Your Move
|
Reveal Your Move
|
||||||
@@ -320,7 +430,7 @@ export default function Reveal({
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{/* Display ETH winnings */}
|
{/* Display ETH winnings */}
|
||||||
{gameDetails && (
|
{gameDetails && outcome !== 0 && (
|
||||||
<div className="mt-4 pt-4 border-t border-current border-opacity-20">
|
<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">
|
<p className="text-sm text-slate-600 dark:text-slate-300 mb-2">
|
||||||
{outcome === 1 ? "You Won" : outcome === 3 ? "Draw" : "You Lost"}
|
{outcome === 1 ? "You Won" : outcome === 3 ? "Draw" : "You Lost"}
|
||||||
@@ -357,6 +467,8 @@ export default function Reveal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user