bet creation

This commit is contained in:
Sewmina 2025-04-02 07:32:57 +05:30
parent ac8cc4db9e
commit 98d6c3bd80
16 changed files with 1187 additions and 29 deletions

124
package-lock.json generated
View File

@ -8,6 +8,7 @@
"name": "duelfi", "name": "duelfi",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@coral-xyz/anchor": "^0.31.0",
"@fontsource/manrope": "^5.2.5", "@fontsource/manrope": "^5.2.5",
"@privy-io/react-auth": "^2.7.2", "@privy-io/react-auth": "^2.7.2",
"@solana/web3.js": "^1.98.0", "@solana/web3.js": "^1.98.0",
@ -19,6 +20,7 @@
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-toastify": "^11.0.5", "react-toastify": "^11.0.5",
"sonner": "^2.0.3",
"viem": "^2.24.2" "viem": "^2.24.2"
}, },
"devDependencies": { "devDependencies": {
@ -76,6 +78,97 @@
"preact": "^10.24.2" "preact": "^10.24.2"
} }
}, },
"node_modules/@coral-xyz/anchor": {
"version": "0.31.0",
"resolved": "https://registry.npmjs.org/@coral-xyz/anchor/-/anchor-0.31.0.tgz",
"integrity": "sha512-Yb1NwP1s4cWhAw7wL7vOLHSWWw3cD5D9pRCVSeJpdqPaI+w7sfRLScnVJL6ViYMZynB7nAG/5HcUPKUnY0L9rw==",
"license": "(MIT OR Apache-2.0)",
"dependencies": {
"@coral-xyz/anchor-errors": "^0.31.0",
"@coral-xyz/borsh": "^0.31.0",
"@noble/hashes": "^1.3.1",
"@solana/web3.js": "^1.69.0",
"bn.js": "^5.1.2",
"bs58": "^4.0.1",
"buffer-layout": "^1.2.2",
"camelcase": "^6.3.0",
"cross-fetch": "^3.1.5",
"eventemitter3": "^4.0.7",
"pako": "^2.0.3",
"superstruct": "^0.15.4",
"toml": "^3.0.0"
},
"engines": {
"node": ">=17"
}
},
"node_modules/@coral-xyz/anchor-errors": {
"version": "0.31.0",
"resolved": "https://registry.npmjs.org/@coral-xyz/anchor-errors/-/anchor-errors-0.31.0.tgz",
"integrity": "sha512-SUERksFSQ+4F11hkROIwHq4mcoSMXJxwVWLoklefi4dU679zVWFVcTq6O7otvjY8wlUaRXeE+iYcQWZTw2ll6w==",
"license": "Apache-2.0",
"engines": {
"node": ">=10"
}
},
"node_modules/@coral-xyz/anchor/node_modules/base-x": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz",
"integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/@coral-xyz/anchor/node_modules/bs58": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
"integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
"license": "MIT",
"dependencies": {
"base-x": "^3.0.2"
}
},
"node_modules/@coral-xyz/anchor/node_modules/camelcase": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@coral-xyz/anchor/node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"license": "MIT"
},
"node_modules/@coral-xyz/anchor/node_modules/superstruct": {
"version": "0.15.5",
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.15.5.tgz",
"integrity": "sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ==",
"license": "MIT"
},
"node_modules/@coral-xyz/borsh": {
"version": "0.31.0",
"resolved": "https://registry.npmjs.org/@coral-xyz/borsh/-/borsh-0.31.0.tgz",
"integrity": "sha512-DwdQ5fuj+rGQCTKRnxnW1W2lvcpBaFc9m9M1TcGGlm+bwCcggmDgbLKLgF+LjIrKnc7Nd+bCACx5RA9YTK2I4Q==",
"license": "Apache-2.0",
"dependencies": {
"bn.js": "^5.1.2",
"buffer-layout": "^1.2.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@solana/web3.js": "^1.69.0"
}
},
"node_modules/@emnapi/core": { "node_modules/@emnapi/core": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.3.1.tgz", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.3.1.tgz",
@ -4269,6 +4362,15 @@
"ieee754": "^1.2.1" "ieee754": "^1.2.1"
} }
}, },
"node_modules/buffer-layout": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/buffer-layout/-/buffer-layout-1.2.2.tgz",
"integrity": "sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA==",
"license": "MIT",
"engines": {
"node": ">=4.5"
}
},
"node_modules/bufferutil": { "node_modules/bufferutil": {
"version": "4.0.9", "version": "4.0.9",
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.9.tgz", "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.9.tgz",
@ -8023,6 +8125,12 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/pako": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
"license": "(MIT AND Zlib)"
},
"node_modules/parent-module": { "node_modules/parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@ -9023,6 +9131,16 @@
"atomic-sleep": "^1.0.0" "atomic-sleep": "^1.0.0"
} }
}, },
"node_modules/sonner": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.3.tgz",
"integrity": "sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA==",
"license": "MIT",
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@ -9508,6 +9626,12 @@
"node": ">=8.0" "node": ">=8.0"
} }
}, },
"node_modules/toml": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz",
"integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==",
"license": "MIT"
},
"node_modules/tr46": { "node_modules/tr46": {
"version": "0.0.3", "version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",

View File

@ -9,6 +9,7 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@coral-xyz/anchor": "^0.31.0",
"@fontsource/manrope": "^5.2.5", "@fontsource/manrope": "^5.2.5",
"@privy-io/react-auth": "^2.7.2", "@privy-io/react-auth": "^2.7.2",
"@solana/web3.js": "^1.98.0", "@solana/web3.js": "^1.98.0",
@ -20,6 +21,7 @@
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-toastify": "^11.0.5", "react-toastify": "^11.0.5",
"sonner": "^2.0.3",
"viem": "^2.24.2" "viem": "^2.24.2"
}, },
"devDependencies": { "devDependencies": {

View File

@ -2,9 +2,10 @@
import Footer from "@/components/Footer"; import Footer from "@/components/Footer";
import Header from "@/components/Header"; import Header from "@/components/Header";
import HeroSection from "@/components/Home"; import HeroSection from "@/components/HeroSection";
import { PrivyProvider } from "@privy-io/react-auth"; import { PrivyProvider } from "@privy-io/react-auth";
import { toSolanaWalletConnectors } from "@privy-io/react-auth/solana"; import { toSolanaWalletConnectors } from "@privy-io/react-auth/solana";
import { Toaster } from "sonner";
export default function Home() { export default function Home() {
return ( return (
@ -30,6 +31,7 @@ export default function Home() {
}} }}
> >
<Toaster position="top-right" richColors />
<Header/> <Header/>
<HeroSection/> <HeroSection/>
<Footer/> <Footer/>

View File

@ -0,0 +1,137 @@
"use client";
import { useEffect, useState } from "react";
import { ConnectedSolanaWallet, usePrivy, useSolanaWallets } from "@privy-io/react-auth";
import { Connection, PublicKey } from "@solana/web3.js";
import { AnchorProvider, Program, BN } from "@coral-xyz/anchor";
import idl from "../idl/bets_idl.json"; // Ensure this is the correct IDL path
import { toast } from "sonner";
import { Bets } from "@/idl/bets";
import { Game, games } from "../data/games"; // Assuming you have a games data file
import { PriceSelection } from "./PriceSelection";
import { GameSelection } from "./GameSelection";
import { CLUSTER_URL } from "@/data/shared";
const PROGRAM_ID = new PublicKey("HxsDuhD7wcPxcMsrYdteMYxkffuwff8HoxhZ7NuFtM37");
// Change to Mainnet when deploying
interface GameModalProps {
isOpen: boolean;
onClose: () => void;
}
export default function GameModal({ isOpen, onClose }: GameModalProps) {
const { wallets } = useSolanaWallets();
const { authenticated } = usePrivy();
const [program, setProgram] = useState<Program<Bets>>();
const [solanaWallet, setSolanaWallet] = useState<ConnectedSolanaWallet>();
const [selectedGame, setSelectedGame] = useState<Game | null>(null);
const [selectedPrice, setSelectedPrice] = useState<number | null>(null);
useEffect(() => {
if (wallets.length > 0) {
const solWallet = wallets[0];
setSolanaWallet(solWallet);
const connection = new Connection(CLUSTER_URL, "confirmed");
const wallet = {
publicKey: new PublicKey(solWallet.address),
signTransaction: solWallet.signTransaction,
signAllTransactions: solWallet.signAllTransactions,
};
const provider = new AnchorProvider(connection, wallet, {
preflightCommitment: "processed",
});
const programInstance = new Program<Bets>(idl, provider);
setProgram(programInstance);
}
}, [wallets]);
const handleCreateGame = async () => {
if (!authenticated) {
toast.error("Please log in with Privy.");
return;
}
const wallet = solanaWallet; // Get connected Privy wallet
if (!wallet) {
toast.error("Please connect your wallet.");
return;
}
if (!selectedGame || selectedPrice === null) {
toast.error("Please select a game and a price.");
return;
}
if (!program) {
toast.error("Solana program not initialized.");
return;
}
try {
const nonce = 1;
const [bet_list_pda] = await PublicKey.findProgramAddress(
[Buffer.from("bets_list")],
program.programId
);
const connection = new Connection(CLUSTER_URL, "confirmed");
console.log(`bets list : ${bet_list_pda}`);
// Create transaction
const tx = await program.methods
.createBet(new BN(selectedPrice * 1000000000), selectedGame.id, new BN(nonce))
.accounts({
betsList: bet_list_pda,
})
.transaction(); // Get transaction object instead of RPC call
tx.feePayer = new PublicKey(wallet.address);
tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
// Sign transaction with Privy
const signedTx = await wallet.signTransaction(tx);
// Send transaction// Replace with correct RPC endpoint
const txId = await connection.sendRawTransaction(signedTx.serialize());
toast.success(`Bet created successfully! TX: ${txId}`);
console.log(`Transaction ID: ${txId}`);
onClose();
} catch (error) {
console.error("Error creating bet:", error);
toast.error("Failed to create bet.");
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black/70 flex justify-center items-start pt-10 z-50" onClick={onClose}>
<div
className="bg-[rgb(30,30,30)] text-white w-full max-w-lg p-6 rounded-2xl shadow-lg"
onClick={(e) => e.stopPropagation()}
>
<h2 className="text-xl font-bold mb-4">Create Game</h2>
{/* Game Selection */}
<GameSelection games={games} selectedGame={selectedGame} onSelect={setSelectedGame} />
{/* Price Selection */}
<PriceSelection selectedPrice={selectedPrice} onSelect={setSelectedPrice} />
<button
className="mt-6 w-full py-2 rounded-xl font-semibold bg-[rgb(248,144,22)] text-black hover:bg-white hover:scale-105"
onClick={handleCreateGame}
>
Confirm & Create Bet
</button>
</div>
</div>
);
}

View File

@ -0,0 +1,33 @@
import Image from "next/image";
import { Game } from "../data/games";
interface GameSelectionProps {
games: Game[];
selectedGame: Game | null;
onSelect: (game: Game) => void;
}
export function GameSelection({ games, selectedGame, onSelect }: GameSelectionProps) {
return (
<div className="grid grid-cols-3 gap-4 mb-6">
{games.map((game) => (
<div
key={game.id}
className={`p-4 cursor-pointer rounded-xl border-2 transition-transform duration-300
${selectedGame?.id === game.id ? "scale-110 border-gray-400 bg-gray-800" : "border-gray-600 bg-[rgb(10,10,10)]"}
hover:scale-105 hover:border-gray-400`}
onClick={() => onSelect(game)}
>
<Image
src={game.thumbnail}
alt={game.name}
width={100}
height={100}
className="mb-2 rounded-md"
/>
<h3 className="text-center text-sm">{game.name}</h3>
</div>
))}
</div>
);
}

View File

@ -0,0 +1,67 @@
"use client";
import { useState } from "react";
import Image from "next/image";
import OpenGames from "./OpenGames";
import { Game, games } from "../data/games";
import GameModal from "./GameModal";
import { HowItWorksModal } from "./HowItWorksModal";
import YourGames from "./YourGames";
export default function HeroSection() {
const [isModalOpen, setIsModalOpen] = useState(false);
const [isGameModalOpen, setIsGameModalOpen] = useState(false);
return (
<>
<section className="flex flex-col items-center text-center py-16">
<Image
src="/duelfiassets/Playing on Arcade Machine no BG.png"
alt="DuelFi Hero"
width={150}
height={50}
className="mb-6"
/>
<h1 className="text-4xl font-bold text-white">
Instant <span className="text-[rgb(248,144,22)]">Duels</span>, Instant{" "}
<span className="text-[rgb(248,144,22)]">Wins</span>
</h1>
<button
className="mt-12 px-12 py-4 bg-[rgb(248,144,22)] hover:bg-white text-black text-sm font-semibold rounded-xl flex items-center gap-2 transition duration-300 hover:scale-110"
onClick={() => setIsGameModalOpen(true)}
>
Create a Game
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-5 h-5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M7 7h10v10" />
<path d="M7 17 17 7" />
</svg>
</button>
<p
className="mt-4 text-gray-400 cursor-pointer hover:underline font-mono"
onClick={() => setIsModalOpen(true)}
>
How it works?..
</p>
<YourGames />
<OpenGames />
</section>
<GameModal isOpen={isGameModalOpen} onClose={() => setIsGameModalOpen(false)} />
<HowItWorksModal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} />
</>
);
}

View File

@ -8,7 +8,7 @@ import { usePrivy } from "@privy-io/react-auth";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
export default function HeroSection() { export default function HeroSection() {
const { ready, authenticated } = usePrivy(); const { authenticated } = usePrivy();
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [isGameModalOpen, setIsGameModalOpen] = useState(false); const [isGameModalOpen, setIsGameModalOpen] = useState(false);
const [selectedGame, setSelectedGame] = useState<Game | null>(null); const [selectedGame, setSelectedGame] = useState<Game | null>(null);

View File

@ -0,0 +1,41 @@
interface HowItWorksModalProps {
isOpen: boolean;
onClose: () => void;
}
export function HowItWorksModal({ isOpen, onClose }: HowItWorksModalProps) {
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black/70 flex justify-center items-start pt-10 z-50" onClick={onClose}>
<div
className="bg-[rgb(30,30,30)] text-white w-full max-w-lg p-6 rounded-2xl shadow-lg transform transition-transform duration-300 animate-slide-down"
onClick={(e) => e.stopPropagation()}
>
<h2 className="text-xl font-bold mb-4">How It Works</h2>
<div className="space-y-4">
{[
{ step: "Connect Your Wallet", desc: "Start by linking your wallet securely." },
{ step: "Create or Join Game", desc: "Pick a game and set a wager, or join an existing match." },
{ step: "Place Your Bet", desc: "Confirm your wager and get ready to play." },
{ step: "Claim Your Winnings", desc: "Win the game and collect your rewards instantly!" },
].map(({ step, desc }, index) => (
<div key={index}>
<h3 className="text-[rgb(248,144,22)] font-bold text-lg">{index + 1}. {step}</h3>
<p className="text-xs text-gray-400 font-mono">{desc}</p>
</div>
))}
</div>
<button
className="mt-6 w-full bg-[rgb(248,144,22)] text-black font-semibold py-2 rounded-xl transition hover:bg-white hover:scale-105"
onClick={onClose}
>
Okay
</button>
</div>
</div>
);
}

View File

@ -30,7 +30,7 @@ export default function OpenGames() {
{/* Game Info - Left Aligned */} {/* Game Info - Left Aligned */}
<div className="mt-4 px-3 text-left"> <div className="mt-4 px-3 text-left">
<h3 className="text-lg font-semibold text-white py-2">{game.name}</h3> <h3 className="text-lg font-semibold text-white py-2">{game.name}</h3>
<p className="text-xs text-gray-400 font-mono py-1">Entry Fee</p> <p className="text-xs text-gray-400 font-mono py-1">Wager</p>
<p className="text-xs text-white font-mono py-1">{game.entryFee}</p> <p className="text-xs text-white font-mono py-1">{game.entryFee}</p>
</div> </div>
</div> </div>

View File

@ -0,0 +1,32 @@
import Image from "next/image";
interface PriceSelectionProps {
selectedPrice: number | null;
onSelect: (price: number) => void;
}
export function PriceSelection({ selectedPrice, onSelect }: PriceSelectionProps) {
const prices = [0.2, 0.5, 0.8];
return (
<div className="mb-6 space-y-2">
{prices.map((price) => (
<p
key={price}
className={`flex items-center gap-2 text-gray-400 font-mono cursor-pointer transition-transform duration-500
${selectedPrice === price ? "text-white translate-x-2" : ""}`}
onClick={() => onSelect(price)}
>
<Image
src="/duelfiassets/solana logo.png"
alt="SOL"
width={16}
height={16}
className="inline"
/>
{price} SOL
</p>
))}
</div>
);
}

View File

@ -5,6 +5,7 @@ import { usePrivy, useSolanaWallets } from "@privy-io/react-auth";
import { Connection, PublicKey } from "@solana/web3.js"; // Solana Web3.js imports import { Connection, PublicKey } from "@solana/web3.js"; // Solana Web3.js imports
import { toast, ToastContainer } from "react-toastify"; import { toast, ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css"; import "react-toastify/dist/ReactToastify.css";
import { CLUSTER_URL } from "@/data/shared";
export default function PrivyButton() { export default function PrivyButton() {
const { login, logout, user } = usePrivy(); const { login, logout, user } = usePrivy();
@ -14,29 +15,30 @@ export default function PrivyButton() {
const [solBalance, setSolBalance] = useState("--"); const [solBalance, setSolBalance] = useState("--");
const modalRef = useRef<HTMLDivElement>(null); const modalRef = useRef<HTMLDivElement>(null);
useEffect(() => { const fetchSolBalance = async () => {
const fetchSolBalance = async () => {
wallets.forEach((wallet)=>{ wallets.forEach((wallet)=>{
console.log(wallet.address + " : " + wallet.type); console.log(wallet.address + " : " + wallet.type);
if(wallet.type == "solana"){ if(wallet.type == "solana"){
setSolWallet(wallet.address); setSolWallet(wallet.address);
}
})
if (solWallet!="") {
const walletAddress = solWallet; // Access the Solana wallet address
try {
const connection = new Connection("https://api.devnet.solana.com", "confirmed");
const publicKey = new PublicKey(walletAddress);
const balance = await connection.getBalance(publicKey);
setSolBalance((balance / 1e9).toFixed(2)); // Convert lamports to SOL (1 SOL = 1e9 lamports)
} catch (error) {
console.error("Failed to get balance:", error);
setSolBalance("Error");
}
} }
}; })
if (solWallet!="") {
const walletAddress = solWallet; // Access the Solana wallet address
try {
const connection = new Connection(CLUSTER_URL, "confirmed");
const publicKey = new PublicKey(walletAddress);
const balance = await connection.getBalance(publicKey);
setSolBalance((balance / 1e9).toFixed(2)); // Convert lamports to SOL (1 SOL = 1e9 lamports)
} catch (error) {
console.error("Failed to get balance:", error);
setSolBalance("Error");
}
}
};
useEffect(() => {
if (wallets) { if (wallets) {
fetchSolBalance(); fetchSolBalance();
@ -76,7 +78,7 @@ export default function PrivyButton() {
<> <>
{user ? ( {user ? (
<button <button
onClick={() => setIsModalOpen(true)} onClick={() =>{setIsModalOpen(true); fetchSolBalance();}}
className="bg-[rgb(248,144,22)] hover:bg-[rgb(248,200,100)] text-black px-6 py-3 rounded-md transition duration-300 hover:scale-105" className="bg-[rgb(248,144,22)] hover:bg-[rgb(248,200,100)] text-black px-6 py-3 rounded-md transition duration-300 hover:scale-105"
> >
<span className="font-bold">Connected</span> <span className="font-bold">Connected</span>

View File

@ -0,0 +1,126 @@
"use client";
import { useEffect, useState } from "react";
import Image from "next/image";
import { games } from "../data/games";
import { Connection, PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js";
import { AnchorProvider, Program } from "@coral-xyz/anchor";
import { CLUSTER_URL } from "../data/shared";
import { Bets } from "../idl/bets";
import { FiTrash } from "react-icons/fi";
import idl from "../idl/bets_idl.json";
import { useSolanaWallets } from "@privy-io/react-auth";
export default function YourGames() {
const { wallets, ready } = useSolanaWallets();
const [openBets, setOpenBets] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
// Function to fetch open bets
const fetchOpenBets = async () => {
try {
if (!wallets[0]) return;
const connection = new Connection(CLUSTER_URL);
const wallet = {
publicKey: new PublicKey(wallets[0].address),
signTransaction: wallets[0].signTransaction,
signAllTransactions: wallets[0].signAllTransactions,
};
const provider = new AnchorProvider(connection, wallet, {
preflightCommitment: "confirmed",
});
const program = new Program<Bets>(idl, provider);
const [bet_list_pda] = await PublicKey.findProgramAddress(
[Buffer.from("bets_list")],
program.programId
);
// Fetch all open bet accounts
const bet_list = await program.account.betsList.fetch(bet_list_pda);
// Extract required bet data
const formattedBets = await Promise.all(
bet_list.bets.map(async (bet) => {
const betAcc = await program.account.betVault.fetch(bet);
console.log(betAcc.gameId);
return {
id: betAcc.gameId,
owner: betAcc.owner.toBase58(),
joiner: betAcc.joiner ? betAcc.joiner.toBase58() : "Open",
wager: betAcc.wager.toNumber() / LAMPORTS_PER_SOL
};
})
);
console.log(`Got ${formattedBets.length} bets`);
setOpenBets(formattedBets);
setLoading(false);
} catch (error) {
console.error("Error fetching open bets:", error);
setLoading(false);
}
};
// Auto-refresh every 10 seconds
useEffect(() => {
if (!ready) return;
fetchOpenBets(); // Initial fetch
const interval = setInterval(fetchOpenBets, 10000); // Refresh every 10s
return () => clearInterval(interval); // Cleanup on unmount
}, [ready]);
return (
<section className="py-16 px-6">
<h2 className="text-3xl font-bold text-white mb-6">Your Games</h2>
{loading ? (
<p className="text-gray-400">Loading open games...</p>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{openBets.map((bet) => {
const game = games.find((g) => g.id === bet.gameId); // Match game
if (!game) return null; // Skip unmatched bets
return (
<div
key={bet.id}
className="relative group bg-[rgb(30,30,30)] rounded-2xl p-2 w-50 overflow-hidden cursor-pointer transition-all duration-300"
>
{/* Game Thumbnail */}
<div className="relative w-full h-40 overflow-hidden rounded-xl">
<Image
src={game.thumbnail}
alt={game.name}
layout="fill"
objectFit="cover"
className="transition-transform duration-300 group-hover:scale-110"
/>
</div>
{/* Game Info */}
<div className="mt-4 px-3 text-left">
<h3 className="text-lg font-semibold text-white py-2">{game.name}</h3>
<p className="text-xs text-gray-400 font-mono py-1">Wager</p>
<p className="text-xs text-white font-mono py-1">{bet.wager} SOL</p>
</div>
{/* Close Button (Trash Icon) */}
<button
className="absolute bottom-3 right-3 bg-red-600 text-white p-2 rounded-full transition duration-300 hover:bg-red-800"
onClick={() => console.log(`Removing bet ${bet.id}`)}
>
<FiTrash size={16} />
</button>
</div>
);
})}
</div>
)}
</section>
);
}

View File

@ -1,18 +1,18 @@
export const games = [ export const games = [
{ {
id: 1, id: "tetris",
name: "Block Drop", name: "Block Drop",
entryFee: "0.1 SOL", entryFee: "0.1 SOL",
thumbnail: "/duelfiassets/Block Drop Illustration.jpeg", thumbnail: "/duelfiassets/Block Drop Illustration.jpeg",
}, },
{ {
id: 2, id: "snakes",
name: "Venom Trails", name: "Venom Trails",
entryFee: "0.02 ETH", entryFee: "0.02 ETH",
thumbnail: "/duelfiassets/Venom Trail Game Cover Illustration.png", thumbnail: "/duelfiassets/Venom Trail Game Cover Illustration.png",
}, },
{ {
id: 3, id: "bubbles",
name: "Wall Smash", name: "Wall Smash",
entryFee: "0.05 ETH", entryFee: "0.05 ETH",
thumbnail: "/duelfiassets/Wall Smash Game Cover Illustration.png", thumbnail: "/duelfiassets/Wall Smash Game Cover Illustration.png",
@ -20,7 +20,7 @@ export const games = [
]; ];
export interface Game { export interface Game {
id: number; id: string;
name: string; name: string;
entryFee: string; entryFee: string;
thumbnail: string; thumbnail: string;

1
src/data/shared.ts Normal file
View File

@ -0,0 +1 @@
export const CLUSTER_URL = "https://api.devnet.solana.com";

299
src/idl/bets.ts Normal file
View File

@ -0,0 +1,299 @@
/**
* Program IDL in camelCase format in order to be used in JS/TS.
*
* Note that this is only a type helper and is not the actual IDL. The original
* IDL can be found at `target/idl/bets.json`.
*/
export type Bets = {
"address": "HxsDuhD7wcPxcMsrYdteMYxkffuwff8HoxhZ7NuFtM37",
"metadata": {
"name": "bets",
"version": "0.1.0",
"spec": "0.1.0",
"description": "Created with Anchor"
},
"instructions": [
{
"name": "closeBet",
"discriminator": [
185,
206,
13,
184,
176,
108,
140,
107
],
"accounts": [
{
"name": "betsList",
"writable": true
},
{
"name": "betVault",
"writable": true
},
{
"name": "winner",
"writable": true
},
{
"name": "payer",
"writable": true,
"signer": true
},
{
"name": "systemProgram",
"address": "11111111111111111111111111111111"
}
],
"args": [
{
"name": "winner",
"type": "pubkey"
}
]
},
{
"name": "createBet",
"discriminator": [
197,
42,
153,
2,
59,
63,
143,
246
],
"accounts": [
{
"name": "payer",
"writable": true,
"signer": true
},
{
"name": "betsList",
"writable": true
},
{
"name": "betVault",
"writable": true,
"pda": {
"seeds": [
{
"kind": "const",
"value": [
98,
101,
116,
95,
118,
97,
117,
108,
116
]
},
{
"kind": "account",
"path": "payer"
},
{
"kind": "arg",
"path": "gameId"
},
{
"kind": "arg",
"path": "nonce"
}
]
}
},
{
"name": "systemProgram",
"address": "11111111111111111111111111111111"
}
],
"args": [
{
"name": "wager",
"type": "u64"
},
{
"name": "gameId",
"type": "string"
},
{
"name": "nonce",
"type": "u64"
}
]
},
{
"name": "initialize",
"discriminator": [
175,
175,
109,
31,
13,
152,
155,
237
],
"accounts": [
{
"name": "betsList",
"writable": true,
"pda": {
"seeds": [
{
"kind": "const",
"value": [
98,
101,
116,
115,
95,
108,
105,
115,
116
]
}
]
}
},
{
"name": "payer",
"writable": true,
"signer": true
},
{
"name": "systemProgram",
"address": "11111111111111111111111111111111"
}
],
"args": []
},
{
"name": "joinBet",
"discriminator": [
69,
116,
82,
26,
144,
192,
58,
238
],
"accounts": [
{
"name": "betVault",
"writable": true
},
{
"name": "payer",
"writable": true,
"signer": true
},
{
"name": "systemProgram",
"address": "11111111111111111111111111111111"
}
],
"args": [
{
"name": "gameId",
"type": "string"
}
]
}
],
"accounts": [
{
"name": "betVault",
"discriminator": [
103,
78,
21,
234,
18,
250,
230,
209
]
},
{
"name": "betsList",
"discriminator": [
231,
234,
50,
58,
81,
179,
239,
117
]
}
],
"errors": [
{
"code": 6000,
"name": "customError",
"msg": "Custom error message"
}
],
"types": [
{
"name": "betVault",
"type": {
"kind": "struct",
"fields": [
{
"name": "gameId",
"type": "string"
},
{
"name": "owner",
"type": "pubkey"
},
{
"name": "joiner",
"type": "pubkey"
},
{
"name": "wager",
"type": "u64"
}
]
}
},
{
"name": "betsList",
"type": {
"kind": "struct",
"fields": [
{
"name": "bets",
"type": {
"vec": "pubkey"
}
}
]
}
}
],
"constants": [
{
"name": "seed",
"type": "string",
"value": "\"anchor\""
}
]
};

292
src/idl/bets_idl.json Normal file
View File

@ -0,0 +1,292 @@
{
"address": "HxsDuhD7wcPxcMsrYdteMYxkffuwff8HoxhZ7NuFtM37",
"metadata": {
"name": "bets",
"version": "0.1.0",
"spec": "0.1.0",
"description": "Created with Anchor"
},
"instructions": [
{
"name": "close_bet",
"discriminator": [
185,
206,
13,
184,
176,
108,
140,
107
],
"accounts": [
{
"name": "bets_list",
"writable": true
},
{
"name": "bet_vault",
"writable": true
},
{
"name": "winner",
"writable": true
},
{
"name": "payer",
"writable": true,
"signer": true
},
{
"name": "system_program",
"address": "11111111111111111111111111111111"
}
],
"args": [
{
"name": "winner",
"type": "pubkey"
}
]
},
{
"name": "create_bet",
"discriminator": [
197,
42,
153,
2,
59,
63,
143,
246
],
"accounts": [
{
"name": "payer",
"writable": true,
"signer": true
},
{
"name": "bets_list",
"writable": true
},
{
"name": "bet_vault",
"writable": true,
"pda": {
"seeds": [
{
"kind": "const",
"value": [
98,
101,
116,
95,
118,
97,
117,
108,
116
]
},
{
"kind": "account",
"path": "payer"
},
{
"kind": "arg",
"path": "game_id"
},
{
"kind": "arg",
"path": "_nonce"
}
]
}
},
{
"name": "system_program",
"address": "11111111111111111111111111111111"
}
],
"args": [
{
"name": "wager",
"type": "u64"
},
{
"name": "game_id",
"type": "string"
},
{
"name": "nonce",
"type": "u64"
}
]
},
{
"name": "initialize",
"discriminator": [
175,
175,
109,
31,
13,
152,
155,
237
],
"accounts": [
{
"name": "bets_list",
"writable": true,
"pda": {
"seeds": [
{
"kind": "const",
"value": [
98,
101,
116,
115,
95,
108,
105,
115,
116
]
}
]
}
},
{
"name": "payer",
"writable": true,
"signer": true
},
{
"name": "system_program",
"address": "11111111111111111111111111111111"
}
],
"args": []
},
{
"name": "join_bet",
"discriminator": [
69,
116,
82,
26,
144,
192,
58,
238
],
"accounts": [
{
"name": "bet_vault",
"writable": true
},
{
"name": "payer",
"writable": true,
"signer": true
},
{
"name": "system_program",
"address": "11111111111111111111111111111111"
}
],
"args": [
{
"name": "game_id",
"type": "string"
}
]
}
],
"accounts": [
{
"name": "BetVault",
"discriminator": [
103,
78,
21,
234,
18,
250,
230,
209
]
},
{
"name": "BetsList",
"discriminator": [
231,
234,
50,
58,
81,
179,
239,
117
]
}
],
"errors": [
{
"code": 6000,
"name": "CustomError",
"msg": "Custom error message"
}
],
"types": [
{
"name": "BetVault",
"type": {
"kind": "struct",
"fields": [
{
"name": "game_id",
"type": "string"
},
{
"name": "owner",
"type": "pubkey"
},
{
"name": "joiner",
"type": "pubkey"
},
{
"name": "wager",
"type": "u64"
}
]
}
},
{
"name": "BetsList",
"type": {
"kind": "struct",
"fields": [
{
"name": "bets",
"type": {
"vec": "pubkey"
}
}
]
}
}
],
"constants": [
{
"name": "SEED",
"type": "string",
"value": "\"anchor\""
}
]
}