mirror of
https://github.com/averel10/crypto_clash.git
synced 2026-03-12 10:58:11 +01:00
update game logic and UI components for move selection and reveal phases; remove Lobby component
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"API_URL": "https://rpc.hasrv.averel10.app/",
|
"API_URL": "http://185.48.228.49:8545",
|
||||||
"CONTRACT_ADDRESS": "0x8B7746B922a80d9B29cA939fD90577932527112C",
|
"CONTRACT_ADDRESS": "0x8aF379ED8C452612676ACB856E26955237358944",
|
||||||
"ABI": [
|
"ABI": [
|
||||||
{
|
{
|
||||||
"inputs": [
|
"inputs": [
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
"type": "function"
|
"type": "function"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"GAME_CONTRACT_ADDRESS": "0x9230eBBB05c3e953a5F135F032b352c6F0B6E164",
|
"GAME_CONTRACT_ADDRESS": "0x267d1b8bf8C13ea60D39F90cC68AEc4b8389bb64",
|
||||||
"GAME_ABI": [
|
"GAME_ABI": [
|
||||||
{
|
{
|
||||||
"inputs": [],
|
"inputs": [],
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ contract Game {
|
|||||||
uint gameId = playerToActiveGame[msg.sender];
|
uint gameId = playerToActiveGame[msg.sender];
|
||||||
GameState storage game = games[gameId];
|
GameState storage game = games[gameId];
|
||||||
|
|
||||||
bytes32 encrMove = sha256(abi.encodePacked(clearMove)); // Hash of clear input (= "move-password")
|
bytes32 encrMove = keccak256(abi.encodePacked(clearMove)); // Hash of clear input (= "move-password")
|
||||||
Moves move = Moves(getFirstChar(clearMove)); // Actual move (Rock / Paper / Scissors)
|
Moves move = Moves(getFirstChar(clearMove)); // Actual move (Rock / Paper / Scissors)
|
||||||
|
|
||||||
// If move invalid, exit
|
// If move invalid, exit
|
||||||
@@ -381,7 +381,7 @@ contract Game {
|
|||||||
if (gameId == 0) return false;
|
if (gameId == 0) return false;
|
||||||
|
|
||||||
GameState storage game = games[gameId];
|
GameState storage game = games[gameId];
|
||||||
return (game.playerA.encrMove != 0x0 && game.playerB.encrMove != 0x0);
|
return (game.playerA.encrMove != bytes32(0) && game.playerB.encrMove != bytes32(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return 'true' if both players have revealed their move, 'false' otherwise.
|
// Return 'true' if both players have revealed their move, 'false' otherwise.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
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";
|
||||||
@@ -9,25 +9,60 @@ interface CommitProps {
|
|||||||
config: Config | null;
|
config: Config | null;
|
||||||
web3: Web3 | null;
|
web3: Web3 | null;
|
||||||
setStatus: (status: string) => void;
|
setStatus: (status: string) => void;
|
||||||
|
selectedMove: string | null;
|
||||||
|
setSelectedMove: (move: string | null) => void;
|
||||||
|
secret: string;
|
||||||
|
setSecret: (secret: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Move = "1" | "2" | "3" | null;
|
||||||
|
type MoveName = "Rock" | "Paper" | "Scissors";
|
||||||
|
|
||||||
|
const MOVES: Record<string, { name: MoveName; icon: string }> = {
|
||||||
|
"1": { name: "Rock", icon: "✊" },
|
||||||
|
"2": { name: "Paper", icon: "✋" },
|
||||||
|
"3": { name: "Scissors", icon: "✌️" },
|
||||||
|
};
|
||||||
|
|
||||||
export default function Commit({
|
export default function Commit({
|
||||||
account,
|
account,
|
||||||
contract,
|
contract,
|
||||||
config,
|
config,
|
||||||
web3,
|
web3,
|
||||||
setStatus,
|
setStatus,
|
||||||
|
selectedMove,
|
||||||
|
setSelectedMove,
|
||||||
|
secret,
|
||||||
|
setSecret,
|
||||||
}: 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 [bothPlayed, setBothPlayed] = useState<string>("");
|
const [bothPlayed, setBothPlayed] = useState<string>("");
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
const clearMove = `${selectedMove}-${secret}`;
|
||||||
|
// Use keccak256 (Ethereum's standard hash function)
|
||||||
|
const hash = Web3.utils.keccak256(clearMove);
|
||||||
|
setPlayMove(hash);
|
||||||
|
}
|
||||||
|
}, [selectedMove, secret]);
|
||||||
|
|
||||||
// Commit phase read-only handlers
|
// Commit phase read-only handlers
|
||||||
const handleBothPlayed = async () => {
|
const handleBothPlayed = async () => {
|
||||||
if (!contract) return;
|
if (!contract) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await contract.methods.bothPlayed().call();
|
const res = await contract.methods.bothPlayed().call({from : account});
|
||||||
setBothPlayed(res ? "true" : "false");
|
setBothPlayed(res ? "true" : "false");
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setStatus("Failed to fetch bothPlayed: " + err.message);
|
setStatus("Failed to fetch bothPlayed: " + err.message);
|
||||||
@@ -37,7 +72,7 @@ export default function Commit({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handlePlay = async () => {
|
const handlePlay = async () => {
|
||||||
if (!contract || !web3 || !account) return;
|
if (!contract || !web3 || !account || !playMove) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setStatus("");
|
setStatus("");
|
||||||
try {
|
try {
|
||||||
@@ -63,33 +98,111 @@ export default function Commit({
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
|
||||||
<div className="border p-4 rounded-lg">
|
|
||||||
<h2 className="font-semibold mb-2">play(bytes32 encrMove)</h2>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
placeholder="Encrypted Move (bytes32)"
|
|
||||||
value={playMove}
|
|
||||||
onChange={(e) => setPlayMove(e.target.value)}
|
|
||||||
className="mr-2"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
onClick={handlePlay}
|
|
||||||
disabled={loading || !account || !contract}
|
|
||||||
variant="primary"
|
|
||||||
>
|
|
||||||
Play
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="mt-4 space-y-2">
|
const regenerateSecret = () => {
|
||||||
|
const randomHex = Math.random().toString(16).slice(2, 18);
|
||||||
|
setSecret(randomHex);
|
||||||
|
};
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
||||||
|
{/* Move Selection */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<p className="text-sm text-slate-600 dark:text-slate-300 mb-4 font-medium">
|
||||||
|
Choose your move:
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-4 justify-center">
|
||||||
|
{(["1", "2", "3"] as const).map((move) => (
|
||||||
|
<button
|
||||||
|
key={move}
|
||||||
|
onClick={() => setSelectedMove(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"
|
||||||
|
: "bg-white dark:bg-slate-600 text-slate-700 dark:text-slate-200 shadow hover:shadow-md hover:scale-105"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="text-5xl mb-2">{MOVES[move].icon}</span>
|
||||||
|
<span className="font-semibold text-sm">{MOVES[move].name}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Secret Input */}
|
||||||
|
<div className="mb-8 bg-white dark:bg-slate-700 p-4 rounded-lg">
|
||||||
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-3">
|
||||||
|
Secret:
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={secret}
|
||||||
|
onChange={(e) => setSecret(e.target.value)}
|
||||||
|
placeholder="Your secret passphrase"
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={regenerateSecret}
|
||||||
|
variant="secondary"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
🔄 New
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-500 dark:text-slate-400 mt-2">
|
||||||
|
Keep this secret safe! It's needed to reveal your move later.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Encrypted Move Display */}
|
||||||
|
<div className="mb-8 bg-blue-50 dark:bg-blue-900 p-4 rounded-lg">
|
||||||
|
<label className="block text-sm font-medium text-slate-700 dark:text-blue-200 mb-2">
|
||||||
|
Encrypted Move (to be sent):
|
||||||
|
</label>
|
||||||
|
<div className="bg-white dark:bg-slate-700 p-3 rounded border border-blue-200 dark:border-blue-700 overflow-x-auto">
|
||||||
|
<code className="text-xs text-slate-600 dark:text-slate-300 font-mono break-all">
|
||||||
|
{playMove || "Select a move and enter a secret"}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Button
|
||||||
|
onClick={handlePlay}
|
||||||
|
disabled={loading || !account || !contract || !selectedMove || !secret}
|
||||||
|
variant="primary"
|
||||||
|
className="w-full py-3 text-lg"
|
||||||
|
>
|
||||||
|
{loading ? "Submitting..." : "Submit Move"}
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={handleBothPlayed}
|
onClick={handleBothPlayed}
|
||||||
|
disabled={loading}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
className="w-full"
|
||||||
>
|
>
|
||||||
bothPlayed
|
Check Both Played
|
||||||
</Button>
|
</Button>
|
||||||
<span className="ml-2 text-xs">{bothPlayed}</span>
|
{bothPlayed && (
|
||||||
<br />
|
<span
|
||||||
|
className={`text-center text-sm font-medium ${
|
||||||
|
bothPlayed === "true"
|
||||||
|
? "text-green-600 dark:text-green-400"
|
||||||
|
: "text-slate-600 dark:text-slate-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{bothPlayed === "true"
|
||||||
|
? "✓ Both players have committed!"
|
||||||
|
: "Waiting for opponent..."}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ export default function GameList({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setGames(gameDetails);
|
setGames(gameDetails);
|
||||||
|
|
||||||
// Check which games the user is participating in
|
// Check which games the user is participating in
|
||||||
if (account) {
|
if (account) {
|
||||||
const userGames = new Set<number>();
|
const userGames = new Set<number>();
|
||||||
@@ -85,7 +84,7 @@ export default function GameList({
|
|||||||
return () => {
|
return () => {
|
||||||
if (interval) clearInterval(interval);
|
if (interval) clearInterval(interval);
|
||||||
};
|
};
|
||||||
}, [contract, web3]);
|
}, [contract, web3, account]);
|
||||||
|
|
||||||
// Join an existing game
|
// Join an existing game
|
||||||
const handleJoinGame = async (gameId: number, bet: string) => {
|
const handleJoinGame = async (gameId: number, bet: string) => {
|
||||||
|
|||||||
@@ -1,238 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import Web3 from "web3";
|
|
||||||
import { Button } from "./Button";
|
|
||||||
import { Input } from "./Input";
|
|
||||||
|
|
||||||
interface LobbyProps {
|
|
||||||
account: string;
|
|
||||||
contract: any;
|
|
||||||
config: Config | null;
|
|
||||||
web3: Web3 | null;
|
|
||||||
setStatus: (status: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Lobby({
|
|
||||||
account,
|
|
||||||
contract,
|
|
||||||
config,
|
|
||||||
web3,
|
|
||||||
setStatus,
|
|
||||||
}: Readonly<LobbyProps>) {
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [registerGameId, setRegisterGameId] = useState<string>("0");
|
|
||||||
const [registerBet, setRegisterBet] = useState<string>("");
|
|
||||||
const [betMin, setBetMin] = useState<string>("");
|
|
||||||
const [activeGameIds, setActiveGameIds] = useState<string>("");
|
|
||||||
const [contractBalance, setContractBalance] = useState<string>("");
|
|
||||||
const [gameDetailsId, setGameDetailsId] = useState<string>("");
|
|
||||||
const [gameDetails, setGameDetails] = useState<any>(null);
|
|
||||||
const [myActiveGameId, setMyActiveGameId] = useState<string>("");
|
|
||||||
const [pastGameIndex, setPastGameIndex] = useState<string>("");
|
|
||||||
const [pastGame, setPastGame] = useState<any>(null);
|
|
||||||
const [pastGamesCount, setPastGamesCount] = useState<string>("");
|
|
||||||
const [whoAmI, setWhoAmI] = useState<string>("");
|
|
||||||
|
|
||||||
// Read-only contract function handlers
|
|
||||||
const handleGetBetMin = async () => {
|
|
||||||
if (!contract || !web3) return;
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const res = await contract.methods.BET_MIN().call();
|
|
||||||
setBetMin(web3.utils.fromWei(res, "ether") + " ETH");
|
|
||||||
} catch (err: any) {
|
|
||||||
setStatus("Failed to fetch BET_MIN: " + err.message);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGetActiveGameIds = async () => {
|
|
||||||
if (!contract) return;
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const res = await contract.methods.getActiveGameIds().call();
|
|
||||||
setActiveGameIds(res.join(", "));
|
|
||||||
} catch (err: any) {
|
|
||||||
setStatus("Failed to fetch active game IDs: " + err.message);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGetContractBalance = async () => {
|
|
||||||
if (!contract || !web3) return;
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const res = await contract.methods.getContractBalance().call();
|
|
||||||
setContractBalance(web3.utils.fromWei(res, "ether") + " ETH");
|
|
||||||
} catch (err: any) {
|
|
||||||
setStatus("Failed to fetch contract balance: " + err.message);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGetGameDetails = async () => {
|
|
||||||
if (!contract) return;
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const res = await contract.methods
|
|
||||||
.getGameDetails(Number(gameDetailsId))
|
|
||||||
.call();
|
|
||||||
setGameDetails(res);
|
|
||||||
} catch (err: any) {
|
|
||||||
setStatus("Failed to fetch game details: " + err.message);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGetMyActiveGameId = async () => {
|
|
||||||
if (!contract || !account) return;
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const res = await contract.methods
|
|
||||||
.getMyActiveGameId()
|
|
||||||
.call({ from: account });
|
|
||||||
setMyActiveGameId(res.toString());
|
|
||||||
} catch (err: any) {
|
|
||||||
setStatus("Failed to fetch my active game ID: " + err.message);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGetPastGame = async () => {
|
|
||||||
if (!contract) return;
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const res = await contract.methods
|
|
||||||
.getPastGame(Number(pastGameIndex))
|
|
||||||
.call();
|
|
||||||
setPastGame(res);
|
|
||||||
} catch (err: any) {
|
|
||||||
setStatus("Failed to fetch past game: " + err.message);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGetPastGamesCount = async () => {
|
|
||||||
if (!contract) return;
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const res = await contract.methods.getPastGamesCount().call();
|
|
||||||
setPastGamesCount(res.toString());
|
|
||||||
} catch (err: any) {
|
|
||||||
setStatus("Failed to fetch past games count: " + err.message);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleWhoAmI = async () => {
|
|
||||||
if (!contract || !account) return;
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const res = await contract.methods.whoAmI().call({ from: account });
|
|
||||||
setWhoAmI(res.toString());
|
|
||||||
} catch (err: any) {
|
|
||||||
setStatus("Failed to fetch whoAmI: " + err.message);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRegister = async () => {
|
|
||||||
if (!contract || !web3 || !account) return;
|
|
||||||
setLoading(true);
|
|
||||||
setStatus("");
|
|
||||||
try {
|
|
||||||
console.log(registerBet);
|
|
||||||
const bet = web3.utils.toWei(registerBet || "0.01", "ether");
|
|
||||||
console.log(bet);
|
|
||||||
console.log(web3.utils.toHex(bet));
|
|
||||||
const tx = contract.methods.register(Number(registerGameId || 0));
|
|
||||||
const gas = await tx.estimateGas({ from: account, value: bet });
|
|
||||||
const result = await (globalThis as any).ethereum.request({
|
|
||||||
method: "eth_sendTransaction",
|
|
||||||
params: [
|
|
||||||
{
|
|
||||||
from: account,
|
|
||||||
to: config?.GAME_CONTRACT_ADDRESS,
|
|
||||||
data: tx.encodeABI(),
|
|
||||||
value: web3.utils.numberToHex(bet),
|
|
||||||
gas: web3.utils.toHex(gas),
|
|
||||||
chainId: web3.utils.toHex(await web3.eth.net.getId()),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
setStatus("Register tx sent: " + result);
|
|
||||||
} catch (err: any) {
|
|
||||||
setStatus("Register failed: " + err.message);
|
|
||||||
console.error(err);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div className="border p-4 rounded-lg">
|
|
||||||
<h2 className="font-semibold mb-2">register(uint gameId) (payable)</h2>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
placeholder="Game ID (0 = auto)"
|
|
||||||
value={registerGameId}
|
|
||||||
onChange={(e) => setRegisterGameId(e.target.value)}
|
|
||||||
className="mr-2"
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
min="0.01"
|
|
||||||
step="0.01"
|
|
||||||
placeholder="Bet in ETH (default 0.01)"
|
|
||||||
value={registerBet}
|
|
||||||
onChange={(e) => setRegisterBet(e.target.value)}
|
|
||||||
className="mr-2"
|
|
||||||
/>
|
|
||||||
<div className="text-xs text-slate-500 dark:text-slate-400 mt-1">
|
|
||||||
Enter amount in ETH (e.g., 0.01 for 0.01 ETH). Entering 1 means 1 full
|
|
||||||
ETH.
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
onClick={handleRegister}
|
|
||||||
disabled={loading || !account || !contract}
|
|
||||||
variant="primary"
|
|
||||||
>
|
|
||||||
Register
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="mt-4 space-y-2">
|
|
||||||
<Button onClick={handleGetBetMin} variant="secondary">BET_MIN</Button>
|
|
||||||
<span className="ml-2 text-xs">{betMin}</span>
|
|
||||||
<br />
|
|
||||||
<Button onClick={handleGetActiveGameIds} variant="secondary">getActiveGameIds</Button>
|
|
||||||
<span className="ml-2 text-xs">{activeGameIds}</span>
|
|
||||||
<br />
|
|
||||||
<Button onClick={handleGetContractBalance} variant="secondary">getContractBalance</Button>
|
|
||||||
<span className="ml-2 text-xs">{contractBalance}</span>
|
|
||||||
<br />
|
|
||||||
<Input type="text" placeholder="Game ID" value={gameDetailsId} onChange={e => setGameDetailsId(e.target.value)} className="mr-2" />
|
|
||||||
<Button onClick={handleGetGameDetails} variant="secondary">getGameDetails</Button>
|
|
||||||
<span className="ml-2 text-xs">{gameDetails && <pre className="inline whitespace-pre-wrap">{JSON.stringify(gameDetails, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2)}</pre>}</span>
|
|
||||||
<br />
|
|
||||||
<Button onClick={handleGetMyActiveGameId} variant="secondary">getMyActiveGameId</Button>
|
|
||||||
<span className="ml-2 text-xs">{myActiveGameId}</span>
|
|
||||||
<br />
|
|
||||||
<Input type="text" placeholder="Past Game Index" value={pastGameIndex} onChange={e => setPastGameIndex(e.target.value)} className="mr-2" />
|
|
||||||
<Button onClick={handleGetPastGame} variant="secondary">getPastGame</Button>
|
|
||||||
<span className="ml-2 text-xs">{pastGame && <pre className="inline whitespace-pre-wrap">{JSON.stringify(pastGame, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2)}</pre>}</span>
|
|
||||||
<br />
|
|
||||||
<Button onClick={handleGetPastGamesCount} variant="secondary">getPastGamesCount</Button>
|
|
||||||
<span className="ml-2 text-xs">{pastGamesCount}</span>
|
|
||||||
<br />
|
|
||||||
<Button onClick={handleWhoAmI} variant="secondary">whoAmI</Button>
|
|
||||||
<span className="ml-2 text-xs">{whoAmI}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useState } from "react";
|
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";
|
|
||||||
|
|
||||||
interface RevealProps {
|
interface RevealProps {
|
||||||
account: string;
|
account: string;
|
||||||
@@ -9,23 +8,104 @@ interface RevealProps {
|
|||||||
config: Config | null;
|
config: Config | null;
|
||||||
web3: Web3 | null;
|
web3: Web3 | null;
|
||||||
setStatus: (status: string) => void;
|
setStatus: (status: string) => void;
|
||||||
|
selectedMove: string | null;
|
||||||
|
secret: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MoveName = "Rock" | "Paper" | "Scissors";
|
||||||
|
|
||||||
|
const MOVES: Record<string, { name: MoveName; icon: string }> = {
|
||||||
|
"1": { name: "Rock", icon: "✊" },
|
||||||
|
"2": { name: "Paper", icon: "✋" },
|
||||||
|
"3": { name: "Scissors", icon: "✌️" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const OUTCOMES: Record<number, { name: string; emoji: string; color: string }> =
|
||||||
|
{
|
||||||
|
0: { name: "None", emoji: "❓", color: "gray" },
|
||||||
|
1: { name: "You Won!", emoji: "🏆", color: "green" },
|
||||||
|
2: { name: "You Lost", emoji: "😢", color: "red" },
|
||||||
|
3: { name: "Draw", emoji: "🤝", color: "yellow" },
|
||||||
|
};
|
||||||
|
|
||||||
export default function Reveal({
|
export default function Reveal({
|
||||||
account,
|
account,
|
||||||
contract,
|
contract,
|
||||||
config,
|
config,
|
||||||
web3,
|
web3,
|
||||||
setStatus,
|
setStatus,
|
||||||
|
selectedMove,
|
||||||
|
secret,
|
||||||
}: Readonly<RevealProps>) {
|
}: Readonly<RevealProps>) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [revealMove, setRevealMove] = useState<string>("");
|
const [bothRevealed, setBothRevealed] = useState(false);
|
||||||
const [bothRevealed, setBothRevealed] = useState<string>("");
|
const [playerARevealed, setPlayerARevealed] = useState(false);
|
||||||
const [playerARevealed, setPlayerARevealed] = useState<string>("");
|
const [playerBRevealed, setPlayerBRevealed] = useState(false);
|
||||||
const [playerBRevealed, setPlayerBRevealed] = useState<string>("");
|
const [revealTimeLeft, setRevealTimeLeft] = useState<number>(0);
|
||||||
const [revealTimeLeft, setRevealTimeLeft] = useState<string>("");
|
const [outcome, setOutcome] = useState<number>(0);
|
||||||
const [outcome, setOutcome] = useState<string>("");
|
const [revealed, setRevealed] = useState(false);
|
||||||
// getOutcome handler
|
|
||||||
|
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}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const interval = setInterval(checkStatus, 3000);
|
||||||
|
checkStatus();
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [contract, account]);
|
||||||
|
|
||||||
|
const handleReveal = async () => {
|
||||||
|
if (!contract || !web3 || !account || !clearMove) return;
|
||||||
|
setLoading(true);
|
||||||
|
setStatus("");
|
||||||
|
try {
|
||||||
|
const tx = contract.methods.reveal(clearMove);
|
||||||
|
const gas = await tx.estimateGas({ from: account });
|
||||||
|
const result = await (globalThis as any).ethereum.request({
|
||||||
|
method: "eth_sendTransaction",
|
||||||
|
params: [
|
||||||
|
{
|
||||||
|
from: account,
|
||||||
|
to: config?.GAME_CONTRACT_ADDRESS,
|
||||||
|
data: tx.encodeABI(),
|
||||||
|
gas: web3.utils.toHex(gas),
|
||||||
|
chainId: web3.utils.toHex(await web3.eth.net.getId()),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
setStatus("✅ Reveal tx sent: " + result);
|
||||||
|
setRevealed(true);
|
||||||
|
} catch (err: any) {
|
||||||
|
setStatus("❌ Reveal failed: " + err.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleGetOutcome = async () => {
|
const handleGetOutcome = async () => {
|
||||||
if (!contract || !web3 || !account) return;
|
if (!contract || !web3 || !account) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -45,157 +125,170 @@ export default function Reveal({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
setStatus("getOutcome tx sent: " + result);
|
setStatus("✅ Claim tx sent: " + result);
|
||||||
setOutcome("Transaction sent: " + result);
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setStatus("getOutcome failed: " + err.message);
|
setStatus("❌ Claim failed: " + err.message);
|
||||||
setOutcome("");
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reveal phase read-only handlers
|
const outcomeData = OUTCOMES[outcome] || OUTCOMES[0];
|
||||||
const handleBothRevealed = async () => {
|
|
||||||
if (!contract) return;
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const res = await contract.methods.bothRevealed().call();
|
|
||||||
setBothRevealed(res ? "true" : "false");
|
|
||||||
} catch (err: any) {
|
|
||||||
setStatus("Failed to fetch bothRevealed: " + err.message);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePlayerARevealed = async () => {
|
|
||||||
if (!contract) return;
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const res = await contract.methods.playerARevealed().call();
|
|
||||||
setPlayerARevealed(res ? "true" : "false");
|
|
||||||
} catch (err: any) {
|
|
||||||
setStatus("Failed to fetch playerARevealed: " + err.message);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePlayerBRevealed = async () => {
|
|
||||||
if (!contract) return;
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const res = await contract.methods.playerBRevealed().call();
|
|
||||||
setPlayerBRevealed(res ? "true" : "false");
|
|
||||||
} catch (err: any) {
|
|
||||||
setStatus("Failed to fetch playerBRevealed: " + err.message);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRevealTimeLeft = async () => {
|
|
||||||
if (!contract) return;
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const res = await contract.methods.revealTimeLeft().call();
|
|
||||||
setRevealTimeLeft(res.toString());
|
|
||||||
} catch (err: any) {
|
|
||||||
setStatus("Failed to fetch revealTimeLeft: " + err.message);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReveal = async () => {
|
|
||||||
if (!contract || !web3 || !account) return;
|
|
||||||
setLoading(true);
|
|
||||||
setStatus("");
|
|
||||||
try {
|
|
||||||
const tx = contract.methods.reveal(revealMove);
|
|
||||||
const gas = await tx.estimateGas({ from: account });
|
|
||||||
const result = await (globalThis as any).ethereum.request({
|
|
||||||
method: "eth_sendTransaction",
|
|
||||||
params: [
|
|
||||||
{
|
|
||||||
from: account,
|
|
||||||
to: config?.GAME_CONTRACT_ADDRESS,
|
|
||||||
data: tx.encodeABI(),
|
|
||||||
gas: web3.utils.toHex(gas),
|
|
||||||
chainId: web3.utils.toHex(await web3.eth.net.getId()),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
setStatus("Reveal tx sent: " + result);
|
|
||||||
} catch (err: any) {
|
|
||||||
setStatus("Reveal failed: " + err.message);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<div className="border p-4 rounded-lg">
|
<div className="space-y-6">
|
||||||
<h2 className="font-semibold mb-2">reveal(string clearMove)</h2>
|
{/* Your Move Section */}
|
||||||
<Input
|
<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">
|
||||||
type="text"
|
<h2 className="font-semibold text-lg mb-4 text-slate-900 dark:text-white">
|
||||||
placeholder="Clear Move (e.g. 1-password)"
|
Your Move
|
||||||
value={revealMove}
|
</h2>
|
||||||
onChange={(e) => setRevealMove(e.target.value)}
|
{selectedMove ? (
|
||||||
className="mr-2"
|
<div className="flex items-center justify-center gap-4">
|
||||||
/>
|
<div className="flex flex-col items-center">
|
||||||
<Button
|
<span className="text-6xl mb-2">{MOVES[selectedMove].icon}</span>
|
||||||
onClick={handleReveal}
|
<span className="font-semibold text-lg">
|
||||||
disabled={loading || !account || !contract}
|
{MOVES[selectedMove].name}
|
||||||
variant="primary"
|
</span>
|
||||||
>
|
</div>
|
||||||
Reveal
|
<div className="text-3xl text-slate-400">→</div>
|
||||||
</Button>
|
<div className="bg-white dark:bg-slate-600 p-4 rounded-lg">
|
||||||
|
<p className="text-xs text-slate-600 dark:text-slate-300 mb-1">
|
||||||
|
Clear Move:
|
||||||
|
</p>
|
||||||
|
<code className="text-sm font-mono text-slate-700 dark:text-slate-200">
|
||||||
|
{clearMove}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-center text-slate-600 dark:text-slate-400">
|
||||||
|
No move selected yet
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-4">
|
{/* 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
|
<Button
|
||||||
onClick={handleGetOutcome}
|
onClick={handleReveal}
|
||||||
disabled={loading || !account || !contract}
|
disabled={loading || !account || !contract || !clearMove || revealed}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
|
className="w-full py-3 text-lg"
|
||||||
>
|
>
|
||||||
getOutcome
|
{loading ? "Submitting..." : revealed ? "✅ Revealed" : "Reveal Move"}
|
||||||
</Button>
|
</Button>
|
||||||
<span className="ml-2 text-xs">{outcome}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 space-y-2">
|
{/* Winner Section - Only show if both revealed */}
|
||||||
<Button
|
{bothRevealed && (
|
||||||
onClick={handleBothRevealed}
|
<div
|
||||||
variant="secondary"
|
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"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
bothRevealed
|
<p
|
||||||
</Button>
|
className={`text-6xl mb-3 ${
|
||||||
<span className="ml-2 text-xs">{bothRevealed}</span>
|
outcomeData.color === "green"
|
||||||
<br />
|
? "text-green-600 dark:text-green-400"
|
||||||
<Button
|
: outcomeData.color === "red"
|
||||||
onClick={handlePlayerARevealed}
|
? "text-red-600 dark:text-red-400"
|
||||||
variant="secondary"
|
: outcomeData.color === "yellow"
|
||||||
>
|
? "text-yellow-600 dark:text-yellow-400"
|
||||||
playerARevealed
|
: "text-slate-600 dark:text-slate-400"
|
||||||
</Button>
|
}`}
|
||||||
<span className="ml-2 text-xs">{playerARevealed}</span>
|
>
|
||||||
<br />
|
{outcomeData.emoji}
|
||||||
<Button
|
</p>
|
||||||
onClick={handlePlayerBRevealed}
|
<h3
|
||||||
variant="secondary"
|
className={`text-2xl font-bold mb-2 ${
|
||||||
>
|
outcomeData.color === "green"
|
||||||
playerBRevealed
|
? "text-green-700 dark:text-green-300"
|
||||||
</Button>
|
: outcomeData.color === "red"
|
||||||
<span className="ml-2 text-xs">{playerBRevealed}</span>
|
? "text-red-700 dark:text-red-300"
|
||||||
<br />
|
: outcomeData.color === "yellow"
|
||||||
<Button
|
? "text-yellow-700 dark:text-yellow-300"
|
||||||
onClick={handleRevealTimeLeft}
|
: "text-slate-700 dark:text-slate-300"
|
||||||
variant="secondary"
|
}`}
|
||||||
>
|
>
|
||||||
revealTimeLeft
|
{outcomeData.name}
|
||||||
</Button>
|
</h3>
|
||||||
<span className="ml-2 text-xs">{revealTimeLeft}</span>
|
<Button
|
||||||
</div>
|
onClick={handleGetOutcome}
|
||||||
|
disabled={loading || !account || !contract}
|
||||||
|
variant="primary"
|
||||||
|
className="mt-4 w-full py-3 text-lg"
|
||||||
|
>
|
||||||
|
{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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ export default function Clash() {
|
|||||||
|
|
||||||
// Inputs for contract functions
|
// Inputs for contract functions
|
||||||
const [phase, setPhase] = useState<"games" | "commit" | "reveal">("games");
|
const [phase, setPhase] = useState<"games" | "commit" | "reveal">("games");
|
||||||
|
const [selectedMove, setSelectedMove] = useState<string | null>(null);
|
||||||
|
const [secret, setSecret] = useState<string>("");
|
||||||
|
|
||||||
const handlePlayClick = (gameId: number) => {
|
const handlePlayClick = (gameId: number) => {
|
||||||
setPhase("commit");
|
setPhase("commit");
|
||||||
@@ -138,6 +140,10 @@ export default function Clash() {
|
|||||||
config={config}
|
config={config}
|
||||||
web3={web3}
|
web3={web3}
|
||||||
setStatus={setStatus}
|
setStatus={setStatus}
|
||||||
|
selectedMove={selectedMove}
|
||||||
|
setSelectedMove={setSelectedMove}
|
||||||
|
secret={secret}
|
||||||
|
setSecret={setSecret}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{phase === "reveal" && (
|
{phase === "reveal" && (
|
||||||
@@ -147,6 +153,8 @@ export default function Clash() {
|
|||||||
config={config}
|
config={config}
|
||||||
web3={web3}
|
web3={web3}
|
||||||
setStatus={setStatus}
|
setStatus={setStatus}
|
||||||
|
selectedMove={selectedMove}
|
||||||
|
secret={secret}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"API_URL": "https://rpc.hasrv.averel10.app/",
|
"API_URL": "http://185.48.228.49:8545",
|
||||||
"CONTRACT_ADDRESS": "0x8B7746B922a80d9B29cA939fD90577932527112C",
|
"CONTRACT_ADDRESS": "0x8aF379ED8C452612676ACB856E26955237358944",
|
||||||
"ABI": [
|
"ABI": [
|
||||||
{
|
{
|
||||||
"inputs": [
|
"inputs": [
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
"type": "function"
|
"type": "function"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"GAME_CONTRACT_ADDRESS": "0x9230eBBB05c3e953a5F135F032b352c6F0B6E164",
|
"GAME_CONTRACT_ADDRESS": "0x267d1b8bf8C13ea60D39F90cC68AEc4b8389bb64",
|
||||||
"GAME_ABI": [
|
"GAME_ABI": [
|
||||||
{
|
{
|
||||||
"inputs": [],
|
"inputs": [],
|
||||||
|
|||||||
Reference in New Issue
Block a user