diff --git a/packages/backend/backend.config.ts b/packages/backend/backend.config.ts deleted file mode 100644 index b0a6df6..0000000 --- a/packages/backend/backend.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const JWT_SECRET = "superhardstring"; -export const PORT = 4001; -export const KEY = ""; diff --git a/packages/backend/controllers/Admin.ts b/packages/backend/controllers/Admin.ts index 78d14e4..7516113 100644 --- a/packages/backend/controllers/Admin.ts +++ b/packages/backend/controllers/Admin.ts @@ -3,9 +3,10 @@ import Invites from "../models/Invites"; import bcrypt from "bcrypt"; import { Request, Response } from "express"; import jwt from "jsonwebtoken"; -import { JWT_SECRET } from "../backend.config"; import { ably } from ".."; +const JWT_SECRET = process.env.JWT_SECRET || "superhardstring"; + async function generateUniqueInvite(length: number) { let invites = await Invites.findOne(); @@ -40,7 +41,7 @@ async function generateUniqueInvite(length: number) { export const createGame = async (req: Request, res: Response) => { try { - const { maxPlayers, diceCount, hiddenChars, privateKey, prize, mode, adminAddress } = req.body; + const { diceCount, hiddenChars, privateKey, hiddenPrivateKey, mode, adminAddress } = req.body; const salt = await bcrypt.genSalt(); // const privateKeyHash = await bcrypt.hash(privateKey, salt); @@ -49,12 +50,11 @@ export const createGame = async (req: Request, res: Response) => { adminAddress, status: "ongoing", inviteCode: await generateUniqueInvite(8), - maxPlayers, diceCount, mode, privateKey, + hiddenPrivateKey, hiddenChars, - prize, }); let token; @@ -68,6 +68,36 @@ export const createGame = async (req: Request, res: Response) => { } }; +export const restartWithNewPk = async (req: Request, res: Response) => { + try { + const { diceCount, hiddenChars, privateKey, hiddenPrivateKey, adminAddress } = req.body; + const { id } = req.params; + const game = await Game.findById(id); + + if (game?.status !== "finished") { + return res.status(400).json({ error: "Game is not finished." }); + } + + game.diceCount = diceCount; + game.hiddenChars = hiddenChars; + game.privateKey = privateKey; + game.hiddenPrivateKey = hiddenPrivateKey; + game.mode = "manual"; + game.adminAddress = adminAddress; + game.winner = undefined; + game.status = "ongoing"; + + const updatedGame = await game.save(); + console.log(updatedGame); + + const channel = ably.channels.get(`gameUpdate`); + channel.publish(`gameUpdate`, updatedGame); + res.status(200).json(updatedGame); + } catch (err) { + res.status(500).json({ error: (err as Error).message }); + } +}; + export const pauseGame = async (req: Request, res: Response) => { try { const { id } = req.params; @@ -167,7 +197,7 @@ export const changeGameMode = async (req: Request, res: Response) => { // return res.status(400).json({ error: "Game is not paused." }); // } - if (mode !== "auto" && mode !== "manual") { + if (mode !== "auto" && mode !== "manual" && mode !== "brute") { return res.status(400).json({ error: "Invalid game mode." }); } @@ -184,30 +214,6 @@ export const changeGameMode = async (req: Request, res: Response) => { } }; -export const changePrize = async (req: Request, res: Response) => { - try { - const { gameId } = req.params; - const { newPrize } = req.body; - - const game = await Game.findById(gameId); - - if (!game) { - return res.status(404).json({ error: "Game not found." }); - } - - if (game.status !== "ongoing") { - return res.status(400).json({ error: "Game is not ongoing." }); - } - - game.prize = newPrize; - const updatedGame = await game.save(); - - res.status(200).json(updatedGame); - } catch (err) { - res.status(500).json({ error: (err as Error).message }); - } -}; - export const kickPlayer = async (req: Request, res: Response) => { try { const { id } = req.params; diff --git a/packages/backend/controllers/Player.ts b/packages/backend/controllers/Player.ts index 1cdf7f8..50af732 100644 --- a/packages/backend/controllers/Player.ts +++ b/packages/backend/controllers/Player.ts @@ -18,10 +18,6 @@ export const join = async (req: Request, res: Response) => { return res.status(400).json({ error: "Game is not ongoing." }); } - if (game.players.length >= game.maxPlayers) { - return res.status(400).json({ error: "Game is full." }); - } - if (game.players.includes(playerAddress)) { return res.status(200).json(game); // Player is already in the game } @@ -32,7 +28,7 @@ export const join = async (req: Request, res: Response) => { game.players.push(playerAddress); const savedGame = await game.save(); - + const channel = ably.channels.get(`gameUpdate`); channel.publish(`gameUpdate`, savedGame); res.status(200).json({ token, game: savedGame }); diff --git a/packages/backend/index.ts b/packages/backend/index.ts index daaeafc..6fe5fa3 100644 --- a/packages/backend/index.ts +++ b/packages/backend/index.ts @@ -32,7 +32,9 @@ app.use(helmet.crossOriginResourcePolicy({ policy: "cross-origin" })); app.use(morgan("common")); app.use(bodyParser.json({ limit: "30mb" })); app.use(bodyParser.urlencoded({ limit: "30mb", extended: true })); -app.use(cors()); +app.use( + cors(), +); /**Ably Setup */ diff --git a/packages/backend/models/Game.ts b/packages/backend/models/Game.ts index c07a128..c012dd0 100644 --- a/packages/backend/models/Game.ts +++ b/packages/backend/models/Game.ts @@ -15,12 +15,6 @@ const gameSchema = new mongoose.Schema( type: String, required: true, }, - maxPlayers: { - type: Number, - required: true, - min: 5, - max: 30, - }, diceCount: { type: Number, required: true, @@ -29,27 +23,26 @@ const gameSchema = new mongoose.Schema( }, mode: { type: String, - enum: ["auto", "manual"], + enum: ["auto", "manual", "brute"], required: true, }, privateKey: { type: String, required: true, }, + hiddenPrivateKey: { + type: String, + required: false, + }, hiddenChars: { type: Object, required: true, }, - prize: { - type: Number, - required: true, - }, players: { type: [String], default: [], validate: { validator: function (value: [string]) { - // Check if the array only contains unique strings const uniqueStrings: string[] = []; value.forEach(item => { if (!uniqueStrings.includes(item)) { @@ -58,7 +51,7 @@ const gameSchema = new mongoose.Schema( }); return uniqueStrings.length === value.length; }, - message: "The players array must contain unique strings.", + message: "The players array must contain unique addresses.", }, }, winner: { diff --git a/packages/backend/models/Player.ts b/packages/backend/models/Player.ts index 01a489b..f91d7ad 100644 --- a/packages/backend/models/Player.ts +++ b/packages/backend/models/Player.ts @@ -34,10 +34,6 @@ const playerSchema = new mongoose.Schema( key: String, value: String, }, - prize: { - type: Number, - required: true, - }, }, { timestamps: true }, ); diff --git a/packages/backend/package.json b/packages/backend/package.json index 8f5f957..d18d256 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -2,7 +2,8 @@ "name": "@se-2/backend", "version": "0.0.1", "scripts": { - "backend": "ts-node index.ts" + "backend": "ts-node index.ts", + "start": "ts-node index.ts" }, "dependencies": { "ably": "^1.2.45", diff --git a/packages/backend/routes/admin.ts b/packages/backend/routes/admin.ts index c283f8f..bb0279d 100644 --- a/packages/backend/routes/admin.ts +++ b/packages/backend/routes/admin.ts @@ -1,5 +1,5 @@ import express from "express"; -import { createGame, changeGameMode, pauseGame, resumeGame, kickPlayer } from "../controllers/Admin"; +import { createGame, changeGameMode, pauseGame, resumeGame, kickPlayer, restartWithNewPk } from "../controllers/Admin"; import { verifyToken } from "../middleware/auth"; const router = express.Router(); @@ -9,5 +9,6 @@ router.patch("/changemode/:id", verifyToken, changeGameMode); router.patch("/pause/:id", verifyToken, pauseGame); router.patch("/resume/:id", verifyToken, resumeGame); router.patch("/kickplayer/:id", verifyToken, kickPlayer); +router.patch("/restartwithnewpk/:id", verifyToken, restartWithNewPk); export default router; diff --git a/packages/backend/vercel.json b/packages/backend/vercel.json new file mode 100644 index 0000000..5e02a5b --- /dev/null +++ b/packages/backend/vercel.json @@ -0,0 +1,6 @@ +{ + "version": 2, + "name": "dice-demonstration-backend", + "builds": [{ "src": "index.ts", "use": "@vercel/node" }], + "routes": [{ "src": "/(.*)", "dest": "/index.ts" }] +} diff --git a/packages/nextjs/components/Congrats.tsx b/packages/nextjs/components/Congrats.tsx deleted file mode 100644 index 911978c..0000000 --- a/packages/nextjs/components/Congrats.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { Dispatch, SetStateAction } from "react"; -import { Hex, createWalletClient } from "viem"; -import { http } from "viem"; -import { parseEther } from "viem"; -import { privateKeyToAccount } from "viem/accounts"; -import { useTransactor } from "~~/hooks/scaffold-eth"; -import useGameData from "~~/hooks/useGameData"; -import { getTargetNetwork } from "~~/utils/scaffold-eth"; - -const Congrats = ({ - isOpen, - setIsOpen, - message, -}: { - isOpen: boolean; - setIsOpen: Dispatch>; - message: string; -}) => { - const closePopup = () => { - setIsOpen(false); - }; - - const configuredNetwork = getTargetNetwork(); - - const walletClient = createWalletClient({ - chain: configuredNetwork, - transport: http(), - }); - - const { loadGameState } = useGameData(); - const transferTx = useTransactor(walletClient); - const { game } = loadGameState(); - const privateKey = "0x" + game?.privateKey; - - const account = privateKeyToAccount(privateKey as Hex); - - return ( -
- {isOpen && ( -
-
- -

{message}

- -
-
- )} -
- ); -}; - -export default Congrats; diff --git a/packages/nextjs/components/GameCreateForm.tsx b/packages/nextjs/components/GameCreateForm.tsx deleted file mode 100644 index 6dd39cb..0000000 --- a/packages/nextjs/components/GameCreateForm.tsx +++ /dev/null @@ -1,214 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { useRouter } from "next/router"; -import { EtherInput, InputBase } from "./scaffold-eth"; -import { useAccount } from "wagmi"; -import { loadBurnerSK } from "~~/hooks/scaffold-eth"; -import serverConfig from "~~/server.config"; -import { saveGameState } from "~~/utils/diceDemo/game"; -import { notification } from "~~/utils/scaffold-eth"; - -interface FormData { - maxPlayers: number; - diceCount: number; - mode: "auto" | "manual"; - privateKey: string; - hiddenChars: { - number?: string; - }; - prize: string; - adminAddress: string | undefined; -} - -const GameCreationForm = () => { - const router = useRouter(); - const { address: adminAddress } = useAccount(); - - const serverUrl = serverConfig.isLocal ? serverConfig.localUrl : serverConfig.liveUrl; - - const [formData, setFormData] = useState({ - maxPlayers: 5, - diceCount: 0, - mode: "manual", - privateKey: loadBurnerSK().toString().substring(2), - hiddenChars: {}, - prize: "", - adminAddress, - }); - const [selectedSlots, setSelectedSlots] = useState([]); - const [privateKey, setPrivateKey] = useState(""); - const [loading, setloading] = useState(false); - const disabled = parseFloat(formData.prize) == 0 || formData.prize == "" || selectedSlots.length == 0; - - useEffect(() => { - const pk = loadBurnerSK().toString().substring(2); - setPrivateKey(pk); - }, []); - - const handlePlayersChange = (value: number) => { - setFormData({ ...formData, maxPlayers: value }); - }; - - const handlePrizeChange = (value: string) => { - setFormData({ ...formData, prize: value }); - }; - - const handleModeChange = (value: "auto" | "manual") => { - setFormData({ ...formData, mode: value }); - }; - - const handleCharClick = (index: number) => { - const updatedSelectedSlots = [...selectedSlots]; - - if (updatedSelectedSlots.includes(index)) { - const indexToRemove = updatedSelectedSlots.indexOf(index); - updatedSelectedSlots.splice(indexToRemove, 1); - } else { - updatedSelectedSlots.push(index); - } - setSelectedSlots(updatedSelectedSlots.sort((a, b) => a - b)); - - setFormData({ - ...formData, - diceCount: updatedSelectedSlots.length, - }); - }; - - const createHiddenCharObject = (selectedSlots: number[]) => { - const characterObject: { [key: number]: string } = {}; - - const selectedCharacters = privateKey.split("").filter((char, index) => selectedSlots.includes(index)); - - selectedCharacters.forEach((char, index) => { - const selectedIndex = selectedSlots[index]; - characterObject[selectedIndex] = char; - }); - - setFormData({ - ...formData, - hiddenChars: characterObject, - }); - }; - - const handleSubmit = async (event: React.FormEvent) => { - event.preventDefault(); - setloading(true); - const createGameResponse = await fetch(`${serverUrl}/admin/create`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(formData), - }); - - const createdGame = await createGameResponse.json(); - setloading(false); - if (createdGame.error) { - notification.error(createdGame.error); - return; - } - - saveGameState(JSON.stringify(createdGame)); - router.push({ - pathname: `/game/[id]`, - query: { id: createdGame.game.inviteCode }, - }); - notification.success("Created game successfully"); - - setFormData({ - maxPlayers: 5, - diceCount: 0, - mode: "auto", - privateKey: loadBurnerSK(), - hiddenChars: {}, - prize: "", - adminAddress, - }); - }; - - useEffect(() => { - createHiddenCharObject(selectedSlots); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedSlots]); - - useEffect(() => { - setFormData({ - ...formData, - adminAddress: adminAddress, - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [adminAddress]); - - return ( -
-
- -
- -
- -
-
- -
- -
-
- ); -}; - -export default GameCreationForm; diff --git a/packages/nextjs/components/JoinForm.tsx b/packages/nextjs/components/JoinForm.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/packages/nextjs/components/Wallet.tsx b/packages/nextjs/components/Wallet.tsx index 7e703c4..4dcba08 100644 --- a/packages/nextjs/components/Wallet.tsx +++ b/packages/nextjs/components/Wallet.tsx @@ -88,9 +88,9 @@ export default function Wallet() { extraPkDisplayAdded[wallet.address] = true; extraPkDisplay.push(
- +
- +
, ); for (const key in localStorage) { @@ -101,9 +101,22 @@ export default function Wallet() { extraPkDisplayAdded[pastwallet.address] = true; extraPkDisplay.push(
- -
- + { + const currentPrivateKey = window.localStorage.getItem("scaffoldEth2.burnerWallet.sk"); + if (currentPrivateKey) { + window.localStorage.setItem( + "scaffoldEth2.burnerWallet.sk_backup" + Date.now(), + currentPrivateKey, + ); + } + window.localStorage.setItem("scaffoldEth2.burnerWallet.sk", pastpk as string); + window.location.reload(); + }} + > +
+
, ); } @@ -200,7 +213,7 @@ export default function Wallet() {

Known Private Keys:

{extraPkDisplay} + + + ); +}; + +export default GameCreationForm; diff --git a/packages/nextjs/components/GameJoinForm.tsx b/packages/nextjs/components/dicedemo/GameJoinForm.tsx similarity index 77% rename from packages/nextjs/components/GameJoinForm.tsx rename to packages/nextjs/components/dicedemo/GameJoinForm.tsx index 23b9fd3..8eaa961 100644 --- a/packages/nextjs/components/GameJoinForm.tsx +++ b/packages/nextjs/components/dicedemo/GameJoinForm.tsx @@ -1,6 +1,6 @@ import React, { Dispatch, SetStateAction, useEffect, useRef, useState } from "react"; import { useRouter } from "next/router"; -import { InputBase } from "./scaffold-eth"; +import { InputBase } from "../scaffold-eth"; import QrReader from "react-qr-reader-es6"; import { useAccount } from "wagmi"; import serverConfig from "~~/server.config"; @@ -19,6 +19,8 @@ const GameJoinForm = ({ const [scanning, setScanning] = useState(false); const [loading, setLoading] = useState(false); + const { invite } = router.query; + const handleChange = (value: string) => { setInviteCode(value); }; @@ -26,8 +28,7 @@ const GameJoinForm = ({ const { address: playerAddress } = useAccount(); const serverUrl = serverConfig.isLocal ? serverConfig.localUrl : serverConfig.liveUrl; - const handleJoinGame = async (event: React.FormEvent) => { - event.preventDefault(); + const handleJoinGame = async (invite: string) => { setLoading(true); const response = await fetch(`${serverUrl}/player/join`, { method: "PATCH", @@ -35,21 +36,19 @@ const GameJoinForm = ({ Authorization: `Bearer`, "Content-Type": "application/json", }, - body: JSON.stringify({ inviteCode, playerAddress }), + body: JSON.stringify({ inviteCode: invite, playerAddress }), }); const updatedGame = await response.json(); + saveGameState(JSON.stringify(updatedGame)); setLoading(false); if (updatedGame.error) { notification.error(updatedGame.error); return; } - - saveGameState(JSON.stringify(updatedGame)); - await router.push({ pathname: `/game/[id]`, - query: { id: inviteCode }, + query: { id: invite }, }); notification.success("Joined game successfully"); @@ -70,9 +69,9 @@ const GameJoinForm = ({ setScanning(false); }; - const openCamera = () => { - setScanning(true); - }; + // const openCamera = () => { + // setScanning(true); + // }; useEffect(() => { if (labelRef.current) { @@ -80,9 +79,22 @@ const GameJoinForm = ({ } }, []); + useEffect(() => { + if (invite) { + handleJoinGame(invite as string); + } + if (Object.keys(router.query).length > 0) { + router.replace({ + pathname: router.pathname, + query: {}, + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [invite]); + return (
-
+ handleJoinGame(inviteCode)}> +
+ + )} + + ); +}; + +export default RestartWithNewPk; diff --git a/packages/nextjs/components/dicedemo/WinnerAnnouncement.tsx b/packages/nextjs/components/dicedemo/WinnerAnnouncement.tsx new file mode 100644 index 0000000..f1280ea --- /dev/null +++ b/packages/nextjs/components/dicedemo/WinnerAnnouncement.tsx @@ -0,0 +1,7 @@ +import React from "react"; + +const WinnerAnnouncement = () => { + return
WinnerAnnouncement
; +}; + +export default WinnerAnnouncement; diff --git a/packages/nextjs/hooks/useGameData.ts b/packages/nextjs/hooks/useGameData.tsx similarity index 100% rename from packages/nextjs/hooks/useGameData.ts rename to packages/nextjs/hooks/useGameData.tsx diff --git a/packages/nextjs/hooks/useSweepWallet.tsx b/packages/nextjs/hooks/useSweepWallet.tsx new file mode 100644 index 0000000..6845ea7 --- /dev/null +++ b/packages/nextjs/hooks/useSweepWallet.tsx @@ -0,0 +1,105 @@ +import { useState } from "react"; +import { ethers } from "ethers"; +import { useAccount } from "wagmi"; +import { Game } from "~~/types/game/game"; +import { endGame } from "~~/utils/diceDemo/apiUtils"; +import { getApiKey, getBlockExplorerTxLink, getTargetNetwork } from "~~/utils/scaffold-eth"; +import { notification } from "~~/utils/scaffold-eth"; + +const TxnNotification = ({ message, blockExplorerLink }: { message: string; blockExplorerLink?: string }) => { + return ( +
+

{message}

+ {blockExplorerLink && blockExplorerLink.length > 0 ? ( + + check out transaction + + ) : null} +
+ ); +}; + +const useSweepWallet = ({ game, token }: { game?: Game; token?: string }) => { + const { address } = useAccount(); + const configuredNetwork = getTargetNetwork(); + const apiKey = getApiKey(); + const [isSweeping, setIsSweeping] = useState(false); + + const provider = new ethers.providers.AlchemyProvider(configuredNetwork.network, apiKey); + + const sweepWallet = async (privateKey: string) => { + setIsSweeping(true); + const wallet = new ethers.Wallet(privateKey, provider); + const balance = await wallet.getBalance(); + if (balance.eq(0)) { + const message = "Wallet balance is 0"; + console.log(message); + setIsSweeping(false); + notification.info(message); + return; + } + + const gasPrice = await provider.getGasPrice(); + + const gasLimit = 21000000; + const gasCost = gasPrice.mul(35000000); // gasLimit * 1.667 + + // const totalToSend = balance.sub(gasCost.mul(2)); + const totalToSend = balance.sub(gasCost); + + console.log(totalToSend.toNumber()); + console.log(gasCost.toNumber()); + + if (totalToSend.lte(0)) { + const message = "Balance is not enough to cover gas fees."; + console.log(message); + setIsSweeping(false); + notification.info(message); + return; + } + + const tx = { + to: address, + value: totalToSend, + gasLimit: gasLimit, + gasPrice: gasPrice, + }; + + let txReceipt = null; + + let notificationId = null; + try { + txReceipt = await wallet.sendTransaction(tx); + const transactionHash = txReceipt.hash; + + notificationId = notification.loading(); + + const blockExplorerTxURL = configuredNetwork ? getBlockExplorerTxLink(configuredNetwork.id, transactionHash) : ""; + await txReceipt.wait(); + notification.remove(notificationId); + + notification.success( + , + { + icon: "🎉", + }, + ); + + endGame(game as Game, token as string, address as string); + setIsSweeping(false); + } catch (error: any) { + setIsSweeping(false); + if (notificationId) { + notification.remove(notificationId); + } + console.error("⚡️ ~ Sweep Wallet ~ error", error); + notification.error(error.message); + } + + console.log("Transaction sent:", txReceipt); + }; + + return { sweepWallet, isSweeping }; +}; + +export default useSweepWallet; diff --git a/packages/nextjs/pages/game/[id].tsx b/packages/nextjs/pages/game/[id].tsx index a4bb985..29ebcd5 100644 --- a/packages/nextjs/pages/game/[id].tsx +++ b/packages/nextjs/pages/game/[id].tsx @@ -1,30 +1,28 @@ -// pages/game/[id].js import { useEffect, useRef, useState } from "react"; import { useRouter } from "next/router"; import Ably from "ably"; import QRCode from "qrcode.react"; import CopyToClipboard from "react-copy-to-clipboard"; -import { useAccount } from "wagmi"; +import { useAccount, useBalance } from "wagmi"; import { CheckCircleIcon, DocumentDuplicateIcon } from "@heroicons/react/24/outline"; -import Condolence from "~~/components/Condolence"; -import Congrats from "~~/components/Congrats"; +import Congrats from "~~/components/dicedemo/Congrats"; +import RestartWithNewPk from "~~/components/dicedemo/RestartWithNewPk"; import { Address } from "~~/components/scaffold-eth"; import { Price } from "~~/components/scaffold-eth/Price"; import useGameData from "~~/hooks/useGameData"; -import serverConfig from "~~/server.config"; +import useSweepWallet from "~~/hooks/useSweepWallet"; import { Game } from "~~/types/game/game"; -import { notification } from "~~/utils/scaffold-eth"; +import { kickPlayer, pauseResumeGame, toggleMode } from "~~/utils/diceDemo/apiUtils"; function GamePage() { const router = useRouter(); const { id } = router.query; - const serverUrl = serverConfig.isLocal ? serverConfig.localUrl : serverConfig.liveUrl; const ablyApiKey = process.env.NEXT_PUBLIC_ABLY_API_KEY; const { loadGameState, updateGameState } = useGameData(); const { address } = useAccount(); - const videoRef = useRef(null); + const [isRolling, setIsRolling] = useState(false); const [isUnitRolling, setIsUnitRolling] = useState([false]); const [rolled, setRolled] = useState(false); @@ -34,35 +32,34 @@ function GamePage() { const [game, setGame] = useState(); const [token, setToken] = useState(""); const [isOpen, setIsOpen] = useState(true); + const [restartOpen, setRestartOpen] = useState(false); const [inviteCopied, setInviteCopied] = useState(false); const [inviteUrl, setInviteUrl] = useState(""); const [inviteUrlCopied, setInviteUrlCopied] = useState(false); - - const congratulatoryMessage = "Congratulations! You won the game!"; - const condolenceMessage = "Sorry Fren! You Lost"; const [autoRolling, setAutoRolling] = useState(false); - + const [bruteRolling, setBruteRolling] = useState(false); const [screenwidth, setScreenWidth] = useState(768); + const [isHacked, setIsHacked] = useState(false); - console.log(isUnitRolling); - - const calculateLength = () => { - const maxLength = 200; - const diceCount = game?.diceCount ?? 0; - const calculatedLength = Math.max(maxLength - (diceCount - 1) * 3.8, 10); + const prize = useBalance({ address: game?.adminAddress }); + const { sweepWallet } = useSweepWallet({ game, token }); - return calculatedLength; - }; - - const isAdmin = address == game?.adminAddress; - const isPlayer = game?.players?.includes(address as string); + // const calculateLength = () => { + // const maxLength = 200; + // const diceCount = game?.diceCount ?? 0; + // const calculatedLength = Math.max(maxLength - (diceCount - 1) * 3.8, 10); + // return calculatedLength; + // }; const generateRandomHex = () => { - const hexDigits = "0123456789abcdef"; + const hexDigits = "0123456789ABCDEF"; const randomIndex = Math.floor(Math.random() * hexDigits.length); return hexDigits[randomIndex]; }; + const isAdmin = address == game?.adminAddress; + const isPlayer = game?.players?.includes(address as string); + const rollTheDice = () => { if (game) { setIsRolling(true); @@ -76,7 +73,6 @@ function GamePage() { rolls.push(generateRandomHex()); } setRolls(rolls); - let iterations = 0; for (let i = 0; i < isUnitRolling.length; i++) { setTimeout(() => { @@ -98,73 +94,28 @@ function GamePage() { } }; - const length = calculateLength(); - console.log(length); - - const compareResult = () => { - if (rolled && rolledResult.length > 0 && game?.hiddenChars) - return rolledResult.every((value, index) => value === Object.values(game?.hiddenChars)[index]); - }; - - const endGame = async () => { - await fetch(`${serverUrl}/game/${game?._id}`, { - method: "PATCH", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ winner: address }), - }); - }; - - const toggleMode = async () => { - const response = await fetch(`${serverUrl}/admin/changemode/${game?._id}`, { - method: "PATCH", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ mode: game?.mode == "manual" ? "auto" : "manual" }), - }); - - const responseData = await response.json(); - if (responseData.error) { - notification.error(responseData.error); - return; - } - }; - - const pauseResumeGame = async () => { - const response = await fetch(`${serverUrl}/admin/${game?.status == "ongoing" ? "pause" : "resume"}/${game?._id}`, { - method: "PATCH", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - }); - - const responseData = await response.json(); - if (responseData.error) { - notification.error(responseData.error); - return; + const bruteRoll = () => { + if (game) { + setIsRolling(true); + if (!rolled) { + setRolled(true); + } + setSpinning(true); + const rolls: string[] = []; + for (let index = 0; index < game?.diceCount; index++) { + rolls.push(generateRandomHex()); + } + setRolls(rolls); + setSpinning(false); + setRolledResult(rolls); } }; - const kickPlayer = async (playerAddress: string) => { - const response = await fetch(`${serverUrl}/admin/kickplayer/${game?._id}`, { - method: "PATCH", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ playerAddress: playerAddress }), - }); - - const responseData = await response.json(); - if (responseData.error) { - notification.error(responseData.error); - return; - } + const compareResult = () => { + if (rolled && rolledResult.length > 0 && game?.hiddenChars) + return rolledResult.every( + (value, index) => value.toLowerCase() === Object.values(game?.hiddenChars)[index].toLowerCase(), + ); }; useEffect(() => { @@ -190,10 +141,15 @@ function GamePage() { useEffect(() => { const isHiiddenChars = compareResult(); + if (isHiiddenChars) { - endGame(); setAutoRolling(false); + setBruteRolling(false); + setIsRolling(false); + setSpinning(false); setIsOpen(true); + setIsHacked(true); + sweepWallet(game?.privateKey as string); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [rolledResult]); @@ -238,46 +194,78 @@ function GamePage() { timeout = setTimeout(autoRoll, 5500); } }; - if (game?.winner) { return; } - autoRoll(); - return () => { clearTimeout(timeout); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [autoRolling, game]); + useEffect(() => { + if (game?.winner) { + setIsRolling(false); + return; + } + + if (bruteRolling && game?.mode === "brute") { + const intervalId = setInterval(() => { + bruteRoll(); + }, 1); + + return () => clearInterval(intervalId); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [bruteRolling, game]); + + useEffect(() => { + if (game?.status == "paused") { + setAutoRolling(false); + setBruteRolling(false); + setIsRolling(false); + setSpinning(false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [game]); + + useEffect(() => { + setAutoRolling(false); + setBruteRolling(false); + setIsRolling(false); + setSpinning(false); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [game?.mode]); + if (game) { return (
-
-
-
+
+
+
-

INFO

+

INFO

Role: {isAdmin ? "Host" : isPlayer ? "Player" : "Kicked"}

-
+
{isAdmin && ( -
-
- Invite Code: {id} - {inviteCopied ? ( +
+
+ Copy Invite Url + {inviteUrlCopied ? (
-
+
+ Invite: {id?.toString()} + {inviteCopied ? ( +
+ {/*
{inviteUrlCopied ? ( Copied invite Url ) : ( @@ -312,10 +324,10 @@ function GamePage() { Copy invite Url )} -
+
*/}
)} -
+
Status: {game.status} {isAdmin && ( @@ -324,7 +336,7 @@ function GamePage() { type="checkbox" className="toggle toggle-primary bg-primary tooltip tooltip-bottom tooltip-primary" data-tip={game?.status == "ongoing" ? "pause" : game?.status == "paused" ? "resume" : ""} - onChange={pauseResumeGame} + onChange={() => pauseResumeGame(game, token)} checked={game?.status == "ongoing"} /> )} @@ -332,7 +344,7 @@ function GamePage() {
Mode: {game.mode} {isAdmin && ( -
+
@@ -353,7 +365,19 @@ function GamePage() { className="radio checked:bg-blue-500" checked={game?.mode == "manual"} onClick={() => { - if (game?.mode == "auto") toggleMode(); + if (game?.mode != "manual") toggleMode(game, "manual", token); + }} + /> + + @@ -361,59 +385,89 @@ function GamePage() { )}
-
- Prize: - +
+ Pk Balance: +
-
+
Dice count: {game.diceCount}
- {game.winner && ( -
- Winner
+ {screenwidth <= 768 && ( +
+
+

HIDDEN CHARACTERS

+
+

{Object.values(game?.hiddenPrivateKey)}

)}
+ {game.winner && ( +
+ Winner
+
+ )} + {/* {isAdmin && game.winner && ( + + )} */}
-
-
-
-

- HIDDEN CHARACTERS -

+ {screenwidth > 768 && ( +
+
+

HIDDEN CHARACTERS

-

{Object.values(game?.hiddenChars).join(" , ")}

-
-
-

- PLAYERS -

-
-
- {game?.players?.map((player: string) => ( -
-
768 ? "long" : "short"} address={player} /> - {isAdmin && ( - - )} +

{Object.values(game?.hiddenPrivateKey)}

+
+
+

+ PLAYERS: {game?.players.length} +

+
+
+ {game?.players?.map((player: string) => ( +
+
+ {isAdmin && ( + + )} +
+ ))}
- ))} +
-
+ )}
+ {isPlayer && (
@@ -423,10 +477,11 @@ function GamePage() {
{Object.entries(game.hiddenChars).map(([key], index) => rolled ? ( - isUnitRolling[index] ? ( + isUnitRolling[index] || (isRolling && game.mode == "brute") ? (
{" "} - {game?.winner == address && ( - - )} - {game.winner && game?.winner != address && ( - + {(isHacked || game.winner) && ( + )}
)} + {screenwidth <= 768 && game.players.length > 0 && ( +
+
+
+

PLAYERS

+
+
+ {game?.players?.map((player: string) => ( +
+
+ {isAdmin && ( + + )} +
+ ))} +
+
+
+ )} + {isAdmin && game.winner && } {!isAdmin && !isPlayer &&

Sorry fren, You have been kicked

}
diff --git a/packages/nextjs/pages/index.tsx b/packages/nextjs/pages/index.tsx index f21ae6f..a9da536 100644 --- a/packages/nextjs/pages/index.tsx +++ b/packages/nextjs/pages/index.tsx @@ -1,9 +1,9 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import type { NextPage } from "next"; -import GameCreationForm from "~~/components/GameCreateForm"; -import GameJoinForm from "~~/components/GameJoinForm"; import { MetaHeader } from "~~/components/MetaHeader"; +import GameCreationForm from "~~/components/dicedemo/GameCreateForm"; +import GameJoinForm from "~~/components/dicedemo/GameJoinForm"; const Home: NextPage = () => { const router = useRouter(); diff --git a/packages/nextjs/server.config.ts b/packages/nextjs/server.config.ts index b54076e..9121921 100644 --- a/packages/nextjs/server.config.ts +++ b/packages/nextjs/server.config.ts @@ -1,7 +1,7 @@ const serverConfig = { isLocal: false, localUrl: "http://localhost:6001", - liveUrl: "https://weak-teal-haddock-sari.cyclic.app", + liveUrl: "https://rich-ruby-cygnet-tie.cyclic.app/", }; export default serverConfig; diff --git a/packages/nextjs/types/game/game.ts b/packages/nextjs/types/game/game.ts index 476cfc2..104054a 100644 --- a/packages/nextjs/types/game/game.ts +++ b/packages/nextjs/types/game/game.ts @@ -3,12 +3,11 @@ export interface Game { adminAddress: string; status: "lobby" | "ongoing" | "paused" | "finished"; inviteCode: string; - maxPlayers: number; diceCount: number; - mode: "auto" | "manual"; + mode: "auto" | "manual" | "brute"; privateKey: string; + hiddenPrivateKey: string; hiddenChars: Record; - prize: number; players: string[]; winner?: string | null; } diff --git a/packages/nextjs/utils/diceDemo/apiUtils.ts b/packages/nextjs/utils/diceDemo/apiUtils.ts new file mode 100644 index 0000000..1cd33e6 --- /dev/null +++ b/packages/nextjs/utils/diceDemo/apiUtils.ts @@ -0,0 +1,66 @@ +import { notification } from "../scaffold-eth"; +import serverConfig from "~~/server.config"; +import { Game } from "~~/types/game/game"; + +const serverUrl = serverConfig.isLocal ? serverConfig.localUrl : serverConfig.liveUrl; + +export const endGame = async (game: Game, token: string, address: string) => { + await fetch(`${serverUrl}/game/${game?._id}`, { + method: "PATCH", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ winner: address }), + }); +}; + +export const toggleMode = async (game: Game, mode: string, token: string) => { + const response = await fetch(`${serverUrl}/admin/changemode/${game?._id}`, { + method: "PATCH", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ mode: mode }), + }); + + const responseData = await response.json(); + if (responseData.error) { + notification.error(responseData.error); + return; + } +}; + +export const pauseResumeGame = async (game: Game, token: string) => { + const response = await fetch(`${serverUrl}/admin/${game?.status == "ongoing" ? "pause" : "resume"}/${game?._id}`, { + method: "PATCH", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }); + + const responseData = await response.json(); + if (responseData.error) { + notification.error(responseData.error); + return; + } +}; + +export const kickPlayer = async (game: Game, token: string, playerAddress: string) => { + const response = await fetch(`${serverUrl}/admin/kickplayer/${game?._id}`, { + method: "PATCH", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ playerAddress: playerAddress }), + }); + + const responseData = await response.json(); + if (responseData.error) { + notification.error(responseData.error); + return; + } +}; diff --git a/packages/nextjs/utils/diceDemo/game.ts b/packages/nextjs/utils/diceDemo/game.ts index 90f6c58..0146c7a 100644 --- a/packages/nextjs/utils/diceDemo/game.ts +++ b/packages/nextjs/utils/diceDemo/game.ts @@ -9,7 +9,6 @@ export const saveGameState = (gameState: string) => { export const loadGameState = () => { if (typeof window != "undefined" && window != null) { const gameState = window.localStorage.getItem(STORAGE_KEY); - // console.log(gameState); if (gameState) return JSON.parse(gameState); } else return { token: null, game: null }; }; diff --git a/packages/nextjs/utils/scaffold-eth/networks.ts b/packages/nextjs/utils/scaffold-eth/networks.ts index f9e576e..e01c9d0 100644 --- a/packages/nextjs/utils/scaffold-eth/networks.ts +++ b/packages/nextjs/utils/scaffold-eth/networks.ts @@ -113,3 +113,8 @@ export function getTargetNetwork(): chains.Chain & Partial { ...NETWORKS_EXTRA_DATA[configuredNetwork.id], }; } + +export function getApiKey() { + const apiKey = scaffoldConfig.alchemyApiKey; + return apiKey; +}