diff --git a/packages/backend/controllers/Admin.ts b/packages/backend/controllers/Admin.ts index b401b50..7516113 100644 --- a/packages/backend/controllers/Admin.ts +++ b/packages/backend/controllers/Admin.ts @@ -5,8 +5,9 @@ import { Request, Response } from "express"; import jwt from "jsonwebtoken"; import { ably } from ".."; +const JWT_SECRET = process.env.JWT_SECRET || "superhardstring"; + async function generateUniqueInvite(length: number) { - const JWT_SECRET = process.env.JWT_SECRET || "superhardstring"; let invites = await Invites.findOne(); if (!invites) { @@ -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..581ef12 100644 --- a/packages/backend/index.ts +++ b/packages/backend/index.ts @@ -32,7 +32,11 @@ 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({ + origin: "*", + }), +); /**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..a3b7958 --- /dev/null +++ b/packages/backend/vercel.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "name": "dice-demonstration-backend", + "builds": [ + { "src": "index.ts", "use": "@vercel/node" } + ], + "routes": [ + { "src": "/(.*)", "dest": "/index.ts" } + ] + } + \ No newline at end of file diff --git a/packages/nextjs/components/GameCreateForm.tsx b/packages/nextjs/components/GameCreateForm.tsx deleted file mode 100644 index 61058e5..0000000 --- a/packages/nextjs/components/GameCreateForm.tsx +++ /dev/null @@ -1,291 +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: { [key: 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 initialPrivateKey = loadBurnerSK().toString().substring(2); - const firstCharacterHidden = initialPrivateKey.charAt(0) ? "*" : ""; - - - const [formData, setFormData] = useState({ - maxPlayers: 5, - diceCount: 1, - mode: "manual", - privateKey: loadBurnerSK().toString().substring(2), - hiddenChars: { 0: firstCharacterHidden }, - prize: "", - adminAddress, - }); - - const [selectedSlots, setSelectedSlots] = useState([0]); - const [sliderValue, setSliderValue] = useState(1); // State for slider value - 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 initialHiddenChars = {0: pk.charAt(0) ? "*" : ""}; - const initialSelectedSlots = [0]; - - setFormData(prevFormData => ({ - ...prevFormData, - privateKey: pk, - hiddenChars: initialHiddenChars, - })); - - setSelectedSlots(initialSelectedSlots); - }, []); - - useEffect(() => { - - const hiddenCount = sliderValue; - const hiddenChars: Record = {} - for (let i = 0; i < hiddenCount; i++) { - hiddenChars[i] = privateKey[i] ? "*" : ""; - } - - setFormData(formData => ({ - ...formData, - hiddenChars, - diceCount: hiddenCount, - })); - }, [sliderValue, privateKey]); - - - useEffect(() => { - setFormData(formData => ({ - ...formData, - adminAddress: adminAddress, - })); - }, [adminAddress]); - - - 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]); - - const handleSliderChange = (event: React.ChangeEvent) => { - const value = parseInt(event.target.value, 10); - setSliderValue(value); - - - const newSelectedSlots = Array.from({ length: value }, (_, i) => i); - setSelectedSlots(newSelectedSlots); - - const newHiddenChars: { [key: number]: string } = {}; - for (let i = 0; i < value; i++) { - newHiddenChars[i] = privateKey[i] ? "*" : ""; - } - - setFormData(prevFormData => ({ - ...prevFormData, - diceCount: value, - hiddenChars: newHiddenChars, - })); -}; - - - return ( -
-
- {/* Slider input for selecting a number between 1 and 64 */} - -
- -
- -
- -
-
- -
- -
-
- ); -}; - -export default GameCreationForm; \ No newline at end of file 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..c8b7dbd 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(); + }} + > +
+
, ); } @@ -302,7 +315,7 @@ export default function Wallet() { to: toAddress, value, chain: configuredNetwork, - // gas: BigInt("21000"), + gas: BigInt("31500"), }); setOpen(!open); setQr(""); diff --git a/packages/nextjs/components/Condolence.tsx b/packages/nextjs/components/dicedemo/Condolence.tsx similarity index 100% rename from packages/nextjs/components/Condolence.tsx rename to packages/nextjs/components/dicedemo/Condolence.tsx diff --git a/packages/nextjs/components/Congrats.tsx b/packages/nextjs/components/dicedemo/Congrats.tsx similarity index 51% rename from packages/nextjs/components/Congrats.tsx rename to packages/nextjs/components/dicedemo/Congrats.tsx index 911978c..fc7ae77 100644 --- a/packages/nextjs/components/Congrats.tsx +++ b/packages/nextjs/components/dicedemo/Congrats.tsx @@ -1,11 +1,6 @@ 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"; +import useSweepWallet from "~~/hooks/useSweepWallet"; const Congrats = ({ isOpen, @@ -20,19 +15,9 @@ const Congrats = ({ 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); + const { sweepWallet } = useSweepWallet(); return (
@@ -45,22 +30,11 @@ const Congrats = ({

{message}

diff --git a/packages/nextjs/components/dicedemo/GameCreateForm.tsx b/packages/nextjs/components/dicedemo/GameCreateForm.tsx new file mode 100644 index 0000000..0674c81 --- /dev/null +++ b/packages/nextjs/components/dicedemo/GameCreateForm.tsx @@ -0,0 +1,138 @@ +import React, { useEffect, useState } from "react"; +import { useRouter } from "next/router"; +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 { + diceCount: number; + mode: "auto" | "manual" | "brute"; + privateKey: string; + hiddenPrivateKey: string; + hiddenChars: { [key: number]: string }; + adminAddress: string | undefined; +} + +const GameCreationForm = () => { + const router = useRouter(); + const { address: adminAddress } = useAccount(); + + const serverUrl = serverConfig.isLocal ? serverConfig.localUrl : serverConfig.liveUrl; + const initialPrivateKey = loadBurnerSK().toString().substring(2); + + const [formData, setFormData] = useState({ + diceCount: 1, + mode: "manual", + hiddenPrivateKey: "*" + initialPrivateKey.slice(1), + privateKey: initialPrivateKey, + hiddenChars: { 0: initialPrivateKey.charAt(0) }, + adminAddress, + }); + + const [privateKey, setPrivateKey] = useState(""); + const [sliderValue, setSliderValue] = useState(1); + const [loading, setloading] = useState(false); + + useEffect(() => { + setPrivateKey(initialPrivateKey); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [initialPrivateKey]); + + 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 => ({ + ...formData, + hiddenChars: characterObject, + diceCount: selectedSlots.length, + hiddenPrivateKey: "*".repeat(selectedSlots.length) + privateKey.slice(selectedSlots.length), + })); + }; + + 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({ + diceCount: 0, + mode: "auto", + privateKey: loadBurnerSK(), + hiddenChars: {}, + hiddenPrivateKey: "", + adminAddress, + }); + }; + + useEffect(() => { + setFormData({ + ...formData, + adminAddress: adminAddress, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [adminAddress]); + + const handleSliderChange = (event: React.ChangeEvent) => { + const value = parseInt(event.target.value, 10); + setSliderValue(value); + + const selectedSlots = Array.from({ length: value }, (_, i) => i); + createHiddenCharObject(selectedSlots); + }; + + return ( +
+
+ +
+ +
+
+ ); +}; + +export default GameCreationForm; diff --git a/packages/nextjs/components/GameJoinForm.tsx b/packages/nextjs/components/dicedemo/GameJoinForm.tsx similarity index 98% rename from packages/nextjs/components/GameJoinForm.tsx rename to packages/nextjs/components/dicedemo/GameJoinForm.tsx index 23b9fd3..a126278 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"; diff --git a/packages/nextjs/components/dicedemo/RestartWithNewPk.tsx b/packages/nextjs/components/dicedemo/RestartWithNewPk.tsx new file mode 100644 index 0000000..a57d463 --- /dev/null +++ b/packages/nextjs/components/dicedemo/RestartWithNewPk.tsx @@ -0,0 +1,143 @@ +import { Dispatch, SetStateAction, useEffect, useState } from "react"; +import { Hex } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { generatePrivateKey } from "viem/accounts"; +import useGameData from "~~/hooks/useGameData"; +import serverConfig from "~~/server.config"; +import { updateGameState } from "~~/utils/diceDemo/game"; +import { notification } from "~~/utils/scaffold-eth"; + +interface FormData { + diceCount: number; + privateKey: string; + hiddenPrivateKey: string; + hiddenChars: { [key: number]: string }; + adminAddress: string | undefined; +} + +const RestartWithNewPk = ({ isOpen, setIsOpen }: { isOpen: boolean; setIsOpen: Dispatch> }) => { + const closePopup = () => { + setIsOpen(false); + }; + const serverUrl = serverConfig.isLocal ? serverConfig.localUrl : serverConfig.liveUrl; + const { loadGameState } = useGameData(); + const { token, game } = loadGameState(); + const [sliderValue, setSliderValue] = useState(1); + const [newPk, setNewPk] = useState(""); + const [hexPk, setHexPk] = useState(""); + const [formData, setFormData] = useState({ + diceCount: 0, + hiddenPrivateKey: "", + privateKey: "", + hiddenChars: {}, + adminAddress: undefined, + }); + + const handleRestart = async () => { + const response = await fetch(`${serverUrl}/admin/restartwithnewpk/${game?._id}`, { + method: "PATCH", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(formData), + }); + + const responseData = await response.json(); + if (responseData.error) { + notification.error(responseData.error); + return; + } + 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", hexPk); + updateGameState(responseData); + notification.success("Restarted game successfully"); + setTimeout(() => { + window.location.reload(); + }, 100); + }; + + const handleSliderChange = (event: React.ChangeEvent) => { + const value = parseInt(event.target.value, 10); + setSliderValue(value); + + const selectedSlots = Array.from({ length: value }, (_, i) => i); + createHiddenCharObject(selectedSlots); + }; + + const createHiddenCharObject = (selectedSlots: number[]) => { + const characterObject: { [key: number]: string } = {}; + const selectedCharacters = newPk.split("").filter((char, index) => selectedSlots.includes(index)); + selectedCharacters.forEach((char, index) => { + const selectedIndex = selectedSlots[index]; + characterObject[selectedIndex] = char; + }); + + setFormData(formData => ({ + ...formData, + diceCount: selectedSlots.length, + hiddenChars: characterObject, + hiddenPrivateKey: "*".repeat(selectedSlots.length) + newPk.slice(selectedSlots.length), + })); + }; + + useEffect(() => { + const privateKey = generatePrivateKey(); + const pk = privateKey.substring(2); + const account = privateKeyToAccount(privateKey as Hex); + setNewPk(pk); + setHexPk(privateKey); + setFormData(formData => ({ + ...formData, + diceCount: 1, + adminAddress: account.address, + hiddenPrivateKey: "*" + pk.slice(1), + privateKey: pk, + hiddenChars: { 0: pk.charAt(0) }, + })); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( +
+ {isOpen && ( +
+
+ +

You are about to create a new game

+ + +
+
+ )} +
+ ); +}; + +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..a74a69d --- /dev/null +++ b/packages/nextjs/hooks/useSweepWallet.tsx @@ -0,0 +1,93 @@ +import { ethers } from "ethers"; +import { useAccount } from "wagmi"; +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 = () => { + const { address } = useAccount(); + const configuredNetwork = getTargetNetwork(); + const apiKey = getApiKey(); + + const provider = new ethers.providers.AlchemyProvider(configuredNetwork.network, apiKey); + + const sweepWallet = async (privateKey: string) => { + const wallet = new ethers.Wallet(privateKey, provider); + const balance = await wallet.getBalance(); + if (balance.eq(0)) { + console.log("Wallet balance is 0"); + notification.info("Wallet balance is 0"); + return; + } + + const gasPrice = await provider.getGasPrice(); + + const gasLimit = 21000; + const gasCost = gasPrice.mul(gasLimit); + + // const totalToSend = balance.sub(gasCost.mul(2)); + let totalToSend = balance.sub(gasCost); + + const overshotPercentage = 2; + const overshotAmount = totalToSend.mul(overshotPercentage).div(100); + totalToSend = totalToSend.sub(overshotAmount); + + if (totalToSend.lte(0)) { + console.log("Balance is not enough to cover gas fees."); + notification.info("Balance is not enough to cover gas fees."); + 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: "🎉", + }, + ); + } catch (error: any) { + if (notificationId) { + notification.remove(notificationId); + } + console.error("⚡️ ~ Sweep Wallet ~ error", error); + notification.error(error.message); + } + + console.log("Transaction sent:", txReceipt); + }; + + return { sweepWallet }; +}; + +export default useSweepWallet; diff --git a/packages/nextjs/pages/game/[id].tsx b/packages/nextjs/pages/game/[id].tsx index ff453e4..13705f0 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 Condolence from "~~/components/dicedemo/Condolence"; +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 { Game } from "~~/types/game/game"; -import { notification } from "~~/utils/scaffold-eth"; +import { endGame, 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); - console.log(isUnitRolling); - - const calculateLength = () => { - const maxLength = 200; - const diceCount = game?.diceCount ?? 0; - const calculatedLength = Math.max(maxLength - (diceCount - 1) * 3.8, 10); - - return calculatedLength; - }; + const prize = useBalance({ address: game?.adminAddress }); + const congratulatoryMessage = "Congratulations! You won the game!"; + const condolenceMessage = "Sorry Fren! You Lost"; - 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(() => { @@ -191,8 +142,9 @@ function GamePage() { useEffect(() => { const isHiiddenChars = compareResult(); if (isHiiddenChars) { - endGame(); + endGame(game as Game, token, address as string); setAutoRolling(false); + setBruteRolling(false); setIsOpen(true); } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -238,19 +190,51 @@ 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 (
@@ -324,7 +308,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 +316,7 @@ function GamePage() {
Mode: {game.mode} {isAdmin && ( -
+
@@ -353,7 +337,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); + }} + /> + + @@ -362,8 +358,8 @@ function GamePage() {
- Prize: - + Pk Balance: +
Dice count: {game.diceCount} @@ -373,6 +369,16 @@ function GamePage() { Winner
)} + {isAdmin && game.winner && ( + + )}
@@ -382,7 +388,7 @@ function GamePage() { HIDDEN CHARACTERS
-

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

+

{Object.values(game?.hiddenPrivateKey)}

@@ -394,7 +400,7 @@ function GamePage() {
768 ? "long" : "short"} address={player} /> {isAdmin && ( - )} @@ -408,12 +414,23 @@ function GamePage() {
@@ -421,36 +438,35 @@ function GamePage() { {rolledResult.length > 0 && !spinning && {rolledResult.join(" , ")}}
- {Object.entries(game.hiddenChars).map(([key], index) => - rolled ? ( - isUnitRolling[index] ? ( -
{" "} {game?.winner == address && ( @@ -461,6 +477,7 @@ function GamePage() { )}
)} + {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..19c8da0 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://dice-demonstration-backend.vercel.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; +}