diff --git a/config.json b/config.json index 3ce71b7..3ebd314 100644 --- a/config.json +++ b/config.json @@ -59,7 +59,7 @@ "type": "function" } ], - "GAME_CONTRACT_ADDRESS": "0x503d096a9a163180F79B1AC2F1d9F7C63f5DC75a", + "GAME_CONTRACT_ADDRESS": "0xAA7057A0203539d9BE86EfB471B831Dd833a9e22", "GAME_ABI": [ { "inputs": [], @@ -87,32 +87,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [], - "name": "bothPlayed", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "bothRevealed", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "getActiveGameIds", @@ -150,14 +124,68 @@ "name": "getGameDetails", "outputs": [ { - "internalType": "address", - "name": "playerAAddr", - "type": "address" + "components": [ + { + "internalType": "address payable", + "name": "addr", + "type": "address" + }, + { + "internalType": "uint256", + "name": "bet", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "encrMove", + "type": "bytes32" + }, + { + "internalType": "enum Game.Moves", + "name": "move", + "type": "uint8" + }, + { + "internalType": "string", + "name": "nickname", + "type": "string" + } + ], + "internalType": "struct Game.Player", + "name": "playerA", + "type": "tuple" }, { - "internalType": "address", - "name": "playerBAddr", - "type": "address" + "components": [ + { + "internalType": "address payable", + "name": "addr", + "type": "address" + }, + { + "internalType": "uint256", + "name": "bet", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "encrMove", + "type": "bytes32" + }, + { + "internalType": "enum Game.Moves", + "name": "move", + "type": "uint8" + }, + { + "internalType": "string", + "name": "nickname", + "type": "string" + } + ], + "internalType": "struct Game.Player", + "name": "playerB", + "type": "tuple" }, { "internalType": "uint256", @@ -173,31 +201,10 @@ "internalType": "bool", "name": "isActive", "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getLastWinner", - "outputs": [ - { - "internalType": "enum Game.Outcomes", - "name": "", - "type": "uint8" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getMyActiveGameId", - "outputs": [ + }, { "internalType": "uint256", - "name": "", + "name": "returnGameId", "type": "uint256" } ], @@ -205,7 +212,13 @@ "type": "function" }, { - "inputs": [], + "inputs": [ + { + "internalType": "uint256", + "name": "gameId", + "type": "uint256" + } + ], "name": "getOutcome", "outputs": [ { @@ -221,51 +234,9 @@ "inputs": [ { "internalType": "uint256", - "name": "index", - "type": "uint256" - } - ], - "name": "getPastGame", - "outputs": [ - { - "internalType": "address", - "name": "playerAAddr", - "type": "address" - }, - { - "internalType": "address", - "name": "playerBAddr", - "type": "address" - }, - { - "internalType": "uint256", - "name": "initialBet", + "name": "gameId", "type": "uint256" }, - { - "internalType": "enum Game.Outcomes", - "name": "outcome", - "type": "uint8" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getPastGamesCount", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ { "internalType": "bytes32", "name": "encrMove", @@ -283,32 +254,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [], - "name": "playerARevealed", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "playerBRevealed", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { @@ -335,6 +280,11 @@ }, { "inputs": [ + { + "internalType": "uint256", + "name": "gameId", + "type": "uint256" + }, { "internalType": "string", "name": "clearMove", @@ -353,7 +303,13 @@ "type": "function" }, { - "inputs": [], + "inputs": [ + { + "internalType": "uint256", + "name": "gameId", + "type": "uint256" + } + ], "name": "revealTimeLeft", "outputs": [ { @@ -366,7 +322,13 @@ "type": "function" }, { - "inputs": [], + "inputs": [ + { + "internalType": "uint256", + "name": "gameId", + "type": "uint256" + } + ], "name": "whoAmI", "outputs": [ { diff --git a/crypto_clash_contract/contracts/Game.sol b/crypto_clash_contract/contracts/Game.sol index 81895c2..b43c120 100644 --- a/crypto_clash_contract/contracts/Game.sol +++ b/crypto_clash_contract/contracts/Game.sol @@ -38,9 +38,6 @@ contract Game { bool isActive; } - // Mapping from player address to their active game ID - mapping(address => uint) private playerToActiveGame; - // Mapping from game ID to game state mapping(uint => GameState) private games; @@ -50,9 +47,6 @@ contract Game { // Counter for generating unique game IDs uint private nextGameId = 1; - // Array to store completed games - GameState[] private pastGames; - // ------------------------- Registration ------------------------- // modifier validBet(uint gameId) { @@ -65,14 +59,6 @@ contract Game { _; } - modifier notAlreadyInGame() { - require( - playerToActiveGame[msg.sender] == 0, - "Player already in an active game" - ); - _; - } - // Register a player to an existing game or create a new game. // If gameId is 0, player will join or create the first available game. // Return player's ID and game ID upon successful registration. @@ -82,12 +68,11 @@ contract Game { public payable validBet(gameId) - notAlreadyInGame returns (uint playerId, uint returnGameId) { // If gameId is 0, find an open game or create a new one if (gameId == 0) { - gameId = findOrCreateGame(); + gameId = createNewGame(); } require(games[gameId].isActive, "Game is not active"); @@ -97,7 +82,6 @@ contract Game { if (game.playerA.addr == address(0x0)) { game.playerA.addr = payable(msg.sender); game.initialBet = msg.value; - playerToActiveGame[msg.sender] = gameId; return (1, gameId); } else if (game.playerB.addr == address(0x0)) { require( @@ -105,31 +89,12 @@ contract Game { "Cannot play against yourself" ); game.playerB.addr = payable(msg.sender); - playerToActiveGame[msg.sender] = gameId; return (2, gameId); } revert("Game is full"); } - // Find an open game or create a new one - function findOrCreateGame() private returns (uint) { - // Look for a game with only one player - for (uint i = 0; i < gameIds.length; i++) { - uint gId = gameIds[i]; - GameState storage game = games[gId]; - if ( - game.isActive && - game.playerA.addr != address(0x0) && - game.playerB.addr == address(0x0) - ) { - return gId; - } - } - - // No open game found, create a new one - return createNewGame(); - } // Create a new game function createNewGame() private returns (uint) { @@ -145,9 +110,8 @@ contract Game { // ------------------------- Commit ------------------------- // - modifier isRegistered() { - uint gameId = playerToActiveGame[msg.sender]; - require(gameId != 0, "Player not in any active game"); + modifier isRegistered(uint gameId) { + require(gameId != 0, "Invalid game ID"); require( msg.sender == games[gameId].playerA.addr || msg.sender == games[gameId].playerB.addr, @@ -158,8 +122,7 @@ contract Game { // Save player's encrypted move. encrMove must be "<1|2|3>-password" hashed with sha256. // Return 'true' if move was valid, 'false' otherwise. - function play(bytes32 encrMove) public isRegistered returns (bool) { - uint gameId = playerToActiveGame[msg.sender]; + function play(uint gameId, bytes32 encrMove) public isRegistered(gameId) returns (bool) { GameState storage game = games[gameId]; // Basic sanity checks with explicit errors to help debugging @@ -185,9 +148,8 @@ contract Game { // ------------------------- Reveal ------------------------- // - modifier commitPhaseEnded() { - uint gameId = playerToActiveGame[msg.sender]; - require(gameId != 0, "Player not in any active game"); + modifier commitPhaseEnded(uint gameId) { + require(gameId != 0, "Invalid game ID"); require( games[gameId].playerA.encrMove != bytes32(0) && games[gameId].playerB.encrMove != bytes32(0), @@ -199,9 +161,9 @@ contract Game { // Compare clear move given by the player with saved encrypted move. // Return clear move upon success, 'Moves.None' otherwise. function reveal( + uint gameId, string memory clearMove - ) public isRegistered commitPhaseEnded returns (Moves) { - uint gameId = playerToActiveGame[msg.sender]; + ) public isRegistered(gameId) commitPhaseEnded(gameId) returns (Moves) { GameState storage game = games[gameId]; bytes32 encrMove = keccak256(abi.encodePacked(clearMove)); // Hash of clear input (= "move-password") @@ -273,9 +235,8 @@ contract Game { // ------------------------- Result ------------------------- // - modifier revealPhaseEnded() { - uint gameId = playerToActiveGame[msg.sender]; - require(gameId != 0, "Player not in any active game"); + modifier revealPhaseEnded(uint gameId) { + require(gameId != 0, "Invalid game ID"); require( (games[gameId].playerA.move != Moves.None && games[gameId].playerB.move != Moves.None) || @@ -289,8 +250,7 @@ contract Game { // Compute the outcome and pay the winner(s). // Return the outcome. - function getOutcome() public revealPhaseEnded returns (Outcomes) { - uint gameId = playerToActiveGame[msg.sender]; + function getOutcome(uint gameId) public revealPhaseEnded(gameId) returns (Outcomes) { GameState storage game = games[gameId]; require( @@ -302,9 +262,6 @@ contract Game { address payable addrB = game.playerB.addr; uint betPlayerA = game.initialBet; - // Move game to past games before resetting - pastGames.push(game); - // Reset and cleanup resetGame(gameId); // Reset game before paying to avoid reentrancy attacks pay(addrA, addrB, betPlayerA, game.outcome); @@ -320,12 +277,12 @@ contract Game { Outcomes outcome ) private { if (outcome == Outcomes.PlayerA) { - addrA.transfer(address(this).balance); + addrA.transfer(betPlayerA * 2); } else if (outcome == Outcomes.PlayerB) { - addrB.transfer(address(this).balance); + addrB.transfer(betPlayerA * 2); } else { addrA.transfer(betPlayerA); - addrB.transfer(address(this).balance); + addrB.transfer(betPlayerA); } } @@ -333,14 +290,6 @@ contract Game { function resetGame(uint gameId) private { GameState storage game = games[gameId]; - // Clear player mappings - if (game.playerA.addr != address(0x0)) { - playerToActiveGame[game.playerA.addr] = 0; - } - if (game.playerB.addr != address(0x0)) { - playerToActiveGame[game.playerB.addr] = 0; - } - // Mark game as inactive game.isActive = false; @@ -356,8 +305,7 @@ contract Game { } // Return player's ID in their active game - function whoAmI() public view returns (uint) { - uint gameId = playerToActiveGame[msg.sender]; + function whoAmI(uint gameId) public view returns (uint) { if (gameId == 0) { return 0; } @@ -372,51 +320,8 @@ contract Game { } } - // Get the active game ID for the caller - function getMyActiveGameId() public view returns (uint) { - return playerToActiveGame[msg.sender]; - } - - // Return 'true' if both players have commited a move, 'false' otherwise. - function bothPlayed() public view returns (bool) { - uint gameId = playerToActiveGame[msg.sender]; - if (gameId == 0) return false; - - GameState storage game = games[gameId]; - return (game.playerA.encrMove != bytes32(0) && game.playerB.encrMove != bytes32(0)); - } - - // Return 'true' if both players have revealed their move, 'false' otherwise. - function bothRevealed() public view returns (bool) { - uint gameId = playerToActiveGame[msg.sender]; - if (gameId == 0) return false; - - GameState storage game = games[gameId]; - return (game.playerA.move != Moves.None && - game.playerB.move != Moves.None); - } - - // Return 'true' if player A has revealed their move, 'false' otherwise. - function playerARevealed() public view returns (bool) { - uint gameId = playerToActiveGame[msg.sender]; - if (gameId == 0) return false; - - GameState storage game = games[gameId]; - return (game.playerA.move != Moves.None); - } - - // Return 'true' if player B has revealed their move, 'false' otherwise. - function playerBRevealed() public view returns (bool) { - uint gameId = playerToActiveGame[msg.sender]; - if (gameId == 0) return false; - - GameState storage game = games[gameId]; - return (game.playerB.move != Moves.None); - } - // Return time left before the end of the revelation phase. - function revealTimeLeft() public view returns (int) { - uint gameId = playerToActiveGame[msg.sender]; + function revealTimeLeft(uint gameId) public view returns (int) { if (gameId == 0) return int(REVEAL_TIMEOUT); GameState storage game = games[gameId]; @@ -426,13 +331,6 @@ contract Game { return int(REVEAL_TIMEOUT); } - function getLastWinner() public view returns (Outcomes) { - uint gameId = playerToActiveGame[msg.sender]; - if (gameId == 0) return Outcomes.None; - - return games[gameId].outcome; - } - // ------------------------- Game Management ------------------------- // // Get details of a specific game (for viewing any game) @@ -442,21 +340,23 @@ contract Game { public view returns ( - address playerAAddr, - address playerBAddr, + Player memory playerA, + Player memory playerB, uint initialBet, Outcomes outcome, - bool isActive + bool isActive, + uint returnGameId ) { GameState storage game = games[gameId]; require(game.gameId != 0, "Game does not exist"); return ( - game.playerA.addr, - game.playerB.addr, + game.playerA, + game.playerB, game.initialBet, game.outcome, - game.isActive + game.isActive, + game.gameId ); } @@ -483,32 +383,4 @@ contract Game { return activeIds; } - - // Get number of past games - function getPastGamesCount() public view returns (uint) { - return pastGames.length; - } - - // Get details of a past game by index - function getPastGame( - uint index - ) - public - view - returns ( - address playerAAddr, - address playerBAddr, - uint initialBet, - Outcomes outcome - ) - { - require(index < pastGames.length, "Index out of bounds"); - GameState storage game = pastGames[index]; - return ( - game.playerA.addr, - game.playerB.addr, - game.initialBet, - game.outcome - ); - } } diff --git a/crypto_clash_contract/contracts/testcontract.sol b/crypto_clash_contract/contracts/testcontract.sol deleted file mode 100644 index 235328e..0000000 --- a/crypto_clash_contract/contracts/testcontract.sol +++ /dev/null @@ -1,31 +0,0 @@ -// Specifies the version of Solidity, using semantic versioning. -// Learn more: https://solidity.readthedocs.io/en/v0.5.10/layout-of-source-files.html#pragma -pragma solidity >=0.7.3; - -// Defines a contract named `HelloWorld`. -// A contract is a collection of functions and data (its state). Once deployed, a contract resides at a specific address on the Ethereum blockchain. Learn more: https://solidity.readthedocs.io/en/v0.5.10/structure-of-a-contract.html -contract HelloWorld { - - //Emitted when update function is called - //Smart contract events are a way for your contract to communicate that something happened on the blockchain to your app front-end, which can be 'listening' for certain events and take action when they happen. - event UpdatedMessages(string oldStr, string newStr); - - // Declares a state variable `message` of type `string`. - // State variables are variables whose values are permanently stored in contract storage. The keyword `public` makes variables accessible from outside a contract and creates a function that other contracts or clients can call to access the value. - string public message; - - // Similar to many class-based object-oriented languages, a constructor is a special function that is only executed upon contract creation. - // Constructors are used to initialize the contract's data. Learn more:https://solidity.readthedocs.io/en/v0.5.10/contracts.html#constructors - constructor(string memory initMessage) { - - // Accepts a string argument `initMessage` and sets the value into the contract's `message` storage variable). - message = initMessage; - } - - // A public function that accepts a string argument and updates the `message` storage variable. - function update(string memory newMessage) public { - string memory oldMsg = message; - message = newMessage; - emit UpdatedMessages(oldMsg, newMessage); - } -} \ No newline at end of file diff --git a/crypto_clash_contract/scripts/deploy.ts b/crypto_clash_contract/scripts/deploy.ts index e335b27..feed799 100644 --- a/crypto_clash_contract/scripts/deploy.ts +++ b/crypto_clash_contract/scripts/deploy.ts @@ -64,15 +64,6 @@ async function main() { // Define contracts to deploy const contractsToDeploy: ContractDeploymentConfig[] = [ - { - name: "HelloWorld", - artifactPath: "testcontract.sol/HelloWorld.json", - deployArgs: ["Hello World!"], - configKeys: { - address: "CONTRACT_ADDRESS", - abi: "ABI" - } - }, { name: "Game", artifactPath: "Game.sol/Game.json", diff --git a/crypto_clash_frontend/app/clash/Commit.tsx b/crypto_clash_frontend/app/clash/Commit.tsx index 01ef540..3ee13f2 100644 --- a/crypto_clash_frontend/app/clash/Commit.tsx +++ b/crypto_clash_frontend/app/clash/Commit.tsx @@ -2,6 +2,8 @@ import { useState, useEffect } from "react"; import Web3 from "web3"; import { Button } from "./Button"; import { Input } from "./Input"; +import { GameDetails } from "./GameModal"; +import { showToast } from "@/app/lib/toast"; interface CommitProps { account: string; @@ -12,11 +14,12 @@ interface CommitProps { selectedMove: string | null; setSelectedMove: (move: string | null) => void; secret: string; + whoAmI: "player1" | "player2" | ""; + gameDetails: GameDetails | null; setSecret: (secret: string) => void; - onBothPlayersCommitted?: () => void; + savePlayMove: (playMove: string) => void; } -type Move = "1" | "2" | "3" | null; type MoveName = "Rock" | "Paper" | "Scissors"; const MOVES: Record = { @@ -35,22 +38,17 @@ export default function Commit({ setSelectedMove, secret, setSecret, - onBothPlayersCommitted, + savePlayMove, + whoAmI, + gameDetails }: Readonly) { const [loading, setLoading] = useState(false); const [playMove, setPlayMove] = useState(""); + const [selfPlayed, setSelfPlayed] = 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(() => { - if (!secret) { - const randomHex = Math.random().toString(16).slice(2, 18); - setSecret(randomHex); - } - }, []); - // Update encrypted move when move or secret changes useEffect(() => { if (selectedMove && secret) { @@ -58,27 +56,42 @@ export default function Commit({ // Use keccak256 (Ethereum's standard hash function) const hash = Web3.utils.keccak256(clearMove); setPlayMove(hash); + // Persist to sessionStorage through parent + savePlayMove(hash); } - }, [selectedMove, secret]); + }, [selectedMove, secret, savePlayMove]); // Auto-check if both players have committed and trigger callback useEffect(() => { - if (!contract || !account || !playMove || bothPlayed === "true") { + if (!contract || !account || !whoAmI || !gameDetails) { // Clear interval if conditions not met or already both played if (autoCheckInterval) clearInterval(autoCheckInterval); setAutoCheckInterval(null); return; } + const checkSelfPlayed = async () => { + try { + const encrMove = gameDetails[whoAmI === "player1" ? "playerA" : "playerB"].encrMove; + + setSelfPlayed(Number(encrMove) !== 0 ? "true" : "false"); + } catch (err: any) { + console.error("Auto-check self played failed:", err.message); + } + }; + + checkSelfPlayed(); + // Check immediately on mount or when dependencies change const checkBothPlayed = async () => { try { - const res = await contract.methods.bothPlayed().call({ from: account }); + const playerAEncrMove = gameDetails.playerA.encrMove; + const playerBEncrMove = gameDetails.playerB.encrMove; + + const res = Number(playerAEncrMove) !== 0 && Number(playerBEncrMove) !== 0; + console.log("Both played check:", res); if (res) { setBothPlayed("true"); - if (onBothPlayersCommitted) { - onBothPlayersCommitted(); - } } } catch (err: any) { console.error("Auto-check failed:", err.message); @@ -94,16 +107,15 @@ export default function Commit({ return () => { if (interval) clearInterval(interval); }; - }, [contract, account, playMove, bothPlayed, onBothPlayersCommitted]); + }, [contract, account, playMove, bothPlayed, gameDetails]); // Commit phase read-only handlers const handlePlay = async () => { if (!contract || !web3 || !account || !playMove) return; setLoading(true); - setStatus(""); try { // playMove should be a hex string (bytes32) - const tx = contract.methods.play(playMove); + const tx = contract.methods.play(gameDetails?.returnGameId, playMove); const gas = await tx.estimateGas({ from: account }); const result = await (globalThis as any).ethereum.request({ method: "eth_sendTransaction", @@ -117,10 +129,10 @@ export default function Commit({ }, ], }); - setStatus("Play tx sent: " + result); + showToast("Play tx sent: " + result, "success"); setMoveSubmitted(true); } catch (err: any) { - setStatus("Play failed: " + err.message); + showToast("Play failed: " + err.message, "error"); } finally { setLoading(false); } @@ -131,13 +143,21 @@ export default function Commit({ setSecret(randomHex); }; + const handleSecretChange = (value: string) => { + setSecret(value); + }; + + const handleMoveSelect = (move: string) => { + setSelectedMove(move); + }; + return (

Select Your Move

- {moveSubmitted ? ( + {moveSubmitted || selfPlayed === "true" ? ( // Waiting animation after move is submitted
@@ -161,7 +181,7 @@ export default function Commit({ {(["1", "2", "3"] as const).map((move) => (

- Enter the bet amount in ETH (e.g., 0.01 for 0.01 ETH). The first - player to join with the same or higher bet will play against you. + Enter the bet amount in ETH (e.g., 0.01 for 0.01 ETH).

@@ -203,90 +201,102 @@ export default function GameList({
) : (
- {games.map((game) => ( + {games.map((game) => { + const isUserInGame = userGameIds.has(game.returnGameId); + return (
- {/* Game ID */} -
-

- Game ID -

+ {/* Game ID Header */} +

- #{game.gameId} + Game #{game.returnGameId}

-
- - {/* Players Info */} -
-

- Players -

-
-

- A: {formatAddress(game.playerA)} -

-

- B: {game.playerB === "0x0000000000000000000000000000000000000000" - ? "Waiting..." - : formatAddress(game.playerB)} +

+ + {getGamePhase(game).phase} + +

+ {web3 ? web3.utils.fromWei(game.initialBet, "ether") : "-"} ETH

- {/* Bet Amount */} -
-

- Bet -

-

- {game.initialBet} ETH -

+ {/* Players VS Layout */} +
+ {/* Player A */} +
+

+ Player A +

+

+ {formatAddress(game.playerA.addr)} +

+
+ + {/* VS */} +
+

+ VS +

+
+ + {/* Player B */} +
+

+ Player B +

+

+ {game.playerB.addr === "0x0000000000000000000000000000000000000000" + ? "⏳ Waiting..." + : formatAddress(game.playerB.addr)} +

+
{/* Join/Play Button */} -
- {userGameIds.has(game.gameId) ? ( +
+ {userGameIds.has(game.returnGameId) ? ( ) : ( )}
- ))} + ); + })}
)}
- {/* Refresh Info */} -
-

🔄 Games refresh automatically every 2 seconds

-
); } diff --git a/crypto_clash_frontend/app/clash/GameModal.tsx b/crypto_clash_frontend/app/clash/GameModal.tsx new file mode 100644 index 0000000..d580a6d --- /dev/null +++ b/crypto_clash_frontend/app/clash/GameModal.tsx @@ -0,0 +1,249 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Web3 from "web3"; +import Commit from "./Commit"; +import Reveal from "./Reveal"; +import { showErrorToast } from "@/app/lib/toast"; + +export type Player = { + addr: string; + bet: string; + encrMove: string; + move: number; + nickname: string; +}; + +export type GameDetails = { + playerA: Player; + playerB: Player; + initialBet: string; + outcome: number; + isActive: boolean; + returnGameId: number; +}; + +interface GameModalProps { + gameId?: number; + isOpen: boolean; + onClose: () => void; + account: string; + contract: any; + config: Config | null; + web3: Web3 | null; + setStatus: (status: string) => void; +} + +export default function GameModal({ + gameId, + isOpen, + onClose, + account, + contract, + config, + web3, + setStatus, +}: Readonly) { + const [phase, setPhase] = useState<"commit" | "reveal">("commit"); + const [whoAmI, setWhoAmI] = useState<"player1" | "player2" | "">(""); + const [gameDetails, setGameDetails] = useState(null); + const [selectedMove, setSelectedMove] = useState(null); + const [secret, setSecret] = useState(""); + + // Helper function to generate game-specific storage key + const getGameStorageKey = () => `game_${gameDetails?.returnGameId}`; + + // Game storage object structure + type GameStorage = { + secret: string; + selectedMove: string | null; + playMove: string; + }; + + // Storage helper functions + const loadFromStorage = () => { + if (!gameDetails) return; + const storedData = sessionStorage.getItem(getGameStorageKey()); + if (storedData) { + try { + const parsed: GameStorage = JSON.parse(storedData); + if (parsed.secret) setSecret(parsed.secret); + if (parsed.selectedMove) setSelectedMove(parsed.selectedMove); + } catch (err) { + console.error("Failed to parse stored game data:", err); + } + } + }; + + const saveGameData = (updates: Partial) => { + const storedData = sessionStorage.getItem(getGameStorageKey()); + let currentData: GameStorage = { secret: "", selectedMove: null, playMove: "" }; + + if (storedData) { + try { + currentData = JSON.parse(storedData); + } catch (err) { + console.error("Failed to parse stored game data:", err); + } + } + + const updatedData = { ...currentData, ...updates }; + sessionStorage.setItem(getGameStorageKey(), JSON.stringify(updatedData)); + }; + + const saveSecret = (value: string) => { + setSecret(value); + saveGameData({ secret: value }); + }; + + const saveMoveSelection = (move: string | null) => { + setSelectedMove(move); + if (move !== null) { + saveGameData({ selectedMove: move }); + } + }; + + const savePlayMove = (playMove: string) => { + saveGameData({ playMove }); + }; + + + useEffect(() => { + const fetchPlayerInfo = async () => { + if (contract && account && gameId !== undefined) { + try { + let player = await contract.methods.whoAmI(gameId).call({ from: account }); + if(player == 1) player = "player1"; + else if(player == 2) player = "player2"; + else player = ""; + setWhoAmI(player); + } catch (err: any) { + showErrorToast("Error fetching player info: " + err.message); + } + } + } + const fetchGameDetails = async () => { + if (contract && gameId !== undefined) { + try { + const details = await contract.methods.getGameDetails(gameId).call(); + console.log("Game details:", details); + setGameDetails(details); + + // Determine the correct phase based on game state + const playerAHasMove = Number(details.playerA.encrMove) !== 0; + const playerBHasMove = Number(details.playerB.encrMove) !== 0; + const playerARevealed = Number(details.playerA.move) !== 0; + const playerBRevealed = Number(details.playerB.move) !== 0; + + // If both players have revealed their moves, show reveal phase (with results) + if (playerARevealed && playerBRevealed) { + setPhase("reveal"); + } + // If both players have committed but not revealed, show reveal phase + else if (playerAHasMove && playerBHasMove) { + setPhase("reveal"); + } + // Otherwise, show commit phase + else { + setPhase("commit"); + } + } catch (err: any) { + showErrorToast("Error fetching game details: " + err.message); + } + } + }; + + // Only reset state when game ID actually changes (not on first render) + if (gameDetails) { + setSelectedMove(null); + setSecret(""); + } + + fetchGameDetails(); + fetchPlayerInfo(); + + // Refetch game details periodically every 2 seconds + const intervalId = setInterval(fetchGameDetails, 2000); + + return () => clearInterval(intervalId); + }, [contract, account, gameId]); + + // Load from storage after game details are fetched + useEffect(() => { + loadFromStorage(); + }, [gameDetails]); + + const handleClose = () => { + // Reset state when closing + setPhase("commit"); + setSelectedMove(null); + setSecret(""); + onClose(); + }; + + + if (!isOpen) return null; + + return ( +
+
+ {/* Header */} +
+
+

+ {gameId ? `Game #${gameId}` : "Game"} +

+

+ {phase === "commit" + ? "Commit your move" + : "Reveal your move"} +

+
+ +
+ + {/* Content */} +
+ + {/* Phase Content */} +
+ {phase === "commit" && ( + + )} + {phase === "reveal" && ( + + )} +
+
+
+
+ ); +} diff --git a/crypto_clash_frontend/app/clash/Reveal.tsx b/crypto_clash_frontend/app/clash/Reveal.tsx index a0a7e01..da70971 100644 --- a/crypto_clash_frontend/app/clash/Reveal.tsx +++ b/crypto_clash_frontend/app/clash/Reveal.tsx @@ -1,6 +1,8 @@ import { useState, useEffect } from "react"; import Web3 from "web3"; import { Button } from "./Button"; +import { GameDetails } from "./GameModal"; +import { showSuccessToast, showErrorToast } from "@/app/lib/toast"; interface RevealProps { account: string; @@ -10,6 +12,8 @@ interface RevealProps { setStatus: (status: string) => void; selectedMove: string | null; secret: string; + gameDetails: GameDetails | null; + whoAmI: "player1" | "player2" | ""; } type MoveName = "Rock" | "Paper" | "Scissors"; @@ -36,54 +40,50 @@ export default function Reveal({ setStatus, selectedMove, secret, + gameDetails, + whoAmI, }: Readonly) { const [loading, setLoading] = useState(false); + const [selfRevealed, setSelfRevealed] = useState(false); + const [opponentRevealed, setOpponentRevealed] = useState(false); const [bothRevealed, setBothRevealed] = useState(false); - const [playerARevealed, setPlayerARevealed] = useState(false); - const [playerBRevealed, setPlayerBRevealed] = useState(false); - const [revealTimeLeft, setRevealTimeLeft] = useState(0); const [outcome, setOutcome] = useState(0); - const [revealed, setRevealed] = useState(false); const clearMove = selectedMove && secret ? `${selectedMove}-${secret}` : ""; // Check game status on mount useEffect(() => { - const checkStatus = async () => { - if (!contract) return; - try { - const [br, par, pbr, rtl, out] = await Promise.all([ - await contract.methods.bothRevealed().call({ from : account}), - await contract.methods.playerARevealed().call({ from : account}), - await contract.methods.playerBRevealed().call({ from : account}), - await contract.methods.revealTimeLeft().call({ from : account}), - await contract.methods.getLastWinner().call({ from : account}), - ]); + const setStateFromGameDetails = () => { + if (!gameDetails) return; + const playerARevealed = Number(gameDetails.playerA.move) !== 0; + const playerBRevealed = Number(gameDetails.playerB.move) !== 0; - console.log("Status:", { - br, par, pbr, rtl, out - }); - setBothRevealed(br); - setPlayerARevealed(par); - setPlayerBRevealed(pbr); - setRevealTimeLeft(Number(rtl)); - setOutcome(Number(out)); - } catch (err: any) { - console.error("Failed to check status:", err); + setSelfRevealed( + (whoAmI === "player1" && playerARevealed) || + (whoAmI === "player2" && playerBRevealed) + ); + setOpponentRevealed( + (whoAmI === "player1" && playerBRevealed) || + (whoAmI === "player2" && playerARevealed) + ); + setBothRevealed(playerARevealed && playerBRevealed); + if(bothRevealed){ + if(Number(gameDetails.outcome) === 1 && whoAmI === "player1") setOutcome(1); + else if(Number(gameDetails.outcome) === 2 && whoAmI === "player2") setOutcome(1); + else if(Number(gameDetails.outcome) === 1 && whoAmI === "player2") setOutcome(2); + else if(Number(gameDetails.outcome) === 2 && whoAmI === "player1") setOutcome(2); + else setOutcome(3); } }; - const interval = setInterval(checkStatus, 3000); - checkStatus(); - return () => clearInterval(interval); - }, [contract, account]); + setStateFromGameDetails(); + }, [gameDetails, contract, account, whoAmI]); const handleReveal = async () => { if (!contract || !web3 || !account || !clearMove) return; setLoading(true); - setStatus(""); try { - const tx = contract.methods.reveal(clearMove); + const tx = contract.methods.reveal(gameDetails?.returnGameId, clearMove); const gas = await tx.estimateGas({ from: account }); const result = await (globalThis as any).ethereum.request({ method: "eth_sendTransaction", @@ -97,10 +97,9 @@ export default function Reveal({ }, ], }); - setStatus("✅ Reveal tx sent: " + result); - setRevealed(true); + showSuccessToast("Reveal tx sent: " + result); } catch (err: any) { - setStatus("❌ Reveal failed: " + err.message); + showErrorToast("Reveal failed: " + err.message); } finally { setLoading(false); } @@ -109,9 +108,8 @@ export default function Reveal({ const handleGetOutcome = async () => { if (!contract || !web3 || !account) return; setLoading(true); - setStatus(""); try { - const tx = contract.methods.getOutcome(); + const tx = contract.methods.getOutcome(gameDetails?.returnGameId); const gas = await tx.estimateGas({ from: account }); const result = await (globalThis as any).ethereum.request({ method: "eth_sendTransaction", @@ -125,9 +123,10 @@ export default function Reveal({ }, ], }); - setStatus("✅ Claim tx sent: " + result); + showSuccessToast("Claim tx sent: " + result); } catch (err: any) { - setStatus("❌ Claim failed: " + err.message); + console.error(err); + showErrorToast("Claim failed: " + err.message); } finally { setLoading(false); } @@ -137,156 +136,201 @@ export default function Reveal({ return (
- {/* Your Move Section */} -
-

- Your Move -

- {selectedMove ? ( -
-
- {MOVES[selectedMove].icon} - - {MOVES[selectedMove].name} - -
-
-
-

- Clear Move: -

- - {clearMove} - + {/* Your Move Section - Hidden when both revealed */} + {!bothRevealed && ( +
+

+ Your Move +

+ {selectedMove ? ( +
+
+ {MOVES[selectedMove].icon} + + {MOVES[selectedMove].name} + +
+
+
+

+ Clear Move: +

+ + {clearMove} + +
+ ) : ( +

+ No move selected yet +

+ )} +
+ )} + + {/* Game Status Section - Hidden when both revealed */} + {!bothRevealed && ( +
+
+

{selfRevealed ? "✅" : "⏳"}

+

+ Me +

+

+ {selfRevealed ? "Revealed" : "Waiting"} +

- ) : ( -

- No move selected yet -

- )} -
- - {/* Game Status Section */} -
-
-

{playerARevealed ? "✅" : "⏳"}

-

- Player A -

-

- {playerARevealed ? "Revealed" : "Waiting"} -

-
-
-

{playerBRevealed ? "✅" : "⏳"}

-

- Player B -

-

- {playerBRevealed ? "Revealed" : "Waiting"} -

-
-
-

- ⏱️ -

-

- Time Left -

-

- {revealTimeLeft > 0 ? `${revealTimeLeft}s` : "Expired"} -

-
-
- - {/* Reveal Section */} -
-

- Reveal Your Move -

-

- Submit your clear move and secret to the blockchain. This proves you - didn't cheat! -

- -
- - {/* Winner Section - Only show if both revealed */} - {bothRevealed && ( -
-

- {outcomeData.emoji} +

{opponentRevealed ? "✅" : "⏳"}

+

+ Opponent +

+

+ {opponentRevealed ? "Revealed" : "Waiting"} +

+
+
+

+ ⏱️ +

+

+ Time Left +

+

+ {0} +

+
+
+ )} + + {/* Reveal Section - Hidden when both revealed */} + {!bothRevealed && ( +
+

+ Reveal Your Move +

+

+ Submit your clear move and secret to the blockchain. This proves you + didn't cheat!

-

- {outcomeData.name} -

)} - {/* Status Messages */} - {!bothRevealed && !revealed && ( -
-

- ⏳ Waiting for both players to reveal... -

+ {/* Winner Section - Only show if both revealed */} + {bothRevealed && ( +
+ {/* Moves Comparison */} +
+

+ Final Moves +

+
+ {/* Your Move (always on left) */} +
+ + {gameDetails && MOVES[String(whoAmI === "player1" ? gameDetails.playerA.move : gameDetails.playerB.move)]?.icon} + + + You + + + {gameDetails && MOVES[String(whoAmI === "player1" ? gameDetails.playerA.move : gameDetails.playerB.move)]?.name} + +
+ + {/* VS */} +
+ + VS + +
+ + {/* Opponent Move (always on right) */} +
+ + {gameDetails && MOVES[String(whoAmI === "player1" ? gameDetails.playerB.move : gameDetails.playerA.move)]?.icon} + + + Opponent + + + {gameDetails && MOVES[String(whoAmI === "player1" ? gameDetails.playerB.move : gameDetails.playerA.move)]?.name} + +
+
+
+ + {/* Outcome Section */} +
+

+ {outcomeData.emoji} +

+

+ {outcomeData.name} +

+ + {/* Show Claim Coins button only on win or draw and if game is active */} + {(outcome === 1 || outcome === 3) && gameDetails?.isActive && ( + + )} +
)}
diff --git a/crypto_clash_frontend/app/clash/page.tsx b/crypto_clash_frontend/app/clash/page.tsx index 8a333e8..c14ed4b 100644 --- a/crypto_clash_frontend/app/clash/page.tsx +++ b/crypto_clash_frontend/app/clash/page.tsx @@ -3,32 +3,26 @@ import { useEffect, useState } from "react"; import Web3 from "web3"; import GameList from "./GameList"; -import Commit from "./Commit"; -import Reveal from "./Reveal"; +import GameModal from "./GameModal"; +import { showErrorToast } from "@/app/lib/toast"; export default function Clash() { const [config, setConfig] = useState(null); const [web3, setWeb3] = useState(null); const [contract, setContract] = useState(null); - const [account, setAccount] = useState(""); const [status, setStatus] = useState(""); - // Inputs for contract functions - const [phase, setPhase] = useState<"games" | "commit" | "reveal">("games"); - const [selectedMove, setSelectedMove] = useState(null); - const [secret, setSecret] = useState(""); + // Modal state + const [isModalOpen, setIsModalOpen] = useState(false); + const [selectedGameId, setSelectedGameId] = useState(); const [availableAccounts, setAvailableAccounts] = useState([]); const [selectedAccount, setSelectedAccount] = useState(""); const handlePlayClick = (gameId: number) => { - setPhase("commit"); + setSelectedGameId(gameId); + setIsModalOpen(true); }; - // Clear status when phase changes - useEffect(() => { - setStatus(""); - }, [phase]); - // Load config and contract useEffect(() => { const loadConfig = async () => { @@ -50,16 +44,13 @@ export default function Clash() { method: "eth_requestAccounts", }); setAvailableAccounts(accounts); - setAccount(accounts[0]); setSelectedAccount(accounts[0]); } catch (err: any) { - setStatus( - "MetaMask not available or user denied access: " + err.message - ); + showErrorToast("MetaMask not available or user denied access: " + err.message); } } } catch (err: any) { - setStatus("Failed to load config: " + err.message); + showErrorToast("Failed to load config: " + err.message); } }; loadConfig(); @@ -78,9 +69,7 @@ export default function Clash() { Crypto Clash

- {phase === "games" && "Browse and join games."} - {phase === "commit" && "Commit your move."} - {phase === "reveal" && "Reveal your move."} + Browse and join games.

@@ -92,7 +81,6 @@ export default function Clash() { value={selectedAccount} onChange={(e) => { setSelectedAccount(e.target.value); - setAccount(e.target.value); }} className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500" > @@ -117,100 +105,29 @@ export default function Clash() { {config?.GAME_CONTRACT_ADDRESS}

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

{status}

-
- )} -
-

ℹ️ Note:

-
    -
  • - MetaMask or a compatible Web3 wallet is required for write - operations -
  • -
  • - Use bytes32 for encrypted move (see contract docs for details) -
  • -
  • ETH values are in Ether (not Wei)
  • -
+
+ + {/* Game Modal */} + setIsModalOpen(false)} + account={selectedAccount} + contract={contract} + config={config} + web3={web3} + setStatus={setStatus} + />
); diff --git a/crypto_clash_frontend/app/components/ToastContainer.tsx b/crypto_clash_frontend/app/components/ToastContainer.tsx new file mode 100644 index 0000000..6fcc4ed --- /dev/null +++ b/crypto_clash_frontend/app/components/ToastContainer.tsx @@ -0,0 +1,76 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { Toast, setToastListener } from '@/app/lib/toast'; + +export default function ToastContainer() { + const [toasts, setToasts] = useState([]); + + useEffect(() => { + setToastListener((toast: Toast) => { + setToasts((prev) => [...prev, toast]); + + if (toast.duration && toast.duration > 0) { + const timer = setTimeout(() => { + setToasts((prev) => prev.filter((t) => t.id !== toast.id)); + }, toast.duration); + + return () => clearTimeout(timer); + } + }); + }, []); + + const removeToast = (id: string) => { + setToasts((prev) => prev.filter((t) => t.id !== id)); + }; + + const getToastStyles = (type: string) => { + switch (type) { + case 'success': + return 'bg-green-50 dark:bg-green-900 border-green-200 dark:border-green-700 text-green-800 dark:text-green-200'; + case 'error': + return 'bg-red-50 dark:bg-red-900 border-red-200 dark:border-red-700 text-red-800 dark:text-red-200'; + case 'warning': + return 'bg-yellow-50 dark:bg-yellow-900 border-yellow-200 dark:border-yellow-700 text-yellow-800 dark:text-yellow-200'; + case 'info': + default: + return 'bg-blue-50 dark:bg-blue-900 border-blue-200 dark:border-blue-700 text-blue-800 dark:text-blue-200'; + } + }; + + const getIcon = (type: string) => { + switch (type) { + case 'success': + return '✅'; + case 'error': + return '❌'; + case 'warning': + return '⚠️'; + case 'info': + default: + return 'ℹ️'; + } + }; + + return ( +
+ {toasts.map((toast) => ( +
+ {getIcon(toast.type)} +
+

{toast.message}

+
+ +
+ ))} +
+ ); +} diff --git a/crypto_clash_frontend/app/globals.css b/crypto_clash_frontend/app/globals.css index a2dc41e..b39a8b5 100644 --- a/crypto_clash_frontend/app/globals.css +++ b/crypto_clash_frontend/app/globals.css @@ -24,3 +24,20 @@ body { color: var(--foreground); font-family: Arial, Helvetica, sans-serif; } + +@keyframes fade-in { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@layer utilities { + .animate-fade-in { + animation: fade-in 0.3s ease-out; + } +} diff --git a/crypto_clash_frontend/app/hello_world/page.tsx b/crypto_clash_frontend/app/hello_world/page.tsx deleted file mode 100644 index 836bb68..0000000 --- a/crypto_clash_frontend/app/hello_world/page.tsx +++ /dev/null @@ -1,245 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import Web3 from "web3"; - - - -export default function Home() { - const [config, setConfig] = useState(null); - const [web3, setWeb3] = useState(null); - const [contract, setContract] = useState(null); - const [message, setMessage] = useState(""); - const [newMessage, setNewMessage] = useState(""); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(""); - const [account, setAccount] = useState(""); - - // Initialize Web3 and contract - useEffect(() => { - const loadConfig = async () => { - try { - const response = await fetch("/crypto_clash/config.json"); - const data = await response.json(); - setConfig(data); - - // Initialize Web3 - const web3Instance = new Web3(data.API_URL); - setWeb3(web3Instance); - - // Create contract instance - const contractInstance = new web3Instance.eth.Contract( - data.ABI, - data.CONTRACT_ADDRESS - ); - setContract(contractInstance); - - // Try to get connected account (if MetaMask or similar is available) - if (typeof window !== "undefined" && (window as any).ethereum) { - try { - const accounts = await (window as any).ethereum.request({ - method: "eth_requestAccounts", - }); - setAccount(accounts[0]); - - // Get the network ID from the RPC endpoint - const networkId = await web3Instance.eth.net.getId(); - const currentChainId = await (window as any).ethereum.request({ - method: "eth_chainId", - }); - - // If on different network, notify user (they may need to switch networks manually) - console.log( - `RPC Network ID: ${networkId}, MetaMask Chain ID: ${currentChainId}` - ); - } catch (err) { - console.log("MetaMask not available or user denied access"); - } - } - - // Load initial message - await readMessage(contractInstance); - } catch (err) { - setError(`Failed to load config: ${err}`); - console.error(err); - } - }; - - loadConfig(); - }, []); - - // Read message from contract - const readMessage = async (contractInstance: any) => { - try { - setLoading(true); - setError(""); - const result = await contractInstance.methods.message().call(); - setMessage(result); - } catch (err) { - setError(`Failed to read message: ${err}`); - console.error(err); - } finally { - setLoading(false); - } - }; - - // Update message on contract - const updateMessage = async () => { - if (!newMessage.trim()) { - setError("Please enter a message"); - return; - } - - try { - setLoading(true); - setError(""); - - if (!web3 || !contract) { - throw new Error("Web3 or contract not initialized"); - } - - // Check if MetaMask is available - if (!account && typeof window !== "undefined" && !(window as any).ethereum) { - throw new Error( - "MetaMask not available. Please install MetaMask to update the message." - ); - } - - // Create transaction - const tx = contract.methods.update(newMessage); - const gas = await tx.estimateGas({ from: account }); - - console.log(await web3.eth.net.getId()); - - // Send transaction via MetaMask - const result = await (window as any).ethereum.request({ - method: "eth_sendTransaction", - params: [ - { - from: account, - to: config?.CONTRACT_ADDRESS, - data: tx.encodeABI(), - gas: web3.utils.toHex(gas), - chainId: web3.utils.toHex(await web3.eth.net.getId()), - }, - ], - }); - - - - setError(`Transaction sent: ${result}`); - setNewMessage(""); - - // Wait a bit and refresh message from the RPC endpoint - setTimeout(() => { - if (contract) { - readMessage(contract); - } - }, 2000); - } catch (err) { - setError(`Failed to update message: ${err}`); - console.error(err); - } finally { - setLoading(false); - } - }; - - return ( -
-
-
-

- Smart Contract Interaction -

-

- Read and update messages on the blockchain -

- - {/* Status Section */} -
-

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

-

- Contract Address:{" "} - {config?.CONTRACT_ADDRESS} -

-
- - {/* Current Message Display */} -
-

- Current Message: -

- {loading && !message ? ( -

Loading...

- ) : message ? ( -

- {message} -

- ) : ( -

- No message yet -

- )} -
- - {/* Refresh Button */} - - - {/* Update Message Form */} -
-

- Update Message: -

- setNewMessage(e.target.value)} - placeholder="Enter new message..." - className="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500" - /> - -
- - {/* Error/Status Messages */} - {error && ( -
-

{error}

-
- )} - - {/* Info Section */} -
-

ℹ️ Note:

-
    -
  • To update messages, you need MetaMask or a compatible Web3 wallet
  • -
  • Make sure your MetaMask is connected to the correct test network
  • -
  • Reading messages is free and doesn't require a wallet
  • -
  • Updates are written to the blockchain and may take time to confirm
  • -
-
-
-
-
- ); -} diff --git a/crypto_clash_frontend/app/layout.tsx b/crypto_clash_frontend/app/layout.tsx index cb2b4e2..3d8ca39 100644 --- a/crypto_clash_frontend/app/layout.tsx +++ b/crypto_clash_frontend/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; +import ToastContainer from "./components/ToastContainer"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -28,6 +29,7 @@ export default function RootLayout({ className={`${geistSans.variable} ${geistMono.variable} antialiased`} > {children} + ); diff --git a/crypto_clash_frontend/app/lib/toast.ts b/crypto_clash_frontend/app/lib/toast.ts new file mode 100644 index 0000000..924efac --- /dev/null +++ b/crypto_clash_frontend/app/lib/toast.ts @@ -0,0 +1,47 @@ +export type ToastType = 'success' | 'error' | 'info' | 'warning'; + +export interface Toast { + id: string; + message: string; + type: ToastType; + duration?: number; +} + +let toastListener: ((toast: Toast) => void) | null = null; +let toastId = 0; + +export function setToastListener(listener: (toast: Toast) => void) { + toastListener = listener; +} + +export function showToast(message: string, type: ToastType = 'info', duration = 4000) { + const id = String(toastId++); + const toast: Toast = { + id, + message, + type, + duration, + }; + + if (toastListener) { + toastListener(toast); + } + + return id; +} + +export function showSuccessToast(message: string, duration?: number) { + return showToast(message, 'success', duration); +} + +export function showErrorToast(message: string, duration?: number) { + return showToast(message, 'error', duration); +} + +export function showInfoToast(message: string, duration?: number) { + return showToast(message, 'info', duration); +} + +export function showWarningToast(message: string, duration?: number) { + return showToast(message, 'warning', duration); +} diff --git a/crypto_clash_frontend/app/page.tsx b/crypto_clash_frontend/app/page.tsx index 66c4ab9..865a0b5 100644 --- a/crypto_clash_frontend/app/page.tsx +++ b/crypto_clash_frontend/app/page.tsx @@ -15,11 +15,6 @@ export default function Home() { Crypto Clash - Battle with others -
  • - - Hello World - A simple introduction - -
  • diff --git a/crypto_clash_frontend/public/config.json b/crypto_clash_frontend/public/config.json index 3ce71b7..3ebd314 100644 --- a/crypto_clash_frontend/public/config.json +++ b/crypto_clash_frontend/public/config.json @@ -59,7 +59,7 @@ "type": "function" } ], - "GAME_CONTRACT_ADDRESS": "0x503d096a9a163180F79B1AC2F1d9F7C63f5DC75a", + "GAME_CONTRACT_ADDRESS": "0xAA7057A0203539d9BE86EfB471B831Dd833a9e22", "GAME_ABI": [ { "inputs": [], @@ -87,32 +87,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [], - "name": "bothPlayed", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "bothRevealed", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "getActiveGameIds", @@ -150,14 +124,68 @@ "name": "getGameDetails", "outputs": [ { - "internalType": "address", - "name": "playerAAddr", - "type": "address" + "components": [ + { + "internalType": "address payable", + "name": "addr", + "type": "address" + }, + { + "internalType": "uint256", + "name": "bet", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "encrMove", + "type": "bytes32" + }, + { + "internalType": "enum Game.Moves", + "name": "move", + "type": "uint8" + }, + { + "internalType": "string", + "name": "nickname", + "type": "string" + } + ], + "internalType": "struct Game.Player", + "name": "playerA", + "type": "tuple" }, { - "internalType": "address", - "name": "playerBAddr", - "type": "address" + "components": [ + { + "internalType": "address payable", + "name": "addr", + "type": "address" + }, + { + "internalType": "uint256", + "name": "bet", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "encrMove", + "type": "bytes32" + }, + { + "internalType": "enum Game.Moves", + "name": "move", + "type": "uint8" + }, + { + "internalType": "string", + "name": "nickname", + "type": "string" + } + ], + "internalType": "struct Game.Player", + "name": "playerB", + "type": "tuple" }, { "internalType": "uint256", @@ -173,31 +201,10 @@ "internalType": "bool", "name": "isActive", "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getLastWinner", - "outputs": [ - { - "internalType": "enum Game.Outcomes", - "name": "", - "type": "uint8" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getMyActiveGameId", - "outputs": [ + }, { "internalType": "uint256", - "name": "", + "name": "returnGameId", "type": "uint256" } ], @@ -205,7 +212,13 @@ "type": "function" }, { - "inputs": [], + "inputs": [ + { + "internalType": "uint256", + "name": "gameId", + "type": "uint256" + } + ], "name": "getOutcome", "outputs": [ { @@ -221,51 +234,9 @@ "inputs": [ { "internalType": "uint256", - "name": "index", - "type": "uint256" - } - ], - "name": "getPastGame", - "outputs": [ - { - "internalType": "address", - "name": "playerAAddr", - "type": "address" - }, - { - "internalType": "address", - "name": "playerBAddr", - "type": "address" - }, - { - "internalType": "uint256", - "name": "initialBet", + "name": "gameId", "type": "uint256" }, - { - "internalType": "enum Game.Outcomes", - "name": "outcome", - "type": "uint8" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getPastGamesCount", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ { "internalType": "bytes32", "name": "encrMove", @@ -283,32 +254,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [], - "name": "playerARevealed", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "playerBRevealed", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { @@ -335,6 +280,11 @@ }, { "inputs": [ + { + "internalType": "uint256", + "name": "gameId", + "type": "uint256" + }, { "internalType": "string", "name": "clearMove", @@ -353,7 +303,13 @@ "type": "function" }, { - "inputs": [], + "inputs": [ + { + "internalType": "uint256", + "name": "gameId", + "type": "uint256" + } + ], "name": "revealTimeLeft", "outputs": [ { @@ -366,7 +322,13 @@ "type": "function" }, { - "inputs": [], + "inputs": [ + { + "internalType": "uint256", + "name": "gameId", + "type": "uint256" + } + ], "name": "whoAmI", "outputs": [ {