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

@@ -48,6 +48,8 @@ export default function Reveal({
const [opponentRevealed, setOpponentRevealed] = useState(false);
const [bothRevealed, setBothRevealed] = useState(false);
const [outcome, setOutcome] = useState<number>(0);
const [revealTimeLeft, setRevealTimeLeft] = useState<number>(0);
const [timeoutExpired, setTimeoutExpired] = useState(false);
const clearMove = selectedMove && secret ? `${selectedMove}-${secret}` : "";
@@ -77,6 +79,25 @@ export default function Reveal({
};
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]);
const handleReveal = async () => {
@@ -99,6 +120,7 @@ export default function Reveal({
});
showSuccessToast("Reveal tx sent: " + result);
} catch (err: any) {
console.error("Reveal failed:", err);
showErrorToast("Reveal failed: " + err.message);
} finally {
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];
// 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 (
<div className="space-y-6">
{/* Your Move Section - Hidden when both revealed */}
{!bothRevealed && (
<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">
Your Move
</h2>
{selectedMove ? (
<div className="flex items-center justify-center gap-4">
<div className="flex flex-col items-center">
<span className="text-6xl mb-2">{MOVES[selectedMove].icon}</span>
<span className="font-semibold text-lg">
{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>
{/* 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>
) : (
<p className="text-center text-slate-600 dark:text-slate-400">
No move selected yet
</p>
<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>
)}
{/* Game Status Section - Hidden when both revealed */}
{!bothRevealed && (
<div className="grid grid-cols-3 gap-4">
<div
className={`p-4 rounded-lg text-center ${
selfRevealed
? "bg-green-50 dark:bg-green-900"
: "bg-slate-100 dark:bg-slate-700"
}`}
>
<p className="text-2xl mb-1">{selfRevealed ? "✅" : "⏳"}</p>
<p className="text-xs font-semibold text-slate-600 dark:text-slate-300">
Me
</p>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
{selfRevealed ? "Revealed" : "Waiting"}
</p>
</div>
<div
className={`p-4 rounded-lg text-center ${
opponentRevealed
? "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">
{0}
</p>
</div>
</div>
)}
{!isGameFinishedByTimeout && (
<>
{/* Your Move Section - Hidden when both revealed */}
{!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">
<h2 className="font-semibold text-lg mb-4 text-slate-900 dark:text-white">
Your Move
</h2>
{selectedMove ? (
<div className="flex items-center justify-center gap-4">
<div className="flex flex-col items-center">
<span className="text-6xl mb-2">{MOVES[selectedMove].icon}</span>
<span className="font-semibold text-lg">
{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>
) : (
<p className="text-center text-slate-600 dark:text-slate-400">
No move selected yet
</p>
)}
</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 */}
{!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 */}
{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 className="grid grid-cols-3 gap-4">
<div
className={`p-4 rounded-lg text-center ${
selfRevealed
? "bg-green-50 dark:bg-green-900"
: "bg-slate-100 dark:bg-slate-700"
}`}
>
<p className="text-2xl mb-1">{selfRevealed ? "✅" : "⏳"}</p>
<p className="text-xs font-semibold text-slate-600 dark:text-slate-300">
Me
</p>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
{selfRevealed ? "Revealed" : "Waiting"}
</p>
</div>
<div
className={`p-4 rounded-lg text-center ${
opponentRevealed
? "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>
{/* VS */}
<div className="flex flex-col items-center">
<span className="text-4xl text-slate-400 dark:text-slate-500">
VS
</span>
</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>
{/* 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>
{/* Display ETH winnings */}
{gameDetails && outcome !== 0 && (
<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>
{/* 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>
);