diff --git a/config.json b/config.json index 0b3e280..3ce71b7 100644 --- a/config.json +++ b/config.json @@ -1,6 +1,6 @@ { "API_URL": "http://185.48.228.49:8545", - "CONTRACT_ADDRESS": "0x8aF379ED8C452612676ACB856E26955237358944", + "CONTRACT_ADDRESS": "0x6da7dE8330EF1ff087C66e61Aa87FbC29E9b2869", "ABI": [ { "inputs": [ @@ -59,7 +59,7 @@ "type": "function" } ], - "GAME_CONTRACT_ADDRESS": "0x267d1b8bf8C13ea60D39F90cC68AEc4b8389bb64", + "GAME_CONTRACT_ADDRESS": "0x503d096a9a163180F79B1AC2F1d9F7C63f5DC75a", "GAME_ABI": [ { "inputs": [], diff --git a/crypto_clash_contract/contracts/Game.sol b/crypto_clash_contract/contracts/Game.sol index 0627196..81895c2 100644 --- a/crypto_clash_contract/contracts/Game.sol +++ b/crypto_clash_contract/contracts/Game.sol @@ -228,6 +228,27 @@ contract Game { game.firstReveal = block.timestamp; } + if( + game.playerA.move != Moves.None && + game.playerB.move != Moves.None + ) { + // Both players have revealed, compute outcome + if (game.playerA.move == game.playerB.move) { + game.outcome = Outcomes.Draw; + } else if ( + (game.playerA.move == Moves.Rock && + game.playerB.move == Moves.Scissors) || + (game.playerA.move == Moves.Paper && + game.playerB.move == Moves.Rock) || + (game.playerA.move == Moves.Scissors && + game.playerB.move == Moves.Paper) + ) { + game.outcome = Outcomes.PlayerA; + } else { + game.outcome = Outcomes.PlayerB; + } + } + return move; } @@ -272,29 +293,10 @@ contract Game { uint gameId = playerToActiveGame[msg.sender]; GameState storage game = games[gameId]; - // Only calculate outcome once - require(game.outcome == Outcomes.None, "Outcome already determined"); - - Outcomes outcome; - - if (game.playerA.move == game.playerB.move) { - outcome = Outcomes.Draw; - } else if ( - (game.playerA.move == Moves.Rock && - game.playerB.move == Moves.Scissors) || - (game.playerA.move == Moves.Paper && - game.playerB.move == Moves.Rock) || - (game.playerA.move == Moves.Scissors && - game.playerB.move == Moves.Paper) || - (game.playerA.move != Moves.None && game.playerB.move == Moves.None) - ) { - outcome = Outcomes.PlayerA; - } else { - outcome = Outcomes.PlayerB; - } - - // Store the outcome permanently before resetting - game.outcome = outcome; + require( + game.outcome != Outcomes.None, + "Outcome not yet determined" + ); address payable addrA = game.playerA.addr; address payable addrB = game.playerB.addr; @@ -305,9 +307,9 @@ contract Game { // Reset and cleanup resetGame(gameId); // Reset game before paying to avoid reentrancy attacks - pay(addrA, addrB, betPlayerA, outcome); + pay(addrA, addrB, betPlayerA, game.outcome); - return outcome; + return game.outcome; } // Pay the winner(s). diff --git a/crypto_clash_frontend/app/clash/Commit.tsx b/crypto_clash_frontend/app/clash/Commit.tsx index 65590af..01ef540 100644 --- a/crypto_clash_frontend/app/clash/Commit.tsx +++ b/crypto_clash_frontend/app/clash/Commit.tsx @@ -13,6 +13,7 @@ interface CommitProps { setSelectedMove: (move: string | null) => void; secret: string; setSecret: (secret: string) => void; + onBothPlayersCommitted?: () => void; } type Move = "1" | "2" | "3" | null; @@ -34,10 +35,13 @@ export default function Commit({ setSelectedMove, secret, setSecret, + onBothPlayersCommitted, }: Readonly) { const [loading, setLoading] = useState(false); const [playMove, setPlayMove] = useState(""); const [bothPlayed, setBothPlayed] = useState(""); + const [autoCheckInterval, setAutoCheckInterval] = useState(null); + const [moveSubmitted, setMoveSubmitted] = useState(false); // Generate random secret on mount if not already set useEffect(() => { @@ -57,20 +61,42 @@ export default function Commit({ } }, [selectedMove, secret]); - // Commit phase read-only handlers - const handleBothPlayed = async () => { - if (!contract) return; - setLoading(true); - try { - const res = await contract.methods.bothPlayed().call({from : account}); - setBothPlayed(res ? "true" : "false"); - } catch (err: any) { - setStatus("Failed to fetch bothPlayed: " + err.message); - } finally { - setLoading(false); + // Auto-check if both players have committed and trigger callback + useEffect(() => { + if (!contract || !account || !playMove || bothPlayed === "true") { + // Clear interval if conditions not met or already both played + if (autoCheckInterval) clearInterval(autoCheckInterval); + setAutoCheckInterval(null); + return; } - }; + // Check immediately on mount or when dependencies change + const checkBothPlayed = async () => { + try { + const res = await contract.methods.bothPlayed().call({ from: account }); + if (res) { + setBothPlayed("true"); + if (onBothPlayersCommitted) { + onBothPlayersCommitted(); + } + } + } catch (err: any) { + console.error("Auto-check failed:", err.message); + } + }; + + checkBothPlayed(); + + // Set up interval to check every 2 seconds + const interval = setInterval(checkBothPlayed, 2000); + setAutoCheckInterval(interval); + + return () => { + if (interval) clearInterval(interval); + }; + }, [contract, account, playMove, bothPlayed, onBothPlayersCommitted]); + + // Commit phase read-only handlers const handlePlay = async () => { if (!contract || !web3 || !account || !playMove) return; setLoading(true); @@ -92,6 +118,7 @@ export default function Commit({ ], }); setStatus("Play tx sent: " + result); + setMoveSubmitted(true); } catch (err: any) { setStatus("Play failed: " + err.message); } finally { @@ -110,100 +137,95 @@ export default function Commit({ Select Your Move - {/* Move Selection */} -
-

- Choose your move: -

-
- {(["1", "2", "3"] as const).map((move) => ( - + ))} +
+
+ + {/* Secret Input */} +
+ +
+ setSecret(e.target.value)} + placeholder="Your secret passphrase" + className="flex-1" + /> + +
+

+ Keep this secret safe! It's needed to reveal your move later. +

+
+ + {/* Encrypted Move Display */} +
+ +
+ + {playMove || "Select a move and enter a secret"} + +
+
+ + {/* Action Buttons */} +
+ - ))} -
- - - {/* Secret Input */} -
- -
- setSecret(e.target.value)} - placeholder="Your secret passphrase" - className="flex-1" - /> - -
-

