mirror of
https://github.com/averel10/crypto_clash.git
synced 2026-03-12 19:08:11 +01:00
Refactor Clash component to use GameModal for game interactions, remove Hello World page, and implement toast notifications for error handling
This commit is contained in:
218
config.json
218
config.json
@@ -59,7 +59,7 @@
|
|||||||
"type": "function"
|
"type": "function"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"GAME_CONTRACT_ADDRESS": "0x503d096a9a163180F79B1AC2F1d9F7C63f5DC75a",
|
"GAME_CONTRACT_ADDRESS": "0xAA7057A0203539d9BE86EfB471B831Dd833a9e22",
|
||||||
"GAME_ABI": [
|
"GAME_ABI": [
|
||||||
{
|
{
|
||||||
"inputs": [],
|
"inputs": [],
|
||||||
@@ -87,32 +87,6 @@
|
|||||||
"stateMutability": "view",
|
"stateMutability": "view",
|
||||||
"type": "function"
|
"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": [],
|
"inputs": [],
|
||||||
"name": "getActiveGameIds",
|
"name": "getActiveGameIds",
|
||||||
@@ -150,14 +124,68 @@
|
|||||||
"name": "getGameDetails",
|
"name": "getGameDetails",
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
"internalType": "address",
|
"components": [
|
||||||
"name": "playerAAddr",
|
{
|
||||||
"type": "address"
|
"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",
|
"components": [
|
||||||
"name": "playerBAddr",
|
{
|
||||||
"type": "address"
|
"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",
|
"internalType": "uint256",
|
||||||
@@ -173,31 +201,10 @@
|
|||||||
"internalType": "bool",
|
"internalType": "bool",
|
||||||
"name": "isActive",
|
"name": "isActive",
|
||||||
"type": "bool"
|
"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",
|
"internalType": "uint256",
|
||||||
"name": "",
|
"name": "returnGameId",
|
||||||
"type": "uint256"
|
"type": "uint256"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -205,7 +212,13 @@
|
|||||||
"type": "function"
|
"type": "function"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"inputs": [],
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": "uint256",
|
||||||
|
"name": "gameId",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
"name": "getOutcome",
|
"name": "getOutcome",
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
@@ -221,51 +234,9 @@
|
|||||||
"inputs": [
|
"inputs": [
|
||||||
{
|
{
|
||||||
"internalType": "uint256",
|
"internalType": "uint256",
|
||||||
"name": "index",
|
"name": "gameId",
|
||||||
"type": "uint256"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"name": "getPastGame",
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"internalType": "address",
|
|
||||||
"name": "playerAAddr",
|
|
||||||
"type": "address"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"internalType": "address",
|
|
||||||
"name": "playerBAddr",
|
|
||||||
"type": "address"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"internalType": "uint256",
|
|
||||||
"name": "initialBet",
|
|
||||||
"type": "uint256"
|
"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",
|
"internalType": "bytes32",
|
||||||
"name": "encrMove",
|
"name": "encrMove",
|
||||||
@@ -283,32 +254,6 @@
|
|||||||
"stateMutability": "nonpayable",
|
"stateMutability": "nonpayable",
|
||||||
"type": "function"
|
"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": [
|
"inputs": [
|
||||||
{
|
{
|
||||||
@@ -335,6 +280,11 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"inputs": [
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": "uint256",
|
||||||
|
"name": "gameId",
|
||||||
|
"type": "uint256"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"internalType": "string",
|
"internalType": "string",
|
||||||
"name": "clearMove",
|
"name": "clearMove",
|
||||||
@@ -353,7 +303,13 @@
|
|||||||
"type": "function"
|
"type": "function"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"inputs": [],
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": "uint256",
|
||||||
|
"name": "gameId",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
"name": "revealTimeLeft",
|
"name": "revealTimeLeft",
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
@@ -366,7 +322,13 @@
|
|||||||
"type": "function"
|
"type": "function"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"inputs": [],
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": "uint256",
|
||||||
|
"name": "gameId",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
"name": "whoAmI",
|
"name": "whoAmI",
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -38,9 +38,6 @@ contract Game {
|
|||||||
bool isActive;
|
bool isActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mapping from player address to their active game ID
|
|
||||||
mapping(address => uint) private playerToActiveGame;
|
|
||||||
|
|
||||||
// Mapping from game ID to game state
|
// Mapping from game ID to game state
|
||||||
mapping(uint => GameState) private games;
|
mapping(uint => GameState) private games;
|
||||||
|
|
||||||
@@ -50,9 +47,6 @@ contract Game {
|
|||||||
// Counter for generating unique game IDs
|
// Counter for generating unique game IDs
|
||||||
uint private nextGameId = 1;
|
uint private nextGameId = 1;
|
||||||
|
|
||||||
// Array to store completed games
|
|
||||||
GameState[] private pastGames;
|
|
||||||
|
|
||||||
// ------------------------- Registration ------------------------- //
|
// ------------------------- Registration ------------------------- //
|
||||||
|
|
||||||
modifier validBet(uint gameId) {
|
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.
|
// 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.
|
// If gameId is 0, player will join or create the first available game.
|
||||||
// Return player's ID and game ID upon successful registration.
|
// Return player's ID and game ID upon successful registration.
|
||||||
@@ -82,12 +68,11 @@ contract Game {
|
|||||||
public
|
public
|
||||||
payable
|
payable
|
||||||
validBet(gameId)
|
validBet(gameId)
|
||||||
notAlreadyInGame
|
|
||||||
returns (uint playerId, uint returnGameId)
|
returns (uint playerId, uint returnGameId)
|
||||||
{
|
{
|
||||||
// If gameId is 0, find an open game or create a new one
|
// If gameId is 0, find an open game or create a new one
|
||||||
if (gameId == 0) {
|
if (gameId == 0) {
|
||||||
gameId = findOrCreateGame();
|
gameId = createNewGame();
|
||||||
}
|
}
|
||||||
|
|
||||||
require(games[gameId].isActive, "Game is not active");
|
require(games[gameId].isActive, "Game is not active");
|
||||||
@@ -97,7 +82,6 @@ contract Game {
|
|||||||
if (game.playerA.addr == address(0x0)) {
|
if (game.playerA.addr == address(0x0)) {
|
||||||
game.playerA.addr = payable(msg.sender);
|
game.playerA.addr = payable(msg.sender);
|
||||||
game.initialBet = msg.value;
|
game.initialBet = msg.value;
|
||||||
playerToActiveGame[msg.sender] = gameId;
|
|
||||||
return (1, gameId);
|
return (1, gameId);
|
||||||
} else if (game.playerB.addr == address(0x0)) {
|
} else if (game.playerB.addr == address(0x0)) {
|
||||||
require(
|
require(
|
||||||
@@ -105,31 +89,12 @@ contract Game {
|
|||||||
"Cannot play against yourself"
|
"Cannot play against yourself"
|
||||||
);
|
);
|
||||||
game.playerB.addr = payable(msg.sender);
|
game.playerB.addr = payable(msg.sender);
|
||||||
playerToActiveGame[msg.sender] = gameId;
|
|
||||||
return (2, gameId);
|
return (2, gameId);
|
||||||
}
|
}
|
||||||
|
|
||||||
revert("Game is full");
|
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
|
// Create a new game
|
||||||
function createNewGame() private returns (uint) {
|
function createNewGame() private returns (uint) {
|
||||||
@@ -145,9 +110,8 @@ contract Game {
|
|||||||
|
|
||||||
// ------------------------- Commit ------------------------- //
|
// ------------------------- Commit ------------------------- //
|
||||||
|
|
||||||
modifier isRegistered() {
|
modifier isRegistered(uint gameId) {
|
||||||
uint gameId = playerToActiveGame[msg.sender];
|
require(gameId != 0, "Invalid game ID");
|
||||||
require(gameId != 0, "Player not in any active game");
|
|
||||||
require(
|
require(
|
||||||
msg.sender == games[gameId].playerA.addr ||
|
msg.sender == games[gameId].playerA.addr ||
|
||||||
msg.sender == games[gameId].playerB.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.
|
// Save player's encrypted move. encrMove must be "<1|2|3>-password" hashed with sha256.
|
||||||
// Return 'true' if move was valid, 'false' otherwise.
|
// Return 'true' if move was valid, 'false' otherwise.
|
||||||
function play(bytes32 encrMove) public isRegistered returns (bool) {
|
function play(uint gameId, bytes32 encrMove) public isRegistered(gameId) returns (bool) {
|
||||||
uint gameId = playerToActiveGame[msg.sender];
|
|
||||||
GameState storage game = games[gameId];
|
GameState storage game = games[gameId];
|
||||||
|
|
||||||
// Basic sanity checks with explicit errors to help debugging
|
// Basic sanity checks with explicit errors to help debugging
|
||||||
@@ -185,9 +148,8 @@ contract Game {
|
|||||||
|
|
||||||
// ------------------------- Reveal ------------------------- //
|
// ------------------------- Reveal ------------------------- //
|
||||||
|
|
||||||
modifier commitPhaseEnded() {
|
modifier commitPhaseEnded(uint gameId) {
|
||||||
uint gameId = playerToActiveGame[msg.sender];
|
require(gameId != 0, "Invalid game ID");
|
||||||
require(gameId != 0, "Player not in any active game");
|
|
||||||
require(
|
require(
|
||||||
games[gameId].playerA.encrMove != bytes32(0) &&
|
games[gameId].playerA.encrMove != bytes32(0) &&
|
||||||
games[gameId].playerB.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.
|
// Compare clear move given by the player with saved encrypted move.
|
||||||
// Return clear move upon success, 'Moves.None' otherwise.
|
// Return clear move upon success, 'Moves.None' otherwise.
|
||||||
function reveal(
|
function reveal(
|
||||||
|
uint gameId,
|
||||||
string memory clearMove
|
string memory clearMove
|
||||||
) public isRegistered commitPhaseEnded returns (Moves) {
|
) public isRegistered(gameId) commitPhaseEnded(gameId) returns (Moves) {
|
||||||
uint gameId = playerToActiveGame[msg.sender];
|
|
||||||
GameState storage game = games[gameId];
|
GameState storage game = games[gameId];
|
||||||
|
|
||||||
bytes32 encrMove = keccak256(abi.encodePacked(clearMove)); // Hash of clear input (= "move-password")
|
bytes32 encrMove = keccak256(abi.encodePacked(clearMove)); // Hash of clear input (= "move-password")
|
||||||
@@ -273,9 +235,8 @@ contract Game {
|
|||||||
|
|
||||||
// ------------------------- Result ------------------------- //
|
// ------------------------- Result ------------------------- //
|
||||||
|
|
||||||
modifier revealPhaseEnded() {
|
modifier revealPhaseEnded(uint gameId) {
|
||||||
uint gameId = playerToActiveGame[msg.sender];
|
require(gameId != 0, "Invalid game ID");
|
||||||
require(gameId != 0, "Player not in any active game");
|
|
||||||
require(
|
require(
|
||||||
(games[gameId].playerA.move != Moves.None &&
|
(games[gameId].playerA.move != Moves.None &&
|
||||||
games[gameId].playerB.move != Moves.None) ||
|
games[gameId].playerB.move != Moves.None) ||
|
||||||
@@ -289,8 +250,7 @@ contract Game {
|
|||||||
|
|
||||||
// Compute the outcome and pay the winner(s).
|
// Compute the outcome and pay the winner(s).
|
||||||
// Return the outcome.
|
// Return the outcome.
|
||||||
function getOutcome() public revealPhaseEnded returns (Outcomes) {
|
function getOutcome(uint gameId) public revealPhaseEnded(gameId) returns (Outcomes) {
|
||||||
uint gameId = playerToActiveGame[msg.sender];
|
|
||||||
GameState storage game = games[gameId];
|
GameState storage game = games[gameId];
|
||||||
|
|
||||||
require(
|
require(
|
||||||
@@ -302,9 +262,6 @@ contract Game {
|
|||||||
address payable addrB = game.playerB.addr;
|
address payable addrB = game.playerB.addr;
|
||||||
uint betPlayerA = game.initialBet;
|
uint betPlayerA = game.initialBet;
|
||||||
|
|
||||||
// Move game to past games before resetting
|
|
||||||
pastGames.push(game);
|
|
||||||
|
|
||||||
// Reset and cleanup
|
// Reset and cleanup
|
||||||
resetGame(gameId); // Reset game before paying to avoid reentrancy attacks
|
resetGame(gameId); // Reset game before paying to avoid reentrancy attacks
|
||||||
pay(addrA, addrB, betPlayerA, game.outcome);
|
pay(addrA, addrB, betPlayerA, game.outcome);
|
||||||
@@ -320,12 +277,12 @@ contract Game {
|
|||||||
Outcomes outcome
|
Outcomes outcome
|
||||||
) private {
|
) private {
|
||||||
if (outcome == Outcomes.PlayerA) {
|
if (outcome == Outcomes.PlayerA) {
|
||||||
addrA.transfer(address(this).balance);
|
addrA.transfer(betPlayerA * 2);
|
||||||
} else if (outcome == Outcomes.PlayerB) {
|
} else if (outcome == Outcomes.PlayerB) {
|
||||||
addrB.transfer(address(this).balance);
|
addrB.transfer(betPlayerA * 2);
|
||||||
} else {
|
} else {
|
||||||
addrA.transfer(betPlayerA);
|
addrA.transfer(betPlayerA);
|
||||||
addrB.transfer(address(this).balance);
|
addrB.transfer(betPlayerA);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,14 +290,6 @@ contract Game {
|
|||||||
function resetGame(uint gameId) private {
|
function resetGame(uint gameId) private {
|
||||||
GameState storage game = games[gameId];
|
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
|
// Mark game as inactive
|
||||||
game.isActive = false;
|
game.isActive = false;
|
||||||
|
|
||||||
@@ -356,8 +305,7 @@ contract Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Return player's ID in their active game
|
// Return player's ID in their active game
|
||||||
function whoAmI() public view returns (uint) {
|
function whoAmI(uint gameId) public view returns (uint) {
|
||||||
uint gameId = playerToActiveGame[msg.sender];
|
|
||||||
if (gameId == 0) {
|
if (gameId == 0) {
|
||||||
return 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.
|
// Return time left before the end of the revelation phase.
|
||||||
function revealTimeLeft() public view returns (int) {
|
function revealTimeLeft(uint gameId) public view returns (int) {
|
||||||
uint gameId = playerToActiveGame[msg.sender];
|
|
||||||
if (gameId == 0) return int(REVEAL_TIMEOUT);
|
if (gameId == 0) return int(REVEAL_TIMEOUT);
|
||||||
|
|
||||||
GameState storage game = games[gameId];
|
GameState storage game = games[gameId];
|
||||||
@@ -426,13 +331,6 @@ contract Game {
|
|||||||
return int(REVEAL_TIMEOUT);
|
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 ------------------------- //
|
// ------------------------- Game Management ------------------------- //
|
||||||
|
|
||||||
// Get details of a specific game (for viewing any game)
|
// Get details of a specific game (for viewing any game)
|
||||||
@@ -442,21 +340,23 @@ contract Game {
|
|||||||
public
|
public
|
||||||
view
|
view
|
||||||
returns (
|
returns (
|
||||||
address playerAAddr,
|
Player memory playerA,
|
||||||
address playerBAddr,
|
Player memory playerB,
|
||||||
uint initialBet,
|
uint initialBet,
|
||||||
Outcomes outcome,
|
Outcomes outcome,
|
||||||
bool isActive
|
bool isActive,
|
||||||
|
uint returnGameId
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
GameState storage game = games[gameId];
|
GameState storage game = games[gameId];
|
||||||
require(game.gameId != 0, "Game does not exist");
|
require(game.gameId != 0, "Game does not exist");
|
||||||
return (
|
return (
|
||||||
game.playerA.addr,
|
game.playerA,
|
||||||
game.playerB.addr,
|
game.playerB,
|
||||||
game.initialBet,
|
game.initialBet,
|
||||||
game.outcome,
|
game.outcome,
|
||||||
game.isActive
|
game.isActive,
|
||||||
|
game.gameId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -483,32 +383,4 @@ contract Game {
|
|||||||
|
|
||||||
return activeIds;
|
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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -64,15 +64,6 @@ async function main() {
|
|||||||
|
|
||||||
// Define contracts to deploy
|
// Define contracts to deploy
|
||||||
const contractsToDeploy: ContractDeploymentConfig[] = [
|
const contractsToDeploy: ContractDeploymentConfig[] = [
|
||||||
{
|
|
||||||
name: "HelloWorld",
|
|
||||||
artifactPath: "testcontract.sol/HelloWorld.json",
|
|
||||||
deployArgs: ["Hello World!"],
|
|
||||||
configKeys: {
|
|
||||||
address: "CONTRACT_ADDRESS",
|
|
||||||
abi: "ABI"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "Game",
|
name: "Game",
|
||||||
artifactPath: "Game.sol/Game.json",
|
artifactPath: "Game.sol/Game.json",
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { useState, useEffect } from "react";
|
|||||||
import Web3 from "web3";
|
import Web3 from "web3";
|
||||||
import { Button } from "./Button";
|
import { Button } from "./Button";
|
||||||
import { Input } from "./Input";
|
import { Input } from "./Input";
|
||||||
|
import { GameDetails } from "./GameModal";
|
||||||
|
import { showToast } from "@/app/lib/toast";
|
||||||
|
|
||||||
interface CommitProps {
|
interface CommitProps {
|
||||||
account: string;
|
account: string;
|
||||||
@@ -12,11 +14,12 @@ interface CommitProps {
|
|||||||
selectedMove: string | null;
|
selectedMove: string | null;
|
||||||
setSelectedMove: (move: string | null) => void;
|
setSelectedMove: (move: string | null) => void;
|
||||||
secret: string;
|
secret: string;
|
||||||
|
whoAmI: "player1" | "player2" | "";
|
||||||
|
gameDetails: GameDetails | null;
|
||||||
setSecret: (secret: string) => void;
|
setSecret: (secret: string) => void;
|
||||||
onBothPlayersCommitted?: () => void;
|
savePlayMove: (playMove: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Move = "1" | "2" | "3" | null;
|
|
||||||
type MoveName = "Rock" | "Paper" | "Scissors";
|
type MoveName = "Rock" | "Paper" | "Scissors";
|
||||||
|
|
||||||
const MOVES: Record<string, { name: MoveName; icon: string }> = {
|
const MOVES: Record<string, { name: MoveName; icon: string }> = {
|
||||||
@@ -35,22 +38,17 @@ export default function Commit({
|
|||||||
setSelectedMove,
|
setSelectedMove,
|
||||||
secret,
|
secret,
|
||||||
setSecret,
|
setSecret,
|
||||||
onBothPlayersCommitted,
|
savePlayMove,
|
||||||
|
whoAmI,
|
||||||
|
gameDetails
|
||||||
}: Readonly<CommitProps>) {
|
}: Readonly<CommitProps>) {
|
||||||
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 [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);
|
||||||
|
|
||||||
// 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
|
// Update encrypted move when move or secret changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedMove && secret) {
|
if (selectedMove && secret) {
|
||||||
@@ -58,27 +56,42 @@ export default function Commit({
|
|||||||
// Use keccak256 (Ethereum's standard hash function)
|
// Use keccak256 (Ethereum's standard hash function)
|
||||||
const hash = Web3.utils.keccak256(clearMove);
|
const hash = Web3.utils.keccak256(clearMove);
|
||||||
setPlayMove(hash);
|
setPlayMove(hash);
|
||||||
|
// Persist to sessionStorage through parent
|
||||||
|
savePlayMove(hash);
|
||||||
}
|
}
|
||||||
}, [selectedMove, secret]);
|
}, [selectedMove, secret, savePlayMove]);
|
||||||
|
|
||||||
// Auto-check if both players have committed and trigger callback
|
// Auto-check if both players have committed and trigger callback
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!contract || !account || !playMove || bothPlayed === "true") {
|
if (!contract || !account || !whoAmI || !gameDetails) {
|
||||||
// Clear interval if conditions not met or already both played
|
// Clear interval if conditions not met or already both played
|
||||||
if (autoCheckInterval) clearInterval(autoCheckInterval);
|
if (autoCheckInterval) clearInterval(autoCheckInterval);
|
||||||
setAutoCheckInterval(null);
|
setAutoCheckInterval(null);
|
||||||
return;
|
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
|
// Check immediately on mount or when dependencies change
|
||||||
const checkBothPlayed = async () => {
|
const checkBothPlayed = async () => {
|
||||||
try {
|
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) {
|
if (res) {
|
||||||
setBothPlayed("true");
|
setBothPlayed("true");
|
||||||
if (onBothPlayersCommitted) {
|
|
||||||
onBothPlayersCommitted();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("Auto-check failed:", err.message);
|
console.error("Auto-check failed:", err.message);
|
||||||
@@ -94,16 +107,15 @@ export default function Commit({
|
|||||||
return () => {
|
return () => {
|
||||||
if (interval) clearInterval(interval);
|
if (interval) clearInterval(interval);
|
||||||
};
|
};
|
||||||
}, [contract, account, playMove, bothPlayed, onBothPlayersCommitted]);
|
}, [contract, account, playMove, bothPlayed, gameDetails]);
|
||||||
|
|
||||||
// Commit phase read-only handlers
|
// Commit phase read-only handlers
|
||||||
const handlePlay = async () => {
|
const handlePlay = async () => {
|
||||||
if (!contract || !web3 || !account || !playMove) return;
|
if (!contract || !web3 || !account || !playMove) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setStatus("");
|
|
||||||
try {
|
try {
|
||||||
// playMove should be a hex string (bytes32)
|
// 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 gas = await tx.estimateGas({ from: account });
|
||||||
const result = await (globalThis as any).ethereum.request({
|
const result = await (globalThis as any).ethereum.request({
|
||||||
method: "eth_sendTransaction",
|
method: "eth_sendTransaction",
|
||||||
@@ -117,10 +129,10 @@ export default function Commit({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
setStatus("Play tx sent: " + result);
|
showToast("Play tx sent: " + result, "success");
|
||||||
setMoveSubmitted(true);
|
setMoveSubmitted(true);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setStatus("Play failed: " + err.message);
|
showToast("Play failed: " + err.message, "error");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -131,13 +143,21 @@ export default function Commit({
|
|||||||
setSecret(randomHex);
|
setSecret(randomHex);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSecretChange = (value: string) => {
|
||||||
|
setSecret(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMoveSelect = (move: string) => {
|
||||||
|
setSelectedMove(move);
|
||||||
|
};
|
||||||
|
|
||||||
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">
|
<h2 className="font-semibold text-lg mb-6 text-slate-900 dark:text-white">
|
||||||
Select Your Move
|
Select Your Move
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{moveSubmitted ? (
|
{moveSubmitted || selfPlayed === "true" ? (
|
||||||
// Waiting animation after move is submitted
|
// Waiting animation after move is submitted
|
||||||
<div className="flex flex-col items-center justify-center py-16">
|
<div className="flex flex-col items-center justify-center py-16">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
@@ -161,7 +181,7 @@ export default function Commit({
|
|||||||
{(["1", "2", "3"] as const).map((move) => (
|
{(["1", "2", "3"] as const).map((move) => (
|
||||||
<button
|
<button
|
||||||
key={move}
|
key={move}
|
||||||
onClick={() => setSelectedMove(move)}
|
onClick={() => handleMoveSelect(move)}
|
||||||
className={`flex flex-col items-center justify-center p-6 rounded-lg transition-all transform ${
|
className={`flex flex-col items-center justify-center p-6 rounded-lg transition-all transform ${
|
||||||
selectedMove === move
|
selectedMove === move
|
||||||
? "bg-blue-500 text-white shadow-lg scale-110"
|
? "bg-blue-500 text-white shadow-lg scale-110"
|
||||||
@@ -184,7 +204,7 @@ export default function Commit({
|
|||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={secret}
|
value={secret}
|
||||||
onChange={(e) => setSecret(e.target.value)}
|
onChange={(e) => handleSecretChange(e.target.value)}
|
||||||
placeholder="Your secret passphrase"
|
placeholder="Your secret passphrase"
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { useState, useEffect } from "react";
|
|||||||
import Web3 from "web3";
|
import Web3 from "web3";
|
||||||
import { Button } from "./Button";
|
import { Button } from "./Button";
|
||||||
import { Input } from "./Input";
|
import { Input } from "./Input";
|
||||||
|
import { GameDetails } from "./GameModal";
|
||||||
|
import { showSuccessToast, showErrorToast } from "@/app/lib/toast";
|
||||||
|
|
||||||
interface GameListProps {
|
interface GameListProps {
|
||||||
account: string;
|
account: string;
|
||||||
@@ -12,15 +14,6 @@ interface GameListProps {
|
|||||||
onPlayClick?: (gameId: number) => void;
|
onPlayClick?: (gameId: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GameInfo {
|
|
||||||
gameId: number;
|
|
||||||
playerA: string;
|
|
||||||
playerB: string;
|
|
||||||
initialBet: string;
|
|
||||||
isActive: boolean;
|
|
||||||
outcome: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function GameList({
|
export default function GameList({
|
||||||
account,
|
account,
|
||||||
contract,
|
contract,
|
||||||
@@ -29,7 +22,7 @@ export default function GameList({
|
|||||||
setStatus,
|
setStatus,
|
||||||
onPlayClick,
|
onPlayClick,
|
||||||
}: Readonly<GameListProps>) {
|
}: Readonly<GameListProps>) {
|
||||||
const [games, setGames] = useState<GameInfo[]>([]);
|
const [games, setGames] = useState<GameDetails[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [newGameBet, setNewGameBet] = useState<string>("0.01");
|
const [newGameBet, setNewGameBet] = useState<string>("0.01");
|
||||||
const [refreshInterval, setRefreshInterval] = useState<NodeJS.Timeout | null>(null);
|
const [refreshInterval, setRefreshInterval] = useState<NodeJS.Timeout | null>(null);
|
||||||
@@ -40,18 +33,11 @@ export default function GameList({
|
|||||||
if (!contract || !web3) return;
|
if (!contract || !web3) return;
|
||||||
try {
|
try {
|
||||||
const activeGameIds = await contract.methods.getActiveGameIds().call();
|
const activeGameIds = await contract.methods.getActiveGameIds().call();
|
||||||
const gameDetails: GameInfo[] = [];
|
const gameDetails: GameDetails[] = [];
|
||||||
|
|
||||||
for (const gameId of activeGameIds) {
|
for (const gameId of activeGameIds) {
|
||||||
const details = await contract.methods.getGameDetails(gameId).call();
|
const details = await contract.methods.getGameDetails(gameId).call();
|
||||||
gameDetails.push({
|
gameDetails.push(details);
|
||||||
gameId: Number(gameId),
|
|
||||||
playerA: details.playerAAddr,
|
|
||||||
playerB: details.playerBAddr,
|
|
||||||
initialBet: web3.utils.fromWei(details.initialBet, "ether"),
|
|
||||||
isActive: details.isActive,
|
|
||||||
outcome: Number(details.outcome),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setGames(gameDetails);
|
setGames(gameDetails);
|
||||||
@@ -60,10 +46,10 @@ export default function GameList({
|
|||||||
const userGames = new Set<number>();
|
const userGames = new Set<number>();
|
||||||
for (const game of gameDetails) {
|
for (const game of gameDetails) {
|
||||||
if (
|
if (
|
||||||
game.playerA.toLowerCase() === account.toLowerCase() ||
|
game.playerA.addr.toLowerCase() === account.toLowerCase() ||
|
||||||
game.playerB.toLowerCase() === account.toLowerCase()
|
game.playerB.addr.toLowerCase() === account.toLowerCase()
|
||||||
) {
|
) {
|
||||||
userGames.add(game.gameId);
|
userGames.add(game.returnGameId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setUserGameIds(userGames);
|
setUserGameIds(userGames);
|
||||||
@@ -90,9 +76,8 @@ export default function GameList({
|
|||||||
const handleJoinGame = async (gameId: number, bet: string) => {
|
const handleJoinGame = async (gameId: number, bet: string) => {
|
||||||
if (!contract || !web3 || !account) return;
|
if (!contract || !web3 || !account) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setStatus("");
|
|
||||||
try {
|
try {
|
||||||
const betWei = web3.utils.toWei(bet || "0.01", "ether");
|
const betWei = bet;
|
||||||
const tx = contract.methods.register(gameId);
|
const tx = contract.methods.register(gameId);
|
||||||
const gas = await tx.estimateGas({ from: account, value: betWei });
|
const gas = await tx.estimateGas({ from: account, value: betWei });
|
||||||
const result = await (globalThis as any).ethereum.request({
|
const result = await (globalThis as any).ethereum.request({
|
||||||
@@ -108,11 +93,11 @@ export default function GameList({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
setStatus("✅ Joined game! Transaction: " + result);
|
showSuccessToast("Joined game! Transaction: " + result);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
fetchActiveGames();
|
fetchActiveGames();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setStatus("❌ Failed to join game: " + err.message);
|
showErrorToast("Failed to join game: " + err.message);
|
||||||
console.error(err);
|
console.error(err);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -123,7 +108,6 @@ export default function GameList({
|
|||||||
const handleCreateGame = async () => {
|
const handleCreateGame = async () => {
|
||||||
if (!contract || !web3 || !account) return;
|
if (!contract || !web3 || !account) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setStatus("");
|
|
||||||
try {
|
try {
|
||||||
const betWei = web3.utils.toWei(newGameBet || "0.01", "ether");
|
const betWei = web3.utils.toWei(newGameBet || "0.01", "ether");
|
||||||
const tx = contract.methods.register(0); // 0 means create new game
|
const tx = contract.methods.register(0); // 0 means create new game
|
||||||
@@ -141,12 +125,12 @@ export default function GameList({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
setStatus("✅ Created new game! Transaction: " + result);
|
showSuccessToast("Created new game! Transaction: " + result);
|
||||||
setNewGameBet("0.01");
|
setNewGameBet("0.01");
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
fetchActiveGames();
|
fetchActiveGames();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setStatus("❌ Failed to create game: " + err.message);
|
showErrorToast("Failed to create game: " + err.message);
|
||||||
console.error(err);
|
console.error(err);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -158,6 +142,21 @@ export default function GameList({
|
|||||||
return `${addr.slice(0, 6)}...${addr.slice(-4)}`;
|
return `${addr.slice(0, 6)}...${addr.slice(-4)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getGamePhase = (game: GameDetails) => {
|
||||||
|
const playerARevealed = Number(game.playerA.move) !== 0;
|
||||||
|
const playerBRevealed = Number(game.playerB.move) !== 0;
|
||||||
|
const playerACommitted = Number(game.playerA.encrMove) !== 0;
|
||||||
|
const playerBCommitted = Number(game.playerB.encrMove) !== 0;
|
||||||
|
|
||||||
|
if (playerARevealed && playerBRevealed) {
|
||||||
|
return { phase: "Outcome", color: "bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200" };
|
||||||
|
} else if (playerACommitted && playerBCommitted) {
|
||||||
|
return { phase: "Reveal", color: "bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200" };
|
||||||
|
} else {
|
||||||
|
return { phase: "Commit", color: "bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200" };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Create New Game Section */}
|
{/* Create New Game Section */}
|
||||||
@@ -185,8 +184,7 @@ export default function GameList({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-slate-600 dark:text-slate-400 mt-2">
|
<p className="text-xs text-slate-600 dark:text-slate-400 mt-2">
|
||||||
Enter the bet amount in ETH (e.g., 0.01 for 0.01 ETH). The first
|
Enter the bet amount in ETH (e.g., 0.01 for 0.01 ETH).
|
||||||
player to join with the same or higher bet will play against you.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -203,90 +201,102 @@ export default function GameList({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{games.map((game) => (
|
{games.map((game) => {
|
||||||
|
const isUserInGame = userGameIds.has(game.returnGameId);
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
key={game.gameId}
|
key={game.returnGameId}
|
||||||
className="flex items-center gap-4 bg-white dark:bg-slate-700 p-4 rounded-lg shadow-sm hover:shadow-md transition-shadow border border-gray-200 dark:border-slate-600"
|
className={`flex flex-col p-4 rounded-lg shadow-sm hover:shadow-md transition-shadow border ${
|
||||||
|
isUserInGame
|
||||||
|
? "bg-green-50 dark:bg-green-900/30 border-green-300 dark:border-green-600 ring-2 ring-green-400 dark:ring-green-500"
|
||||||
|
: "bg-white dark:bg-slate-700 border-gray-200 dark:border-slate-600"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{/* Game ID */}
|
{/* Game ID Header */}
|
||||||
<div className="min-w-[80px]">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<p className="text-xs text-slate-500 dark:text-slate-400">
|
|
||||||
Game ID
|
|
||||||
</p>
|
|
||||||
<p className="font-semibold text-lg text-indigo-600 dark:text-indigo-400">
|
<p className="font-semibold text-lg text-indigo-600 dark:text-indigo-400">
|
||||||
#{game.gameId}
|
Game #{game.returnGameId}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
<div className="flex gap-3 items-center">
|
||||||
|
<span className={`text-xs font-semibold px-2 py-1 rounded ${getGamePhase(game).color}`}>
|
||||||
{/* Players Info */}
|
{getGamePhase(game).phase}
|
||||||
<div className="flex-1 min-w-[200px]">
|
</span>
|
||||||
<p className="text-xs text-slate-500 dark:text-slate-400 mb-1">
|
<p className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
Players
|
{web3 ? web3.utils.fromWei(game.initialBet, "ether") : "-"} ETH
|
||||||
</p>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<p className="font-mono text-sm text-slate-700 dark:text-slate-300">
|
|
||||||
<span className="text-xs text-slate-500">A:</span> {formatAddress(game.playerA)}
|
|
||||||
</p>
|
|
||||||
<p className="font-mono text-sm text-slate-700 dark:text-slate-300">
|
|
||||||
<span className="text-xs text-slate-500">B:</span> {game.playerB === "0x0000000000000000000000000000000000000000"
|
|
||||||
? "Waiting..."
|
|
||||||
: formatAddress(game.playerB)}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bet Amount */}
|
{/* Players VS Layout */}
|
||||||
<div className="min-w-[100px]">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<p className="text-xs text-slate-500 dark:text-slate-400">
|
{/* Player A */}
|
||||||
Bet
|
<div className="flex-1 text-center">
|
||||||
</p>
|
<p className="text-xs text-slate-500 dark:text-slate-400 mb-1 font-semibold">
|
||||||
<p className="font-semibold text-slate-900 dark:text-white">
|
Player A
|
||||||
{game.initialBet} ETH
|
</p>
|
||||||
</p>
|
<p className="font-mono text-sm text-slate-700 dark:text-slate-300 break-all">
|
||||||
|
{formatAddress(game.playerA.addr)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* VS */}
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<p className="text-xl font-bold text-slate-400 dark:text-slate-500">
|
||||||
|
VS
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Player B */}
|
||||||
|
<div className="flex-1 text-center">
|
||||||
|
<p className="text-xs text-slate-500 dark:text-slate-400 mb-1 font-semibold">
|
||||||
|
Player B
|
||||||
|
</p>
|
||||||
|
<p className="font-mono text-sm text-slate-700 dark:text-slate-300 break-all">
|
||||||
|
{game.playerB.addr === "0x0000000000000000000000000000000000000000"
|
||||||
|
? "⏳ Waiting..."
|
||||||
|
: formatAddress(game.playerB.addr)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Join/Play Button */}
|
{/* Join/Play Button */}
|
||||||
<div className="flex gap-2">
|
<div className="mt-4 flex justify-center">
|
||||||
{userGameIds.has(game.gameId) ? (
|
{userGameIds.has(game.returnGameId) ? (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => onPlayClick?.(game.gameId)}
|
onClick={() => onPlayClick?.(game.returnGameId)}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
className="whitespace-nowrap bg-emerald-600 hover:bg-emerald-500 focus-visible:outline-emerald-600"
|
className="bg-emerald-600 hover:bg-emerald-500 focus-visible:outline-emerald-600"
|
||||||
>
|
>
|
||||||
▶ Play
|
▶ Play
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleJoinGame(game.gameId, game.initialBet)
|
handleJoinGame(game.returnGameId, game.initialBet)
|
||||||
}
|
}
|
||||||
disabled={
|
disabled={
|
||||||
loading ||
|
loading ||
|
||||||
!account ||
|
!account ||
|
||||||
!contract ||
|
!contract ||
|
||||||
game.playerB !==
|
game.playerB.addr !==
|
||||||
"0x0000000000000000000000000000000000000000"
|
"0x0000000000000000000000000000000000000000"
|
||||||
}
|
}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
className="whitespace-nowrap"
|
|
||||||
>
|
>
|
||||||
{game.playerB ===
|
{game.playerB.addr ===
|
||||||
"0x0000000000000000000000000000000000000000"
|
"0x0000000000000000000000000000000000000000"
|
||||||
? "Join"
|
? "Join Game"
|
||||||
: "Full"}
|
: "Full"}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Refresh Info */}
|
|
||||||
<div className="text-center text-xs text-slate-500 dark:text-slate-400">
|
|
||||||
<p>🔄 Games refresh automatically every 2 seconds</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
249
crypto_clash_frontend/app/clash/GameModal.tsx
Normal file
249
crypto_clash_frontend/app/clash/GameModal.tsx
Normal file
@@ -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<GameModalProps>) {
|
||||||
|
const [phase, setPhase] = useState<"commit" | "reveal">("commit");
|
||||||
|
const [whoAmI, setWhoAmI] = useState<"player1" | "player2" | "">("");
|
||||||
|
const [gameDetails, setGameDetails] = useState<GameDetails | null>(null);
|
||||||
|
const [selectedMove, setSelectedMove] = useState<string | null>(null);
|
||||||
|
const [secret, setSecret] = useState<string>("");
|
||||||
|
|
||||||
|
// 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<GameStorage>) => {
|
||||||
|
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 (
|
||||||
|
<div className="fixed inset-0 bg-black/50 dark:bg-black/70 flex items-center justify-center z-50 p-4">
|
||||||
|
<div className="bg-white dark:bg-slate-800 rounded-lg shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="sticky top-0 bg-white dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700 p-6 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold text-slate-900 dark:text-white">
|
||||||
|
{gameId ? `Game #${gameId}` : "Game"}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-slate-600 dark:text-slate-400 mt-1">
|
||||||
|
{phase === "commit"
|
||||||
|
? "Commit your move"
|
||||||
|
: "Reveal your move"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleClose}
|
||||||
|
className="text-2xl text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="p-6">
|
||||||
|
|
||||||
|
{/* Phase Content */}
|
||||||
|
<div>
|
||||||
|
{phase === "commit" && (
|
||||||
|
<Commit
|
||||||
|
account={account}
|
||||||
|
contract={contract}
|
||||||
|
config={config}
|
||||||
|
web3={web3}
|
||||||
|
whoAmI={whoAmI}
|
||||||
|
gameDetails={gameDetails}
|
||||||
|
setStatus={setStatus}
|
||||||
|
selectedMove={selectedMove}
|
||||||
|
setSelectedMove={saveMoveSelection}
|
||||||
|
secret={secret}
|
||||||
|
setSecret={saveSecret}
|
||||||
|
savePlayMove={savePlayMove}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{phase === "reveal" && (
|
||||||
|
<Reveal
|
||||||
|
account={account}
|
||||||
|
contract={contract}
|
||||||
|
config={config}
|
||||||
|
web3={web3}
|
||||||
|
setStatus={setStatus}
|
||||||
|
selectedMove={selectedMove}
|
||||||
|
secret={secret}
|
||||||
|
gameDetails={gameDetails}
|
||||||
|
whoAmI={whoAmI}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import Web3 from "web3";
|
import Web3 from "web3";
|
||||||
import { Button } from "./Button";
|
import { Button } from "./Button";
|
||||||
|
import { GameDetails } from "./GameModal";
|
||||||
|
import { showSuccessToast, showErrorToast } from "@/app/lib/toast";
|
||||||
|
|
||||||
interface RevealProps {
|
interface RevealProps {
|
||||||
account: string;
|
account: string;
|
||||||
@@ -10,6 +12,8 @@ interface RevealProps {
|
|||||||
setStatus: (status: string) => void;
|
setStatus: (status: string) => void;
|
||||||
selectedMove: string | null;
|
selectedMove: string | null;
|
||||||
secret: string;
|
secret: string;
|
||||||
|
gameDetails: GameDetails | null;
|
||||||
|
whoAmI: "player1" | "player2" | "";
|
||||||
}
|
}
|
||||||
|
|
||||||
type MoveName = "Rock" | "Paper" | "Scissors";
|
type MoveName = "Rock" | "Paper" | "Scissors";
|
||||||
@@ -36,54 +40,50 @@ export default function Reveal({
|
|||||||
setStatus,
|
setStatus,
|
||||||
selectedMove,
|
selectedMove,
|
||||||
secret,
|
secret,
|
||||||
|
gameDetails,
|
||||||
|
whoAmI,
|
||||||
}: Readonly<RevealProps>) {
|
}: Readonly<RevealProps>) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [selfRevealed, setSelfRevealed] = useState(false);
|
||||||
|
const [opponentRevealed, setOpponentRevealed] = useState(false);
|
||||||
const [bothRevealed, setBothRevealed] = useState(false);
|
const [bothRevealed, setBothRevealed] = useState(false);
|
||||||
const [playerARevealed, setPlayerARevealed] = useState(false);
|
|
||||||
const [playerBRevealed, setPlayerBRevealed] = useState(false);
|
|
||||||
const [revealTimeLeft, setRevealTimeLeft] = useState<number>(0);
|
|
||||||
const [outcome, setOutcome] = useState<number>(0);
|
const [outcome, setOutcome] = useState<number>(0);
|
||||||
const [revealed, setRevealed] = useState(false);
|
|
||||||
|
|
||||||
const clearMove = selectedMove && secret ? `${selectedMove}-${secret}` : "";
|
const clearMove = selectedMove && secret ? `${selectedMove}-${secret}` : "";
|
||||||
|
|
||||||
// Check game status on mount
|
// Check game status on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkStatus = async () => {
|
const setStateFromGameDetails = () => {
|
||||||
if (!contract) return;
|
if (!gameDetails) return;
|
||||||
try {
|
const playerARevealed = Number(gameDetails.playerA.move) !== 0;
|
||||||
const [br, par, pbr, rtl, out] = await Promise.all([
|
const playerBRevealed = Number(gameDetails.playerB.move) !== 0;
|
||||||
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}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
console.log("Status:", {
|
setSelfRevealed(
|
||||||
br, par, pbr, rtl, out
|
(whoAmI === "player1" && playerARevealed) ||
|
||||||
});
|
(whoAmI === "player2" && playerBRevealed)
|
||||||
setBothRevealed(br);
|
);
|
||||||
setPlayerARevealed(par);
|
setOpponentRevealed(
|
||||||
setPlayerBRevealed(pbr);
|
(whoAmI === "player1" && playerBRevealed) ||
|
||||||
setRevealTimeLeft(Number(rtl));
|
(whoAmI === "player2" && playerARevealed)
|
||||||
setOutcome(Number(out));
|
);
|
||||||
} catch (err: any) {
|
setBothRevealed(playerARevealed && playerBRevealed);
|
||||||
console.error("Failed to check status:", err);
|
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);
|
setStateFromGameDetails();
|
||||||
checkStatus();
|
}, [gameDetails, contract, account, whoAmI]);
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, [contract, account]);
|
|
||||||
|
|
||||||
const handleReveal = async () => {
|
const handleReveal = async () => {
|
||||||
if (!contract || !web3 || !account || !clearMove) return;
|
if (!contract || !web3 || !account || !clearMove) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setStatus("");
|
|
||||||
try {
|
try {
|
||||||
const tx = contract.methods.reveal(clearMove);
|
const tx = contract.methods.reveal(gameDetails?.returnGameId, clearMove);
|
||||||
const gas = await tx.estimateGas({ from: account });
|
const gas = await tx.estimateGas({ from: account });
|
||||||
const result = await (globalThis as any).ethereum.request({
|
const result = await (globalThis as any).ethereum.request({
|
||||||
method: "eth_sendTransaction",
|
method: "eth_sendTransaction",
|
||||||
@@ -97,10 +97,9 @@ export default function Reveal({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
setStatus("✅ Reveal tx sent: " + result);
|
showSuccessToast("Reveal tx sent: " + result);
|
||||||
setRevealed(true);
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setStatus("❌ Reveal failed: " + err.message);
|
showErrorToast("Reveal failed: " + err.message);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -109,9 +108,8 @@ export default function Reveal({
|
|||||||
const handleGetOutcome = async () => {
|
const handleGetOutcome = async () => {
|
||||||
if (!contract || !web3 || !account) return;
|
if (!contract || !web3 || !account) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setStatus("");
|
|
||||||
try {
|
try {
|
||||||
const tx = contract.methods.getOutcome();
|
const tx = contract.methods.getOutcome(gameDetails?.returnGameId);
|
||||||
const gas = await tx.estimateGas({ from: account });
|
const gas = await tx.estimateGas({ from: account });
|
||||||
const result = await (globalThis as any).ethereum.request({
|
const result = await (globalThis as any).ethereum.request({
|
||||||
method: "eth_sendTransaction",
|
method: "eth_sendTransaction",
|
||||||
@@ -125,9 +123,10 @@ export default function Reveal({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
setStatus("✅ Claim tx sent: " + result);
|
showSuccessToast("Claim tx sent: " + result);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setStatus("❌ Claim failed: " + err.message);
|
console.error(err);
|
||||||
|
showErrorToast("Claim failed: " + err.message);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -137,156 +136,201 @@ export default function Reveal({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Your Move Section */}
|
{/* Your Move Section - Hidden when both revealed */}
|
||||||
<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">
|
{!bothRevealed && (
|
||||||
<h2 className="font-semibold text-lg mb-4 text-slate-900 dark:text-white">
|
<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">
|
||||||
Your Move
|
<h2 className="font-semibold text-lg mb-4 text-slate-900 dark:text-white">
|
||||||
</h2>
|
Your Move
|
||||||
{selectedMove ? (
|
</h2>
|
||||||
<div className="flex items-center justify-center gap-4">
|
{selectedMove ? (
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex items-center justify-center gap-4">
|
||||||
<span className="text-6xl mb-2">{MOVES[selectedMove].icon}</span>
|
<div className="flex flex-col items-center">
|
||||||
<span className="font-semibold text-lg">
|
<span className="text-6xl mb-2">{MOVES[selectedMove].icon}</span>
|
||||||
{MOVES[selectedMove].name}
|
<span className="font-semibold text-lg">
|
||||||
</span>
|
{MOVES[selectedMove].name}
|
||||||
</div>
|
</span>
|
||||||
<div className="text-3xl text-slate-400">→</div>
|
</div>
|
||||||
<div className="bg-white dark:bg-slate-600 p-4 rounded-lg">
|
<div className="text-3xl text-slate-400">→</div>
|
||||||
<p className="text-xs text-slate-600 dark:text-slate-300 mb-1">
|
<div className="bg-white dark:bg-slate-600 p-4 rounded-lg">
|
||||||
Clear Move:
|
<p className="text-xs text-slate-600 dark:text-slate-300 mb-1">
|
||||||
</p>
|
Clear Move:
|
||||||
<code className="text-sm font-mono text-slate-700 dark:text-slate-200">
|
</p>
|
||||||
{clearMove}
|
<code className="text-sm font-mono text-slate-700 dark:text-slate-200">
|
||||||
</code>
|
{clearMove}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-center text-slate-600 dark:text-slate-400">
|
||||||
|
No move selected yet
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</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>
|
||||||
) : (
|
<div
|
||||||
<p className="text-center text-slate-600 dark:text-slate-400">
|
className={`p-4 rounded-lg text-center ${
|
||||||
No move selected yet
|
opponentRevealed
|
||||||
</p>
|
? "bg-green-50 dark:bg-green-900"
|
||||||
)}
|
: "bg-slate-100 dark:bg-slate-700"
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Game Status Section */}
|
|
||||||
<div className="grid grid-cols-3 gap-4">
|
|
||||||
<div
|
|
||||||
className={`p-4 rounded-lg text-center ${
|
|
||||||
playerARevealed
|
|
||||||
? "bg-green-50 dark:bg-green-900"
|
|
||||||
: "bg-slate-100 dark:bg-slate-700"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<p className="text-2xl mb-1">{playerARevealed ? "✅" : "⏳"}</p>
|
|
||||||
<p className="text-xs font-semibold text-slate-600 dark:text-slate-300">
|
|
||||||
Player A
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
|
|
||||||
{playerARevealed ? "Revealed" : "Waiting"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={`p-4 rounded-lg text-center ${
|
|
||||||
playerBRevealed
|
|
||||||
? "bg-green-50 dark:bg-green-900"
|
|
||||||
: "bg-slate-100 dark:bg-slate-700"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<p className="text-2xl mb-1">{playerBRevealed ? "✅" : "⏳"}</p>
|
|
||||||
<p className="text-xs font-semibold text-slate-600 dark:text-slate-300">
|
|
||||||
Player B
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
|
|
||||||
{playerBRevealed ? "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 > 0 ? `${revealTimeLeft}s` : "Expired"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Reveal Section */}
|
|
||||||
<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 || revealed}
|
|
||||||
variant="primary"
|
|
||||||
className="w-full py-3 text-lg"
|
|
||||||
>
|
|
||||||
{loading ? "Submitting..." : revealed ? "✅ Revealed" : "Reveal Move"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Winner Section - Only show if both revealed */}
|
|
||||||
{bothRevealed && (
|
|
||||||
<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 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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 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>
|
</p>
|
||||||
<h3
|
|
||||||
className={`text-2xl font-bold mb-2 ${
|
|
||||||
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>
|
|
||||||
<Button
|
<Button
|
||||||
onClick={handleGetOutcome}
|
onClick={handleReveal}
|
||||||
disabled={loading || !account || !contract}
|
disabled={loading || !account || !contract || !clearMove || selfRevealed}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
className="mt-4 w-full py-3 text-lg"
|
className="w-full py-3 text-lg"
|
||||||
>
|
>
|
||||||
{loading ? "Processing..." : "💰 Claim Coins"}
|
{loading ? "Submitting..." : selfRevealed ? "✅ Revealed" : "Reveal Move"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Status Messages */}
|
{/* Winner Section - Only show if both revealed */}
|
||||||
{!bothRevealed && !revealed && (
|
{bothRevealed && (
|
||||||
<div className="p-4 bg-yellow-50 dark:bg-yellow-900 rounded-lg">
|
<div className="space-y-4">
|
||||||
<p className="text-sm text-yellow-800 dark:text-yellow-200">
|
{/* Moves Comparison */}
|
||||||
⏳ Waiting for both players to reveal...
|
<div className="border-2 border-slate-300 dark:border-slate-600 p-6 rounded-lg bg-slate-50 dark:bg-slate-800">
|
||||||
</p>
|
<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>
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
|
||||||
|
{/* Show Claim Coins button only on win or draw and if game is active */}
|
||||||
|
{(outcome === 1 || outcome === 3) && gameDetails?.isActive && (
|
||||||
|
<Button
|
||||||
|
onClick={handleGetOutcome}
|
||||||
|
disabled={loading || !account || !contract}
|
||||||
|
variant="primary"
|
||||||
|
className="mt-4 w-full py-3 text-lg"
|
||||||
|
>
|
||||||
|
{loading ? "Processing..." : "💰 Claim Coins"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,32 +3,26 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Web3 from "web3";
|
import Web3 from "web3";
|
||||||
import GameList from "./GameList";
|
import GameList from "./GameList";
|
||||||
import Commit from "./Commit";
|
import GameModal from "./GameModal";
|
||||||
import Reveal from "./Reveal";
|
import { showErrorToast } from "@/app/lib/toast";
|
||||||
|
|
||||||
export default function Clash() {
|
export default function Clash() {
|
||||||
const [config, setConfig] = useState<Config | null>(null);
|
const [config, setConfig] = useState<Config | null>(null);
|
||||||
const [web3, setWeb3] = useState<Web3 | null>(null);
|
const [web3, setWeb3] = useState<Web3 | null>(null);
|
||||||
const [contract, setContract] = useState<any>(null);
|
const [contract, setContract] = useState<any>(null);
|
||||||
const [account, setAccount] = useState<string>("");
|
|
||||||
const [status, setStatus] = useState<string>("");
|
const [status, setStatus] = useState<string>("");
|
||||||
|
|
||||||
// Inputs for contract functions
|
// Modal state
|
||||||
const [phase, setPhase] = useState<"games" | "commit" | "reveal">("games");
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [selectedMove, setSelectedMove] = useState<string | null>(null);
|
const [selectedGameId, setSelectedGameId] = useState<number | undefined>();
|
||||||
const [secret, setSecret] = useState<string>("");
|
|
||||||
const [availableAccounts, setAvailableAccounts] = useState<string[]>([]);
|
const [availableAccounts, setAvailableAccounts] = useState<string[]>([]);
|
||||||
const [selectedAccount, setSelectedAccount] = useState<string>("");
|
const [selectedAccount, setSelectedAccount] = useState<string>("");
|
||||||
|
|
||||||
const handlePlayClick = (gameId: number) => {
|
const handlePlayClick = (gameId: number) => {
|
||||||
setPhase("commit");
|
setSelectedGameId(gameId);
|
||||||
|
setIsModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Clear status when phase changes
|
|
||||||
useEffect(() => {
|
|
||||||
setStatus("");
|
|
||||||
}, [phase]);
|
|
||||||
|
|
||||||
// Load config and contract
|
// Load config and contract
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadConfig = async () => {
|
const loadConfig = async () => {
|
||||||
@@ -50,16 +44,13 @@ export default function Clash() {
|
|||||||
method: "eth_requestAccounts",
|
method: "eth_requestAccounts",
|
||||||
});
|
});
|
||||||
setAvailableAccounts(accounts);
|
setAvailableAccounts(accounts);
|
||||||
setAccount(accounts[0]);
|
|
||||||
setSelectedAccount(accounts[0]);
|
setSelectedAccount(accounts[0]);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setStatus(
|
showErrorToast("MetaMask not available or user denied access: " + err.message);
|
||||||
"MetaMask not available or user denied access: " + err.message
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setStatus("Failed to load config: " + err.message);
|
showErrorToast("Failed to load config: " + err.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
loadConfig();
|
loadConfig();
|
||||||
@@ -78,9 +69,7 @@ export default function Clash() {
|
|||||||
Crypto Clash
|
Crypto Clash
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-center text-slate-600 dark:text-slate-300 mb-8">
|
<p className="text-center text-slate-600 dark:text-slate-300 mb-8">
|
||||||
{phase === "games" && "Browse and join games."}
|
Browse and join games.
|
||||||
{phase === "commit" && "Commit your move."}
|
|
||||||
{phase === "reveal" && "Reveal your move."}
|
|
||||||
</p>
|
</p>
|
||||||
<div className="mb-8 p-4 bg-slate-100 dark:bg-slate-700 rounded-lg">
|
<div className="mb-8 p-4 bg-slate-100 dark:bg-slate-700 rounded-lg">
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
@@ -92,7 +81,6 @@ export default function Clash() {
|
|||||||
value={selectedAccount}
|
value={selectedAccount}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSelectedAccount(e.target.value);
|
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"
|
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}
|
{config?.GAME_CONTRACT_ADDRESS}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-center mb-6 space-x-4">
|
|
||||||
<button
|
|
||||||
onClick={() => setPhase("games")}
|
|
||||||
className={`px-4 py-2 rounded ${
|
|
||||||
phase === "games"
|
|
||||||
? "bg-blue-600 text-white"
|
|
||||||
: "bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Games
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setPhase("commit")}
|
|
||||||
className={`px-4 py-2 rounded ${
|
|
||||||
phase === "commit"
|
|
||||||
? "bg-blue-600 text-white"
|
|
||||||
: "bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Commit
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setPhase("reveal")}
|
|
||||||
className={`px-4 py-2 rounded ${
|
|
||||||
phase === "reveal"
|
|
||||||
? "bg-blue-600 text-white"
|
|
||||||
: "bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Reveal
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{phase === "games" && (
|
<GameList
|
||||||
<GameList
|
account={selectedAccount}
|
||||||
account={selectedAccount}
|
contract={contract}
|
||||||
contract={contract}
|
config={config}
|
||||||
config={config}
|
web3={web3}
|
||||||
web3={web3}
|
setStatus={setStatus}
|
||||||
setStatus={setStatus}
|
onPlayClick={handlePlayClick}
|
||||||
onPlayClick={handlePlayClick}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{phase === "commit" && (
|
|
||||||
<Commit
|
|
||||||
account={selectedAccount}
|
|
||||||
contract={contract}
|
|
||||||
config={config}
|
|
||||||
web3={web3}
|
|
||||||
setStatus={setStatus}
|
|
||||||
selectedMove={selectedMove}
|
|
||||||
setSelectedMove={setSelectedMove}
|
|
||||||
secret={secret}
|
|
||||||
setSecret={setSecret}
|
|
||||||
onBothPlayersCommitted={() => setPhase("reveal")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{phase === "reveal" && (
|
|
||||||
<Reveal
|
|
||||||
account={selectedAccount}
|
|
||||||
contract={contract}
|
|
||||||
config={config}
|
|
||||||
web3={web3}
|
|
||||||
setStatus={setStatus}
|
|
||||||
selectedMove={selectedMove}
|
|
||||||
secret={secret}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{status && (
|
|
||||||
<div
|
|
||||||
className={`mt-6 p-4 rounded-lg ${
|
|
||||||
status.includes("✅") || status.includes("tx sent")
|
|
||||||
? "bg-green-50 dark:bg-green-900 text-green-800 dark:text-green-200"
|
|
||||||
: "bg-red-50 dark:bg-red-900 text-red-800 dark:text-red-200"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<p className="text-sm break-words">{status}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="mt-8 p-4 bg-yellow-50 dark:bg-yellow-900 rounded-lg text-sm text-yellow-800 dark:text-yellow-200">
|
|
||||||
<p className="font-semibold mb-2">ℹ️ Note:</p>
|
|
||||||
<ul className="list-disc list-inside space-y-1">
|
|
||||||
<li>
|
|
||||||
MetaMask or a compatible Web3 wallet is required for write
|
|
||||||
operations
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Use bytes32 for encrypted move (see contract docs for details)
|
|
||||||
</li>
|
|
||||||
<li>ETH values are in Ether (not Wei)</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Game Modal */}
|
||||||
|
<GameModal
|
||||||
|
gameId={selectedGameId}
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
onClose={() => setIsModalOpen(false)}
|
||||||
|
account={selectedAccount}
|
||||||
|
contract={contract}
|
||||||
|
config={config}
|
||||||
|
web3={web3}
|
||||||
|
setStatus={setStatus}
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
76
crypto_clash_frontend/app/components/ToastContainer.tsx
Normal file
76
crypto_clash_frontend/app/components/ToastContainer.tsx
Normal file
@@ -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<Toast[]>([]);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="fixed bottom-4 right-4 z-50 space-y-2">
|
||||||
|
{toasts.map((toast) => (
|
||||||
|
<div
|
||||||
|
key={toast.id}
|
||||||
|
className={`border rounded-lg p-3 shadow-lg animate-fade-in ${getToastStyles(toast.type)} flex items-start gap-3 max-w-sm`}
|
||||||
|
>
|
||||||
|
<span className="text-xl flex-shrink-0">{getIcon(toast.type)}</span>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm break-words">{toast.message}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => removeToast(toast.id)}
|
||||||
|
className="flex-shrink-0 text-lg opacity-60 hover:opacity-100 transition-opacity"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -24,3 +24,20 @@ body {
|
|||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,245 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import Web3 from "web3";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
const [config, setConfig] = useState<Config | null>(null);
|
|
||||||
const [web3, setWeb3] = useState<Web3 | null>(null);
|
|
||||||
const [contract, setContract] = useState<any>(null);
|
|
||||||
const [message, setMessage] = useState<string>("");
|
|
||||||
const [newMessage, setNewMessage] = useState<string>("");
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string>("");
|
|
||||||
const [account, setAccount] = useState<string>("");
|
|
||||||
|
|
||||||
// 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 (
|
|
||||||
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-slate-900 dark:to-slate-800 font-sans">
|
|
||||||
<main className="w-full max-w-2xl mx-auto py-12 px-6">
|
|
||||||
<div className="bg-white dark:bg-slate-800 rounded-lg shadow-lg p-8">
|
|
||||||
<h1 className="text-4xl font-bold text-center mb-2 text-slate-900 dark:text-white">
|
|
||||||
Smart Contract Interaction
|
|
||||||
</h1>
|
|
||||||
<p className="text-center text-slate-600 dark:text-slate-300 mb-8">
|
|
||||||
Read and update messages on the blockchain
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Status Section */}
|
|
||||||
<div className="mb-8 p-4 bg-slate-100 dark:bg-slate-700 rounded-lg">
|
|
||||||
<p className="text-sm text-slate-600 dark:text-slate-300">
|
|
||||||
<span className="font-semibold">Connected Account:</span>{" "}
|
|
||||||
{account ? `${account.slice(0, 6)}...${account.slice(-4)}` : "Not connected"}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-slate-600 dark:text-slate-300 mt-2">
|
|
||||||
<span className="font-semibold">Contract Address:</span>{" "}
|
|
||||||
{config?.CONTRACT_ADDRESS}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Current Message Display */}
|
|
||||||
<div className="mb-8 p-6 bg-indigo-50 dark:bg-slate-700 rounded-lg border-2 border-indigo-200 dark:border-slate-600">
|
|
||||||
<h2 className="text-lg font-semibold text-slate-900 dark:text-white mb-2">
|
|
||||||
Current Message:
|
|
||||||
</h2>
|
|
||||||
{loading && !message ? (
|
|
||||||
<p className="text-slate-600 dark:text-slate-300 italic">Loading...</p>
|
|
||||||
) : message ? (
|
|
||||||
<p className="text-xl text-indigo-900 dark:text-indigo-200 break-words">
|
|
||||||
{message}
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<p className="text-slate-500 dark:text-slate-400 italic">
|
|
||||||
No message yet
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Refresh Button */}
|
|
||||||
<button
|
|
||||||
onClick={() => readMessage(contract)}
|
|
||||||
disabled={loading || !contract}
|
|
||||||
className="w-full mb-6 py-3 px-4 bg-blue-500 hover:bg-blue-600 disabled:bg-slate-400 text-white font-semibold rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
{loading ? "Loading..." : "Refresh Message"}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Update Message Form */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h2 className="text-lg font-semibold text-slate-900 dark:text-white">
|
|
||||||
Update Message:
|
|
||||||
</h2>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={newMessage}
|
|
||||||
onChange={(e) => 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"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={updateMessage}
|
|
||||||
disabled={loading || !account || !contract}
|
|
||||||
className="w-full py-3 px-4 bg-green-500 hover:bg-green-600 disabled:bg-slate-400 text-white font-semibold rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
{loading ? "Updating..." : "Update Message"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Error/Status Messages */}
|
|
||||||
{error && (
|
|
||||||
<div
|
|
||||||
className={`mt-6 p-4 rounded-lg ${
|
|
||||||
error.includes("Transaction sent")
|
|
||||||
? "bg-green-50 dark:bg-green-900 text-green-800 dark:text-green-200"
|
|
||||||
: "bg-red-50 dark:bg-red-900 text-red-800 dark:text-red-200"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<p className="text-sm break-words">{error}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Info Section */}
|
|
||||||
<div className="mt-8 p-4 bg-yellow-50 dark:bg-yellow-900 rounded-lg text-sm text-yellow-800 dark:text-yellow-200">
|
|
||||||
<p className="font-semibold mb-2">ℹ️ Note:</p>
|
|
||||||
<ul className="list-disc list-inside space-y-1">
|
|
||||||
<li>To update messages, you need MetaMask or a compatible Web3 wallet</li>
|
|
||||||
<li>Make sure your MetaMask is connected to the correct test network</li>
|
|
||||||
<li>Reading messages is free and doesn't require a wallet</li>
|
|
||||||
<li>Updates are written to the blockchain and may take time to confirm</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
import ToastContainer from "./components/ToastContainer";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
@@ -28,6 +29,7 @@ export default function RootLayout({
|
|||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
<ToastContainer />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
47
crypto_clash_frontend/app/lib/toast.ts
Normal file
47
crypto_clash_frontend/app/lib/toast.ts
Normal file
@@ -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);
|
||||||
|
}
|
||||||
@@ -15,11 +15,6 @@ export default function Home() {
|
|||||||
Crypto Clash - Battle with others
|
Crypto Clash - Battle with others
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<Link href="/hello_world" className="text-blue-600 hover:bg-gray-100 hover:text-blue-800 px-2 py-1 inline-block">
|
|
||||||
Hello World - A simple introduction
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
"type": "function"
|
"type": "function"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"GAME_CONTRACT_ADDRESS": "0x503d096a9a163180F79B1AC2F1d9F7C63f5DC75a",
|
"GAME_CONTRACT_ADDRESS": "0xAA7057A0203539d9BE86EfB471B831Dd833a9e22",
|
||||||
"GAME_ABI": [
|
"GAME_ABI": [
|
||||||
{
|
{
|
||||||
"inputs": [],
|
"inputs": [],
|
||||||
@@ -87,32 +87,6 @@
|
|||||||
"stateMutability": "view",
|
"stateMutability": "view",
|
||||||
"type": "function"
|
"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": [],
|
"inputs": [],
|
||||||
"name": "getActiveGameIds",
|
"name": "getActiveGameIds",
|
||||||
@@ -150,14 +124,68 @@
|
|||||||
"name": "getGameDetails",
|
"name": "getGameDetails",
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
"internalType": "address",
|
"components": [
|
||||||
"name": "playerAAddr",
|
{
|
||||||
"type": "address"
|
"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",
|
"components": [
|
||||||
"name": "playerBAddr",
|
{
|
||||||
"type": "address"
|
"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",
|
"internalType": "uint256",
|
||||||
@@ -173,31 +201,10 @@
|
|||||||
"internalType": "bool",
|
"internalType": "bool",
|
||||||
"name": "isActive",
|
"name": "isActive",
|
||||||
"type": "bool"
|
"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",
|
"internalType": "uint256",
|
||||||
"name": "",
|
"name": "returnGameId",
|
||||||
"type": "uint256"
|
"type": "uint256"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -205,7 +212,13 @@
|
|||||||
"type": "function"
|
"type": "function"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"inputs": [],
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": "uint256",
|
||||||
|
"name": "gameId",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
"name": "getOutcome",
|
"name": "getOutcome",
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
@@ -221,51 +234,9 @@
|
|||||||
"inputs": [
|
"inputs": [
|
||||||
{
|
{
|
||||||
"internalType": "uint256",
|
"internalType": "uint256",
|
||||||
"name": "index",
|
"name": "gameId",
|
||||||
"type": "uint256"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"name": "getPastGame",
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"internalType": "address",
|
|
||||||
"name": "playerAAddr",
|
|
||||||
"type": "address"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"internalType": "address",
|
|
||||||
"name": "playerBAddr",
|
|
||||||
"type": "address"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"internalType": "uint256",
|
|
||||||
"name": "initialBet",
|
|
||||||
"type": "uint256"
|
"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",
|
"internalType": "bytes32",
|
||||||
"name": "encrMove",
|
"name": "encrMove",
|
||||||
@@ -283,32 +254,6 @@
|
|||||||
"stateMutability": "nonpayable",
|
"stateMutability": "nonpayable",
|
||||||
"type": "function"
|
"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": [
|
"inputs": [
|
||||||
{
|
{
|
||||||
@@ -335,6 +280,11 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"inputs": [
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": "uint256",
|
||||||
|
"name": "gameId",
|
||||||
|
"type": "uint256"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"internalType": "string",
|
"internalType": "string",
|
||||||
"name": "clearMove",
|
"name": "clearMove",
|
||||||
@@ -353,7 +303,13 @@
|
|||||||
"type": "function"
|
"type": "function"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"inputs": [],
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": "uint256",
|
||||||
|
"name": "gameId",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
"name": "revealTimeLeft",
|
"name": "revealTimeLeft",
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
@@ -366,7 +322,13 @@
|
|||||||
"type": "function"
|
"type": "function"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"inputs": [],
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": "uint256",
|
||||||
|
"name": "gameId",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
"name": "whoAmI",
|
"name": "whoAmI",
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user