mirror of
https://github.com/averel10/crypto_clash.git
synced 2026-03-12 10:58: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:
212
config.json
212
config.json
@@ -59,7 +59,7 @@
|
||||
"type": "function"
|
||||
}
|
||||
],
|
||||
"GAME_CONTRACT_ADDRESS": "0x503d096a9a163180F79B1AC2F1d9F7C63f5DC75a",
|
||||
"GAME_CONTRACT_ADDRESS": "0xAA7057A0203539d9BE86EfB471B831Dd833a9e22",
|
||||
"GAME_ABI": [
|
||||
{
|
||||
"inputs": [],
|
||||
@@ -87,32 +87,6 @@
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "bothPlayed",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "bothRevealed",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "getActiveGameIds",
|
||||
@@ -150,15 +124,69 @@
|
||||
"name": "getGameDetails",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "playerAAddr",
|
||||
"components": [
|
||||
{
|
||||
"internalType": "address payable",
|
||||
"name": "addr",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "playerBAddr",
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"components": [
|
||||
{
|
||||
"internalType": "address payable",
|
||||
"name": "addr",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "bet",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "encrMove",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "enum Game.Moves",
|
||||
"name": "move",
|
||||
"type": "uint8"
|
||||
},
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "nickname",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"internalType": "struct Game.Player",
|
||||
"name": "playerB",
|
||||
"type": "tuple"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "initialBet",
|
||||
@@ -173,31 +201,10 @@
|
||||
"internalType": "bool",
|
||||
"name": "isActive",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "getLastWinner",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "enum Game.Outcomes",
|
||||
"name": "",
|
||||
"type": "uint8"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "getMyActiveGameId",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"name": "returnGameId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
@@ -205,7 +212,13 @@
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "gameId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "getOutcome",
|
||||
"outputs": [
|
||||
{
|
||||
@@ -221,51 +234,9 @@
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "index",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "getPastGame",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "playerAAddr",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "playerBAddr",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "initialBet",
|
||||
"name": "gameId",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "enum Game.Outcomes",
|
||||
"name": "outcome",
|
||||
"type": "uint8"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "getPastGamesCount",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "encrMove",
|
||||
@@ -283,32 +254,6 @@
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "playerARevealed",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "playerBRevealed",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
@@ -335,6 +280,11 @@
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "gameId",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "clearMove",
|
||||
@@ -353,7 +303,13 @@
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "gameId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "revealTimeLeft",
|
||||
"outputs": [
|
||||
{
|
||||
@@ -366,7 +322,13 @@
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "gameId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "whoAmI",
|
||||
"outputs": [
|
||||
{
|
||||
|
||||
@@ -38,9 +38,6 @@ contract Game {
|
||||
bool isActive;
|
||||
}
|
||||
|
||||
// Mapping from player address to their active game ID
|
||||
mapping(address => uint) private playerToActiveGame;
|
||||
|
||||
// Mapping from game ID to game state
|
||||
mapping(uint => GameState) private games;
|
||||
|
||||
@@ -50,9 +47,6 @@ contract Game {
|
||||
// Counter for generating unique game IDs
|
||||
uint private nextGameId = 1;
|
||||
|
||||
// Array to store completed games
|
||||
GameState[] private pastGames;
|
||||
|
||||
// ------------------------- Registration ------------------------- //
|
||||
|
||||
modifier validBet(uint gameId) {
|
||||
@@ -65,14 +59,6 @@ contract Game {
|
||||
_;
|
||||
}
|
||||
|
||||
modifier notAlreadyInGame() {
|
||||
require(
|
||||
playerToActiveGame[msg.sender] == 0,
|
||||
"Player already in an active game"
|
||||
);
|
||||
_;
|
||||
}
|
||||
|
||||
// Register a player to an existing game or create a new game.
|
||||
// If gameId is 0, player will join or create the first available game.
|
||||
// Return player's ID and game ID upon successful registration.
|
||||
@@ -82,12 +68,11 @@ contract Game {
|
||||
public
|
||||
payable
|
||||
validBet(gameId)
|
||||
notAlreadyInGame
|
||||
returns (uint playerId, uint returnGameId)
|
||||
{
|
||||
// If gameId is 0, find an open game or create a new one
|
||||
if (gameId == 0) {
|
||||
gameId = findOrCreateGame();
|
||||
gameId = createNewGame();
|
||||
}
|
||||
|
||||
require(games[gameId].isActive, "Game is not active");
|
||||
@@ -97,7 +82,6 @@ contract Game {
|
||||
if (game.playerA.addr == address(0x0)) {
|
||||
game.playerA.addr = payable(msg.sender);
|
||||
game.initialBet = msg.value;
|
||||
playerToActiveGame[msg.sender] = gameId;
|
||||
return (1, gameId);
|
||||
} else if (game.playerB.addr == address(0x0)) {
|
||||
require(
|
||||
@@ -105,31 +89,12 @@ contract Game {
|
||||
"Cannot play against yourself"
|
||||
);
|
||||
game.playerB.addr = payable(msg.sender);
|
||||
playerToActiveGame[msg.sender] = gameId;
|
||||
return (2, gameId);
|
||||
}
|
||||
|
||||
revert("Game is full");
|
||||
}
|
||||
|
||||
// Find an open game or create a new one
|
||||
function findOrCreateGame() private returns (uint) {
|
||||
// Look for a game with only one player
|
||||
for (uint i = 0; i < gameIds.length; i++) {
|
||||
uint gId = gameIds[i];
|
||||
GameState storage game = games[gId];
|
||||
if (
|
||||
game.isActive &&
|
||||
game.playerA.addr != address(0x0) &&
|
||||
game.playerB.addr == address(0x0)
|
||||
) {
|
||||
return gId;
|
||||
}
|
||||
}
|
||||
|
||||
// No open game found, create a new one
|
||||
return createNewGame();
|
||||
}
|
||||
|
||||
// Create a new game
|
||||
function createNewGame() private returns (uint) {
|
||||
@@ -145,9 +110,8 @@ contract Game {
|
||||
|
||||
// ------------------------- Commit ------------------------- //
|
||||
|
||||
modifier isRegistered() {
|
||||
uint gameId = playerToActiveGame[msg.sender];
|
||||
require(gameId != 0, "Player not in any active game");
|
||||
modifier isRegistered(uint gameId) {
|
||||
require(gameId != 0, "Invalid game ID");
|
||||
require(
|
||||
msg.sender == games[gameId].playerA.addr ||
|
||||
msg.sender == games[gameId].playerB.addr,
|
||||
@@ -158,8 +122,7 @@ contract Game {
|
||||
|
||||
// Save player's encrypted move. encrMove must be "<1|2|3>-password" hashed with sha256.
|
||||
// Return 'true' if move was valid, 'false' otherwise.
|
||||
function play(bytes32 encrMove) public isRegistered returns (bool) {
|
||||
uint gameId = playerToActiveGame[msg.sender];
|
||||
function play(uint gameId, bytes32 encrMove) public isRegistered(gameId) returns (bool) {
|
||||
GameState storage game = games[gameId];
|
||||
|
||||
// Basic sanity checks with explicit errors to help debugging
|
||||
@@ -185,9 +148,8 @@ contract Game {
|
||||
|
||||
// ------------------------- Reveal ------------------------- //
|
||||
|
||||
modifier commitPhaseEnded() {
|
||||
uint gameId = playerToActiveGame[msg.sender];
|
||||
require(gameId != 0, "Player not in any active game");
|
||||
modifier commitPhaseEnded(uint gameId) {
|
||||
require(gameId != 0, "Invalid game ID");
|
||||
require(
|
||||
games[gameId].playerA.encrMove != bytes32(0) &&
|
||||
games[gameId].playerB.encrMove != bytes32(0),
|
||||
@@ -199,9 +161,9 @@ contract Game {
|
||||
// Compare clear move given by the player with saved encrypted move.
|
||||
// Return clear move upon success, 'Moves.None' otherwise.
|
||||
function reveal(
|
||||
uint gameId,
|
||||
string memory clearMove
|
||||
) public isRegistered commitPhaseEnded returns (Moves) {
|
||||
uint gameId = playerToActiveGame[msg.sender];
|
||||
) public isRegistered(gameId) commitPhaseEnded(gameId) returns (Moves) {
|
||||
GameState storage game = games[gameId];
|
||||
|
||||
bytes32 encrMove = keccak256(abi.encodePacked(clearMove)); // Hash of clear input (= "move-password")
|
||||
@@ -273,9 +235,8 @@ contract Game {
|
||||
|
||||
// ------------------------- Result ------------------------- //
|
||||
|
||||
modifier revealPhaseEnded() {
|
||||
uint gameId = playerToActiveGame[msg.sender];
|
||||
require(gameId != 0, "Player not in any active game");
|
||||
modifier revealPhaseEnded(uint gameId) {
|
||||
require(gameId != 0, "Invalid game ID");
|
||||
require(
|
||||
(games[gameId].playerA.move != Moves.None &&
|
||||
games[gameId].playerB.move != Moves.None) ||
|
||||
@@ -289,8 +250,7 @@ contract Game {
|
||||
|
||||
// Compute the outcome and pay the winner(s).
|
||||
// Return the outcome.
|
||||
function getOutcome() public revealPhaseEnded returns (Outcomes) {
|
||||
uint gameId = playerToActiveGame[msg.sender];
|
||||
function getOutcome(uint gameId) public revealPhaseEnded(gameId) returns (Outcomes) {
|
||||
GameState storage game = games[gameId];
|
||||
|
||||
require(
|
||||
@@ -302,9 +262,6 @@ contract Game {
|
||||
address payable addrB = game.playerB.addr;
|
||||
uint betPlayerA = game.initialBet;
|
||||
|
||||
// Move game to past games before resetting
|
||||
pastGames.push(game);
|
||||
|
||||
// Reset and cleanup
|
||||
resetGame(gameId); // Reset game before paying to avoid reentrancy attacks
|
||||
pay(addrA, addrB, betPlayerA, game.outcome);
|
||||
@@ -320,12 +277,12 @@ contract Game {
|
||||
Outcomes outcome
|
||||
) private {
|
||||
if (outcome == Outcomes.PlayerA) {
|
||||
addrA.transfer(address(this).balance);
|
||||
addrA.transfer(betPlayerA * 2);
|
||||
} else if (outcome == Outcomes.PlayerB) {
|
||||
addrB.transfer(address(this).balance);
|
||||
addrB.transfer(betPlayerA * 2);
|
||||
} else {
|
||||
addrA.transfer(betPlayerA);
|
||||
addrB.transfer(address(this).balance);
|
||||
addrB.transfer(betPlayerA);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,14 +290,6 @@ contract Game {
|
||||
function resetGame(uint gameId) private {
|
||||
GameState storage game = games[gameId];
|
||||
|
||||
// Clear player mappings
|
||||
if (game.playerA.addr != address(0x0)) {
|
||||
playerToActiveGame[game.playerA.addr] = 0;
|
||||
}
|
||||
if (game.playerB.addr != address(0x0)) {
|
||||
playerToActiveGame[game.playerB.addr] = 0;
|
||||
}
|
||||
|
||||
// Mark game as inactive
|
||||
game.isActive = false;
|
||||
|
||||
@@ -356,8 +305,7 @@ contract Game {
|
||||
}
|
||||
|
||||
// Return player's ID in their active game
|
||||
function whoAmI() public view returns (uint) {
|
||||
uint gameId = playerToActiveGame[msg.sender];
|
||||
function whoAmI(uint gameId) public view returns (uint) {
|
||||
if (gameId == 0) {
|
||||
return 0;
|
||||
}
|
||||
@@ -372,51 +320,8 @@ contract Game {
|
||||
}
|
||||
}
|
||||
|
||||
// Get the active game ID for the caller
|
||||
function getMyActiveGameId() public view returns (uint) {
|
||||
return playerToActiveGame[msg.sender];
|
||||
}
|
||||
|
||||
// Return 'true' if both players have commited a move, 'false' otherwise.
|
||||
function bothPlayed() public view returns (bool) {
|
||||
uint gameId = playerToActiveGame[msg.sender];
|
||||
if (gameId == 0) return false;
|
||||
|
||||
GameState storage game = games[gameId];
|
||||
return (game.playerA.encrMove != bytes32(0) && game.playerB.encrMove != bytes32(0));
|
||||
}
|
||||
|
||||
// Return 'true' if both players have revealed their move, 'false' otherwise.
|
||||
function bothRevealed() public view returns (bool) {
|
||||
uint gameId = playerToActiveGame[msg.sender];
|
||||
if (gameId == 0) return false;
|
||||
|
||||
GameState storage game = games[gameId];
|
||||
return (game.playerA.move != Moves.None &&
|
||||
game.playerB.move != Moves.None);
|
||||
}
|
||||
|
||||
// Return 'true' if player A has revealed their move, 'false' otherwise.
|
||||
function playerARevealed() public view returns (bool) {
|
||||
uint gameId = playerToActiveGame[msg.sender];
|
||||
if (gameId == 0) return false;
|
||||
|
||||
GameState storage game = games[gameId];
|
||||
return (game.playerA.move != Moves.None);
|
||||
}
|
||||
|
||||
// Return 'true' if player B has revealed their move, 'false' otherwise.
|
||||
function playerBRevealed() public view returns (bool) {
|
||||
uint gameId = playerToActiveGame[msg.sender];
|
||||
if (gameId == 0) return false;
|
||||
|
||||
GameState storage game = games[gameId];
|
||||
return (game.playerB.move != Moves.None);
|
||||
}
|
||||
|
||||
// Return time left before the end of the revelation phase.
|
||||
function revealTimeLeft() public view returns (int) {
|
||||
uint gameId = playerToActiveGame[msg.sender];
|
||||
function revealTimeLeft(uint gameId) public view returns (int) {
|
||||
if (gameId == 0) return int(REVEAL_TIMEOUT);
|
||||
|
||||
GameState storage game = games[gameId];
|
||||
@@ -426,13 +331,6 @@ contract Game {
|
||||
return int(REVEAL_TIMEOUT);
|
||||
}
|
||||
|
||||
function getLastWinner() public view returns (Outcomes) {
|
||||
uint gameId = playerToActiveGame[msg.sender];
|
||||
if (gameId == 0) return Outcomes.None;
|
||||
|
||||
return games[gameId].outcome;
|
||||
}
|
||||
|
||||
// ------------------------- Game Management ------------------------- //
|
||||
|
||||
// Get details of a specific game (for viewing any game)
|
||||
@@ -442,21 +340,23 @@ contract Game {
|
||||
public
|
||||
view
|
||||
returns (
|
||||
address playerAAddr,
|
||||
address playerBAddr,
|
||||
Player memory playerA,
|
||||
Player memory playerB,
|
||||
uint initialBet,
|
||||
Outcomes outcome,
|
||||
bool isActive
|
||||
bool isActive,
|
||||
uint returnGameId
|
||||
)
|
||||
{
|
||||
GameState storage game = games[gameId];
|
||||
require(game.gameId != 0, "Game does not exist");
|
||||
return (
|
||||
game.playerA.addr,
|
||||
game.playerB.addr,
|
||||
game.playerA,
|
||||
game.playerB,
|
||||
game.initialBet,
|
||||
game.outcome,
|
||||
game.isActive
|
||||
game.isActive,
|
||||
game.gameId
|
||||
);
|
||||
}
|
||||
|
||||
@@ -483,32 +383,4 @@ contract Game {
|
||||
|
||||
return activeIds;
|
||||
}
|
||||
|
||||
// Get number of past games
|
||||
function getPastGamesCount() public view returns (uint) {
|
||||
return pastGames.length;
|
||||
}
|
||||
|
||||
// Get details of a past game by index
|
||||
function getPastGame(
|
||||
uint index
|
||||
)
|
||||
public
|
||||
view
|
||||
returns (
|
||||
address playerAAddr,
|
||||
address playerBAddr,
|
||||
uint initialBet,
|
||||
Outcomes outcome
|
||||
)
|
||||
{
|
||||
require(index < pastGames.length, "Index out of bounds");
|
||||
GameState storage game = pastGames[index];
|
||||
return (
|
||||
game.playerA.addr,
|
||||
game.playerB.addr,
|
||||
game.initialBet,
|
||||
game.outcome
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
const contractsToDeploy: ContractDeploymentConfig[] = [
|
||||
{
|
||||
name: "HelloWorld",
|
||||
artifactPath: "testcontract.sol/HelloWorld.json",
|
||||
deployArgs: ["Hello World!"],
|
||||
configKeys: {
|
||||
address: "CONTRACT_ADDRESS",
|
||||
abi: "ABI"
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Game",
|
||||
artifactPath: "Game.sol/Game.json",
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useState, useEffect } from "react";
|
||||
import Web3 from "web3";
|
||||
import { Button } from "./Button";
|
||||
import { Input } from "./Input";
|
||||
import { GameDetails } from "./GameModal";
|
||||
import { showToast } from "@/app/lib/toast";
|
||||
|
||||
interface CommitProps {
|
||||
account: string;
|
||||
@@ -12,11 +14,12 @@ interface CommitProps {
|
||||
selectedMove: string | null;
|
||||
setSelectedMove: (move: string | null) => void;
|
||||
secret: string;
|
||||
whoAmI: "player1" | "player2" | "";
|
||||
gameDetails: GameDetails | null;
|
||||
setSecret: (secret: string) => void;
|
||||
onBothPlayersCommitted?: () => void;
|
||||
savePlayMove: (playMove: string) => void;
|
||||
}
|
||||
|
||||
type Move = "1" | "2" | "3" | null;
|
||||
type MoveName = "Rock" | "Paper" | "Scissors";
|
||||
|
||||
const MOVES: Record<string, { name: MoveName; icon: string }> = {
|
||||
@@ -35,22 +38,17 @@ export default function Commit({
|
||||
setSelectedMove,
|
||||
secret,
|
||||
setSecret,
|
||||
onBothPlayersCommitted,
|
||||
savePlayMove,
|
||||
whoAmI,
|
||||
gameDetails
|
||||
}: Readonly<CommitProps>) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [playMove, setPlayMove] = useState<string>("");
|
||||
const [selfPlayed, setSelfPlayed] = useState<string>("");
|
||||
const [bothPlayed, setBothPlayed] = useState<string>("");
|
||||
const [autoCheckInterval, setAutoCheckInterval] = useState<NodeJS.Timeout | null>(null);
|
||||
const [moveSubmitted, setMoveSubmitted] = useState(false);
|
||||
|
||||
// Generate random secret on mount if not already set
|
||||
useEffect(() => {
|
||||
if (!secret) {
|
||||
const randomHex = Math.random().toString(16).slice(2, 18);
|
||||
setSecret(randomHex);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Update encrypted move when move or secret changes
|
||||
useEffect(() => {
|
||||
if (selectedMove && secret) {
|
||||
@@ -58,27 +56,42 @@ export default function Commit({
|
||||
// Use keccak256 (Ethereum's standard hash function)
|
||||
const hash = Web3.utils.keccak256(clearMove);
|
||||
setPlayMove(hash);
|
||||
// Persist to sessionStorage through parent
|
||||
savePlayMove(hash);
|
||||
}
|
||||
}, [selectedMove, secret]);
|
||||
}, [selectedMove, secret, savePlayMove]);
|
||||
|
||||
// Auto-check if both players have committed and trigger callback
|
||||
useEffect(() => {
|
||||
if (!contract || !account || !playMove || bothPlayed === "true") {
|
||||
if (!contract || !account || !whoAmI || !gameDetails) {
|
||||
// Clear interval if conditions not met or already both played
|
||||
if (autoCheckInterval) clearInterval(autoCheckInterval);
|
||||
setAutoCheckInterval(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const checkSelfPlayed = async () => {
|
||||
try {
|
||||
const encrMove = gameDetails[whoAmI === "player1" ? "playerA" : "playerB"].encrMove;
|
||||
|
||||
setSelfPlayed(Number(encrMove) !== 0 ? "true" : "false");
|
||||
} catch (err: any) {
|
||||
console.error("Auto-check self played failed:", err.message);
|
||||
}
|
||||
};
|
||||
|
||||
checkSelfPlayed();
|
||||
|
||||
// Check immediately on mount or when dependencies change
|
||||
const checkBothPlayed = async () => {
|
||||
try {
|
||||
const res = await contract.methods.bothPlayed().call({ from: account });
|
||||
const playerAEncrMove = gameDetails.playerA.encrMove;
|
||||
const playerBEncrMove = gameDetails.playerB.encrMove;
|
||||
|
||||
const res = Number(playerAEncrMove) !== 0 && Number(playerBEncrMove) !== 0;
|
||||
console.log("Both played check:", res);
|
||||
if (res) {
|
||||
setBothPlayed("true");
|
||||
if (onBothPlayersCommitted) {
|
||||
onBothPlayersCommitted();
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("Auto-check failed:", err.message);
|
||||
@@ -94,16 +107,15 @@ export default function Commit({
|
||||
return () => {
|
||||
if (interval) clearInterval(interval);
|
||||
};
|
||||
}, [contract, account, playMove, bothPlayed, onBothPlayersCommitted]);
|
||||
}, [contract, account, playMove, bothPlayed, gameDetails]);
|
||||
|
||||
// Commit phase read-only handlers
|
||||
const handlePlay = async () => {
|
||||
if (!contract || !web3 || !account || !playMove) return;
|
||||
setLoading(true);
|
||||
setStatus("");
|
||||
try {
|
||||
// playMove should be a hex string (bytes32)
|
||||
const tx = contract.methods.play(playMove);
|
||||
const tx = contract.methods.play(gameDetails?.returnGameId, playMove);
|
||||
const gas = await tx.estimateGas({ from: account });
|
||||
const result = await (globalThis as any).ethereum.request({
|
||||
method: "eth_sendTransaction",
|
||||
@@ -117,10 +129,10 @@ export default function Commit({
|
||||
},
|
||||
],
|
||||
});
|
||||
setStatus("Play tx sent: " + result);
|
||||
showToast("Play tx sent: " + result, "success");
|
||||
setMoveSubmitted(true);
|
||||
} catch (err: any) {
|
||||
setStatus("Play failed: " + err.message);
|
||||
showToast("Play failed: " + err.message, "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -131,13 +143,21 @@ export default function Commit({
|
||||
setSecret(randomHex);
|
||||
};
|
||||
|
||||
const handleSecretChange = (value: string) => {
|
||||
setSecret(value);
|
||||
};
|
||||
|
||||
const handleMoveSelect = (move: string) => {
|
||||
setSelectedMove(move);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border p-6 rounded-lg bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-700 dark:to-slate-800">
|
||||
<h2 className="font-semibold text-lg mb-6 text-slate-900 dark:text-white">
|
||||
Select Your Move
|
||||
</h2>
|
||||
|
||||
{moveSubmitted ? (
|
||||
{moveSubmitted || selfPlayed === "true" ? (
|
||||
// Waiting animation after move is submitted
|
||||
<div className="flex flex-col items-center justify-center py-16">
|
||||
<div className="mb-6">
|
||||
@@ -161,7 +181,7 @@ export default function Commit({
|
||||
{(["1", "2", "3"] as const).map((move) => (
|
||||
<button
|
||||
key={move}
|
||||
onClick={() => setSelectedMove(move)}
|
||||
onClick={() => handleMoveSelect(move)}
|
||||
className={`flex flex-col items-center justify-center p-6 rounded-lg transition-all transform ${
|
||||
selectedMove === move
|
||||
? "bg-blue-500 text-white shadow-lg scale-110"
|
||||
@@ -184,7 +204,7 @@ export default function Commit({
|
||||
<Input
|
||||
type="text"
|
||||
value={secret}
|
||||
onChange={(e) => setSecret(e.target.value)}
|
||||
onChange={(e) => handleSecretChange(e.target.value)}
|
||||
placeholder="Your secret passphrase"
|
||||
className="flex-1"
|
||||
/>
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useState, useEffect } from "react";
|
||||
import Web3 from "web3";
|
||||
import { Button } from "./Button";
|
||||
import { Input } from "./Input";
|
||||
import { GameDetails } from "./GameModal";
|
||||
import { showSuccessToast, showErrorToast } from "@/app/lib/toast";
|
||||
|
||||
interface GameListProps {
|
||||
account: string;
|
||||
@@ -12,15 +14,6 @@ interface GameListProps {
|
||||
onPlayClick?: (gameId: number) => void;
|
||||
}
|
||||
|
||||
interface GameInfo {
|
||||
gameId: number;
|
||||
playerA: string;
|
||||
playerB: string;
|
||||
initialBet: string;
|
||||
isActive: boolean;
|
||||
outcome: number;
|
||||
}
|
||||
|
||||
export default function GameList({
|
||||
account,
|
||||
contract,
|
||||
@@ -29,7 +22,7 @@ export default function GameList({
|
||||
setStatus,
|
||||
onPlayClick,
|
||||
}: Readonly<GameListProps>) {
|
||||
const [games, setGames] = useState<GameInfo[]>([]);
|
||||
const [games, setGames] = useState<GameDetails[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [newGameBet, setNewGameBet] = useState<string>("0.01");
|
||||
const [refreshInterval, setRefreshInterval] = useState<NodeJS.Timeout | null>(null);
|
||||
@@ -40,18 +33,11 @@ export default function GameList({
|
||||
if (!contract || !web3) return;
|
||||
try {
|
||||
const activeGameIds = await contract.methods.getActiveGameIds().call();
|
||||
const gameDetails: GameInfo[] = [];
|
||||
const gameDetails: GameDetails[] = [];
|
||||
|
||||
for (const gameId of activeGameIds) {
|
||||
const details = await contract.methods.getGameDetails(gameId).call();
|
||||
gameDetails.push({
|
||||
gameId: Number(gameId),
|
||||
playerA: details.playerAAddr,
|
||||
playerB: details.playerBAddr,
|
||||
initialBet: web3.utils.fromWei(details.initialBet, "ether"),
|
||||
isActive: details.isActive,
|
||||
outcome: Number(details.outcome),
|
||||
});
|
||||
gameDetails.push(details);
|
||||
}
|
||||
|
||||
setGames(gameDetails);
|
||||
@@ -60,10 +46,10 @@ export default function GameList({
|
||||
const userGames = new Set<number>();
|
||||
for (const game of gameDetails) {
|
||||
if (
|
||||
game.playerA.toLowerCase() === account.toLowerCase() ||
|
||||
game.playerB.toLowerCase() === account.toLowerCase()
|
||||
game.playerA.addr.toLowerCase() === account.toLowerCase() ||
|
||||
game.playerB.addr.toLowerCase() === account.toLowerCase()
|
||||
) {
|
||||
userGames.add(game.gameId);
|
||||
userGames.add(game.returnGameId);
|
||||
}
|
||||
}
|
||||
setUserGameIds(userGames);
|
||||
@@ -90,9 +76,8 @@ export default function GameList({
|
||||
const handleJoinGame = async (gameId: number, bet: string) => {
|
||||
if (!contract || !web3 || !account) return;
|
||||
setLoading(true);
|
||||
setStatus("");
|
||||
try {
|
||||
const betWei = web3.utils.toWei(bet || "0.01", "ether");
|
||||
const betWei = bet;
|
||||
const tx = contract.methods.register(gameId);
|
||||
const gas = await tx.estimateGas({ from: account, value: betWei });
|
||||
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));
|
||||
fetchActiveGames();
|
||||
} catch (err: any) {
|
||||
setStatus("❌ Failed to join game: " + err.message);
|
||||
showErrorToast("Failed to join game: " + err.message);
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -123,7 +108,6 @@ export default function GameList({
|
||||
const handleCreateGame = async () => {
|
||||
if (!contract || !web3 || !account) return;
|
||||
setLoading(true);
|
||||
setStatus("");
|
||||
try {
|
||||
const betWei = web3.utils.toWei(newGameBet || "0.01", "ether");
|
||||
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");
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
fetchActiveGames();
|
||||
} catch (err: any) {
|
||||
setStatus("❌ Failed to create game: " + err.message);
|
||||
showErrorToast("Failed to create game: " + err.message);
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -158,6 +142,21 @@ export default function GameList({
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
{/* Create New Game Section */}
|
||||
@@ -185,8 +184,7 @@ export default function GameList({
|
||||
</Button>
|
||||
</div>
|
||||
<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
|
||||
player to join with the same or higher bet will play against you.
|
||||
Enter the bet amount in ETH (e.g., 0.01 for 0.01 ETH).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -203,90 +201,102 @@ export default function GameList({
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{games.map((game) => (
|
||||
{games.map((game) => {
|
||||
const isUserInGame = userGameIds.has(game.returnGameId);
|
||||
return (
|
||||
<div
|
||||
key={game.gameId}
|
||||
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"
|
||||
key={game.returnGameId}
|
||||
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 */}
|
||||
<div className="min-w-[80px]">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||
Game ID
|
||||
</p>
|
||||
{/* Game ID Header */}
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<p className="font-semibold text-lg text-indigo-600 dark:text-indigo-400">
|
||||
#{game.gameId}
|
||||
Game #{game.returnGameId}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Players Info */}
|
||||
<div className="flex-1 min-w-[200px]">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mb-1">
|
||||
Players
|
||||
</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)}
|
||||
<div className="flex gap-3 items-center">
|
||||
<span className={`text-xs font-semibold px-2 py-1 rounded ${getGamePhase(game).color}`}>
|
||||
{getGamePhase(game).phase}
|
||||
</span>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-400">
|
||||
{web3 ? web3.utils.fromWei(game.initialBet, "ether") : "-"} ETH
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bet Amount */}
|
||||
<div className="min-w-[100px]">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||
Bet
|
||||
{/* Players VS Layout */}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
{/* Player A */}
|
||||
<div className="flex-1 text-center">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mb-1 font-semibold">
|
||||
Player A
|
||||
</p>
|
||||
<p className="font-semibold text-slate-900 dark:text-white">
|
||||
{game.initialBet} ETH
|
||||
<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>
|
||||
|
||||
{/* Join/Play Button */}
|
||||
<div className="flex gap-2">
|
||||
{userGameIds.has(game.gameId) ? (
|
||||
<div className="mt-4 flex justify-center">
|
||||
{userGameIds.has(game.returnGameId) ? (
|
||||
<Button
|
||||
onClick={() => onPlayClick?.(game.gameId)}
|
||||
onClick={() => onPlayClick?.(game.returnGameId)}
|
||||
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
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() =>
|
||||
handleJoinGame(game.gameId, game.initialBet)
|
||||
handleJoinGame(game.returnGameId, game.initialBet)
|
||||
}
|
||||
disabled={
|
||||
loading ||
|
||||
!account ||
|
||||
!contract ||
|
||||
game.playerB !==
|
||||
game.playerB.addr !==
|
||||
"0x0000000000000000000000000000000000000000"
|
||||
}
|
||||
variant="primary"
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
{game.playerB ===
|
||||
{game.playerB.addr ===
|
||||
"0x0000000000000000000000000000000000000000"
|
||||
? "Join"
|
||||
? "Join Game"
|
||||
: "Full"}
|
||||
</Button>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
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 Web3 from "web3";
|
||||
import { Button } from "./Button";
|
||||
import { GameDetails } from "./GameModal";
|
||||
import { showSuccessToast, showErrorToast } from "@/app/lib/toast";
|
||||
|
||||
interface RevealProps {
|
||||
account: string;
|
||||
@@ -10,6 +12,8 @@ interface RevealProps {
|
||||
setStatus: (status: string) => void;
|
||||
selectedMove: string | null;
|
||||
secret: string;
|
||||
gameDetails: GameDetails | null;
|
||||
whoAmI: "player1" | "player2" | "";
|
||||
}
|
||||
|
||||
type MoveName = "Rock" | "Paper" | "Scissors";
|
||||
@@ -36,54 +40,50 @@ export default function Reveal({
|
||||
setStatus,
|
||||
selectedMove,
|
||||
secret,
|
||||
gameDetails,
|
||||
whoAmI,
|
||||
}: Readonly<RevealProps>) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selfRevealed, setSelfRevealed] = useState(false);
|
||||
const [opponentRevealed, setOpponentRevealed] = useState(false);
|
||||
const [bothRevealed, setBothRevealed] = useState(false);
|
||||
const [playerARevealed, setPlayerARevealed] = useState(false);
|
||||
const [playerBRevealed, setPlayerBRevealed] = useState(false);
|
||||
const [revealTimeLeft, setRevealTimeLeft] = useState<number>(0);
|
||||
const [outcome, setOutcome] = useState<number>(0);
|
||||
const [revealed, setRevealed] = useState(false);
|
||||
|
||||
const clearMove = selectedMove && secret ? `${selectedMove}-${secret}` : "";
|
||||
|
||||
// Check game status on mount
|
||||
useEffect(() => {
|
||||
const checkStatus = async () => {
|
||||
if (!contract) return;
|
||||
try {
|
||||
const [br, par, pbr, rtl, out] = await Promise.all([
|
||||
await contract.methods.bothRevealed().call({ from : account}),
|
||||
await contract.methods.playerARevealed().call({ from : account}),
|
||||
await contract.methods.playerBRevealed().call({ from : account}),
|
||||
await contract.methods.revealTimeLeft().call({ from : account}),
|
||||
await contract.methods.getLastWinner().call({ from : account}),
|
||||
]);
|
||||
const setStateFromGameDetails = () => {
|
||||
if (!gameDetails) return;
|
||||
const playerARevealed = Number(gameDetails.playerA.move) !== 0;
|
||||
const playerBRevealed = Number(gameDetails.playerB.move) !== 0;
|
||||
|
||||
console.log("Status:", {
|
||||
br, par, pbr, rtl, out
|
||||
});
|
||||
setBothRevealed(br);
|
||||
setPlayerARevealed(par);
|
||||
setPlayerBRevealed(pbr);
|
||||
setRevealTimeLeft(Number(rtl));
|
||||
setOutcome(Number(out));
|
||||
} catch (err: any) {
|
||||
console.error("Failed to check status:", err);
|
||||
setSelfRevealed(
|
||||
(whoAmI === "player1" && playerARevealed) ||
|
||||
(whoAmI === "player2" && playerBRevealed)
|
||||
);
|
||||
setOpponentRevealed(
|
||||
(whoAmI === "player1" && playerBRevealed) ||
|
||||
(whoAmI === "player2" && playerARevealed)
|
||||
);
|
||||
setBothRevealed(playerARevealed && playerBRevealed);
|
||||
if(bothRevealed){
|
||||
if(Number(gameDetails.outcome) === 1 && whoAmI === "player1") setOutcome(1);
|
||||
else if(Number(gameDetails.outcome) === 2 && whoAmI === "player2") setOutcome(1);
|
||||
else if(Number(gameDetails.outcome) === 1 && whoAmI === "player2") setOutcome(2);
|
||||
else if(Number(gameDetails.outcome) === 2 && whoAmI === "player1") setOutcome(2);
|
||||
else setOutcome(3);
|
||||
}
|
||||
};
|
||||
|
||||
const interval = setInterval(checkStatus, 3000);
|
||||
checkStatus();
|
||||
return () => clearInterval(interval);
|
||||
}, [contract, account]);
|
||||
setStateFromGameDetails();
|
||||
}, [gameDetails, contract, account, whoAmI]);
|
||||
|
||||
const handleReveal = async () => {
|
||||
if (!contract || !web3 || !account || !clearMove) return;
|
||||
setLoading(true);
|
||||
setStatus("");
|
||||
try {
|
||||
const tx = contract.methods.reveal(clearMove);
|
||||
const tx = contract.methods.reveal(gameDetails?.returnGameId, clearMove);
|
||||
const gas = await tx.estimateGas({ from: account });
|
||||
const result = await (globalThis as any).ethereum.request({
|
||||
method: "eth_sendTransaction",
|
||||
@@ -97,10 +97,9 @@ export default function Reveal({
|
||||
},
|
||||
],
|
||||
});
|
||||
setStatus("✅ Reveal tx sent: " + result);
|
||||
setRevealed(true);
|
||||
showSuccessToast("Reveal tx sent: " + result);
|
||||
} catch (err: any) {
|
||||
setStatus("❌ Reveal failed: " + err.message);
|
||||
showErrorToast("Reveal failed: " + err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -109,9 +108,8 @@ export default function Reveal({
|
||||
const handleGetOutcome = async () => {
|
||||
if (!contract || !web3 || !account) return;
|
||||
setLoading(true);
|
||||
setStatus("");
|
||||
try {
|
||||
const tx = contract.methods.getOutcome();
|
||||
const tx = contract.methods.getOutcome(gameDetails?.returnGameId);
|
||||
const gas = await tx.estimateGas({ from: account });
|
||||
const result = await (globalThis as any).ethereum.request({
|
||||
method: "eth_sendTransaction",
|
||||
@@ -125,9 +123,10 @@ export default function Reveal({
|
||||
},
|
||||
],
|
||||
});
|
||||
setStatus("✅ Claim tx sent: " + result);
|
||||
showSuccessToast("Claim tx sent: " + result);
|
||||
} catch (err: any) {
|
||||
setStatus("❌ Claim failed: " + err.message);
|
||||
console.error(err);
|
||||
showErrorToast("Claim failed: " + err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -137,7 +136,8 @@ export default function Reveal({
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Your Move Section */}
|
||||
{/* Your Move Section - Hidden when both revealed */}
|
||||
{!bothRevealed && (
|
||||
<div className="border p-6 rounded-lg bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-700 dark:to-slate-800">
|
||||
<h2 className="font-semibold text-lg mb-4 text-slate-900 dark:text-white">
|
||||
Your Move
|
||||
@@ -166,37 +166,39 @@ export default function Reveal({
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Game Status Section */}
|
||||
{/* Game Status Section - Hidden when both revealed */}
|
||||
{!bothRevealed && (
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div
|
||||
className={`p-4 rounded-lg text-center ${
|
||||
playerARevealed
|
||||
selfRevealed
|
||||
? "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-2xl mb-1">{selfRevealed ? "✅" : "⏳"}</p>
|
||||
<p className="text-xs font-semibold text-slate-600 dark:text-slate-300">
|
||||
Player A
|
||||
Me
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
|
||||
{playerARevealed ? "Revealed" : "Waiting"}
|
||||
{selfRevealed ? "Revealed" : "Waiting"}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className={`p-4 rounded-lg text-center ${
|
||||
playerBRevealed
|
||||
opponentRevealed
|
||||
? "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-2xl mb-1">{opponentRevealed ? "✅" : "⏳"}</p>
|
||||
<p className="text-xs font-semibold text-slate-600 dark:text-slate-300">
|
||||
Player B
|
||||
Opponent
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
|
||||
{playerBRevealed ? "Revealed" : "Waiting"}
|
||||
{opponentRevealed ? "Revealed" : "Waiting"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 rounded-lg text-center bg-blue-50 dark:bg-blue-900">
|
||||
@@ -207,12 +209,14 @@ export default function Reveal({
|
||||
Time Left
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
|
||||
{revealTimeLeft > 0 ? `${revealTimeLeft}s` : "Expired"}
|
||||
{0}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Reveal Section */}
|
||||
{/* 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
|
||||
@@ -223,16 +227,60 @@ export default function Reveal({
|
||||
</p>
|
||||
<Button
|
||||
onClick={handleReveal}
|
||||
disabled={loading || !account || !contract || !clearMove || revealed}
|
||||
disabled={loading || !account || !contract || !clearMove || selfRevealed}
|
||||
variant="primary"
|
||||
className="w-full py-3 text-lg"
|
||||
>
|
||||
{loading ? "Submitting..." : revealed ? "✅ Revealed" : "Reveal Move"}
|
||||
{loading ? "Submitting..." : selfRevealed ? "✅ Revealed" : "Reveal Move"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Winner Section - Only show if both revealed */}
|
||||
{bothRevealed && (
|
||||
<div className="space-y-4">
|
||||
{/* Moves Comparison */}
|
||||
<div className="border-2 border-slate-300 dark:border-slate-600 p-6 rounded-lg bg-slate-50 dark:bg-slate-800">
|
||||
<h2 className="font-semibold text-lg mb-4 text-slate-900 dark:text-white text-center">
|
||||
Final Moves
|
||||
</h2>
|
||||
<div className="flex items-center justify-center gap-8">
|
||||
{/* Your Move (always on left) */}
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-6xl mb-2">
|
||||
{gameDetails && MOVES[String(whoAmI === "player1" ? gameDetails.playerA.move : gameDetails.playerB.move)]?.icon}
|
||||
</span>
|
||||
<span className="font-semibold text-slate-700 dark:text-slate-300">
|
||||
You
|
||||
</span>
|
||||
<span className="text-sm text-slate-500 dark:text-slate-400">
|
||||
{gameDetails && MOVES[String(whoAmI === "player1" ? gameDetails.playerA.move : gameDetails.playerB.move)]?.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* VS */}
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-4xl text-slate-400 dark:text-slate-500">
|
||||
VS
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Opponent Move (always on right) */}
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-6xl mb-2">
|
||||
{gameDetails && MOVES[String(whoAmI === "player1" ? gameDetails.playerB.move : gameDetails.playerA.move)]?.icon}
|
||||
</span>
|
||||
<span className="font-semibold text-slate-700 dark:text-slate-300">
|
||||
Opponent
|
||||
</span>
|
||||
<span className="text-sm text-slate-500 dark:text-slate-400">
|
||||
{gameDetails && MOVES[String(whoAmI === "player1" ? gameDetails.playerB.move : gameDetails.playerA.move)]?.name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Outcome Section */}
|
||||
<div
|
||||
className={`border-2 p-6 rounded-lg text-center ${
|
||||
outcomeData.color === "green"
|
||||
@@ -258,7 +306,7 @@ export default function Reveal({
|
||||
{outcomeData.emoji}
|
||||
</p>
|
||||
<h3
|
||||
className={`text-2xl font-bold mb-2 ${
|
||||
className={`text-2xl font-bold ${
|
||||
outcomeData.color === "green"
|
||||
? "text-green-700 dark:text-green-300"
|
||||
: outcomeData.color === "red"
|
||||
@@ -270,6 +318,9 @@ export default function Reveal({
|
||||
>
|
||||
{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}
|
||||
@@ -278,15 +329,8 @@ export default function Reveal({
|
||||
>
|
||||
{loading ? "Processing..." : "💰 Claim Coins"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Status Messages */}
|
||||
{!bothRevealed && !revealed && (
|
||||
<div className="p-4 bg-yellow-50 dark:bg-yellow-900 rounded-lg">
|
||||
<p className="text-sm text-yellow-800 dark:text-yellow-200">
|
||||
⏳ Waiting for both players to reveal...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -3,32 +3,26 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Web3 from "web3";
|
||||
import GameList from "./GameList";
|
||||
import Commit from "./Commit";
|
||||
import Reveal from "./Reveal";
|
||||
import GameModal from "./GameModal";
|
||||
import { showErrorToast } from "@/app/lib/toast";
|
||||
|
||||
export default function Clash() {
|
||||
const [config, setConfig] = useState<Config | null>(null);
|
||||
const [web3, setWeb3] = useState<Web3 | null>(null);
|
||||
const [contract, setContract] = useState<any>(null);
|
||||
const [account, setAccount] = useState<string>("");
|
||||
const [status, setStatus] = useState<string>("");
|
||||
|
||||
// Inputs for contract functions
|
||||
const [phase, setPhase] = useState<"games" | "commit" | "reveal">("games");
|
||||
const [selectedMove, setSelectedMove] = useState<string | null>(null);
|
||||
const [secret, setSecret] = useState<string>("");
|
||||
// Modal state
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [selectedGameId, setSelectedGameId] = useState<number | undefined>();
|
||||
const [availableAccounts, setAvailableAccounts] = useState<string[]>([]);
|
||||
const [selectedAccount, setSelectedAccount] = useState<string>("");
|
||||
|
||||
const handlePlayClick = (gameId: number) => {
|
||||
setPhase("commit");
|
||||
setSelectedGameId(gameId);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
// Clear status when phase changes
|
||||
useEffect(() => {
|
||||
setStatus("");
|
||||
}, [phase]);
|
||||
|
||||
// Load config and contract
|
||||
useEffect(() => {
|
||||
const loadConfig = async () => {
|
||||
@@ -50,16 +44,13 @@ export default function Clash() {
|
||||
method: "eth_requestAccounts",
|
||||
});
|
||||
setAvailableAccounts(accounts);
|
||||
setAccount(accounts[0]);
|
||||
setSelectedAccount(accounts[0]);
|
||||
} catch (err: any) {
|
||||
setStatus(
|
||||
"MetaMask not available or user denied access: " + err.message
|
||||
);
|
||||
showErrorToast("MetaMask not available or user denied access: " + err.message);
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
setStatus("Failed to load config: " + err.message);
|
||||
showErrorToast("Failed to load config: " + err.message);
|
||||
}
|
||||
};
|
||||
loadConfig();
|
||||
@@ -78,9 +69,7 @@ export default function Clash() {
|
||||
Crypto Clash
|
||||
</h1>
|
||||
<p className="text-center text-slate-600 dark:text-slate-300 mb-8">
|
||||
{phase === "games" && "Browse and join games."}
|
||||
{phase === "commit" && "Commit your move."}
|
||||
{phase === "reveal" && "Reveal your move."}
|
||||
Browse and join games.
|
||||
</p>
|
||||
<div className="mb-8 p-4 bg-slate-100 dark:bg-slate-700 rounded-lg">
|
||||
<div className="mb-4">
|
||||
@@ -92,7 +81,6 @@ export default function Clash() {
|
||||
value={selectedAccount}
|
||||
onChange={(e) => {
|
||||
setSelectedAccount(e.target.value);
|
||||
setAccount(e.target.value);
|
||||
}}
|
||||
className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
@@ -117,40 +105,7 @@ export default function Clash() {
|
||||
{config?.GAME_CONTRACT_ADDRESS}
|
||||
</p>
|
||||
</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">
|
||||
{phase === "games" && (
|
||||
<GameList
|
||||
account={selectedAccount}
|
||||
contract={contract}
|
||||
@@ -159,58 +114,20 @@ export default function Clash() {
|
||||
setStatus={setStatus}
|
||||
onPlayClick={handlePlayClick}
|
||||
/>
|
||||
)}
|
||||
{phase === "commit" && (
|
||||
<Commit
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Game Modal */}
|
||||
<GameModal
|
||||
gameId={selectedGameId}
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
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>
|
||||
</main>
|
||||
</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);
|
||||
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 { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import ToastContainer from "./components/ToastContainer";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
@@ -28,6 +29,7 @@ export default function RootLayout({
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
<ToastContainer />
|
||||
</body>
|
||||
</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
|
||||
</Link>
|
||||
</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>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
"type": "function"
|
||||
}
|
||||
],
|
||||
"GAME_CONTRACT_ADDRESS": "0x503d096a9a163180F79B1AC2F1d9F7C63f5DC75a",
|
||||
"GAME_CONTRACT_ADDRESS": "0xAA7057A0203539d9BE86EfB471B831Dd833a9e22",
|
||||
"GAME_ABI": [
|
||||
{
|
||||
"inputs": [],
|
||||
@@ -87,32 +87,6 @@
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "bothPlayed",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "bothRevealed",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "getActiveGameIds",
|
||||
@@ -150,15 +124,69 @@
|
||||
"name": "getGameDetails",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "playerAAddr",
|
||||
"components": [
|
||||
{
|
||||
"internalType": "address payable",
|
||||
"name": "addr",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "playerBAddr",
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"components": [
|
||||
{
|
||||
"internalType": "address payable",
|
||||
"name": "addr",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "bet",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "encrMove",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "enum Game.Moves",
|
||||
"name": "move",
|
||||
"type": "uint8"
|
||||
},
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "nickname",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"internalType": "struct Game.Player",
|
||||
"name": "playerB",
|
||||
"type": "tuple"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "initialBet",
|
||||
@@ -173,31 +201,10 @@
|
||||
"internalType": "bool",
|
||||
"name": "isActive",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "getLastWinner",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "enum Game.Outcomes",
|
||||
"name": "",
|
||||
"type": "uint8"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "getMyActiveGameId",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"name": "returnGameId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
@@ -205,7 +212,13 @@
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "gameId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "getOutcome",
|
||||
"outputs": [
|
||||
{
|
||||
@@ -221,51 +234,9 @@
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "index",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "getPastGame",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "playerAAddr",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "playerBAddr",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "initialBet",
|
||||
"name": "gameId",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "enum Game.Outcomes",
|
||||
"name": "outcome",
|
||||
"type": "uint8"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "getPastGamesCount",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "encrMove",
|
||||
@@ -283,32 +254,6 @@
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "playerARevealed",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "playerBRevealed",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
@@ -335,6 +280,11 @@
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "gameId",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "clearMove",
|
||||
@@ -353,7 +303,13 @@
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "gameId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "revealTimeLeft",
|
||||
"outputs": [
|
||||
{
|
||||
@@ -366,7 +322,13 @@
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "gameId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "whoAmI",
|
||||
"outputs": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user