- Keep this secret safe! It's needed to reveal your move later. -

-
- - {/* Encrypted Move Display */} -
- -
- - {playMove || "Select a move and enter a secret"} - -
-
- - {/* Action Buttons */} -
- - - - {bothPlayed && ( - - {bothPlayed === "true" - ? "✓ Both players have committed!" - : "Waiting for opponent..."} - - )} -
+ {loading ? "Submitting..." : "Submit Move"} + + + + )} ); } diff --git a/crypto_clash_frontend/app/clash/page.tsx b/crypto_clash_frontend/app/clash/page.tsx index 30d8747..8a333e8 100644 --- a/crypto_clash_frontend/app/clash/page.tsx +++ b/crypto_clash_frontend/app/clash/page.tsx @@ -17,6 +17,8 @@ export default function Clash() { const [phase, setPhase] = useState<"games" | "commit" | "reveal">("games"); const [selectedMove, setSelectedMove] = useState(null); const [secret, setSecret] = useState(""); + const [availableAccounts, setAvailableAccounts] = useState([]); + const [selectedAccount, setSelectedAccount] = useState(""); const handlePlayClick = (gameId: number) => { setPhase("commit"); @@ -41,13 +43,15 @@ export default function Clash() { data.GAME_CONTRACT_ADDRESS ); setContract(contractInstance); - // Get account + // Get accounts from MetaMask if (globalThis.window !== undefined && (globalThis as any).ethereum) { try { const accounts = await (globalThis as any).ethereum.request({ method: "eth_requestAccounts", }); + setAvailableAccounts(accounts); setAccount(accounts[0]); + setSelectedAccount(accounts[0]); } catch (err: any) { setStatus( "MetaMask not available or user denied access: " + err.message @@ -79,10 +83,33 @@ export default function Clash() { {phase === "reveal" && "Reveal your move."}

+
+ + {availableAccounts.length > 0 ? ( + + ) : ( +

No accounts available

+ )} +

- Connected Account:{" "} - {account - ? `${account.slice(0, 6)}...${account.slice(-4)}` + Active Account:{" "} + {selectedAccount + ? `${selectedAccount.slice(0, 6)}...${selectedAccount.slice(-4)}` : "Not connected"}

@@ -125,7 +152,7 @@ export default function Clash() {

{phase === "games" && ( setPhase("reveal")} /> )} {phase === "reveal" && (