diff --git a/apps/api/package.json b/apps/api/package.json index bf317e56..e88a5c0e 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -12,9 +12,9 @@ "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist" }, "dependencies": { - "@coral-xyz/anchor": "^0.27.0", - "@solana/spl-token": "^0.3.8", - "@solana/web3.js": "^1.73.5", + "@coral-xyz/anchor": "^0.31.1", + "@solana/spl-token": "^0.4.13", + "@solana/web3.js": "^1.95.4", "apicache": "^1.6.3", "cors": "^2.8.5", "dotenv": "^16.0.3", diff --git a/apps/explorer/.env b/apps/explorer/.env new file mode 100644 index 00000000..20c068f8 --- /dev/null +++ b/apps/explorer/.env @@ -0,0 +1,5 @@ +# Web3 connection +VITE_RPC_ENDPOINT="https://mainnet.helius-rpc.com/?api-key=373105b2-df11-4381-bea4-e9aecdda396e" +VITE_GAMBA_API_ENDPOINT="api.gamba.so" +VITE_HELIUS_API_KEY="373105b2-df11-4381-bea4-e9aecdda396e" + diff --git a/apps/explorer/package.json b/apps/explorer/package.json index 66033b99..14ec2e26 100644 --- a/apps/explorer/package.json +++ b/apps/explorer/package.json @@ -8,7 +8,7 @@ "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist" }, "dependencies": { - "@coral-xyz/anchor": "^0.27.0", + "@coral-xyz/anchor": "^0.31.1", "@preact/signals-react": "^1.3.8", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-navigation-menu": "^1.1.4", @@ -18,7 +18,7 @@ "@radix-ui/react-tooltip": "^1.0.7", "@radix-ui/themes": "^1.1.2", "@solana/spl-token": "^0.3.8", - "@solana/wallet-adapter-react": "^0.15.35", + "@solana/wallet-adapter-react": "^0.15.39", "@solana/wallet-adapter-react-ui": "^0.9.34", "@solana/wallet-adapter-wallets": "^0.19.18", "@solana/web3.js": "^1.98.2", @@ -46,8 +46,8 @@ "zustand": "^4.4.1" }, "devDependencies": { - "@types/react": "^18.2.22", - "@types/react-dom": "^18.0.11", + "@types/react": "^18.2.13", + "@types/react-dom": "^18.2.4", "@typescript-eslint/eslint-plugin": "^6.10.0", "@typescript-eslint/parser": "^6.10.0", "@vitejs/plugin-react": "^3.1.0", diff --git a/apps/explorer/src/index.tsx b/apps/explorer/src/index.tsx index fde658b4..0abc7797 100644 --- a/apps/explorer/src/index.tsx +++ b/apps/explorer/src/index.tsx @@ -23,7 +23,8 @@ function Root() { ], [], ) - + + return ( diff --git a/apps/platform/.env b/apps/platform/.env new file mode 100644 index 00000000..1a54dad7 --- /dev/null +++ b/apps/platform/.env @@ -0,0 +1,4 @@ +# Web3 connection +VITE_RPC_ENDPOINT="https://devnet.helius-rpc.com/?api-key=7b05747c-b100-4159-ba5f-c85e8c8d3997" +# VITE_HELIUS_API_KEY="" +# VITE_REAL_PLAYS_DISABLED=true \ No newline at end of file diff --git a/apps/platform/package.json b/apps/platform/package.json index 5063912c..489c3b26 100644 --- a/apps/platform/package.json +++ b/apps/platform/package.json @@ -9,22 +9,24 @@ "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist" }, "dependencies": { - "@coral-xyz/anchor": "^0.27.0", + "@coral-xyz/anchor": "^0.31.1", + "@solana/spl-token": "^0.4.13", + "@solana/web3.js": "^1.98.2", "@preact/signals-react": "^1.3.8", "@react-three/drei": "^9.89.0", "@react-three/fiber": "^8.15.11", - "@solana/spl-token": "^0.3.8", - "@solana/wallet-adapter-react": "^0.15.35", + "@solana/wallet-adapter-react": "^0.15.39", "@solana/wallet-adapter-react-ui": "^0.9.34", "@solana/wallet-adapter-wallets": "^0.19.18", - "@solana/web3.js": "^1.93.0", "@vercel/kv": "^3.0.0", "buffer": "^6.0.3", + "@gamba-labs/multiplayer-sdk": "workspace:*", "gamba-core-v2": "workspace:*", "gamba-react-ui-v2": "workspace:*", "gamba-react-v2": "workspace:*", "html2canvas": "^1.4.1", - "matter-js": "^0.19.0", + "matter-js": "^0.20.0", + "framer-motion": "^12.16.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.10.0", @@ -34,8 +36,8 @@ "zustand": "^4.4.1" }, "devDependencies": { - "@types/react": "^18.2.22", - "@types/react-dom": "^18.0.11", + "@types/react": "^18.2.13", + "@types/react-dom": "^18.2.4", "@types/matter-js": "^0.19.5", "@types/three": "^0.161.2", "@vitejs/plugin-react": "^3.1.0", diff --git a/apps/platform/public/games/jackpot.png b/apps/platform/public/games/jackpot.png new file mode 100644 index 00000000..1df54646 Binary files /dev/null and b/apps/platform/public/games/jackpot.png differ diff --git a/apps/platform/public/games/plinkorace.png b/apps/platform/public/games/plinkorace.png new file mode 100644 index 00000000..24b953af Binary files /dev/null and b/apps/platform/public/games/plinkorace.png differ diff --git a/apps/platform/src/App.tsx b/apps/platform/src/App.tsx index 17065735..cc423d79 100644 --- a/apps/platform/src/App.tsx +++ b/apps/platform/src/App.tsx @@ -1,20 +1,27 @@ +import React from 'react' +import { Route, Routes, useLocation } from 'react-router-dom' import { useWalletModal } from '@solana/wallet-adapter-react-ui' import { GambaUi } from 'gamba-react-ui-v2' import { useTransactionError } from 'gamba-react-v2' -import React from 'react' -import { Route, Routes, useLocation } from 'react-router-dom' + import { Modal } from './components/Modal' import { TOS_HTML, ENABLE_TROLLBOX } from './constants' import { useToast } from './hooks/useToast' import { useUserStore } from './hooks/useUserStore' + import Dashboard from './sections/Dashboard/Dashboard' import Game from './sections/Game/Game' import Header from './sections/Header' import RecentPlays from './sections/RecentPlays/RecentPlays' import Toasts from './sections/Toasts' -import { MainWrapper, TosInner, TosWrapper } from './styles' import TrollBox from './components/TrollBox' +import { MainWrapper, TosInner, TosWrapper } from './styles' + +/* -------------------------------------------------------------------------- */ +/* Helpers */ +/* -------------------------------------------------------------------------- */ + function ScrollToTop() { const { pathname } = useLocation() React.useEffect(() => window.scrollTo(0, 0), [pathname]) @@ -23,63 +30,65 @@ function ScrollToTop() { function ErrorHandler() { const walletModal = useWalletModal() - const toast = useToast() - const [error, setError] = React.useState() - - useTransactionError( - (error) => { - if (error.message === 'NOT_CONNECTED') { - walletModal.setVisible(true) - return - } - toast({ title: '❌ Transaction error', description: error.error?.errorMessage ?? error.message }) - }, - ) + const toast = useToast() - return ( - <> - {error && ( - setError(undefined)}> -

Error occured

-

{error.message}

-
- )} - - ) + // React‑state not needed; let Toasts surface details + useTransactionError((err) => { + if (err.message === 'NOT_CONNECTED') { + walletModal.setVisible(true) + } else { + toast({ + title: '❌ Transaction error', + description: err.error?.errorMessage ?? err.message, + }) + } + }) + + return null } +/* -------------------------------------------------------------------------- */ +/* App */ +/* -------------------------------------------------------------------------- */ + export default function App() { - const newcomer = useUserStore((state) => state.newcomer) - const set = useUserStore((state) => state.set) + const newcomer = useUserStore((s) => s.newcomer) + const set = useUserStore((s) => s.set) return ( <> + {/* onboarding / ToS */} {newcomer && (

Welcome

-

- By playing on our platform, you confirm your compliance. -

+

By playing on our platform, you confirm your compliance.

set({ newcomer: false })}> Acknowledge
)} + +
+ - } /> - } /> + {/* Normal landing page always shows Dashboard (with optional inline game) */} + } /> + {/* Dedicated game pages */} + } /> +

Recent Plays

+ {ENABLE_TROLLBOX && } ) diff --git a/apps/platform/src/constants.ts b/apps/platform/src/constants.ts index d112d032..ca1a62db 100644 --- a/apps/platform/src/constants.ts +++ b/apps/platform/src/constants.ts @@ -16,10 +16,12 @@ export const EXPLORER_URL = 'https://explorer.gamba.so' export const PLATFORM_SHARABLE_URL = 'play.gamba.so' // Creator fee (in %) -export const PLATFORM_CREATOR_FEE = 0.01 // 1% !!max 5%!! +export const PLATFORM_CREATOR_FEE = 0.01 // 1% !!max 7%!! + +export const MULTIPLAYER_FEE = 0.01 // 1% // Jackpot fee (in %) -export const PLATFORM_JACKPOT_FEE = 0.001 // 0.1% +export const PLATFORM_JACKPOT_FEE = 0.001 // 0.1%, not jackpot game specific, but platform wide // Referral fee (in %) export const PLATFORM_REFERRAL_FEE = 0.0025 // 0.25% @@ -109,3 +111,8 @@ export const TOKEN_METADATA_FETCHER = ( export const ENABLE_LEADERBOARD = true export const ENABLE_TROLLBOX = false // Requires setup in vercel (check tutorial in discord) + +/** If true, the featured game is fully playable inline on the dashboard */ +export const FEATURED_GAME_INLINE = false +export const FEATURED_GAME_ID: string | undefined = 'jackpot' // ← put game id or leave undefined + diff --git a/apps/platform/src/games/Jackpot/Coinfall.tsx b/apps/platform/src/games/Jackpot/Coinfall.tsx new file mode 100644 index 00000000..b012aac3 --- /dev/null +++ b/apps/platform/src/games/Jackpot/Coinfall.tsx @@ -0,0 +1,178 @@ +import React, { useRef, useEffect } from 'react'; +import styled from 'styled-components'; +import * as Matter from 'matter-js'; +import { IdlAccounts, web3 } from '@coral-xyz/anchor'; +import type { Multiplayer } from '@gamba-labs/multiplayer-sdk'; +import { useSound } from 'gamba-react-ui-v2'; +import joinSnd from './sounds/join.mp3'; + +const Container = styled.div` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 0; + pointer-events: none; + overflow: hidden; +`; + +type Player = IdlAccounts['game']['players'][number]; + +interface CoinfallsProps { + players: Player[]; +} + +function usePrevious(value: T) { + const ref = useRef(); + useEffect(() => { + ref.current = value; + }); + return ref.current; +} + +const getRadiusForWager = (wagerLamports: number) => { + const minWager = 0.01 * web3.LAMPORTS_PER_SOL; + const maxWager = 5 * web3.LAMPORTS_PER_SOL; + const minRadius = 10; + const maxRadius = 100; + + if (wagerLamports <= minWager) return minRadius; + if (wagerLamports >= maxWager) return maxRadius; + + const pct = (wagerLamports - minWager) / (maxWager - minWager); + return minRadius + pct * (maxRadius - minRadius); +}; + +export function Coinfalls({ players }: CoinfallsProps) { + const containerRef = useRef(null); + const engineRef = useRef(); + const prevPlayers = usePrevious(players) ?? []; + const { play: playJoin, sounds } = useSound({ join: joinSnd }); + + // SETUP: engine, renderer, ground, background-spawner + useEffect(() => { + const engine = Matter.Engine.create({ + gravity: { y: 0.6 }, + enableSleeping: true, + }); + engineRef.current = engine; + + const container = containerRef.current; + if (!container) return; + + // renderer + const render = Matter.Render.create({ + element: container, + engine, + options: { + width: container.clientWidth, + height: container.clientHeight, + wireframes: false, + background: 'transparent', + }, + }); + + const runner = Matter.Runner.create(); + Matter.Runner.run(runner, engine); + Matter.Render.run(render); + + // ground (invisible) + const width = container.clientWidth; + const height = container.clientHeight; + const thickness = 100; + const ground = Matter.Bodies.rectangle( + width/2, height + thickness/2, + width*2, thickness, + { isStatic: true, render: { visible: false } } + ); + Matter.World.add(engine.world, ground); + + // tint everything gold + semi-transparent + Matter.Events.on(engine, 'afterUpdate', () => { + engine.world.bodies.forEach(body => { + if (!body.isStatic) { + body.render.fillStyle = '#FFD700'; + body.render.opacity = 0.4; + } + }); + }); + + // handle window resize + const handleResize = () => { + if (!container || !render.canvas) return; + render.canvas.width = container.clientWidth; + render.canvas.height = container.clientHeight; + Matter.Body.setPosition(ground, { + x: container.clientWidth/2, + y: container.clientHeight + thickness/2, + }); + }; + window.addEventListener('resize', handleResize); + + // BACKGROUND SPAWNER: tiny non‑colliding coins + const bgInterval = setInterval(() => { + const w = container.clientWidth; + const h = container.clientHeight; + const x = Math.random() * w; + const r = 6 + Math.random() * 4; // 6–10 px + + const smallCoin = Matter.Bodies.circle(x, -40, r, { + isSensor: true, // no collision physics + collisionFilter: { mask: 0 }, + render: { fillStyle: '#FFD700', opacity: 0.15 }, + restitution: 0.2, + friction: 0.02, + }); + + Matter.World.add(engine.world, smallCoin); + + // cleanup after it falls past view + setTimeout(() => { + Matter.World.remove(engine.world, smallCoin); + }, 5000); + }, 4000); + + // teardown + return () => { + clearInterval(bgInterval); + window.removeEventListener('resize', handleResize); + Matter.Runner.stop(runner); + Matter.Render.stop(render); + Matter.Engine.clear(engine); + if (render.canvas && render.canvas.parentNode) { + render.canvas.parentNode.removeChild(render.canvas); + } + }; + }, []); // run once + + // PLAYER SPAWNER: when new players appear + useEffect(() => { + const engine = engineRef.current; + const container = containerRef.current; + if (!engine || !container) return; + + if (players.length > prevPlayers.length) { + const prevKeys = new Set(prevPlayers.map(p => p.user.toBase58())); + const newPlayers = players.filter(p => !prevKeys.has(p.user.toBase58())); + + newPlayers.forEach(player => { + const w = container.clientWidth; + const r = getRadiusForWager(player.wager.toNumber()); + const x = w * 0.2 + Math.random() * (w * 0.6); + + const coin = Matter.Bodies.circle(x, -30, r, { + restitution: 0.5, + friction: 0.1, + render: { fillStyle: '#FFD700' }, + }); + Matter.World.add(engine.world, coin); + + // play join sound per new player (gated by readiness) + if (sounds.join?.ready) playJoin('join'); + }); + } + }, [players, prevPlayers]); + + return ; +} diff --git a/apps/platform/src/games/Jackpot/Countdown.tsx b/apps/platform/src/games/Jackpot/Countdown.tsx new file mode 100644 index 00000000..671f05b1 --- /dev/null +++ b/apps/platform/src/games/Jackpot/Countdown.tsx @@ -0,0 +1,95 @@ +import React, { useEffect, useRef, useState } from 'react' +import styled from 'styled-components' + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + margin: 10px 0; + width: 100%; +` + +const Time = styled.div` + font-size: 2.5rem; + color: #fff; + font-weight: bold; + text-shadow: 0 0 10px rgba(255, 255, 255, 0.5); + font-variant-numeric: tabular-nums; +` + +const ProgressBar = styled.div` + width: 100%; + max-width: 500px; + height: 10px; + background: #2c2c54; + border-radius: 5px; + margin-top: 6px; + overflow: hidden; +` + +const Progress = styled.div` + height: 100%; + background: linear-gradient(90deg, #f39c12, #f1c40f); + border-radius: 5px; + transition: width 0.5s cubic-bezier(0.25, 1, 0.5, 1); +` + +interface CountdownProps { + creationTimestamp: number + softExpiration: number + onComplete: () => void +} + +export const Countdown: React.FC = ({ + creationTimestamp, + softExpiration, + onComplete, +}) => { + // total window + const totalWindowRef = useRef(Math.max(softExpiration - creationTimestamp, 0)) + // time left until soft + const [timeLeft, setTimeLeft] = useState(Math.max(softExpiration - Date.now(), 0)) + + // reset when timestamps change + useEffect(() => { + totalWindowRef.current = Math.max(softExpiration - creationTimestamp, 0) + setTimeLeft(Math.max(softExpiration - Date.now(), 0)) + }, [creationTimestamp, softExpiration]) + + // ticking + const firedRef = useRef(false) + useEffect(() => { + if (timeLeft <= 0) { + if (!firedRef.current) { + firedRef.current = true + onComplete() + } + return + } + const id = setInterval(() => { + const rem = Math.max(softExpiration - Date.now(), 0) + setTimeLeft(rem) + }, 500) + return () => clearInterval(id) + }, [softExpiration, timeLeft, onComplete]) + + // formatting + const pad = (n: number) => (n < 10 ? `0${n}` : `${n}`) + const totalSec = Math.ceil(timeLeft / 1000) + const m = Math.floor(totalSec / 60) + const s = totalSec % 60 + + // progress % + const pct = totalWindowRef.current > 0 + ? Math.min(100, ((totalWindowRef.current - timeLeft) / totalWindowRef.current) * 100) + : 0 + + return ( + + + + + + + ) +} diff --git a/apps/platform/src/games/Jackpot/Jackpot.styles.ts b/apps/platform/src/games/Jackpot/Jackpot.styles.ts new file mode 100644 index 00000000..fc8262df --- /dev/null +++ b/apps/platform/src/games/Jackpot/Jackpot.styles.ts @@ -0,0 +1,133 @@ +import styled from 'styled-components' +import { motion } from 'framer-motion' + +export const ScreenLayout = styled.div` + width: 100%; + max-width: 1200px; + margin: 0 auto; + padding: 15px; + display: flex; + flex-direction: column; + gap: 15px; +` + +export const PageLayout = styled.div` + width: 100%; + display: grid; + grid-template-columns: 200px 1fr 200px; + grid-template-rows: auto; + grid-template-areas: "topplayers game recentgames"; + gap: 15px; + + @media (max-width: 900px) { + grid-template-columns: 1fr; + grid-template-areas: "game"; + } +` + +export const GameContainer = styled.div` + grid-area: game; + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; /* header sticks to top */ + padding: 20px; + background: #1a1a2e; + border-radius: 20px; + box-shadow: 0 5px 20px rgba(0, 0, 0, 0.4); + overflow: hidden; + z-index: 1; + width: 100%; + height: 420px; +` + +export const TopPlayersSidebar = styled.div` + grid-area: topplayers; +` + +export const RecentGamesSidebar = styled.div` + grid-area: recentgames; +` + +export const RecentPlayersContainer = styled.div` + width: 100%; +` + +export const TopPlayersOverlay = styled.div` + position: absolute; + top: 10px; + left: 10px; + z-index: 10; + max-width: 180px; +` + +export const MainContent = styled.div` + position: relative; + z-index: 2; + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + flex: 1; /* this plus CenterBlock layout will animate */ +` + +/* now uses motion.div so layout shifts get smoothed */ +export const CenterBlock = styled(motion.div)` + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +` + +export const Loading = styled.div` + font-size: 1.2rem; + color: #e0e0e0; +` + +export const Header = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + margin-bottom: 10px; +` + +export const Title = styled.h2` + font-size: 1.5rem; + color: #fff; + margin: 0; +` + +export const Badge = styled.span<{ + status: 'waiting' | 'live' | 'settled'; +}>` + padding: 6px 12px; + border-radius: 10px; + font-size: 0.9rem; + background: ${({ status }) => + status === 'waiting' + ? '#f39c12' + : status === 'live' + ? '#2ecc71' + : '#3498db'}; + color: #fff; + font-weight: bold; +` + +export const TestButton = styled.button` + background: #f39c12; + color: white; + border: none; + padding: 8px 16px; + border-radius: 8px; + cursor: pointer; + font-weight: bold; + margin-bottom: 10px; + align-self: center; + + &:hover { + background: #e67e22; + } +` diff --git a/apps/platform/src/games/Jackpot/MyStats.tsx b/apps/platform/src/games/Jackpot/MyStats.tsx new file mode 100644 index 00000000..aeb5f88a --- /dev/null +++ b/apps/platform/src/games/Jackpot/MyStats.tsx @@ -0,0 +1,108 @@ +import React, { useEffect, useRef } from 'react' +import styled from 'styled-components' +import { motion, AnimatePresence, animate } from 'framer-motion' + +function AnimatedNumber({ value }: { value: number }) { + const ref = useRef(null) + + useEffect(() => { + const node = ref.current + if (!node) return + + const controls = animate( + Number(node.textContent) || 0, + value, + { + duration: 0.5, + ease: 'easeOut', + onUpdate(latest) { + node.textContent = latest.toFixed(2) + }, + } + ) + return () => controls.stop() + }, [value]) + + return +} + +const Wrap = styled(motion.div)` + display: flex; + gap: 20px; + margin-top: 10px; + justify-content: center; + color: #fff; + flex-wrap: wrap; + text-align: center; +` + +const Stat = styled(motion.div)` + background: #23233b; + padding: 10px 14px; + border-radius: 12px; + min-width: 120px; + line-height: 1.2; + font-size: 0.9rem; + border: 1px solid #4a4a7c; + + & > div:first-child { + opacity: 0.7; + font-size: 0.8rem; + } + & > div:last-child { + font-weight: bold; + color: #f1c40f; + } +` + +const containerVariants = { + hidden: { opacity: 0, scale: 0.95 }, + visible: { + opacity: 1, + scale: 1, + transition: { + type: 'spring', + stiffness: 500, + damping: 30, + staggerChildren: 0.1, + }, + }, +} + +const itemVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { opacity: 1, y: 0 }, +} + +interface Props { + betSOL: number + chancePct: number +} + +export function MyStats({ betSOL, chancePct }: Props) { + return ( + + {/* keyed so AnimatePresence can detect mount/unmount if needed */} + + +
My Bet
+
+ SOL +
+
+ +
Chance
+
+ % +
+
+
+
+ ) +} diff --git a/apps/platform/src/games/Jackpot/Pot.tsx b/apps/platform/src/games/Jackpot/Pot.tsx new file mode 100644 index 00000000..2524af59 --- /dev/null +++ b/apps/platform/src/games/Jackpot/Pot.tsx @@ -0,0 +1,66 @@ +import React, { useEffect, useRef } from 'react' +import styled from 'styled-components' +import { animate } from 'framer-motion' + +function AnimatedNumber({ value }: { value: number }) { + const ref = useRef(null) + + useEffect(() => { + const node = ref.current + if (!node) return + + const controls = animate( + Number(node.textContent) || 0, + value, + { + duration: 0.5, + ease: 'easeOut', + onUpdate(latest) { + node.textContent = latest.toFixed(2) + }, + } + ) + return () => controls.stop() + }, [value]) + + return +} + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + margin: 15px 0; +` + +const Label = styled.div` + font-size: 1rem; + color: #e0e0e0; +` + +const Value = styled.div` + font-size: 3rem; + line-height: 1.1; + color: #f1c40f; + font-weight: bold; + text-shadow: 0 0 15px #f1c40f; + + @media (max-width: 900px) { + font-size: 2.5rem; + } +` + +interface PotProps { + totalPot: number +} + +export function Pot({ totalPot }: PotProps) { + return ( + + + + SOL + + + ) +} diff --git a/apps/platform/src/games/Jackpot/RecentGames.tsx b/apps/platform/src/games/Jackpot/RecentGames.tsx new file mode 100644 index 00000000..861c9a6d --- /dev/null +++ b/apps/platform/src/games/Jackpot/RecentGames.tsx @@ -0,0 +1,148 @@ +import React from 'react' +import styled from 'styled-components' +import { motion, AnimatePresence } from 'framer-motion' +import { LAMPORTS_PER_SOL } from '@solana/web3.js' +import { useRecentMultiplayerEvents } from 'gamba-react-v2' +import { DESIRED_CREATOR, DESIRED_MAX_PLAYERS } from './config' +import { ParsedEvent } from '@gamba-labs/multiplayer-sdk' + +const Container = styled.div` + background: #23233b; + border-radius: 15px; + padding: 15px; + height: 100%; + display: flex; + flex-direction: column; + position: relative; +` +const Header = styled.header` + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 10px; + flex-shrink: 0; + h3 { margin: 0; font-size: 1rem; color: #fff; } +` +const List = styled.ul` + list-style: none; + margin: 0; padding: 0; + overflow-y: auto; flex-grow: 1; + &::-webkit-scrollbar { width: 0; background: transparent; } +` +const GameItem = styled(motion.li)` + display: flex; + justify-content: space-between; + align-items: center; + background: #2c2c54; + padding: 4px 8px; + border-radius: 8px; + border: 1px solid #4a4a7c; + margin-bottom: 4px; + font-size: 0.8rem; + white-space: nowrap; +` +const GameId = styled.div` font-family: monospace; color: #a9a9b8; flex: 0 0 auto; ` +const PotSize = styled.div` flex: 1 1 auto; text-align: center; color: #e0e0e0; ` +const Multiplier = styled.div` + font-family: monospace; + text-align: right; + color: #2ecc71; + font-weight: bold; + flex: 0 0 auto; +` +const EmptyState = styled.div` + display: flex; align-items: center; justify-content: center; + height: 100%; color: #a9a9b8; font-size: 0.9rem; +` +const Fade = styled.div` + position: absolute; + bottom: 15px; left: 15px; right: 15px; + height: 40px; + pointer-events: none; + background: linear-gradient(rgba(35,35,59,0), rgba(35,35,59,1)); +` + +const toNum = (x: any): number => + typeof x === 'number' + ? x + : typeof x === 'bigint' + ? Number(x) + : x?.toNumber + ? x.toNumber() + : Number(x) + +const toStr = (x: any): string => + typeof x === 'string' + ? x + : x?.toString + ? x.toString() + : String(x) + +const fmt2 = (n: number) => + n.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }) + +export function RecentGames() { + const { events, loading } = useRecentMultiplayerEvents( + 'winnersSelected', + 20, + 0, + ) + + const filtered = React.useMemo< + ParsedEvent<'winnersSelected'>[] + >(() => { + return events.filter(ev => + ev.data.gameMaker.equals(DESIRED_CREATOR) && + ev.data.maxPlayers === DESIRED_MAX_PLAYERS + ) + }, [events]) + + return ( + +

Recent Games

+ + + {filtered.length > 0 ? ( + filtered.map(ev => { + const { gameId, totalWager, payouts, winnerWagers } = + ev.data + const potSOL = toNum(totalWager) / LAMPORTS_PER_SOL + + let mul = 0 + if (winnerWagers?.[0] && payouts?.[0]) { + const bet = toNum(winnerWagers[0]) / LAMPORTS_PER_SOL + const pay = toNum(payouts[0]) / LAMPORTS_PER_SOL + mul = bet > 0 ? pay / bet : 0 + } + + return ( + + #{toStr(gameId)} + {fmt2(potSOL)} SOL + ×{fmt2(mul)} + + ) + }) + ) : ( + + {loading ? 'Loading…' : 'No recent games'} + + )} + + + +
+ ) +} + +export default RecentGames diff --git a/apps/platform/src/games/Jackpot/RecentPlayers.tsx b/apps/platform/src/games/Jackpot/RecentPlayers.tsx new file mode 100644 index 00000000..b7cc98de --- /dev/null +++ b/apps/platform/src/games/Jackpot/RecentPlayers.tsx @@ -0,0 +1,127 @@ +import React, { useMemo } from 'react' +import styled from 'styled-components' +import { AnimatePresence, motion } from 'framer-motion' +import { LAMPORTS_PER_SOL } from '@solana/web3.js' +import type { IdlAccounts } from '@coral-xyz/anchor' +import type { Multiplayer } from '@gamba-labs/multiplayer-sdk' + +const Container = styled.div` + background: #23233b; + border-radius: 15px; + padding: 15px; + + min-height: 120px; +` + +const Title = styled.h3` + margin: 0 0 10px 0; + color: #fff; + font-size: 1rem; + text-align: center; +` + +const List = styled.ul` + list-style: none; + padding: 0; + margin: 0; + display: flex; + gap: 10px; + overflow-x: auto; + padding-bottom: 10px; /* For scrollbar spacing */ + + &::-webkit-scrollbar { + height: 4px; + } + &::-webkit-scrollbar-thumb { + background: #4a4a7c; + border-radius: 2px; + } + &::-webkit-scrollbar-track { + background: transparent; + } + + & > li { + background: #2c2c54; + padding: 8px; + border-radius: 10px; + border: 1px solid #4a4a7c; + flex-shrink: 0; + display: flex; + align-items: center; + gap: 8px; + min-width: 130px; + } +` + +const Avatar = styled.div` + width: 30px; + height: 30px; + border-radius: 50%; + background: #4a4a7c; + flex-shrink: 0; +` + +const PlayerInfo = styled.div` + display: flex; + flex-direction: column; + overflow: hidden; +` + +const PlayerAddress = styled.div` + font-size: 0.8rem; + color: #e0e0e0; + font-family: monospace; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +` + +const PlayerWager = styled.div` + font-size: 0.75rem; + color: #2ecc71; + font-weight: bold; +` + +type Player = IdlAccounts['game']['players'][number] + +interface RecentPlayersProps { + players: Player[] +} + +export function RecentPlayers({ players }: RecentPlayersProps) { + const recentPlayers = useMemo(() => { + return [...players].reverse() + }, [players]) + + const shorten = (str: string) => `${str.slice(0, 4)}...` + + return ( + + Recent Players + + + {recentPlayers.map((player) => ( + + + + + {shorten(player.user.toBase58())} + + + {(player.wager.toNumber() / LAMPORTS_PER_SOL).toFixed(2)} SOL + + + + ))} + + + + ) +} diff --git a/apps/platform/src/games/Jackpot/TopPlayers.tsx b/apps/platform/src/games/Jackpot/TopPlayers.tsx new file mode 100644 index 00000000..deb84beb --- /dev/null +++ b/apps/platform/src/games/Jackpot/TopPlayers.tsx @@ -0,0 +1,210 @@ +import React, { useMemo, useState, useEffect } from 'react' +import styled, { css } from 'styled-components' +import type { IdlAccounts } from '@coral-xyz/anchor' +import type { Multiplayer } from '@gamba-labs/multiplayer-sdk' +import { LAMPORTS_PER_SOL } from '@solana/web3.js' + +type Player = IdlAccounts['game']['players'][number] + +interface TopPlayersProps { + players: Player[] + totalPot: number + $isOverlay?: boolean +} + +const COMPACT_BREAKPOINT = 900 + +function useIsCompact(): boolean { + const [isCompact, setIsCompact] = useState( + () => + typeof window !== 'undefined' && + window.innerWidth <= COMPACT_BREAKPOINT + ) + useEffect(() => { + const onResize = () => + setIsCompact(window.innerWidth <= COMPACT_BREAKPOINT) + window.addEventListener('resize', onResize) + return () => window.removeEventListener('resize', onResize) + }, []) + return isCompact +} + +const Container = styled.div<{ $isOverlay: boolean }>` + position: relative; + ${({ $isOverlay }) => + $isOverlay + ? css` + background: rgba(35, 35, 59, 0.8); + backdrop-filter: blur(5px); + border: 1px solid #4a4a7c; + border-radius: 15px; + padding: 10px; + ` + : css` + background: #23233b; + border-radius: 15px; + padding: 15px; + `} + height: 420px; + overflow: hidden; + + @media (max-width: ${COMPACT_BREAKPOINT}px) { + background: rgba(35, 35, 59, 0.5); + backdrop-filter: blur(5px); + border: 1px solid #4a4a7c; + padding: 8px; + height: auto; + } +` + +const Title = styled.h3` + margin: 0 0 10px; + color: #fff; + font-size: 1rem; + text-align: center; + + @media (max-width: ${COMPACT_BREAKPOINT}px) { + display: none; + } +` + +const List = styled.ul` + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 8px; +` + +const PlayerItem = styled.li` + display: flex; + align-items: center; + background: #2c2c54; + padding: 8px; + border-radius: 8px; + border: 1px solid #4a4a7c; + + @media (max-width: ${COMPACT_BREAKPOINT}px) { + padding: 4px; + border-radius: 6px; + } +` + +const PlayerRank = styled.div` + font-size: 0.9rem; + font-weight: bold; + color: #f39c12; + margin-right: 10px; + min-width: 24px; + text-align: center; + + @media (max-width: ${COMPACT_BREAKPOINT}px) { + font-size: 0.8rem; + margin-right: 6px; + min-width: 20px; + } +` + +const PlayerInfo = styled.div` + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +` + +const PlayerAddress = styled.div` + font-size: 0.8rem; + color: #e0e0e0; + font-family: monospace; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + + @media (max-width: ${COMPACT_BREAKPOINT}px) { + font-size: 0.7rem; + } +` + +const PlayerWager = styled.div` + font-size: 0.8rem; + color: #2ecc71; + font-weight: bold; + margin-top: 2px; + + /* compact */ + @media (max-width: ${COMPACT_BREAKPOINT}px) { + font-size: 0.7rem; + margin-top: 1px; + } +` + +const Fade = styled.div` + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 40px; + pointer-events: none; + background: linear-gradient( + rgba(35, 35, 59, 0), + rgba(35, 35, 59, 1) + ); + + /* hide on compact */ + @media (max-width: ${COMPACT_BREAKPOINT}px) { + display: none; + } +` + +export function TopPlayers({ + players, + totalPot, + $isOverlay = false, +}: TopPlayersProps) { + const isCompact = useIsCompact() + + // sort & limit count based on layout + const sorted = useMemo(() => { + const all = [...players].sort( + (a, b) => b.wager.toNumber() - a.wager.toNumber() + ) + const maxCount = isCompact ? 3 : 7 + return all.slice(0, maxCount) + }, [players, isCompact]) + + const shorten = (addr: string) => + `${addr.slice(0, 4)}…${addr.slice(-4)}` + + return ( + + {!$isOverlay && Top Players} + + + {sorted.map((p, i) => { + const sol = p.wager.toNumber() / LAMPORTS_PER_SOL + const pct = totalPot + ? (p.wager.toNumber() / totalPot) * 100 + : 0 + + return ( + + #{i + 1} + + + {shorten(p.user.toBase58())} + + + {sol.toFixed(2)} SOL • {pct.toFixed(1)} % + + + + ) + })} + + + {!isCompact && players.length > 8 && } + + ) +} diff --git a/apps/platform/src/games/Jackpot/Waiting.tsx b/apps/platform/src/games/Jackpot/Waiting.tsx new file mode 100644 index 00000000..4ccc46aa --- /dev/null +++ b/apps/platform/src/games/Jackpot/Waiting.tsx @@ -0,0 +1,39 @@ +import React from 'react' +import styled from 'styled-components' +import { motion } from 'framer-motion' + +const Overlay = styled.div` + position: absolute; + inset: 0; /* stretch to all edges */ + display: flex; + flex-direction: column; + justify-content: center; /* vertical centre */ + align-items: center; /* horizontal centre */ + pointer-events: none; /* clicks pass through */ + color: #a9a9b8; +` + +const Ghost = styled.div` + font-size: 5rem; + filter: drop-shadow(0 5px 15px rgba(0,0,0,0.3)); +` + +const Text = styled.div` + margin-top: 1rem; + font-size: 1.2rem; + font-weight: bold; +` + +export function Waiting() { + return ( + + + 👻 + + Waiting for game… + + ) +} diff --git a/apps/platform/src/games/Jackpot/WinnerAnimation.tsx b/apps/platform/src/games/Jackpot/WinnerAnimation.tsx new file mode 100644 index 00000000..39c8718a --- /dev/null +++ b/apps/platform/src/games/Jackpot/WinnerAnimation.tsx @@ -0,0 +1,248 @@ +import React, { + useState, + useEffect, + useLayoutEffect, + useRef, +} from 'react' +import styled, { keyframes, css } from 'styled-components' +import { motion, AnimatePresence } from 'framer-motion' +import type { IdlAccounts } from '@coral-xyz/anchor' +import type { Multiplayer } from '@gamba-labs/multiplayer-sdk' +import type { PublicKey } from '@solana/web3.js' +import { useSound } from 'gamba-react-ui-v2' +import tickSnd from './sounds/tick.mp3' +import winSnd from './sounds/win.mp3' + +const winnerGlow = keyframes` + 0%,100%{box-shadow:0 0 15px 5px rgba(46,204,113,.7);transform:scale(1.1)} + 50% {box-shadow:0 0 30px 10px rgba(46,204,113,1);transform:scale(1.15)} +` + +const Wrapper = styled(motion.div)` + position:absolute;inset:0; + display:flex;flex-direction:column;justify-content:center;align-items:center; + background:rgba(26,26,46,.9);backdrop-filter:blur(5px);z-index:100; +` + +const ReelContainer = styled.div` + position:relative; + width:100%; + max-width:80vw; + + overflow-x:hidden; /* hide left / right bleed */ + overflow-y:visible; /* BUT let the glow breathe vertically */ + + padding:40px 0; /* extra headroom above + below cards */ + + mask-image:linear-gradient( + to right, + transparent, + black 20%, + black 80%, + transparent + ); +` + +const Pointer = styled.div` + position:absolute;top:40px;left:50%;transform:translateX(-50%); + width:4px;height:calc(100% - 80px); /* compensate for 40 px padding */ + background:#f39c12;box-shadow:0 0 10px #f39c12;border-radius:2px;z-index:2; +` + +const PlayerReel = styled(motion.div)`display:flex;` + +const PlayerCard = styled.div<{ $isWinner:boolean;$isYou:boolean }>` + position:relative;flex-shrink:0; + width:100px;height:120px;margin:0 5px; + display:flex;flex-direction:column;align-items:center;justify-content:center; + + background:#2c2c54;border:2px solid #4a4a7c;border-radius:10px;transition:.3s; + + ${({$isYou,$isWinner})=>$isYou&&!$isWinner&&css` + border-color:#3498db;box-shadow:0 0 10px 2px rgba(52,152,219,.6);`} + + ${({$isWinner})=>$isWinner&&css` + border-color:#2ecc71;animation:${winnerGlow} 1.5s ease-in-out infinite;`} +` + +const YouBadge = styled.div`position:absolute;top:-10px;padding:2px 8px;font-size:.7rem;font-weight:700;color:#fff;background:#3498db;border-radius:6px;` +const Avatar = styled.div`width:50px;height:50px;border-radius:50%;background:#4a4a7c;margin-bottom:10px;` +const PlayerAddress = styled.div`font-size:.8rem;color:#e0e0e0;font-family:monospace;` +const BottomBar = styled.div`height:3rem;display:flex;align-items:center;justify-content:center;` +const WinnerText = styled(motion.div)`font-size:1.5rem;color:#fff;font-weight:bold;text-shadow:0 0 10px #2ecc71;` + +type Player = IdlAccounts['game']['players'][number] + +const TARGET = 100 +const short = (s:string)=>`${s.slice(0,4)}…` + +function buildReel(players:Player[], winnerIdx:number):Player[]{ + if(!players.length) return [] + + const total = players.reduce((s,p)=>s+p.wager.toNumber(),0) + const TICKETS = 40 + const pool:Player[]=[] + players.forEach(p=>{ + const share = p.wager.toNumber()/total + const cnt = Math.max(1,Math.round(share*TICKETS)) + for(let i=0;i0;i--){ + const j=Math.floor(Math.random()*(i+1)) + ;[pool[i],pool[j]]=[pool[j],pool[i]] + } + + const reel:Player[]=Array.from({length:TARGET*2},(_,i)=>pool[i%pool.length]) + reel[TARGET]=players[winnerIdx]||players[0] + return reel +} + +export const WinnerAnimation:React.FC<{ + players:Player[];winnerIndexes:number[]; + currentUser?:PublicKey|null; + onClose?:()=>void; +}> = ({ players, winnerIndexes, currentUser, onClose }) => { + if(!players.length||!winnerIndexes.length) return null + const winnerIdx = winnerIndexes[0] + + const frozenPlayers=useRef([]) + const frozenReel =useRef([]) + if(!frozenReel.current.length){ + frozenPlayers.current=[...players] + frozenReel.current =buildReel(frozenPlayers.current,winnerIdx) + } + + const reel=frozenReel.current + const snap=frozenPlayers.current + + const [closing,setClosing]=useState(false) + const [spinDone,setSpinDone]=useState(false) + const [winner,setWinner]=useState(null) + const [offset,setOffset]=useState(0) + const reelEl=useRef(null) + const centersRef = useRef([]) + const lastTickIndexRef = useRef(null) + + useLayoutEffect(()=>{ + const el=reelEl.current + if(!el) return + const card = el.children[TARGET] as HTMLElement|undefined + if(card) setOffset(-(card.offsetLeft+card.offsetWidth/2 - el.clientWidth/2)) + + // precompute card center positions for ticking + const centers: number[] = [] + for (let i = 0; i < el.children.length; i++) { + const c = el.children[i] as HTMLElement + centers.push(c.offsetLeft + c.offsetWidth / 2) + } + centersRef.current = centers + // initialize last tick index at starting position + const pointerX = el.clientWidth / 2 + let nearest = 0, best = Infinity + centers.forEach((cx, idx) => { + const d = Math.abs(cx - pointerX) + if (d < best) { best = d; nearest = idx } + }) + lastTickIndexRef.current = nearest + },[]) + + useEffect(()=>{ + if(!spinDone) return + const t=setTimeout(()=>setWinner(snap[winnerIdx]),1500) + return()=>clearTimeout(t) + },[spinDone,snap,winnerIdx]) + + useEffect(()=>{ + if(!winner) return + const t=setTimeout(()=>setClosing(true),3000) + return()=>clearTimeout(t) + },[winner]) + + // tick & win sounds + const { play: playSfx, sounds: sfx } = useSound({ tick: tickSnd, win: winSnd }) + + // play win SFX if the connected wallet is the winner + useEffect(()=>{ + if (!winner || !currentUser) return + if (winner.user.equals(currentUser)) { + try { if (sfx.win?.ready) playSfx('win') } catch {} + } + },[winner, currentUser, sfx, playSfx]) + + useEffect(()=>{if(closing) onClose?.()},[closing,onClose]) + + const winnerPk=winner?.user.toBase58()??'' + const mePk =currentUser?.toBase58()??'' + + // tick sound handled via onUpdate + + return( + + {!closing&&( + + + + { + const el = reelEl.current + if (!el || typeof x !== 'number') return + const pointerX = -x + el.clientWidth / 2 + const centers = centersRef.current + if (!centers.length) return + let nearest = 0, best = Infinity + for (let i = 0; i < centers.length; i++) { + const d = Math.abs(centers[i] - pointerX) + if (d < best) { best = d; nearest = i } + } + if (nearest !== lastTickIndexRef.current) { + lastTickIndexRef.current = nearest + if (sfx.tick?.ready) playSfx('tick') + } + }} + onAnimationComplete={()=>setSpinDone(true)} + > + {reel.map((p,i)=>{ + const addr = p.user.toBase58() + const win = addr===winnerPk && i===TARGET + const you = addr===mePk + return( + + {you&&!win&&YOU} + + {short(addr)} + + ) + })} + + + + + + {winner&&( + + Winner: {short(winnerPk)} + + )} + + + + )} + + ) +} + +export default WinnerAnimation diff --git a/apps/platform/src/games/Jackpot/config.ts b/apps/platform/src/games/Jackpot/config.ts new file mode 100644 index 00000000..bc0f0a5b --- /dev/null +++ b/apps/platform/src/games/Jackpot/config.ts @@ -0,0 +1,9 @@ +import { PublicKey } from '@solana/web3.js'; +import { WRAPPED_SOL_MINT } from '@gamba-labs/multiplayer-sdk'; + +//gamba creator bot adddress +export const DESIRED_CREATOR = new PublicKey('GamermKQpHDVzw8BwuQnj6XSYXqvwCFu64k9cn88umqn'); + +export const DESIRED_MAX_PLAYERS = 999; +export const DESIRED_WINNERS_TARGET = 1; +export const DESIRED_MINT = WRAPPED_SOL_MINT; diff --git a/apps/platform/src/games/Jackpot/index.tsx b/apps/platform/src/games/Jackpot/index.tsx new file mode 100644 index 00000000..743531a0 --- /dev/null +++ b/apps/platform/src/games/Jackpot/index.tsx @@ -0,0 +1,227 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react' +import { LAMPORTS_PER_SOL } from '@solana/web3.js' +import { GambaUi, Multiplayer } from 'gamba-react-ui-v2' +import { useGame, useSpecificGames } from 'gamba-react-v2' +import { useWallet } from '@solana/wallet-adapter-react' +import { BPS_PER_WHOLE } from 'gamba-core-v2' + +import { Countdown } from './Countdown' +import { Pot } from './Pot' +import { WinnerAnimation } from './WinnerAnimation' +import { Coinfalls } from './Coinfall' +import { TopPlayers } from './TopPlayers' +import { RecentPlayers } from './RecentPlayers' +import { RecentGames } from './RecentGames' +import { Waiting } from './Waiting' +import { MyStats } from './MyStats' + +import { DESIRED_CREATOR, DESIRED_MAX_PLAYERS, DESIRED_WINNERS_TARGET, DESIRED_MINT } from './config' +import { + PLATFORM_CREATOR_ADDRESS, + MULTIPLAYER_FEE, + PLATFORM_REFERRAL_FEE, // referral % +} from '../../constants' +import * as S from './Jackpot.styles' + +// Responsive media query hook +const useMediaQuery = (q: string) => { + const [m, setM] = useState(matchMedia(q).matches) + useEffect(() => { + const mm = matchMedia(q) + const h = () => setM(mm.matches) + mm.addEventListener('change', h) + return () => mm.removeEventListener('change', h) + }, [q]) + return m +} + +// Component +export default function Jackpot() { + const isSmall = useMediaQuery('(max-width: 900px)') + const { publicKey: walletKey } = useWallet() + + // Discover games (no auto polling) + const { + games, loading: gamesLoading, refresh: refreshGames, + } = useSpecificGames({ + creator: DESIRED_CREATOR, + maxPlayers: DESIRED_MAX_PLAYERS, + winnersTarget: DESIRED_WINNERS_TARGET, + mint: DESIRED_MINT, + } as any, 0) + + // Track last consumed gameId + const lastGameIdRef = useRef(null) + + // Use first fresh game (skip previously consumed) + const freshGames = games.filter( + g => g.account.gameId.toNumber() !== lastGameIdRef.current, + ) + const topGame = freshGames[0] ?? null + + // Live subscription + const liveGame = useGame(topGame?.publicKey ?? null).game + + // Phase handling + type Phase = 'playing' | 'animation' | 'waiting' + const [phase, setPhase] = useState('waiting') + + // Set phase based on on-chain state + useEffect(() => { + if (liveGame && liveGame.state.waiting) setPhase('playing') + if (liveGame && liveGame.state.playing) setPhase('playing') + if (liveGame && liveGame.state.settled) setPhase('animation') + }, [liveGame]) + + // Poll while waiting only + useEffect(() => { + if (phase !== 'waiting') return + refreshGames() // kick off immediately + const id = setInterval(refreshGames, 5000) + return () => clearInterval(id) + }, [phase, refreshGames]) + + // After animation, mark game as consumed + const handleAnimationDone = () => { + if (liveGame) lastGameIdRef.current = liveGame.gameId.toNumber() + setPhase('waiting') + } + + // Derived helpers + const players = liveGame?.players ?? [] + const totalPotLamports = players.reduce((s, p) => s + p.wager.toNumber(), 0) + const waitingForPlayers= !!liveGame?.state.waiting + const settled = !!liveGame?.state.settled + + const youJoined = useMemo( + () => !!walletKey && players.some(p => p.user.equals(walletKey)), + [walletKey, players], + ) + const myEntry = players.find(p => walletKey && p.user.equals(walletKey)) + const myBetLamports = myEntry?.wager.toNumber() ?? 0 + const myChancePct = totalPotLamports + ? (myBetLamports / totalPotLamports) * 100 + : 0 + + // Timestamps for progress bar + const creationMs = liveGame ? Number(liveGame.creationTimestamp) * 1e3 : 0 + const softMs = liveGame ? Number(liveGame.softExpirationTimestamp) * 1e3 : 0 + const totalDur = Math.max(softMs - creationMs, 0) + + // Render + return ( + <> + + + + + {!isSmall && ( + + + + )} + + + + {liveGame && } + + {isSmall && players.length > 0 && ( + + + + )} + + + {!liveGame && ( + + + + )} + + {liveGame && ( + <> + + Game #{liveGame.gameId.toString()} + + {waitingForPlayers ? 'Waiting' + : settled ? 'Settled' + : 'Live'} + + + + {totalDur > 0 && ( + {}} + /> + )} + + + + {phase === 'animation' && ( + + )} + + + + {myEntry && ( + + )} + + + )} + + + + {!isSmall && ( + + + + )} + + + + + + + + + + {phase === 'playing' && waitingForPlayers && !youJoined && topGame && ( + + )} + {phase === 'playing' && waitingForPlayers && youJoined && topGame && ( + + )} + + + ) +} diff --git a/apps/platform/src/games/Jackpot/sounds/join.mp3 b/apps/platform/src/games/Jackpot/sounds/join.mp3 new file mode 100644 index 00000000..bc66d0dc Binary files /dev/null and b/apps/platform/src/games/Jackpot/sounds/join.mp3 differ diff --git a/apps/platform/src/games/Jackpot/sounds/tick.mp3 b/apps/platform/src/games/Jackpot/sounds/tick.mp3 new file mode 100644 index 00000000..91068afe Binary files /dev/null and b/apps/platform/src/games/Jackpot/sounds/tick.mp3 differ diff --git a/apps/platform/src/games/Jackpot/sounds/win.mp3 b/apps/platform/src/games/Jackpot/sounds/win.mp3 new file mode 100644 index 00000000..c9f58b9b Binary files /dev/null and b/apps/platform/src/games/Jackpot/sounds/win.mp3 differ diff --git a/apps/platform/src/games/Plinko/game.ts b/apps/platform/src/games/Plinko/game.ts index dd3161df..1033c9bb 100644 --- a/apps/platform/src/games/Plinko/game.ts +++ b/apps/platform/src/games/Plinko/game.ts @@ -1,326 +1,356 @@ -import Matter from 'matter-js' +import Matter from "matter-js"; -const WIDTH = 700 -const HEIGHT = 700 +const WIDTH = 700; +const HEIGHT = 700; +const SIMULATIONS = 50; -const SIMULATIONS = 100 -export const PLINKO_RAIUS = 9 -export const PEG_RADIUS = 11 -const RESTISTUTION = .4 -const GRAVITY = 1 -const SPAWN_OFFSET_RANGE = 10 +export const PLINKO_RAIUS = 9; +export const PEG_RADIUS = 11; +const RESTITUTION = 0.4; +const GRAVITY = 1; +const SPAWN_OFFSET_RANGE = 10; -export const bucketWallHeight = 60 -export const bucketHeight = bucketWallHeight -export const barrierHeight = bucketWallHeight * 1.2 -export const barrierWidth = 4 +export const bucketWallHeight = 60; +export const bucketHeight = bucketWallHeight; +export const barrierHeight = bucketWallHeight * 1.2; +export const barrierWidth = 4; interface PlinkoContactEvent { - plinko?: Matter.Body - peg?: Matter.Body - bucket?: Matter.Body - barrier?: Matter.Body + plinko?: Matter.Body; + peg?: Matter.Body; + bucket?: Matter.Body; + barrier?: Matter.Body; } export interface PlinkoProps { - multipliers: number[] - onContact: (contact: PlinkoContactEvent) => void - rows: number + multipliers: number[]; + onContact: (contact: PlinkoContactEvent) => void; + rows: number; } interface SimulationResult { - bucketIndex: number - plinkoIndex: number - path: {x:number,y:number}[] - collisions: { frame: number, event: PlinkoContactEvent }[] + bucketIndex: number; + plinkoIndex: number; + path: Float32Array; // dense [x0,y0,x1,y1…] + collisions: { frame: number; event: PlinkoContactEvent }[]; } export class Plinko { - width = WIDTH - height = HEIGHT + width = WIDTH; + height = HEIGHT; private engine = Matter.Engine.create({ gravity: { y: GRAVITY }, timing: { timeScale: 1 }, - }) - - private runner = Matter.Runner.create() - private props: PlinkoProps - private ballComposite = Matter.Composite.create() - private bucketComposite = Matter.Composite.create() - private startPositions: number[] - private currentPath: {x:number,y:number}[] | null = null - private replayCollisions: { frame: number, event: PlinkoContactEvent }[] = [] - private currentFrame: number = 0 - private replayBall: Matter.Body | null = null - private animationId: number | null = null - private visualizePath: boolean = false - - setVisualizePath(enabled: boolean) { - this.visualizePath = enabled + }); + + // keep isFixed:true so tick(engine,1) is deterministic + private runner = Matter.Runner.create({ isFixed: true }); + + private props: PlinkoProps; + private ballComposite = Matter.Composite.create(); + private bucketComposite = Matter.Composite.create(); + private startPositions: number[]; + private currentPath: Float32Array | null = null; + private replayCollisions: { frame: number; event: PlinkoContactEvent }[] = []; + private currentFrame = 0; + private replayBall: Matter.Body | null = null; + private animationId: number | null = null; + private visualizePath = false; + + setVisualizePath(on: boolean) { + this.visualizePath = on; + } + + constructor(props: PlinkoProps) { + this.props = props; + // pre-compute 50 random X-offsets in ±5px + this.startPositions = Array.from({ length: SIMULATIONS }).map(() => + Matter.Common.random(-SPAWN_OFFSET_RANGE / 2, SPAWN_OFFSET_RANGE / 2) + ); + + // build peg grid + const rowSize = this.height / (props.rows + 2); + const pegs = Array.from({ length: props.rows }) + .flatMap((_, row, all) => { + const cols = row + 1; + const rowW = (this.width * row) / (all.length - 1); + const spacing = cols === 1 ? 0 : rowW / (cols - 1); + return Array.from({ length: cols }).map((_, col) => { + const x = this.width / 2 - rowW / 2 + spacing * col; + const y = rowSize * row + rowSize / 2; + return Matter.Bodies.circle(x, y, PEG_RADIUS, { + isStatic: true, + label: "Peg", + plugin: { pegIndex: row * cols + col }, + }); + }); + }) + .slice(1); + + Matter.Composite.add(this.bucketComposite, this.makeBuckets()); + Matter.Composite.add(this.engine.world, [ + ...pegs, + this.ballComposite, + this.bucketComposite, + ]); } private makeBuckets() { - const unique = Array.from(new Set(this.props.multipliers)) - const secondHalf = [...unique].slice(1) - const firstHalf = [...secondHalf].reverse() - const center = [unique[0], unique[0], unique[0]] - const buckets = [...firstHalf, ...center, ...secondHalf] - const numBuckets = buckets.length - const bucketWidth = this.width / numBuckets - const barriers = Array.from({ length: numBuckets + 1 }).map((_, i) => { - const x = i * bucketWidth - return Matter.Bodies.rectangle(x, this.height - barrierHeight / 2, barrierWidth, barrierHeight, { + const unique = Array.from(new Set(this.props.multipliers)); + const secondHalf = unique.slice(1); + const firstHalf = [...secondHalf].reverse(); + const center = [unique[0], unique[0], unique[0]]; + const layout = [...firstHalf, ...center, ...secondHalf]; + const w = this.width / layout.length; + + const barriers = Array.from({ length: layout.length + 1 }).map((_, i) => + Matter.Bodies.rectangle(i * w, this.height - barrierHeight / 2, barrierWidth, barrierHeight, { isStatic: true, - label: 'Barrier', + label: "Barrier", chamfer: { radius: 2 }, }) - }) - const sensors = buckets.map((bucketMultiplier, bucketIndex) => { - const x = bucketIndex * bucketWidth + bucketWidth / 2 - return Matter.Bodies.rectangle(x, this.height - bucketHeight / 2, bucketWidth - barrierWidth, bucketHeight, { - isStatic: true, - isSensor: true, - label: 'Bucket', - plugin: { - bucketIndex, - bucketMultiplier, - }, - }) - }) + ); + + const sensors = layout.map((m, idx) => + Matter.Bodies.rectangle( + idx * w + w / 2, + this.height - bucketHeight / 2, + w - barrierWidth, + bucketHeight, + { + isStatic: true, + isSensor: true, + label: "Bucket", + plugin: { bucketIndex: idx, bucketMultiplier: m }, + } + ) + ); - return [...sensors, ...barriers] + return [...sensors, ...barriers]; } - private makePlinko = (offsetX: number, index: number) => { - const x = this.width / 2 + offsetX - const y = -10 - return Matter.Bodies.circle(x, y, PLINKO_RAIUS, { - restitution: RESTISTUTION, + private makePlinko = (offsetX: number, index: number) => + Matter.Bodies.circle(this.width / 2 + offsetX, -10, PLINKO_RAIUS, { + restitution: RESTITUTION, collisionFilter: { group: -6969 }, - label: 'Plinko', + label: "Plinko", plugin: { startPositionIndex: index }, - }) + }); + + getBodies() { + return Matter.Composite.allBodies(this.engine.world); } single() { - Matter.Events.off(this.engine, 'collisionStart', this.collisionHandler) - Matter.Runner.stop(this.runner) - Matter.Events.on(this.engine, 'collisionStart', this.collisionHandler) + Matter.Events.off(this.engine, "collisionStart", this.collisionHandler); + Matter.Runner.stop(this.runner); + Matter.Events.on(this.engine, "collisionStart", this.collisionHandler); Matter.Composite.add( this.ballComposite, - this.makePlinko(Matter.Common.random(-SPAWN_OFFSET_RANGE, SPAWN_OFFSET_RANGE), 0), - ) - Matter.Runner.run(this.runner, this.engine) + this.makePlinko(Matter.Common.random(-SPAWN_OFFSET_RANGE, SPAWN_OFFSET_RANGE), 0) + ); + Matter.Runner.run(this.runner, this.engine); } cleanup() { - Matter.World.clear(this.engine.world, false) - Matter.Engine.clear(this.engine) - if (this.animationId !== null) { - cancelAnimationFrame(this.animationId) - this.animationId = null - } - } - - private makePlinkos() { - return this.startPositions.map(this.makePlinko) + Matter.World.clear(this.engine.world, false); + Matter.Engine.clear(this.engine); + if (this.animationId !== null) cancelAnimationFrame(this.animationId); + this.animationId = null; } - getBodies() { - return Matter.Composite.allBodies(this.engine.world) - } - - constructor(props: PlinkoProps) { - this.props = props - this.startPositions = Array.from({ length: SIMULATIONS }).map(() => Matter.Common.random(-SPAWN_OFFSET_RANGE / 2, SPAWN_OFFSET_RANGE / 2)) - - const rowSize = this.height / (this.props.rows + 2) - const pegs = Array.from({ length: this.props.rows }) - .flatMap((_, row, jarr) => { - const cols = (1 + row) - const rowWidth = this.width * (row / (jarr.length - 1)) - const colSpacing = cols === 1 ? 0 : rowWidth / (cols - 1) - return Array.from({ length: cols }) - .map((_, column, arr) => { - const x = this.width / 2 - rowWidth / 2 + colSpacing * column - const y = rowSize * row + rowSize / 2 - return Matter.Bodies.circle(x, y, PEG_RADIUS, { - isStatic: true, - label: 'Peg', - plugin: { pegIndex: row * arr.length + column }, - }) - }) - }).slice(1) - + reset() { + Matter.Runner.stop(this.runner); + Matter.Composite.clear(this.ballComposite, false); Matter.Composite.add( - this.bucketComposite, - this.makeBuckets(), - ) - - Matter.Composite.add(this.engine.world, [ - ...pegs, this.ballComposite, - this.bucketComposite, - ]) + this.startPositions.map(this.makePlinko) + ); } - reset() { - Matter.Runner.stop(this.runner) - Matter.Composite.clear(this.ballComposite, false) - Matter.Composite.add(this.ballComposite, this.makePlinkos()) - } + /** Simulate up to 1 000 steps, recording *all* paths until the very first + * ball hits the target bucket, then stop. */ + private simulate(desiredBucketIndex: number): SimulationResult | null { + this.reset(); - private recordContactEvent(event: Matter.IEventCollision, frame: number, collisions: { frame: number, event: PlinkoContactEvent }[]) { - for (const pair of event.pairs) { - const contactEvent: PlinkoContactEvent = {} - const assignBody = (key: keyof PlinkoContactEvent, label: string) => { - if (pair.bodyA.label === label) contactEvent[key] = pair.bodyA - if (pair.bodyB.label === label) contactEvent[key] = pair.bodyB - } - assignBody('peg', 'Peg') - assignBody('bucket', 'Bucket') - assignBody('barrier', 'Barrier') - assignBody('plinko', 'Plinko') + // per-ball path buffers + const paths: number[][] = this.startPositions.map(() => []); + // all collisions, to be filtered + const allCollisions: { frame: number; event: PlinkoContactEvent }[] = []; + let chosenIndex: number | null = null; + let frame = 0; - if (contactEvent.peg || contactEvent.bucket || contactEvent.barrier || contactEvent.plinko) { - collisions.push({ frame, event: contactEvent }) + const simHandler = (ev: Matter.IEventCollision) => { + // record every collision + this.recordContactEvent(ev, frame, allCollisions); + + // detect the *first* bucket hit + for (const p of ev.pairs) { + const A = p.bodyA, B = p.bodyB; + if ( + (A.label === "Plinko" && B.label === "Bucket" && B.plugin.bucketIndex === desiredBucketIndex) || + (B.label === "Plinko" && A.label === "Bucket" && A.plugin.bucketIndex === desiredBucketIndex) + ) { + chosenIndex = (A.label === "Plinko" ? A : B).plugin.startPositionIndex; + break; + } } - } - } + }; - simulate(desiredBucketIndex: number) { - const results: Omit[] = [] - const paths: {x:number,y:number}[][] = this.startPositions.map(() => []) - const allCollisions: { frame: number, event: PlinkoContactEvent }[] = [] + Matter.Events.on(this.engine, "collisionStart", simHandler); - let simFrame = 0 - const simHandler = (ev: Matter.IEventCollision) => { - this.recordContactEvent(ev, simFrame, allCollisions) - } + // run up to 1 000 ms-ticks or until chosen ball leaves bottom + for (; frame < 1000; frame++) { + Matter.Runner.tick(this.runner, this.engine, 1); - Matter.Events.on(this.engine, 'collisionStart', simHandler) - this.reset() - - for (let i = 0; i < 1000; i++) { - simFrame = i - Matter.Runner.tick(this.runner, this.engine, 1) - const bodies = Matter.Composite.allBodies(this.ballComposite) - bodies.forEach((b) => { - if (b.label === 'Plinko') { - const idx = b.plugin.startPositionIndex - paths[idx].push({ x: b.position.x, y: b.position.y }) + // record position for every ball this frame + for (const b of this.ballComposite.bodies) { + if (b.label === "Plinko") { + const idx = b.plugin.startPositionIndex; + paths[idx].push(b.position.x, b.position.y); } - }) - } - - Matter.Events.off(this.engine, 'collisionStart', simHandler) - Matter.Runner.stop(this.runner) - Matter.Composite.clear(this.ballComposite, false) + } - const bucketHits: { [plinkoIndex: number]: number } = {} - allCollisions.forEach(({frame, event}) => { - if (event.plinko && event.bucket) { - const plinkoIndex = event.plinko.plugin.startPositionIndex - if (bucketHits[plinkoIndex] === undefined) { - bucketHits[plinkoIndex] = event.bucket.plugin.bucketIndex + // once we know which ball and it has dropped below view, stop + if (chosenIndex !== null) { + const winBall = this.ballComposite.bodies.find( + (b) => b.plugin.startPositionIndex === chosenIndex + )!; + if (winBall.position.y > this.height) { + frame++; + break; } } - }) - - const finalResults = [] - for (let i=0; i bucketIndex === desiredBucketIndex) + Matter.Events.off(this.engine, "collisionStart", simHandler); + Matter.Runner.stop(this.runner); + Matter.Composite.clear(this.ballComposite, false); + + if (chosenIndex === null) return null; + + // build a typed array for the winner’s full path + const winnerPath = new Float32Array(paths[chosenIndex]); + + // filter out only this ball’s collisions + const winnerCollisions = allCollisions.filter( + (c) => c.event.plinko?.plugin.startPositionIndex === chosenIndex + ); + + return { + bucketIndex: desiredBucketIndex, + plinkoIndex: chosenIndex, + path: winnerPath, + collisions: winnerCollisions, + }; } - collisionHandler = (event: Matter.IEventCollision) => { - const contactEvent: PlinkoContactEvent = {} - for (const pair of event.pairs) { - const assignBody = (key: keyof PlinkoContactEvent, label: string) => { - if (pair.bodyA.label === label) contactEvent[key] = pair.bodyA - if (pair.bodyB.label === label) contactEvent[key] = pair.bodyB + private recordContactEvent( + ev: Matter.IEventCollision, + frame: number, + list: { frame: number; event: PlinkoContactEvent }[], + onlyForPlinko?: number + ) { + for (const p of ev.pairs) { + const evt: PlinkoContactEvent = {}; + const tag = (k: keyof PlinkoContactEvent, lbl: string) => { + if (p.bodyA.label === lbl) evt[k] = p.bodyA; + if (p.bodyB.label === lbl) evt[k] = p.bodyB; + }; + tag("peg", "Peg"); + tag("barrier", "Barrier"); + tag("bucket", "Bucket"); + tag("plinko", "Plinko"); + + if ( + evt.plinko && + (onlyForPlinko === undefined || + evt.plinko.plugin.startPositionIndex === onlyForPlinko) + ) { + list.push({ frame, event: evt }); } - assignBody('peg', 'Peg') - assignBody('bucket', 'Bucket') - assignBody('barrier', 'Barrier') - assignBody('plinko', 'Plinko') } - this.props.onContact && this.props.onContact(contactEvent) - } - - runAll() { - Matter.Events.off(this.engine, 'collisionStart', this.collisionHandler) - Matter.Runner.stop(this.runner) - Matter.Composite.clear(this.ballComposite, false) - Matter.Events.on(this.engine, 'collisionStart', this.collisionHandler) - Matter.Composite.add( - this.ballComposite, - this.makePlinkos(), - ) - Matter.Runner.run(this.runner, this.engine) } run(desiredMultiplier: number) { - Matter.Events.off(this.engine, 'collisionStart', this.collisionHandler) + // pick a bucket with matching multiplier const bucket = Matter.Common.choose( - this.bucketComposite.bodies.filter((x) => x.plugin.bucketMultiplier === desiredMultiplier), - ) - const candidates = this.simulate(bucket.plugin.bucketIndex) - if (!candidates.length) throw new Error('Failed to simulate desired outcome') - - const chosen = Matter.Common.choose(candidates) + this.bucketComposite.bodies.filter( + (b) => b.plugin.bucketMultiplier === desiredMultiplier + ) + ); + const sim = this.simulate(bucket.plugin.bucketIndex); + if (!sim) throw new Error("Failed to simulate desired outcome"); if (this.visualizePath) { - console.log("Chosen path:", chosen.path) + console.log("Simulation frames:", sim.path.length / 2); } - this.currentPath = chosen.path - this.currentFrame = 0 - - const chosenIndex = chosen.plinkoIndex - const chosenCollisions = chosen.collisions.filter(({event}) => { - return event.plinko && event.plinko.plugin.startPositionIndex === chosenIndex - }) - this.replayCollisions = chosenCollisions + this.currentPath = sim.path; + this.currentFrame = 0; + this.replayCollisions = sim.collisions; - const ball = this.makePlinko(this.startPositions[chosenIndex], chosenIndex) - Matter.Composite.add(this.ballComposite, ball) - this.replayBall = ball + // spawn the live ball at the same start offset + const liveBall = this.makePlinko( + this.startPositions[sim.plinkoIndex], + sim.plinkoIndex + ); + Matter.Composite.add(this.ballComposite, liveBall); + this.replayBall = liveBall; - Matter.Runner.stop(this.runner) - this.startReplayAnimation() + // purely positional replay—no physics + this.startReplayAnimation(); } private startReplayAnimation() { - if (this.animationId !== null) { - cancelAnimationFrame(this.animationId) - } - const animate = () => { - if (!this.currentPath || !this.replayBall) return - if (this.currentFrame >= this.currentPath.length) { - return - } - const pos = this.currentPath[this.currentFrame] - Matter.Body.setPosition(this.replayBall, { x: pos.x, y: pos.y }) + if (this.animationId !== null) cancelAnimationFrame(this.animationId); - const frameCollisions = this.replayCollisions.filter(c => c.frame === this.currentFrame) - frameCollisions.forEach(({event}) => { - this.props.onContact(event) - }) + const step = () => { + if (!this.currentPath || !this.replayBall) return; + const totalFrames = this.currentPath.length / 2; + if (this.currentFrame >= totalFrames) return; + + const x = this.currentPath[this.currentFrame * 2]; + const y = this.currentPath[this.currentFrame * 2 + 1]; + Matter.Body.setPosition(this.replayBall, { x, y }); - this.currentFrame++ - this.animationId = requestAnimationFrame(animate) + // fire any collisions for this frame + this.replayCollisions + .filter((c) => c.frame === this.currentFrame) + .forEach(({ event }) => this.props.onContact(event)); + + this.currentFrame++; + this.animationId = requestAnimationFrame(step); + }; + + this.animationId = requestAnimationFrame(step); + } + + collisionHandler = (ev: Matter.IEventCollision) => { + const evt: PlinkoContactEvent = {}; + for (const p of ev.pairs) { + const tag = (k: keyof PlinkoContactEvent, lbl: string) => { + if (p.bodyA.label === lbl) evt[k] = p.bodyA; + if (p.bodyB.label === lbl) evt[k] = p.bodyB; + }; + tag("peg", "Peg"); + tag("barrier", "Barrier"); + tag("bucket", "Bucket"); + tag("plinko", "Plinko"); } - this.animationId = requestAnimationFrame(animate) + this.props.onContact(evt); + }; + + runAll() { + Matter.Events.off(this.engine, "collisionStart", this.collisionHandler); + Matter.Runner.stop(this.runner); + Matter.Composite.clear(this.ballComposite, false); + Matter.Events.on(this.engine, "collisionStart", this.collisionHandler); + Matter.Composite.add( + this.ballComposite, + this.startPositions.map(this.makePlinko) + ); + Matter.Runner.run(this.runner, this.engine); } } diff --git a/apps/platform/src/games/PlinkoRace/board/Board.tsx b/apps/platform/src/games/PlinkoRace/board/Board.tsx new file mode 100644 index 00000000..229d0e76 --- /dev/null +++ b/apps/platform/src/games/PlinkoRace/board/Board.tsx @@ -0,0 +1,294 @@ +import React, { useMemo, useEffect, useState, useRef } from 'react' +import { GambaUi } from 'gamba-react-ui-v2' +import { useWallet } from '@solana/wallet-adapter-react' +import { PublicKey } from '@solana/web3.js' +import { useMultiPlinko } from '../hooks/useMultiPlinko' +import { + BUCKET_DEFS, BucketType, DYNAMIC_SEQUENCE, + DYNAMIC_EXTRA_MULT, CENTER_BUCKET, +} from '../engine/constants' +import { PlayerInfo } from '../engine/types' +import BoardHUD, { HudMessage, HudPayload } from './BoardHUD' +import BoardRenderer from './BoardRenderer' +import Scoreboard from './Scoreboard' +import { makeRng } from '../engine/deterministic' + +import extraBallSnd from '../sounds/extraball.mp3' +import readyGoSnd from '../sounds/readygo.mp3' +import fallSnd from '../sounds/fall.mp3' +import bigComboSnd from '../sounds/bigcombo.mp3' +import finishSnd from '../sounds/finsh.mp3' +import ouchSnd from '../sounds/ouch.mp3' + +type Particle = { x:number; y:number; size:number; opacity:number; life:number; vx:number; vy:number } +type LerpState = { px:number; py:number } + +export default function Board({ + players, + winnerIdx, + metadata = {}, + youIndexOverride, + gamePk, + targetPoints = 100, + payouts, + onFinished, +}: { + players: PublicKey[] + winnerIdx: number | null + metadata?: Record + youIndexOverride?: number + gamePk: string + targetPoints?: number + payouts?: number[] + onFinished?: () => void +}) { + const roster: PlayerInfo[] = useMemo(() => { + const DISTINCT_COLORS = [ + '#e6194B', // red + '#3cb44b', // green + '#ffe119', // yellow + '#4363d8', // blue + '#f58231', // orange + '#911eb4', // purple + '#46f0f0', // cyan + '#f032e6', // magenta + '#bcf60c', // lime + '#fabebe', // pink + '#008080', // teal + '#e6beff', // lavender + '#9a6324', // brown + '#fffac8', // beige + '#800000', // maroon + '#aaffc3', // mint + '#808000', // olive + '#ffd8b1', // apricot + '#000075', // navy + '#a9a9a9', // gray + ] as const + return players.map((p, i) => ({ + id: p.toBase58(), + color: DISTINCT_COLORS[i % DISTINCT_COLORS.length], + })) + }, [players]) + const { publicKey } = useWallet() + const youIdx = useMemo( + () => youIndexOverride ?? roster.findIndex(r => r.id === publicKey?.toBase58()), + [roster, publicKey, youIndexOverride] + ) + + const { engine, recordRace, replayRace } = useMultiPlinko(roster, gamePk) + const [scores, setScores] = useState([]) + const [mults, setMults] = useState([]) + const [dynModes, setDynModes] = useState([]) + const [started, setStarted] = useState(false) + const [patternOffsets, setPatternOffsets] = useState([]) + const [finished, setFinished] = useState(false) + const [hud, setHud] = useState(null) + const [popups, setPopups] = useState<{ bucketIndex:number; value:number; life:number; y:number }[]>([]) + + const showHud = (text: HudMessage) => { + setHud({ text, key: Date.now() }) + } + + const bucketAnim = useRef>({}).current + const pegAnim = useRef>({}).current + const particles = useRef([]).current + const arrowPos = useRef>(new Map()).current + const labelPos = useRef>(new Map()).current + + const { play, sounds } = GambaUi.useSound({ + ready: readyGoSnd, + extra: extraBallSnd, + fall : fallSnd, + finish: finishSnd, + bigcombo: bigComboSnd, + ouch: ouchSnd, + }) + + const lastFallMsRef = useRef(0) + + useEffect(() => { + setScores(Array(roster.length).fill(0)) + setMults (Array(roster.length).fill(1)) + setDynModes([]) + setStarted(false) + setFinished(false) + Object.keys(bucketAnim).forEach(k => bucketAnim[+k] = 0) + Object.keys(pegAnim ).forEach(k => pegAnim[+k] = 0) + particles.length = 0 + arrowPos.clear() + labelPos.clear() + }, [roster]) + + useEffect(() => { + if (finished) onFinished?.() + }, [finished, onFinished]) + + useEffect(() => { + if (!engine || winnerIdx == null) return + + showHud('GO') + if (sounds.ready?.ready) play('ready') + setStarted(true) + + const rec = recordRace(winnerIdx, targetPoints) + const ev = [...rec.events] + const runMults = Array(roster.length).fill(1) + + replayRace(rec, frame => { + while (ev.length && ev[0].frame === frame) { + const e = ev.shift()! + + if (e.kind === 'bucketMode') { + setDynModes(m => { + const next = [...(m.length ? m : Array(BUCKET_DEFS.length).fill(0))] + if (e.bucket !== undefined) next[e.bucket] = e.value ?? 0 + else BUCKET_DEFS.forEach((b,i)=>{ if(b.type===BucketType.Dynamic) next[i]=e.value??0 }) + return next + }) + + if (e.bucket !== undefined) { + bucketAnim[e.bucket] = 1 + } else { + BUCKET_DEFS.forEach((b, i) => { + if (b.type === BucketType.Dynamic) bucketAnim[i] = 1 + }) + } + continue + } + + if (e.kind === 'bucketPattern' && e.bucket !== undefined) { + setPatternOffsets(arr => { + const dynIdxs = BUCKET_DEFS + .map((b,i)=> b.type===BucketType.Dynamic ? i : -1) + .filter(i=>i>=0) + const mapIndex = dynIdxs.indexOf(e.bucket!) + const next = [...(arr.length ? arr : Array(dynIdxs.length).fill(0))] + if (mapIndex >= 0) next[mapIndex] = e.value ?? 0 + return next + }) + continue + } + + if (e.bucket !== undefined) { + const idx = e.bucket + bucketAnim[idx] = 1 + const now = performance.now() + if (sounds.fall?.ready && now - lastFallMsRef.current > 60) { + lastFallMsRef.current = now + play('fall') + } + + if (e.kind === 'extraBall') { + showHud('EXTRA BALL') + if (sounds.extra?.ready) play('extra') + } + + const def = BUCKET_DEFS[idx] + const mode = dynModes[idx] ?? 0 + const actual = def.type === BucketType.Dynamic + ? DYNAMIC_SEQUENCE[mode] + : def.type + + if (actual === BucketType.ExtraBall && e.kind !== 'extraBall') { + showHud('EXTRA BALL') + if (sounds.extra?.ready) play('extra') + } + } + + if (e.kind === 'mult') { + const inc = e.value || 1 + const curr = runMults[e.player] + runMults[e.player] = Math.min((curr === 1 ? 0 : curr) + inc, 64) + setMults(m => { + const c = [...m]; c[e.player] = runMults[e.player]; return c + }) + + if (runMults[e.player] >= 5) { + showHud('BIG COMBO') + if (sounds.bigcombo?.ready) play('bigcombo') + } + } + + if (e.kind === 'deduct') { + showHud('DEDUCTION') + if (sounds.ouch?.ready) play('ouch') + setScores(s => { + const c = [...s]; + c[e.player] = Math.max(0, (c[e.player] ?? 0) - (e.value || 0)); + return c + }) + if (e.bucket !== undefined) { + setPopups(arr => [{ bucketIndex: e.bucket!, value: -(e.value || 0), life: 30, y: 0 }, ...arr]) + } + } + + if (e.kind === 'score') { + setScores(s => { + const c = [...s]; c[e.player] += e.value || 0; return c + }) + runMults[e.player] = 1 + setMults(m => { + const c = [...m]; c[e.player] = 1; return c + }) + if (e.bucket !== undefined) { + setPopups(arr => [{ bucketIndex: e.bucket!, value: (e.value || 0), life: 30, y: 0 }, ...arr]) + } + } + + if (e.kind === 'ballKill') { + showHud('PLAYER OUT') + } + } + + if (frame === rec.totalFrames - 1) { + if (sounds.finish?.ready) play('finish') + setFinished(true) + } + }) + }, [engine, winnerIdx, recordRace, replayRace, targetPoints, roster.length]) + + if (roster.length === 0 && winnerIdx !== null) { + return ( +
+ Game settled with 0 players +
+ ) + } + + return ( +
+ + + + + +
+ ) +} diff --git a/apps/platform/src/games/PlinkoRace/board/BoardHUD.tsx b/apps/platform/src/games/PlinkoRace/board/BoardHUD.tsx new file mode 100644 index 00000000..5201ab67 --- /dev/null +++ b/apps/platform/src/games/PlinkoRace/board/BoardHUD.tsx @@ -0,0 +1,46 @@ +import React, { useEffect, useState } from 'react' +import styled, { keyframes } from 'styled-components' + +const pop = keyframes` + 0% { transform: translate(-50%, -50%) scale(0); opacity: 0; } + 12% { transform: translate(-50%, -50%) scale(1.15); opacity: 1; } + 80% { transform: translate(-50%, -50%) scale(1); opacity: 1; } + 100% { transform: translate(-50%, -50%) scale(0); opacity: 0; } +` + +const Banner = styled.div` + position: absolute; + top: 50%; left: 50%; + transform: translate(-50%, -50%); + font-family: 'Impact', sans-serif; + font-size: 64px; + letter-spacing: 2px; + color: #fff; + text-shadow: 0 0 10px #000; + pointer-events: none; + animation: ${pop} 1.5s cubic-bezier(.2,1.2,.6,1) forwards; +` + +export type HudMessage = 'GO' | 'EXTRA BALL' | 'PLAYER OUT' | 'FINISH!' | 'DEDUCTION' | 'BIG COMBO' + +export interface HudPayload { + text: HudMessage + key: number +} + +interface Props { + message: HudPayload | null +} + +export default function BoardHUD({ message }: Props) { + const [visible, setVisible] = useState(null) + + useEffect(() => { + if (!message) return + setVisible(message) + const id = setTimeout(() => setVisible(null), 1500) + return () => clearTimeout(id) + }, [message]) + + return visible ? {visible.text} : null +} diff --git a/apps/platform/src/games/PlinkoRace/board/BoardRenderer.tsx b/apps/platform/src/games/PlinkoRace/board/BoardRenderer.tsx new file mode 100644 index 00000000..c505569b --- /dev/null +++ b/apps/platform/src/games/PlinkoRace/board/BoardRenderer.tsx @@ -0,0 +1,419 @@ +// src/components/BoardRenderer.tsx +import React, { useEffect, useRef } from 'react' +import { GambaUi } from 'gamba-react-ui-v2' +import { toggleMuted, musicManager } from '../musicManager' +import { + WIDTH, HEIGHT, PEG_RADIUS, BALL_RADIUS, + BUCKET_DEFS, BUCKET_HEIGHT, + BucketType, DYNAMIC_SEQUENCE, DYNAMIC_EXTRA_MULT, DYNAMIC_DEDUCT_POINTS, + DYNAMIC_CYCLE_FRAMES, SPEED_FACTOR, +} from '../engine/constants' +import { useMultiPlinko } from '../hooks/useMultiPlinko' + +const ARROW_W = 12, ARROW_H = 10 +const HIT_DIST_SQ = (BALL_RADIUS + PEG_RADIUS) ** 2 + +type Particle = { x:number; y:number; size:number; opacity:number; life:number; vx:number; vy:number } +type LerpState = { px:number; py:number } + +export interface BoardRendererProps { + engine: ReturnType['engine'] | null + dynModes: number[] + patternOffsets: number[] + started: boolean + bucketAnim: Record + pegAnim: Record + particles: Particle[] + arrowPos: Map + labelPos: Map + mults: number[] + roster: { id:string; color:string }[] + metadata: Record + youIdx: number + popups: { bucketIndex: number; value: number; life: number; y: number }[] +} + +function bucketVisual( + def:(typeof BUCKET_DEFS)[number], + dynMode:number, +): { hue:number; label:string } { + const r = def.type === BucketType.Dynamic + ? { + type : DYNAMIC_SEQUENCE[dynMode], + value: DYNAMIC_SEQUENCE[dynMode] === BucketType.Multiplier + ? DYNAMIC_EXTRA_MULT + : (DYNAMIC_SEQUENCE[dynMode] === BucketType.Deduct + ? DYNAMIC_DEDUCT_POINTS + : def.value), + } + : def + switch (r.type) { + case BucketType.Score : return { hue:220, label:`${r.value} ▲` } + case BucketType.Multiplier: return { hue:120, label:`${r.value}×` } + case BucketType.Deduct : return { hue: 10, label:`-${r.value} ▼` } + case BucketType.ExtraBall : return { hue: 60, label:'+1' } + case BucketType.Kill : return { hue: 0, label:'☠' } + case BucketType.Blank : return { hue: 30, label:'–' } + default: return { hue: 30, label: '–' } + } +} + +function bucketNextVisual( + def:(typeof BUCKET_DEFS)[number], + dynMode:number, + offset:number, +): { hue:number; label:string } | null { + if (def.type !== BucketType.Dynamic) return null + const blankIdx = DYNAMIC_SEQUENCE.findIndex(t => t === BucketType.Blank) + const nonBlankIdxs = DYNAMIC_SEQUENCE.map((_,i)=>i).filter(i => i !== blankIdx) + const nextIdx = (dynMode === blankIdx) + ? nonBlankIdxs[(offset) % nonBlankIdxs.length] + : nonBlankIdxs[((nonBlankIdxs.indexOf(dynMode) + 1) % nonBlankIdxs.length)] + const nextType = DYNAMIC_SEQUENCE[nextIdx] + const r = { + type : nextType, + value: nextType === BucketType.Multiplier + ? DYNAMIC_EXTRA_MULT + : (nextType === BucketType.Deduct ? DYNAMIC_DEDUCT_POINTS : def.value), + } + switch (r.type) { + case BucketType.Score : return { hue:220, label:`${r.value} ▼` } + case BucketType.Multiplier: return { hue:120, label:`${r.value}×` } + case BucketType.Deduct : return { hue: 10, label:`-${r.value}` } + case BucketType.ExtraBall : return { hue: 60, label:'+1' } + case BucketType.Kill : return { hue: 0, label:'☠' } + case BucketType.Blank : return { hue: 30, label:'–' } + default: return { hue: 30, label: '–' } + } +} + +export default function BoardRenderer(props: BoardRendererProps) { + const { + engine, dynModes, patternOffsets, started, bucketAnim, pegAnim, particles, + arrowPos, labelPos, mults, roster, metadata, youIdx, popups, + } = props + + const lastChangeMsRef = useRef>({}) + const prevDynModesRef = useRef(dynModes) + if (prevDynModesRef.current !== dynModes) { + BUCKET_DEFS.forEach((b,i) => { + if (b.type !== BucketType.Dynamic) return + const prev = prevDynModesRef.current[i] ?? 0 + const curr = dynModes[i] ?? 0 + if (prev !== curr) lastChangeMsRef.current[i] = performance.now() + }) + prevDynModesRef.current = dynModes + } + + if (started) { + BUCKET_DEFS.forEach((b,i) => { + if (b.type === BucketType.Dynamic && lastChangeMsRef.current[i] == null) { + lastChangeMsRef.current[i] = performance.now() + } + }) + } + const CYCLE_MS = (DYNAMIC_CYCLE_FRAMES * SPEED_FACTOR / 60) * 1000 + + const canvasElRef = useRef(null) + const btnRectRef = useRef<{x:number;y:number;w:number;h:number}>({x:0,y:0,w:0,h:0}) + + useEffect(() => { + const onClick = (ev: MouseEvent) => { + const canvas = canvasElRef.current + if (!canvas) return + const rect = canvas.getBoundingClientRect() + const x = ev.clientX - rect.left + const y = ev.clientY - rect.top + const { x:bx, y:by, w:btnW, h:btnH } = btnRectRef.current + if (x>=bx && x<=bx+btnW && y>=by && y<=by+btnH) { + toggleMuted() + ev.stopPropagation() + ev.preventDefault() + } + } + window.addEventListener('click', onClick) + return () => window.removeEventListener('click', onClick) + }, []) + + return ( + { + if (!engine) return + canvasElRef.current = canvas as HTMLCanvasElement + + /* ─── clear & scale canvas ─── */ + ctx.clearRect(0,0,size.width,size.height) + ctx.fillStyle = '#0b0b13' + ctx.fillRect(0,0,size.width,size.height) + + const scale = Math.min(size.width / WIDTH, size.height / HEIGHT) + const ox = (size.width - WIDTH * scale) / 2 + const oy = (size.height - HEIGHT * scale) / 2 + ctx.save() + ctx.translate(ox, oy) + ctx.scale(scale, scale) + + const bodies = engine.getBodies() + const balls = bodies.filter(b => b.label === 'Ball') + const pegs = bodies.filter(b => b.label === 'Peg') + + const mix = (a:number, b:number, f:number) => a + (b - a) * f + const lerpF = 0.15 // smoothing for arrows & names + + /* ─── PEG‑HIT pulse detection ─── */ + balls.forEach(ball => { + const { x:bx, y:by } = ball.position + pegs.forEach(peg => { + const { x:px, y:py } = peg.position + const dx = bx - px, dy = by - py + if (dx*dx + dy*dy < HIT_DIST_SQ) { + const ix = (peg as any).plugin?.pegIndex ?? -1 + if (ix >= 0) pegAnim[ix] = 1 + } + }) + }) + + /* ─── PARTICLES update & draw ─── */ + for (let i = particles.length - 1; i >= 0; i--) { + const p = particles[i] + if (--p.life <= 0) { particles.splice(i,1); continue } + p.x += p.vx; p.y += p.vy + p.opacity *= 0.96; p.size *= 0.98 + ctx.fillStyle = `rgba(255,180,0,${p.opacity})` + ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, 2*Math.PI); ctx.fill() + } + + /* ─── BUCKETS ─── */ + const bw = WIDTH / BUCKET_DEFS.length + BUCKET_DEFS.forEach((def,i) => { + let a = bucketAnim[i] || 0 + if (a > 0) bucketAnim[i] = a * 0.85 + + const x0 = i * bw + const top = HEIGHT - BUCKET_HEIGHT + const cx = x0 + bw/2 + const ly = top + BUCKET_HEIGHT/2 + const mode = dynModes[i] ?? 0 + const { hue, label } = bucketVisual(def, mode) + // patternOffsets array maps to dynamic buckets in order of appearance + const dynOrderIndex = BUCKET_DEFS.slice(0, i + 1).filter(b => b.type === BucketType.Dynamic).length - 1 + const nextVis = bucketNextVisual(def, mode, patternOffsets[dynOrderIndex] ?? 0) + + if (a > 0.02) { + const h = BUCKET_HEIGHT * 3 * a + const g = ctx.createLinearGradient(0, top, 0, top - h) + g.addColorStop(0, `hsla(${hue},80%,70%,${0.4*a})`) + g.addColorStop(1, 'rgba(0,0,0,0)') + ctx.fillStyle = g + ctx.fillRect(x0, top - h, bw, h) + } + + ctx.fillStyle = `hsla(${hue},70%,50%,0.3)` + ctx.fillRect(x0, top, bw, BUCKET_HEIGHT) + + ctx.font = 'bold 18px sans-serif' + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + ctx.lineWidth = 3 + ctx.strokeStyle = `hsla(${hue},60%,20%,1)` + ctx.strokeText(label, cx, ly) + ctx.fillStyle = `hsla(${hue},80%,75%,1)` + ctx.fillText(label, cx, ly) + + // floating score/deduct popups over this bucket + for (let k = 0; k < popups.length; k++) { + const pp = popups[k] + if (pp.bucketIndex !== i) continue + // update per-frame + pp.life -= 1 + pp.y += 0.8 + const alpha = Math.max(0, Math.min(1, pp.life / 30)) + const positive = pp.value >= 0 + const text = `${positive ? '+' : ''}${Math.abs(pp.value).toFixed(1).replace(/\.0$/, '')}` + const ty = top - 8 - pp.y + ctx.font = 'bold 16px sans-serif' + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + ctx.lineWidth = 4 + ctx.strokeStyle = `rgba(0,0,0,${0.5*alpha})` + ctx.strokeText(text, cx, ty) + ctx.fillStyle = positive + ? `rgba(34,197,94,${alpha})` + : `rgba(239,68,68,${alpha})` + ctx.fillText(text, cx, ty) + } + + // show NEXT icon/label preview for dynamic buckets + if (nextVis) { + ctx.font = '12px sans-serif' + ctx.textAlign = 'center' + ctx.textBaseline = 'top' + ctx.fillStyle = `hsla(${nextVis.hue},80%,75%,0.9)` + ctx.fillText(nextVis.label, cx, top + 4) + + // countdown ring around current label + const last = lastChangeMsRef.current[i] ?? performance.now() + const elapsed = performance.now() - last + const progress = Math.min(Math.max(elapsed / CYCLE_MS, 0), 1) + const remain = 1 - progress + const R = Math.min(bw, BUCKET_HEIGHT) * 0.45 + ctx.save() + ctx.translate(cx, ly) + ctx.beginPath() + ctx.lineWidth = 3 + ctx.strokeStyle = `hsla(${hue},80%,70%,0.9)` + const start = -Math.PI/2 + ctx.arc(0, 0, R, start, start + Math.PI*2*remain) + ctx.stroke() + ctx.restore() + } + }) + + /* ─── BARRIERS & PEGS ─── */ + bodies.forEach(b => { + if (b.label === 'Barrier') { + ctx.beginPath() + b.vertices.forEach((pt,j) => j ? ctx.lineTo(pt.x,pt.y) : ctx.moveTo(pt.x,pt.y)) + ctx.closePath() + ctx.fillStyle = '#444' + ctx.fill() + return + } + if (b.label === 'Peg') { + const ix = (b as any).plugin?.pegIndex ?? -1 + let a = pegAnim[ix] || 0 + if (a > 0) pegAnim[ix] = a * 0.9 + ctx.save() + ctx.translate(b.position.x, b.position.y) + ctx.scale(1 + a*0.4, 1 + a*0.4) + const hue = (b.position.x + b.position.y + Date.now()*0.05) % 360 + ctx.fillStyle = `hsla(${hue},75%,60%,${(1+a*2)*0.2})` + ctx.beginPath(); ctx.arc(0,0,PEG_RADIUS+4,0,2*Math.PI); ctx.fill() + ctx.fillStyle = `hsla(${hue},85%,${75+a*25}%,1)` + ctx.beginPath(); ctx.arc(0,0,PEG_RADIUS,0,2*Math.PI); ctx.fill() + ctx.restore() + return + } + }) + + /* ─── BALLS / ARROWS / NAMES ─── */ + balls.forEach(b => { + const idx = (b as any).plugin?.playerIndex as number + const m = mults[idx] ?? 1 + const { x, y } = b.position + if (!Number.isFinite(x) || !Number.isFinite(y)) return + const playerId = roster[idx].id + const name = metadata[playerId] + + /* multiplier glow */ + if (m > 1) { + ctx.globalCompositeOperation = 'lighter' + const r = BALL_RADIUS * 2 + const g = ctx.createRadialGradient(x,y,0,x,y,r) + g.addColorStop(0,'rgba(255,255,200,0.5)') + g.addColorStop(1,'rgba(255,255,200,0)') + ctx.fillStyle = g + ctx.beginPath(); ctx.arc(x,y,r,0,2*Math.PI); ctx.fill() + ctx.globalCompositeOperation = 'source-over' + } + + /* big flame for high multiplier */ + if (m >= 5 && particles.length < 200) { + ctx.globalCompositeOperation = 'lighter' + const base = BALL_RADIUS * 1.2 + const f = 0.8 + Math.random()*0.4 + const r1 = base * 2.3 * f + const fg = ctx.createRadialGradient(x,y,0,x,y,r1) + fg.addColorStop(0, `rgba(255,180,0,${0.6*f})`) + fg.addColorStop(1, 'rgba(255,0,0,0)') + ctx.fillStyle = fg + ctx.beginPath(); ctx.arc(x,y,r1,0,2*Math.PI); ctx.fill() + + const r2 = base * 0.8 * f + const ig = ctx.createRadialGradient(x,y,0,x,y,r2) + ig.addColorStop(0,'rgba(255,255,220,1)') + ig.addColorStop(1,'rgba(255,200,0,0)') + ctx.fillStyle = ig + ctx.beginPath(); ctx.arc(x,y,r2,0,2*Math.PI); ctx.fill() + ctx.globalCompositeOperation = 'source-over' + + particles.push({ + x: x + (Math.random()-0.5)*5, + y: y + (Math.random()-0.5)*5, + size: 2 + Math.random()*2, + opacity: 0.5 + Math.random()*0.5, + life: 20 + Math.random()*10, + vx: (Math.random()-0.5)*0.5, + vy: (Math.random()-0.5)*0.5 - 0.5, + } as Particle) + } + + /* ball body */ + ctx.fillStyle = roster[idx % roster.length].color + ctx.beginPath(); ctx.arc(x,y,BALL_RADIUS,0,2*Math.PI); ctx.fill() + + /* “you” arrow */ + if (idx === youIdx) { + const destX = x, destY = y - BALL_RADIUS - 2 + const st = arrowPos.get(b.id) ?? { px:destX, py:destY } + st.px = mix(st.px, destX, lerpF) + st.py = mix(st.py, destY, lerpF) + arrowPos.set(b.id, st) + + ctx.fillStyle = '#fff' + ctx.beginPath(); ctx.moveTo(st.px,st.py) + ctx.lineTo(st.px - ARROW_W/2, st.py - ARROW_H) + ctx.lineTo(st.px + ARROW_W/2, st.py - ARROW_H) + ctx.closePath(); ctx.fill() + } + + /* name label */ + if (name) { + const yOff = BALL_RADIUS + 6 + (idx===youIdx ? ARROW_H : 0) + const destX = x, destY = y - yOff + const st = labelPos.get(b.id) ?? { px:destX, py:destY } + st.px = mix(st.px, destX, lerpF) + st.py = mix(st.py, destY, lerpF) + labelPos.set(b.id, st) + + ctx.font = '12px sans-serif' + ctx.textAlign = 'center' + ctx.textBaseline = 'bottom' + ctx.lineWidth = 3 + ctx.strokeStyle='rgba(0,0,0,0.7)' + ctx.strokeText(name, st.px, st.py) + ctx.fillStyle='#ffffff' + ctx.fillText(name, st.px, st.py) + } + }) + + /* ─── cleanup vanished bodies ─── */ + const ids = new Set(balls.map(b => b.id)) + arrowPos.forEach((_,id) => { if (!ids.has(id)) arrowPos.delete(id) }) + labelPos.forEach((_,id) => { if (!ids.has(id)) labelPos.delete(id) }) + + ctx.restore() + + // draw music mute button bottom-right inside canvas + const btnPad = 8 + const btnW = 130, btnH = 34 + const bx = size.width - btnW - btnPad + const by = size.height - btnH - btnPad + btnRectRef.current = { x: bx, y: by, w: btnW, h: btnH } + // background + ctx.fillStyle = 'rgba(0,0,0,0.6)' + ctx.strokeStyle = 'rgba(255,255,255,0.2)' + ctx.lineWidth = 1 + ctx.beginPath() + ctx.roundRect(bx, by, btnW, btnH, 8 as any) + ctx.fill(); ctx.stroke() + // label + ctx.font = '600 13px system-ui, sans-serif' + ctx.fillStyle = '#fff' + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + ctx.fillText(musicManager.muted ? 'Unmute Music' : 'Mute Music', bx + btnW/2, by + btnH/2) + + // interaction handled by a single global listener; nothing to attach per-frame here + }}/> + ) +} diff --git a/apps/platform/src/games/PlinkoRace/board/Scoreboard.tsx b/apps/platform/src/games/PlinkoRace/board/Scoreboard.tsx new file mode 100644 index 00000000..dcc92f7c --- /dev/null +++ b/apps/platform/src/games/PlinkoRace/board/Scoreboard.tsx @@ -0,0 +1,223 @@ +// src/components/Scoreboard.tsx +import React from 'react'; +import { + motion, + AnimatePresence, + LayoutGroup, +} from 'framer-motion'; +import { PlayerInfo } from '../engine/types'; + +interface Props { + roster : PlayerInfo[]; + scores : number[]; + mults : number[]; + targetPoints : number; + final? : boolean; // game finished? + payouts? : number[]; // lamports won + metadata? : Record; // optional on-chain names +} + +const LAMPORTS_PER_SOL = 1e9; + +export default function Scoreboard({ + roster, + scores, + mults, + targetPoints, + final = false, + payouts = [], + metadata = {}, +}: Props) { + // build & sort rows by score desc + const rows = roster + .map((p, i) => ({ + p, + s: scores[i] ?? 0, + m: mults[i] ?? 1, + w: payouts[i] ?? 0, + name: metadata[p.id] ?? '', // look up metadata + })) + .sort((a, b) => b.s - a.s); + + return ( + + + {/* Target/Goal indicator – minimal text */} +
+
+ Race to {targetPoints} +
+ + {/* Leader progress bar */} + {(() => { + const leader = rows[0]?.s ?? 0 + const pct = Math.max(0, Math.min(1, leader / targetPoints)) * 100 + return ( +
+
+
+ ) + })()} +
+ + {final && ( + +
#
+
Player
+
Score
+
Payout
+
+ )} + + + {rows.map(({ p, name, s, m, w }, idx) => ( + + {/* colour chip */} +
+ + {/* index (only in final) */} + {final && ( +
+ {idx + 1} +
+ )} + + {/* name or truncated address */} +
+ {name || `${p.id.slice(0,4)}…`} +
+ + {/* multiplier only in compact mode */} + {!final && m > 1 && ( + + ×{m} + + )} + + {/* score */} +
+ {Number.isInteger(s) + ? s.toString().padStart(targetPoints.toString().length,' ') + : s.toFixed(1)} +
+ + {/* payout (only final) */} + {final && ( + + {(w / LAMPORTS_PER_SOL).toFixed(2)} SOL + + )} + + ))} + + + + ); +} diff --git a/apps/platform/src/games/PlinkoRace/components/CreateGameModal.tsx b/apps/platform/src/games/PlinkoRace/components/CreateGameModal.tsx new file mode 100644 index 00000000..f89297db --- /dev/null +++ b/apps/platform/src/games/PlinkoRace/components/CreateGameModal.tsx @@ -0,0 +1,363 @@ +// src/components/CreateGameModal.tsx +import React, { useState } from 'react'; +import styled from 'styled-components'; +import { motion, AnimatePresence } from 'framer-motion'; +import { useWallet } from '@solana/wallet-adapter-react'; +import { useMultiplayer } from 'gamba-react-v2'; +import { NATIVE_MINT } from '@solana/spl-token'; +import { LAMPORTS_PER_SOL } from '@solana/web3.js'; +import { GambaUi } from 'gamba-react-ui-v2'; + +const Backdrop = styled(motion.div)` + position: absolute; /* inside canvas */ + inset: 0; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(5px); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +`; + +const Modal = styled(motion.div)` + background: #1c1c1c; + padding: 24px; + border-radius: 8px; + width: 92%; + max-width: 420px; + color: #fff; + border: 1px solid #333; + box-shadow: 0 12px 36px rgba(0,0,0,0.5); +`; + +const Title = styled.h2` + margin: 0 0 16px; + font-size: 1.4rem; + font-weight: 700; +`; + +const Field = styled.div` + margin-bottom: 16px; +`; + +const Label = styled.label` + display: block; + font-size: 0.9rem; + margin-bottom: 6px; + color: #a9a9b8; +`; + +const ToggleGroup = styled.div` + display: flex; + gap: 8px; +`; + +const ToggleButton = styled.button<{ active: boolean }>` + flex: 1; + padding: 10px; + border: 1px solid ${({ active }) => (active ? '#fff' : '#333')}; + border-radius: 6px; + background: ${({ active }) => (active ? '#fff' : '#222')}; + color: ${({ active }) => (active ? '#111' : '#fff')}; + font-weight: 600; + cursor: pointer; + transition: background 0.2s ease, border-color 0.2s ease; + &:hover:not(:disabled) { + background: ${({ active }) => (active ? '#eee' : '#333')}; + } +`; + +const Input = styled.input` + box-sizing: border-box; + width: 100%; + padding: 10px 12px; + border: 1px solid #333; + border-radius: 6px; + background: #222; + color: #fff; + font-size: 1rem; + transition: border-color 0.2s ease; + &:focus { + outline: none; + border-color: #555; + } +`; + +const PresetGroup = styled.div` + display: flex; + gap: 8px; + margin-top: 8px; +`; + +const PresetButton = styled.button` + flex: 1; + padding: 8px; + border: 1px solid #333; + border-radius: 6px; + background: #222; + color: #fff; + font-size: 0.9rem; + cursor: pointer; + transition: background 0.2s ease; + &:hover { + background: #333; + } +`; + +const RangeRow = styled.div` + display: flex; + gap: 12px; +`; + +const HalfField = styled(Field)` + flex: 1; + margin-bottom: 0; +`; + +const Warning = styled.p` + font-size: 0.85rem; + color: #bbb; + margin: 12px 0 0; + line-height: 1.3; +`; + +const ButtonRow = styled.div` + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 18px; +`; + +const Button = styled.button<{ variant?: 'primary' }>` + padding: 8px 16px; + border-radius: 8px; + font-weight: 600; + cursor: pointer; + border: none; + transition: background 0.2s ease; + background: ${({ variant }) => (variant === 'primary' ? '#fff' : '#333')}; + color: ${({ variant }) => (variant === 'primary' ? '#111' : '#fff')}; + &:hover:not(:disabled) { + background: ${({ variant }) => (variant === 'primary' ? '#eee' : '#444')}; + } + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +`; + +const ErrorMessage = styled.p` + color: #e74c3c; + margin: 10px 0 0; + text-align: center; + font-size: 0.9rem; +`; + +export default function CreateGameModal({ + isOpen, + onClose, +}: { + isOpen: boolean; + onClose: () => void; +}) { + const { publicKey } = useWallet(); + const { createGame } = useMultiplayer(); + + const [maxPlayers, setMaxPlayers] = useState(10); + const [wagerType, setWagerType] = useState< + 'sameWager' | 'customWager' | 'betRange' + >('sameWager'); + const [fixedWager, setFixedWager] = useState(1); + const [minBet, setMinBet] = useState(0.1); + const [maxBet, setMaxBet] = useState(5); + + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + const handleSubmit = async () => { + if (!publicKey) return setError('Connect wallet first'); + setSubmitting(true); + setError(null); + + const softDuration = 60; + const preAlloc = Math.min(maxPlayers, 5); + const winnersTarget = 1; + + const opts: any = { + mint: NATIVE_MINT, + creatorAddress: publicKey, + maxPlayers, + softDuration, + preAllocPlayers: preAlloc, + winnersTarget, + wagerType: ['sameWager', 'customWager', 'betRange'].indexOf( + wagerType + ), + payoutType: 0, + }; + + if (wagerType === 'sameWager') { + const lam = Math.floor(fixedWager * LAMPORTS_PER_SOL); + opts.wager = lam; + opts.minBet = lam; + opts.maxBet = lam; + } else if (wagerType === 'customWager') { + opts.wager = 0; + opts.minBet = 0; + opts.maxBet = 0; + } else { + const minLam = Math.floor(minBet * LAMPORTS_PER_SOL); + const maxLam = Math.floor(maxBet * LAMPORTS_PER_SOL); + opts.wager = minLam; + opts.minBet = minLam; + opts.maxBet = maxLam; + } + + try { + await createGame(opts); + onClose(); + } catch (err: any) { + console.error(err); + setError(err.message || 'Failed to create game'); + } finally { + setSubmitting(false); + } + }; + + return ( + + + {isOpen && ( + + + Create Plinko Race + + + + setMaxPlayers(Number(e.target.value))} + /> + + + + + + setWagerType('sameWager')} + > + Same + + setWagerType('betRange')} + > + Range + + setWagerType('customWager')} + > + Unlimited + + + + + {wagerType === 'sameWager' && ( + + + + setFixedWager(Number(e.target.value)) + } + /> + + {[0.1, 0.5, 1].map((v) => ( + setFixedWager(v)} + > + {v} SOL + + ))} + + + )} + + {wagerType === 'betRange' && ( + + + + + setMinBet(Number(e.target.value)) + } + /> + + + + + setMaxBet(Number(e.target.value)) + } + /> + + + )} + + {/* rent-explanation warning */} + + ⚠️Creating a game requires paying refundable + “rent” to cover on-chain storage. You’ll get it back + automatically once the game ends. + + + {error && {error}} + + + + + + + + )} + + + ); +} diff --git a/apps/platform/src/games/PlinkoRace/components/DebugGameScreen.tsx b/apps/platform/src/games/PlinkoRace/components/DebugGameScreen.tsx new file mode 100644 index 00000000..32a20d29 --- /dev/null +++ b/apps/platform/src/games/PlinkoRace/components/DebugGameScreen.tsx @@ -0,0 +1,268 @@ +// src/components/DebugGameScreen.tsx +import React, { useState, useCallback, useEffect } from 'react'; +import styled from 'styled-components'; +import { Keypair, PublicKey } from '@solana/web3.js'; +import { GambaUi, useSound } from 'gamba-react-ui-v2'; +import Board from '../board/Board'; +import lobbymusicSnd from '../sounds/lobby.mp3'; +import actionSnd from '../sounds/action.mp3'; +import { + musicManager, + attachMusic, + stopAndDispose, +} from '../musicManager'; + +function randomPk(): PublicKey { + return Keypair.generate().publicKey; +} + +const Page = styled.div` + width: 100%; + max-width: 960px; + margin: 0 auto; + padding: 16px; + box-sizing: border-box; +` + +const Panel = styled.div` + background: #11151f; + border: 1px solid #202533; + border-radius: 12px; + padding: 16px; + box-shadow: 0 6px 24px rgba(0,0,0,0.25); +` + +const PanelHeader = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 12px; + h2 { margin: 0; font-size: 18px; } +` + +const FormGrid = styled.div` + display: grid; + gap: 12px; + grid-template-columns: 1fr 1fr; + @media (max-width: 720px) { + grid-template-columns: 1fr; + } +` + +const Field = styled.label` + display: grid; + gap: 8px; + font-size: 14px; +` + +const Input = styled.input` + appearance: none; + width: 100%; + box-sizing: border-box; + padding: 10px 12px; + border-radius: 8px; + border: 1px solid #2a3142; + background: #0d1118; + color: #e8eefc; + outline: none; + font-size: 14px; + &:focus { + border-color: #5e47ff; + box-shadow: 0 0 0 3px rgba(94,71,255,0.2); + } +` + +const Actions = styled.div` + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; + justify-content: flex-end; + margin-top: 8px; +` + +const Helper = styled.div` + color: #9aa7bd; + font-size: 12px; +` + +export default function DebugGameScreen({ + onBack, +}: { + onBack: () => void; +}) { + const [count, setCount ] = useState(5); + const [winner, setWinner] = useState(0); + const [you, setYou ] = useState(0); // which ball is “you” + const [players, setPlayers] = useState([]); + const [winnerIdx, setWinnerIdx] = useState(null); + + const [seedInput, setSeedInput] = useState(''); + const [gamePk, setGamePk] = useState(null); + + const [gameOver, setGameOver] = useState(false); + + const waiting = players.length === 0; + + useEffect(() => { + // cancel any pending stop + clearTimeout(musicManager.timer); + // bump claim count + musicManager.count += 1; + return () => { + musicManager.count -= 1; + if (musicManager.count === 0) { + musicManager.timer = setTimeout(stopAndDispose, 200); + } + }; + }, []); + + const { play: playLobby, sounds: lobbySounds } = useSound( + { lobby: lobbymusicSnd }, + { disposeOnUnmount: false }, + ); + useEffect(() => { + if (!musicManager.sound) { + const snd = lobbySounds.lobby; + if (snd) { + snd.player.loop = true; + const startWhenReady = () => { + if (snd.ready) { + playLobby('lobby'); + attachMusic(snd); + } else { + setTimeout(startWhenReady, 100); + } + }; + startWhenReady(); + } + } + }, [lobbySounds, playLobby]); + + const { play: playAction, sounds: actionSounds } = useSound( + { action: actionSnd }, + { disposeOnUnmount: false }, + ); + useEffect(() => { + if (!waiting) { + try { musicManager.sound?.player.stop(); } catch {} + const snd = actionSounds.action; + if (snd) { + snd.player.loop = true; + const startWhenReady = () => { + if (snd.ready) { + playAction('action'); + attachMusic(snd); + } else { + setTimeout(startWhenReady, 100); + } + }; + startWhenReady(); + } + } + }, [waiting, actionSounds, playAction]); + + const start = useCallback(() => { + const n = Math.max(1, Math.min(20, count)); + const youClamped = Math.max(0, Math.min(n - 1, you)); + setYou(youClamped); + + setPlayers(Array.from({ length: n }, randomPk)); + setWinnerIdx(Math.max(0, Math.min(n - 1, winner))); + + const seed = seedInput.trim() || Keypair.generate().publicKey.toBase58(); + setGamePk(seed); + + setGameOver(false); + }, [count, winner, you, seedInput]); + + return ( + <> + {players.length === 0 && ( + + + +

🐞 Debug Simulator

+
+ + + Balls + setCount(+e.target.value)} + /> + How many players (1–20) + + + + Winner index + setWinner(+e.target.value)} + /> + Zero‑based index of the winner + + + + Your index + setYou(+e.target.value)} + /> + Which ball is “you” (0…{Math.max(0, count - 1)}) + + + + Seed (optional) + setSeedInput(e.target.value)} + /> + Leave empty to use a random seed + + + + + Run race + +
+
+ )} + + {players.length > 0 && gamePk && ( + setGameOver(true)} + /> + )} + + + {players.length > 0 && gameOver && ( + + ← Back to lobby + + )} + + + ); +} diff --git a/apps/platform/src/games/PlinkoRace/components/GameScreen.tsx b/apps/platform/src/games/PlinkoRace/components/GameScreen.tsx new file mode 100644 index 00000000..91ac7164 --- /dev/null +++ b/apps/platform/src/games/PlinkoRace/components/GameScreen.tsx @@ -0,0 +1,196 @@ +// src/components/GameScreen.tsx +import React, { useEffect, useState } from 'react' +import { PublicKey } from '@solana/web3.js' +import { useWallet } from '@solana/wallet-adapter-react' +import { useGame } from 'gamba-react-v2' +import { GambaUi, Multiplayer } from 'gamba-react-ui-v2' +import { + PLATFORM_CREATOR_ADDRESS, + MULTIPLAYER_FEE, + PLATFORM_REFERRAL_FEE, // ← add this +} from '../../../constants' +import { BPS_PER_WHOLE } from 'gamba-core-v2' +import Board from '../board/Board' +import { musicManager, stopAndDispose, attachMusic } from '../musicManager' +import actionSnd from '../sounds/action.mp3' +import { useSound } from 'gamba-react-ui-v2' + +export default function GameScreen({ + pk, + onBack, +}: { + pk: PublicKey + onBack: () => void +}) { + const { game: chainGame, metadata } = useGame(pk, { fetchMetadata: true }) + const { publicKey } = useWallet() + + const [snapPlayers, setSnapPlayers] = useState(null) + const [snapWinner, setSnapWinner] = useState(null) + const [snapPayouts, setSnapPayouts] = useState(null) + const [replayDone, setReplayDone] = useState(false) + + useEffect(() => { + if (!chainGame?.state.settled || snapPlayers) return + const w = Number(chainGame.winnerIndexes[0]) + setSnapPlayers(chainGame.players.map(p => p.user)) + setSnapWinner(w) + setSnapPayouts( + chainGame.players.map(p => + Number((p as any).pendingPayout ?? (p as any).pending_payout ?? 0), + ), + ) + }, [chainGame, snapPlayers]) + + useEffect(() => { + if (snapPlayers && snapPlayers.length === 0) { + setReplayDone(true) + } + }, [snapPlayers]) + + const [timeLeft, setTimeLeft] = useState(0) + useEffect(() => { + if (!chainGame?.softExpirationTimestamp) return + const end = Number(chainGame.softExpirationTimestamp) * 1000 + const tick = () => setTimeLeft(Math.max(end - Date.now(), 0)) + tick() + const id = setInterval(tick, 1000) + return () => clearInterval(id) + }, [chainGame?.softExpirationTimestamp]) + + const waiting = snapPlayers === null + const boardPlayers = waiting + ? (chainGame?.players.map(p => p.user) || []) + : snapPlayers! + const boardWinnerIdx = waiting ? null : snapWinner + const boardPayouts = waiting ? undefined : snapPayouts! + + const formatTime = (ms: number) => { + const tot = Math.ceil(ms / 1000) + const m = Math.floor(tot / 60) + const s = tot % 60 + return `${m}:${s.toString().padStart(2, '0')}` + } + + useEffect(() => { + clearTimeout(musicManager.timer) + musicManager.count += 1 + + return () => { + musicManager.count -= 1 + if (musicManager.count === 0) { + musicManager.timer = setTimeout(stopAndDispose, 200) + } + } + }, []) + + const { play: playAction, sounds: actionSounds } = useSound( + { action: actionSnd }, + { disposeOnUnmount: false } + ) + useEffect(() => { + if (!waiting) { + // stop lobby immediately + try { musicManager.sound?.player.stop() } catch {} + // start action loop and attach for volume control + const snd = actionSounds.action + if (snd) { + snd.player.loop = true + const startWhenReady = () => { + if (snd.ready) { + playAction('action') + attachMusic(snd) + // re-apply mute state after attaching + try { snd.gain.set({ gain: musicManager.muted ? 0 : snd.gain.get().gain }) } catch {} + } else { + setTimeout(startWhenReady, 100) + } + } + startWhenReady() + } + } + }, [waiting, playAction, actionSounds]) + + return ( + <> + {/* ► Always render the board */} + setReplayDone(true) : undefined} + /> + + {/* ► Top-right status + countdown */} +
+
+ {waiting ? 'Waiting' : (!replayDone ? 'Playing' : 'Settled')} +
+ {waiting && timeLeft > 0 && ( +
+ Starts in {formatTime(timeLeft)} +
+ )} +
+ + {/* ► Gamba controls bar */} + + {/* ← Back to Lobby button */} + + + {/* Conditional game controls */} + {waiting && chainGame?.state.waiting ? ( + publicKey && !chainGame.players.some(p => p.user.equals(publicKey)) ? ( + {}} + /> + ) : ( + {}} + /> + ) + ) : null} + + + ) +} diff --git a/apps/platform/src/games/PlinkoRace/components/Lobby.tsx b/apps/platform/src/games/PlinkoRace/components/Lobby.tsx new file mode 100644 index 00000000..c3c276e3 --- /dev/null +++ b/apps/platform/src/games/PlinkoRace/components/Lobby.tsx @@ -0,0 +1,228 @@ +// src/components/Lobby.tsx +import React, { useState, useEffect } from 'react' +import styled from 'styled-components' +import { PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js' +import { useSpecificGames } from 'gamba-react-v2' +import { useSound } from 'gamba-react-ui-v2' +import CreateGameModal from './CreateGameModal' +import lobbymusicSnd from '../sounds/lobby.mp3' +import LobbyBackground from './LobbyBackground' +import { + musicManager, + attachMusic, + stopAndDispose, + toggleMuted, +} from '../musicManager' + +const sol = (lamports: number) => lamports / LAMPORTS_PER_SOL +const shorten = (pk: PublicKey) => + pk.toBase58().slice(0, 4) + '...' +const formatDuration = (ms: number) => { + const total = Math.ceil(ms / 1000) + const m = Math.floor(total / 60) + const s = total % 60 + return `${m}:${s.toString().padStart(2,'0')}` +} + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + gap: 24px; +` +const Header = styled.div` + display: flex; + justify-content: space-between; + align-items: center; +` +const Button = styled.button` + padding: 8px 16px; + border-radius: 8px; + font-weight: 600; + cursor: pointer; +` +const Table = styled.table` + width: 100%; + border-collapse: collapse; +` +const TH = styled.th` + text-align: left; + padding: 8px 12px; + font-weight: 600; + font-size: 0.9rem; + text-transform: uppercase; + border-bottom: 1px solid #333; +` +const TR = styled.tr<{ $clickable?: boolean }>` + &:hover { + background: ${({ $clickable }) => ($clickable ? '#1c1c1c' : 'inherit')}; + } + cursor: ${({ $clickable }) => ($clickable ? 'pointer' : 'default')}; +` +const TD = styled.td` + padding: 10px 12px; + font-size: 0.95rem; + border-bottom: 1px solid #222; +` + +export default function Lobby({ + onSelect, + onDebug, +}: { + onSelect(pk: PublicKey): void + onDebug(): void +}) { + const { games, loading, refresh } = useSpecificGames({ winnersTarget: 1 }, 0) + + const { play, sounds } = useSound( + { lobby: lobbymusicSnd }, + { disposeOnUnmount: false } + ) + + useEffect(() => { + const snd = sounds.lobby + + clearTimeout(musicManager.timer) + + musicManager.count += 1 + + if (!musicManager.sound) { + snd.player.loop = true + const startWhenReady = () => { + if (snd.ready) { + play('lobby') + attachMusic(snd) + } else { + setTimeout(startWhenReady, 100) + } + } + startWhenReady() + } + + return () => { + musicManager.count -= 1 + if (musicManager.count === 0) { + musicManager.timer = setTimeout(stopAndDispose, 200) + } + } + }, [play, sounds]) + + const [isModalOpen, setIsModalOpen] = useState(false) + const [now, setNow] = useState(Date.now()) + useEffect(() => { + const id = setInterval(() => setNow(Date.now()), 1000) + return () => clearInterval(id) + }, []) + + return ( +
+ + + +
+ + +
+ + + + + + + + + + + + + {games.map(g => { + const { + gameId, + gameMaker, + players, + maxPlayers, + wagerType, + wager, + minBet, + maxBet, + softExpirationTimestamp, + state, + } = g.account as any + + let betLabel: string + if ('sameWager' in wagerType) { + betLabel = `${sol(wager.toNumber()).toFixed(2)} SOL` + } else if ('customWager' in wagerType) { + betLabel = 'Unlimited' + } else { + betLabel = `${sol(minBet.toNumber()).toFixed(2)} – ${sol( + maxBet.toNumber() + ).toFixed(2)} SOL` + } + + const startMs = Number(softExpirationTimestamp) * 1000 + const msLeft = startMs - now + const startsIn = state.waiting + ? msLeft > 0 + ? formatDuration(msLeft) + : 'Ready to start' + : 'Started' + + return ( + onSelect(g.publicKey)} + > + + + + + + + ) + })} + + + + + + {!loading && games.length === 0 && ( + + + + )} + +
IDMakerPlayersBetStarts In
#{gameId.toString()}{shorten(gameMaker)} + {players.length} / {maxPlayers} + {betLabel}{startsIn}
+ 🐞 Debug Simulator +
+ No live games – create one! +
+ + setIsModalOpen(false)} + /> +
+ + {/* Bottom-right Music Mute Button (music only, not SFX) */} + +
+ ) +} diff --git a/apps/platform/src/games/PlinkoRace/components/LobbyBackground.tsx b/apps/platform/src/games/PlinkoRace/components/LobbyBackground.tsx new file mode 100644 index 00000000..90bb8bff --- /dev/null +++ b/apps/platform/src/games/PlinkoRace/components/LobbyBackground.tsx @@ -0,0 +1,162 @@ +// src/components/LobbyBackground.tsx +import React, { useEffect, useRef } from 'react' +import Matter, { Bodies, Composite, Body } from 'matter-js' +import { PhysicsWorld } from '../engine/PhysicsWorld' +import { GambaUi } from 'gamba-react-ui-v2' +import { + WIDTH, + HEIGHT, + BALL_RADIUS, + PEG_RADIUS, + BUCKET_HEIGHT, + BUCKET_DEFS, +} from '../engine/constants' + +const SPAWN_INTERVAL = 300 +const BALL_COLORS = ['#ff9aa2','#ffb7b2','#ffdac1','#e2f0cb','#b5ead7','#c7ceea'] + +export default function LobbyBackground() { + const worldRef = useRef() + const ballsRef = useRef([]) + const lastSpawn = useRef(0) + const bucketHitsRef = useRef>({}) + + useEffect(() => { + const w = new PhysicsWorld() + worldRef.current = w + return () => w.cleanup() + }, []) + + useEffect(() => { + let raf: number + const step = (time: number) => { + const w = worldRef.current! + // spawn a new ball + if (time - lastSpawn.current > SPAWN_INTERVAL) { + lastSpawn.current = time + const x = WIDTH/2 + (Math.random()*200 - 100) + const color = BALL_COLORS[Math.floor(Math.random()*BALL_COLORS.length)] + const ball = Bodies.circle(x, -BALL_RADIUS, BALL_RADIUS, { + restitution: 0.4, + label: 'Ball', + plugin: { color }, + }) + ballsRef.current.push(ball) + Composite.add(w.world, ball) + } + // advance physics + w.tick(16) + // remove fallen balls + ballsRef.current = ballsRef.current.filter(b => { + if (b.position.y > HEIGHT + 80) { + Composite.remove(w.world, b) + return false + } + return true + }) + raf = requestAnimationFrame(step) + } + raf = requestAnimationFrame(step) + return () => cancelAnimationFrame(raf) + }, []) + + return ( + { + const w = worldRef.current + if (!w) return + + const scale = Math.min(size.width / WIDTH, size.height / HEIGHT) + ctx.clearRect(0, 0, size.width, size.height) + ctx.save() + ctx.translate( + (size.width - WIDTH * scale) / 2, + (size.height - HEIGHT * scale) / 2 + ) + ctx.scale(scale, scale) + + // transparent background + // draw pegs + const bodies = w.getBodies() + const pegs = bodies.filter(b => b.label === 'Peg') + ctx.fillStyle = '#555' + pegs.forEach(p => { + ctx.beginPath() + ctx.arc(p.position.x, p.position.y, PEG_RADIUS, 0, 2*Math.PI) + ctx.fill() + }) + + // draw barriers + const barriers = bodies.filter(b => b.label === 'Barrier') + ctx.fillStyle = '#333' + barriers.forEach(b => { + ctx.beginPath() + b.vertices.forEach((v,i) => + i ? ctx.lineTo(v.x,v.y) : ctx.moveTo(v.x,v.y) + ) + ctx.closePath() + ctx.fill() + }) + + // bucket hit detection + const now = performance.now() + const balls = bodies.filter(b => b.label === 'Ball') + const count = BUCKET_DEFS.length + const bw = WIDTH / count + balls.forEach(ball => { + const bx = ball.position.x + const by = ball.position.y + BALL_RADIUS + if (by >= HEIGHT - BUCKET_HEIGHT) { + const idx = Math.floor(bx / bw) + if (idx >= 0 && idx < count) { + bucketHitsRef.current[idx] = now + } + } + }) + + // draw buckets with colored bases and upward glow + for (let i = 0; i < count; i++) { + const x0 = i * bw + const y0 = HEIGHT - BUCKET_HEIGHT + const hit = bucketHitsRef.current[i] || 0 + const age = now - hit + const glow = Math.max(0, 1 - age / 250) // 250ms fade + + // pick a hue per bucket (rainbow spread) + const hue = (i / count) * 360 + + // bucket base (semi-transparent color) + ctx.fillStyle = `hsla(${hue}, 70%, 50%, 0.5)` + ctx.fillRect(x0, y0, bw, BUCKET_HEIGHT) + + // upward glow gradient + if (glow > 0) { + const g = ctx.createLinearGradient(0, y0, 0, y0 - BUCKET_HEIGHT * 3) + g.addColorStop(0, `hsla(${hue}, 80%, 70%, ${0.4 * glow})`) + g.addColorStop(1, 'rgba(0,0,0,0)') + ctx.fillStyle = g + ctx.fillRect(x0, y0 - BUCKET_HEIGHT * 3, bw, BUCKET_HEIGHT * 3) + } + } + + // draw balls with randomized colors + balls.forEach(b => { + const col = (b as any).plugin.color || '#ffb74d' + ctx.fillStyle = col + ctx.beginPath() + ctx.arc(b.position.x, b.position.y, BALL_RADIUS, 0, 2*Math.PI) + ctx.fill() + }) + + ctx.restore() + }} + /> + ) +} diff --git a/apps/platform/src/games/PlinkoRace/engine/PhysicsWorld.ts b/apps/platform/src/games/PlinkoRace/engine/PhysicsWorld.ts new file mode 100644 index 00000000..98f0497d --- /dev/null +++ b/apps/platform/src/games/PlinkoRace/engine/PhysicsWorld.ts @@ -0,0 +1,84 @@ +import Matter, { Composite } from 'matter-js'; +import { + WIDTH, + HEIGHT, + PEG_RADIUS, + BALL_RADIUS, + GRAVITY, + RESTITUTION, + ROWS, + TIME_SCALE, + BUCKET_DEFS, + BUCKET_HEIGHT, +} from './constants'; + +export class PhysicsWorld { + public engine : Matter.Engine; + public world : Matter.World; + private runner: Matter.Runner; + + constructor() { + this.engine = Matter.Engine.create({ + gravity: { y: GRAVITY }, + timing : { timeScale: TIME_SCALE }, + }); + this.runner = Matter.Runner.create({ isFixed: true }); + this.world = this.engine.world; + + Composite.add(this.world, [ + ...this.buildPegs(), + ...this.buildBarriers(), + ]); + } + + tick(dt = 16) { + Matter.Runner.tick(this.runner, this.engine, dt); // one step + } + + getBodies() { + return Composite.allBodies(this.world); + } + + cleanup() { + Matter.Runner.stop(this.runner); + Matter.World.clear(this.world, false); + Matter.Engine.clear(this.engine); + } + + private buildPegs() { + const rowH = HEIGHT / (ROWS + 2); + let pegIx = 0; + return Array.from({ length: ROWS }).flatMap((_, r, all) => { + const cols = r + 1; + const rowW = (WIDTH * r) / (all.length - 1); + const dx = cols === 1 ? 0 : rowW / (cols - 1); + + return Array.from({ length: cols }).map((_, c) => + Matter.Bodies.circle( + WIDTH / 2 - rowW / 2 + dx * c, + rowH * r + rowH / 2, + PEG_RADIUS, + { + isStatic : true, + restitution : RESTITUTION, + label : 'Peg', + plugin : { pegIndex: pegIx++ }, + }, + ) + ); + }).slice(3); // trim top three rows + } + + private buildBarriers() { + const bw = WIDTH / BUCKET_DEFS.length; + return [0, ...BUCKET_DEFS.map((_, i) => bw * (i + 1))].map(x => + Matter.Bodies.rectangle( + x, + HEIGHT - BUCKET_HEIGHT / 2, + 4, + BUCKET_HEIGHT * 1.2, + { isStatic: true, restitution: RESTITUTION, label: 'Barrier' }, + ) + ); + } +} diff --git a/apps/platform/src/games/PlinkoRace/engine/SimulationEngine.ts b/apps/platform/src/games/PlinkoRace/engine/SimulationEngine.ts new file mode 100644 index 00000000..ff30d743 --- /dev/null +++ b/apps/platform/src/games/PlinkoRace/engine/SimulationEngine.ts @@ -0,0 +1,391 @@ +// src/engine/SimulationEngine.ts +import Matter, { Composite, Bodies, Body } from 'matter-js'; +import { PhysicsWorld } from './PhysicsWorld'; +import { + WIDTH, HEIGHT, BALL_RADIUS, BUCKET_HEIGHT, + BUCKET_DEFS, BucketType, + DYNAMIC_SEQUENCE, DYNAMIC_EXTRA_MULT, + DYNAMIC_CYCLE_FRAMES, DYNAMIC_DEDUCT_POINTS, +} from './constants'; +import { makeRng } from './deterministic'; +import { + PlayerInfo, + RecordedRace, + RecordedRaceEvent, +} from './types'; + +const MAX_FRAMES = 200_000; +const MAX_ATTEMPTS = 150; +const TARGET_POINTS = 100; +const SPEED_FACTOR = 4; +const TELEPORT_DY = HEIGHT * 0.5; + +export class SimulationEngine { + private players : PlayerInfo[]; + private rng : () => number; + private staticWorld = new PhysicsWorld(); + private replayWorld?: PhysicsWorld; + + constructor(players: PlayerInfo[], seed?: string) { + this.players = players; + this.rng = seed ? makeRng(seed) : Math.random; + } + + recordRace(winnerIdx:number, target=TARGET_POINTS): RecordedRace { + if (this.players.length === 0) { + return { + winnerIndex:-1, paths:[], offsets:[], pathOwners:[], + events:[], totalFrames:0, + }; + } + + for (let n=1; n<=MAX_ATTEMPTS; n++) { + const rec = this.runSingleAttempt(winnerIdx, target); + if (rec) { + try { + console.log( + `[PlinkoRace] recordRace: success on attempt ${n}/${MAX_ATTEMPTS} (players=${this.players.length}, winnerIdx=${winnerIdx}, frames=${rec.totalFrames})` + ); + } catch {} + return rec; // success + } + } + try { + console.warn( + `[PlinkoRace] recordRace: failed to find a valid run after ${MAX_ATTEMPTS} attempts (players=${this.players.length}, winnerIdx=${winnerIdx})` + ); + } catch {} + throw new Error('No valid run found'); + } + + /*──────────────── SINGLE ATTEMPT ─────────────*/ + private runSingleAttempt(win:number, target:number): RecordedRace|null { + const sim = new PhysicsWorld(); + const layer = Composite.create(); // balls only + Composite.add(sim.world, layer); + + const randSpawn = () => WIDTH/2 + (this.rng()*16 - 8); + + /* dynamic, because ExtraBall can push more */ + const offsets : number[] = []; + const pathOwners : number[] = []; + const paths : number[][] = []; + const balls : Body[] = []; + + const scores = new Float32Array(this.players.length); + // Use float multipliers to preserve fractional values like 1.5× + const mults = new Float32Array(this.players.length).fill(1); + const events : RecordedRaceEvent[] = []; + + /* initial balls */ + this.players.forEach((_,i) => { + offsets [i] = randSpawn(); + pathOwners[i] = i; + paths [i] = []; + const b = Bodies.circle(offsets[i], -10, BALL_RADIUS, { + restitution:0.4, collisionFilter:{group:-1}, + label:'Ball', plugin:{ playerIndex:i }, + }); + balls[i] = b; + Composite.add(layer, b); + }); + + /* dynamic buckets: shared timer, per-bucket patterns (deterministic) */ + const dynamicBucketIndexes = BUCKET_DEFS + .map((b,i) => b.type === BucketType.Dynamic ? i : -1) + .filter(i => i >= 0); + const blankIdx = DYNAMIC_SEQUENCE.findIndex(t => t === BucketType.Blank); + const nonBlankIdxs = DYNAMIC_SEQUENCE.map((_,i)=>i).filter(i => i !== blankIdx); + const patternOffsets = dynamicBucketIndexes.map(() => + Math.floor(this.rng() * nonBlankIdxs.length) + ); + // emit deterministic pattern offsets so UI can mirror exactly from frame 0 + dynamicBucketIndexes.forEach((bucketIndex, di) => { + events.push({ frame: 0, player: -1, kind: 'bucketPattern', value: patternOffsets[di], bucket: bucketIndex }) + }) + const currentModeIdx = new Array(BUCKET_DEFS.length).fill(blankIdx); + let dynTick = 0; // 0 => Blank; 1.. => non-blank modes + + const respawn = (b:Body, idx:number) => { + Matter.Body.setPosition(b, {x:offsets[idx], y:-10}); + Matter.Body.setVelocity(b, {x:0, y:0}); + }; + + let totalFrames = 0; +outer: + for (let frame=0; frame0 && frame % DYNAMIC_CYCLE_FRAMES === 0) { + dynTick++; + for (let di = 0; di < dynamicBucketIndexes.length; di++) { + const bucketIndex = dynamicBucketIndexes[di]; + const seqIdx = dynTick === 0 + ? blankIdx + : nonBlankIdxs[(dynTick - 1 + patternOffsets[di]) % nonBlankIdxs.length]; + currentModeIdx[bucketIndex] = seqIdx; + events.push({ frame, player:-1, kind:'bucketMode', value:seqIdx, bucket: bucketIndex }); + } + } + sim.tick(); + + for (let bi=0; biWIDTH || body.position.y>HEIGHT){ + respawn(body, bi); + continue; + } + + /* bucket? */ + if (body.position.y >= HEIGHT - BUCKET_HEIGHT) { + const bucketW = WIDTH / BUCKET_DEFS.length; + const idx = Math.floor(body.position.x / bucketW); + + this.handleBucketHit({ + bucket : BUCKET_DEFS[idx], + bucketIndex : idx, + dynModeIdx : currentModeIdx[idx] ?? blankIdx, + ballBody : body, + ballPathIx : bi, + playerIx : player, + frame, + events, balls, paths, offsets, pathOwners, mults, scores, layer, + }); + + /* (removed) hit-based dynamic cycles – now time-based only */ + respawn(body, bi); + } + + /* win / reject checks */ + if (player!==win && scores[player]>=target) { + sim.cleanup(); return null; // someone else wins + } + if (player===win && scores[player]>=target && + Array.from(scores).every((s,j)=> j===win || s { p.length = totalFrames*2; }); + events.forEach(e => e.frame *= SPEED_FACTOR); + + return { + winnerIndex : win, + paths : paths.map(p=>new Float32Array(p)), + offsets, + pathOwners, + events, + totalFrames : totalFrames * SPEED_FACTOR, + }; + } + + /*──────────────── BUCKET‑HIT HANDLER ─────────*/ + private handleBucketHit(opts:{ + bucket : {type:BucketType,value?:number}; + bucketIndex: number; + dynModeIdx : number; + ballBody : Body; + ballPathIx : number; + playerIx : number; + frame : number; + + events : RecordedRaceEvent[]; + balls : Body[]; + paths : number[][]; + offsets : number[]; + pathOwners : number[]; + mults : Float32Array; + scores : Float32Array; + layer : Composite; + }) { + const { + bucket, bucketIndex, dynModeIdx, + ballBody, ballPathIx, playerIx, frame, + events, balls, paths, offsets, pathOwners, mults, scores, layer, + } = opts; + + /* resolve dynamic placeholder */ + const t = DYNAMIC_SEQUENCE[dynModeIdx] + const def = bucket.type === BucketType.Dynamic + ? { + type : t, + value: t===BucketType.Multiplier + ? DYNAMIC_EXTRA_MULT + : (t===BucketType.Deduct ? DYNAMIC_DEDUCT_POINTS : bucket.value), + } + : bucket; + + switch (def.type) { + + /* ─── no‑op ───────────────────────────── */ + case BucketType.Blank: + break; + + /* ─── score bucket ────────────────────── */ + case BucketType.Score: { + const pts = (def.value ?? 0) * mults[playerIx]; + scores[playerIx] += pts; + events.push({ + frame, player:playerIx, kind:'score', + value:pts, bucket:bucketIndex, + }); + mults[playerIx] = 1; + } break; + + /* ─── multiplier bucket ───────────────── */ + case BucketType.Multiplier: { + const m = def.value ?? 1; + const current = mults[playerIx]; + // Additive stacking with baseline 1 preserved + const next = Math.min((current === 1 ? 0 : current) + m, 64); + mults[playerIx] = next; + events.push({ + frame, player:playerIx, kind:'mult', + value:m, bucket:bucketIndex, + }); + } break; + + /* ─── deduction bucket ────────────────── */ + case BucketType.Deduct: { + const base = def.value ?? 0; + const applied = base * mults[playerIx]; + scores[playerIx] = Math.max(0, scores[playerIx] - applied); + events.push({ + frame, player:playerIx, kind:'deduct', + value:applied, bucket:bucketIndex, + }); + // Consume multiplier on deduction + mults[playerIx] = 1; + } break; + + /* ─── extra‑ball bucket ───────────────── */ + case BucketType.ExtraBall: { + events.push({ + frame, player:playerIx, kind:'extraBall', + bucket:bucketIndex, + }); + + /* spawn extra ball immediately */ + const spawnX = Math.min(Math.max( + offsets[ballPathIx] + (this.rng()*30 - 15), + BALL_RADIUS), WIDTH - BALL_RADIUS); + + const extra = Bodies.circle(spawnX, -10, BALL_RADIUS, { + restitution:0.4, collisionFilter:{group:-1}, + label:'Ball', plugin:{ playerIndex:playerIx }, + }); + balls.push(extra); + Composite.add(layer, extra); + + offsets .push(spawnX); + pathOwners.push(playerIx); + + /* pre‑fill its path up to current frame */ + const samples = frame + 1; + const p: number[] = []; + for (let f=0; fvoid) { + this.replayWorld?.cleanup(); + const world = new PhysicsWorld(); + this.replayWorld = world; + + const bodies: Body[] = []; + const spawnBody = (pathIdx:number) => { + const owner = rec.pathOwners[pathIdx]; + const body = Bodies.circle(rec.offsets[pathIdx], -10, BALL_RADIUS,{ + isStatic:true, label:'Ball', plugin:{ playerIndex:owner }, + }); + bodies[pathIdx] = body; + Composite.add(world.world, body); + }; + + /* initial bodies = one per player */ + this.players.forEach((_,i)=> spawnBody(i)); + + let f = 0; + const N = rec.totalFrames; + const targetFps = 60; // UI playback rate + const frameMs = 1000 / targetFps; + let lastTs = performance.now(); + let accMs = 0; + + const advanceOneFrame = () => { + // spawn extra balls at their UI frame + rec.events.forEach(e => { + if (e.kind==='extraBall' && e.frame===f) spawnBody(bodies.length); + }); + + const coarse = Math.floor(f / SPEED_FACTOR); + const alpha = (f % SPEED_FACTOR) / SPEED_FACTOR; + + bodies.forEach((b,i) => { + const arr = rec.paths[i]; + if (arr.length < 2) return; + const len = arr.length/2 - 1; + const i0 = Math.min(coarse, len); + const i1 = Math.min(i0+1, len); + const x0 = arr[i0*2], y0 = arr[i0*2+1]; + const x1 = arr[i1*2], y1 = arr[i1*2+1]; + + const [x,y] = (y0 - y1 > TELEPORT_DY) + ? (alpha===0 ? [x0,y0] : [x1,y1]) + : [x0*(1-alpha)+x1*alpha, y0*(1-alpha)+y1*alpha]; + + Matter.Body.setPosition(b, {x,y}); + }); + + onFrame?.(f); + f++; + }; + + const step = (now:number) => { + accMs += now - lastTs; + lastTs = now; + while (accMs >= frameMs && f < N) { + advanceOneFrame(); + accMs -= frameMs; + } + if (f < N) requestAnimationFrame(step); + }; + requestAnimationFrame(step); + } + + /* utils */ + getBodies() { + return [ + ...this.staticWorld.getBodies(), + ...(this.replayWorld ? this.replayWorld.getBodies() : []), + ]; + } + cleanup() { + this.staticWorld.cleanup(); + this.replayWorld?.cleanup(); + } +} diff --git a/apps/platform/src/games/PlinkoRace/engine/constants.ts b/apps/platform/src/games/PlinkoRace/engine/constants.ts new file mode 100644 index 00000000..d724ea72 --- /dev/null +++ b/apps/platform/src/games/PlinkoRace/engine/constants.ts @@ -0,0 +1,58 @@ +// src/engine/constants.ts +export const WIDTH = 700; +export const HEIGHT = 700; + +export const PEG_RADIUS = 5; +export const BALL_RADIUS = 13; +export const GRAVITY = 0.9; +export const RESTITUTION = 0.6; +export const ROWS = 14; +export const TIME_SCALE = 4; // 4× realtime +export const SPEED_FACTOR = 4; // sim‑steps per UI frame (for replay timing) + +export enum BucketType { + Blank = 'blank', // does nothing, ball just respawns + Score = 'score', // awards fixed points (value = points) + Multiplier = 'mult', // multiplies next score (value = multiplier) + ExtraBall = 'extraBall', // spawns an extra ball (no value) + Kill = 'kill', // removes ball forever (no value) + Deduct = 'deduct', // deducts fixed points (value = points) + Dynamic = 'dynamic', // placeholder – cycles through MODES below +} + +export interface BucketDef { + type : BucketType; + value?: number; // used by Score / Multiplier +} + +export const BUCKET_DEFS: BucketDef[] = [ + { type: BucketType.Dynamic }, // left dynamic (index 0) + { type: BucketType.Score, value: 10 }, + { type: BucketType.Multiplier, value: 2.5 }, + { type: BucketType.Score, value: 6 }, + { type: BucketType.Multiplier, value: 1.5 }, + { type: BucketType.Score, value: 3 }, + + { type: BucketType.Dynamic }, // center dynamic + + { type: BucketType.Score, value: 3 }, + { type: BucketType.Multiplier, value: 1.5 }, + { type: BucketType.Score, value: 6 }, + { type: BucketType.Multiplier, value: 2.5 }, + { type: BucketType.Score, value: 10 }, + { type: BucketType.Dynamic }, // right dynamic (last) +]; + +export const DYNAMIC_SEQUENCE: BucketType[] = [ + BucketType.Blank, + BucketType.ExtraBall, + BucketType.Multiplier, // 5× + BucketType.Deduct, // -5 points +]; +export const DYNAMIC_EXTRA_MULT = 5; // value when in Multiplier mode +export const DYNAMIC_DEDUCT_POINTS = 5; // points to deduct in Deduct mode + +export const BUCKET_HEIGHT = 60; +export const CENTER_BUCKET = BUCKET_DEFS.findIndex(b => b.type === BucketType.Dynamic); + +export const DYNAMIC_CYCLE_FRAMES = 180; diff --git a/apps/platform/src/games/PlinkoRace/engine/deterministic.ts b/apps/platform/src/games/PlinkoRace/engine/deterministic.ts new file mode 100644 index 00000000..fee24b5b --- /dev/null +++ b/apps/platform/src/games/PlinkoRace/engine/deterministic.ts @@ -0,0 +1,21 @@ +// src/engine/deterministic.ts +function seedFromString(str: string): number { + const encoder = new TextEncoder(); + const bytes = encoder.encode(str); + return bytes.reduce((seed, b) => ((seed << 5) - seed + b) >>> 0, 0); +} + +function mulberry32(seed: number): () => number { + let t = seed >>> 0; + return () => { + t += 0x6D2B79F5; + let r = Math.imul(t ^ (t >>> 15), 1 | t); + r = (r + Math.imul(r ^ (r >>> 7), 61 | r)) ^ r; + return ((r ^ (r >>> 14)) >>> 0) / 4294967296; + }; +} + +export function makeRng(seedString: string): () => number { + const seed = seedFromString(seedString); + return mulberry32(seed); +} diff --git a/apps/platform/src/games/PlinkoRace/engine/index.ts b/apps/platform/src/games/PlinkoRace/engine/index.ts new file mode 100644 index 00000000..72a130b0 --- /dev/null +++ b/apps/platform/src/games/PlinkoRace/engine/index.ts @@ -0,0 +1,5 @@ +// engine/index.ts + +export * from './types' +export * from './PhysicsWorld' +export { SimulationEngine } from './SimulationEngine' \ No newline at end of file diff --git a/apps/platform/src/games/PlinkoRace/engine/types.ts b/apps/platform/src/games/PlinkoRace/engine/types.ts new file mode 100644 index 00000000..72f6034f --- /dev/null +++ b/apps/platform/src/games/PlinkoRace/engine/types.ts @@ -0,0 +1,21 @@ +// src/engine/types.ts +import { BucketType } from './constants'; + +export interface PlayerInfo { id: string; color: string; } + +export interface RecordedRaceEvent { + frame : number; + player : number; // -1 ⇒ global / no player + kind : 'score' | 'deduct' | 'mult' | 'extraBall' | 'ballKill' | 'bucketMode' | 'bucketPattern'; + value? : number; // points, multiplier, next bucketMode index… + bucket?: number; +} + +export interface RecordedRace { + winnerIndex: number; + paths : Float32Array[]; + offsets : number[]; + pathOwners : number[]; + events : RecordedRaceEvent[]; + totalFrames: number; +} diff --git a/apps/platform/src/games/PlinkoRace/hooks/useMultiPlinko.ts b/apps/platform/src/games/PlinkoRace/hooks/useMultiPlinko.ts new file mode 100644 index 00000000..7b2b4e0e --- /dev/null +++ b/apps/platform/src/games/PlinkoRace/hooks/useMultiPlinko.ts @@ -0,0 +1,35 @@ +import { useEffect, useState, useCallback } from 'react'; +import { SimulationEngine } from '../engine/SimulationEngine'; +import { RecordedRace } from '../engine/types'; +import { PlayerInfo } from '../engine/types'; + +export function useMultiPlinko( + players: PlayerInfo[], + seed?: string, +) { + const [engine, setEngine] = useState(null); + + useEffect(() => { + const sim = new SimulationEngine(players, seed); + setEngine(sim); + return () => sim.cleanup(); + }, [players.map(p => p.id).join(','), seed]); + + const recordRace = useCallback( + (winnerIdx: number, target?: number): RecordedRace => { + if (!engine) throw new Error('Engine not ready'); + return engine.recordRace(winnerIdx, target); + }, + [engine], + ); + + const replayRace = useCallback( + (rec: RecordedRace, onFrame?: (f: number) => void) => { + if (!engine) throw new Error('Engine not ready'); + engine.replayRace(rec, onFrame); + }, + [engine], + ); + + return { engine, recordRace, replayRace }; +} diff --git a/apps/platform/src/games/PlinkoRace/index.tsx b/apps/platform/src/games/PlinkoRace/index.tsx new file mode 100644 index 00000000..a64a0f1c --- /dev/null +++ b/apps/platform/src/games/PlinkoRace/index.tsx @@ -0,0 +1,32 @@ +import React, { useState, useCallback } from 'react'; +import { GambaUi } from 'gamba-react-ui-v2'; +import type { PublicKey } from '@solana/web3.js'; + +import Lobby from './components/Lobby'; +import GameScreen from './components/GameScreen'; +import DebugGameScreen from './components/DebugGameScreen'; + +export default function PlinkoRace() { + const [selectedGame, setSelectedGame] = useState(null); + const [debugMode, setDebugMode] = useState(false); + + const handleBack = useCallback(() => { + setSelectedGame(null); + setDebugMode(false); + }, []); + + return ( + + {debugMode ? ( + setDebugMode(false)} /> + ) : selectedGame ? ( + + ) : ( + setDebugMode(true)} + /> + )} + + ); +} diff --git a/apps/platform/src/games/PlinkoRace/musicManager.ts b/apps/platform/src/games/PlinkoRace/musicManager.ts new file mode 100644 index 00000000..6cc0e44c --- /dev/null +++ b/apps/platform/src/games/PlinkoRace/musicManager.ts @@ -0,0 +1,50 @@ +import { useSoundStore } from 'gamba-react-ui-v2' + +type Unsub = () => void + +export const musicManager = { + sound: null as any, + count: 0, + timer: 0 as any, + sub: null as Unsub | null, + muted: false, +} + +try { + const saved = localStorage.getItem('plinkorace_music_muted') + if (saved != null) musicManager.muted = saved === '1' +} catch {} + +export function attachMusic(snd: any) { + musicManager.sound = snd + + const vol = useSoundStore.getState().volume + snd.gain.set({ gain: musicManager.muted ? 0 : vol }) + + if (!musicManager.sub) { + musicManager.sub = useSoundStore.subscribe(state => { + if (musicManager.sound) { + musicManager.sound.gain.set({ gain: musicManager.muted ? 0 : state.volume }) + } + }) + } +} + +export function stopAndDispose() { + try { musicManager.sound?.player.stop() } catch {} + musicManager.sound = null + + musicManager.sub?.() + musicManager.sub = null +} + +export function setMuted(muted: boolean) { + musicManager.muted = muted + try { localStorage.setItem('plinkorace_music_muted', muted ? '1' : '0') } catch {} + const vol = useSoundStore.getState().volume + try { musicManager.sound?.gain.set({ gain: muted ? 0 : vol }) } catch {} +} + +export function toggleMuted() { + setMuted(!musicManager.muted) +} diff --git a/apps/platform/src/games/PlinkoRace/sounds/action.mp3 b/apps/platform/src/games/PlinkoRace/sounds/action.mp3 new file mode 100644 index 00000000..8ad10724 Binary files /dev/null and b/apps/platform/src/games/PlinkoRace/sounds/action.mp3 differ diff --git a/apps/platform/src/games/PlinkoRace/sounds/bigcombo.mp3 b/apps/platform/src/games/PlinkoRace/sounds/bigcombo.mp3 new file mode 100644 index 00000000..7aff849b Binary files /dev/null and b/apps/platform/src/games/PlinkoRace/sounds/bigcombo.mp3 differ diff --git a/apps/platform/src/games/PlinkoRace/sounds/bump.mp3 b/apps/platform/src/games/PlinkoRace/sounds/bump.mp3 new file mode 100644 index 00000000..ff8c0299 Binary files /dev/null and b/apps/platform/src/games/PlinkoRace/sounds/bump.mp3 differ diff --git a/apps/platform/src/games/PlinkoRace/sounds/extraball.mp3 b/apps/platform/src/games/PlinkoRace/sounds/extraball.mp3 new file mode 100644 index 00000000..9064962c Binary files /dev/null and b/apps/platform/src/games/PlinkoRace/sounds/extraball.mp3 differ diff --git a/apps/platform/src/games/PlinkoRace/sounds/fall.mp3 b/apps/platform/src/games/PlinkoRace/sounds/fall.mp3 new file mode 100644 index 00000000..f1f9c623 Binary files /dev/null and b/apps/platform/src/games/PlinkoRace/sounds/fall.mp3 differ diff --git a/apps/platform/src/games/PlinkoRace/sounds/finsh.mp3 b/apps/platform/src/games/PlinkoRace/sounds/finsh.mp3 new file mode 100644 index 00000000..dc9cb952 Binary files /dev/null and b/apps/platform/src/games/PlinkoRace/sounds/finsh.mp3 differ diff --git a/apps/platform/src/games/PlinkoRace/sounds/lobby.mp3 b/apps/platform/src/games/PlinkoRace/sounds/lobby.mp3 new file mode 100644 index 00000000..b40ea5ca Binary files /dev/null and b/apps/platform/src/games/PlinkoRace/sounds/lobby.mp3 differ diff --git a/apps/platform/src/games/PlinkoRace/sounds/lobbymusic.mp3 b/apps/platform/src/games/PlinkoRace/sounds/lobbymusic.mp3 new file mode 100644 index 00000000..75e1cc8c Binary files /dev/null and b/apps/platform/src/games/PlinkoRace/sounds/lobbymusic.mp3 differ diff --git a/apps/platform/src/games/PlinkoRace/sounds/ouch.mp3 b/apps/platform/src/games/PlinkoRace/sounds/ouch.mp3 new file mode 100644 index 00000000..695d4d3f Binary files /dev/null and b/apps/platform/src/games/PlinkoRace/sounds/ouch.mp3 differ diff --git a/apps/platform/src/games/PlinkoRace/sounds/readygo.mp3 b/apps/platform/src/games/PlinkoRace/sounds/readygo.mp3 new file mode 100644 index 00000000..4ebfb0a9 Binary files /dev/null and b/apps/platform/src/games/PlinkoRace/sounds/readygo.mp3 differ diff --git a/apps/platform/src/games/PlinkoRace/sounds/win.mp3 b/apps/platform/src/games/PlinkoRace/sounds/win.mp3 new file mode 100644 index 00000000..0c68d102 Binary files /dev/null and b/apps/platform/src/games/PlinkoRace/sounds/win.mp3 differ diff --git a/apps/platform/src/games/index.tsx b/apps/platform/src/games/index.tsx index 596060a6..bb238a05 100644 --- a/apps/platform/src/games/index.tsx +++ b/apps/platform/src/games/index.tsx @@ -1,17 +1,15 @@ -import { GameBundle } from 'gamba-react-ui-v2' -import React from 'react' +// src/constants/Games.tsx +import { GameBundle } from 'gamba-react-ui-v2'; +import React from 'react'; -export const GAMES: GameBundle[] = [ - // { - // id: 'example', - // meta: { - // background: '#00ffe1', - // name: 'Example', - // image: '#', - // description: '', - // }, - // app: React.lazy(() => import('./ExampleGame')), - // }, +export interface ExtendedGameBundle extends GameBundle { + meta: GameBundle['meta'] & { + /** optional badge/tag to show on the card */ + tag?: string; + }; +} + +export const GAMES: ExtendedGameBundle[] = [ { id: 'dice', meta: { @@ -39,24 +37,24 @@ export const GAMES: GameBundle[] = [ { id: 'flip', meta: { + background: '#ffe694', name: 'Flip', + image: '/games/flip.png', description: ` Flip offers a straightforward yet thrilling gamble: choose Heads or Tails and double your money or lose it all. This simple, high-stakes game tests your luck and decision-making with every flip of the coin. `, - image: '/games/flip.png', - background: '#ffe694', }, app: React.lazy(() => import('./Flip')), }, { id: 'hilo', meta: { + background: '#ff4f4f', name: 'HiLo', image: '/games/hilo.png', description: ` HiLo is a game of foresight and luck, challenging players to guess whether the next card will be higher or lower. Make consecutive correct guesses to increase your winnings, and decide when to cash out for maximum rewards. `, - background: '#ff4f4f', }, props: { logo: '/logo.svg' }, app: React.lazy(() => import('./HiLo')), @@ -64,24 +62,24 @@ export const GAMES: GameBundle[] = [ { id: 'mines', meta: { + background: '#8376ff', name: 'Mines', + image: '/games/mines.png', description: ` There's money hidden beneath the squares. The reward will increase the more squares you reveal, but watch out for the 5 hidden mines. Touch one and you'll go broke. You can cash out at any time. `, - image: '/games/mines.png', - background: '#8376ff', }, app: React.lazy(() => import('./Mines')), }, { id: 'roulette', meta: { + background: '#1de87e', name: 'Roulette', image: '/games/roulette.png', description: ` Roulette brings the classic wheel-spinning game to life with a digital twist. Bet on where the ball will land and watch as the wheel decides your fate. With straightforward rules and the chance for big wins, Roulette is a timeless game of chance. `, - background: '#1de87e', }, app: React.lazy(() => import('./Roulette')), }, @@ -89,11 +87,10 @@ export const GAMES: GameBundle[] = [ id: 'plinko', meta: { background: '#7272ff', - image: '/games/plinko.png', name: 'Plinko', + image: '/games/plinko.png', description: ` - Plinko is played by dropping chips down a pegged board where they randomly fall into slots with varying win amounts. Each drop is a mix of anticipation and strategy, making Plinko an endlessly entertaining game of chance. - ⚠️ Under development. Results shown might be incorrect. ⚠️ + Plinko is played by dropping chips down a pegged board where they randomly fall into slots with varying win amounts. `, }, app: React.lazy(() => import('./Plinko')), @@ -102,10 +99,10 @@ export const GAMES: GameBundle[] = [ id: 'crash', meta: { background: '#de95e8', - image: '/games/crash.png', name: 'Crash', + image: '/games/crash.png', description: ` - Predict a multiplier target and watch a rocket attempt to reach it. If the rocket crashes before the target, the player loses; if it reaches or exceeds the target, the player wins. + Predict a multiplier target and watch a rocket attempt to reach it. If the rocket crashes before the target, the player loses; if it reaches or exceeds the target, the player wins. `, }, app: React.lazy(() => import('./CrashGame')), @@ -114,12 +111,38 @@ export const GAMES: GameBundle[] = [ id: 'blackjack', meta: { background: '#084700', - image: '/games/blackjack.png', name: 'BlackJack', + image: '/games/blackjack.png', description: ` A simplified blackjack game where you and the dealer each get two cards. Win 2.5x your wager with a blackjack (21 with two cards), or 2x if your total beats the dealer's without exceeding 21. Ties or lower totals result in a loss. Enjoy quick gameplay without the usual complexities. `, }, app: React.lazy(() => import('./BlackJack')), }, -] + { + id: 'jackpot', + meta: { + background: '#38acc9ff', + name: 'JackPot', + image: '/games/jackpot.png', + description: ` + A simple jackpot multiplier game where you spin to win big. + `, + tag: 'Multiplayer', + }, + app: React.lazy(() => import('./Jackpot')), + }, + { + id: 'plinkorace', + meta: { + background: '#62cc34ff', + name: 'PlinkoRace', + image: '/games/plinkorace.png', + description: ` + multiplayer plinko game + `, + tag: 'Multiplayer', + }, + app: React.lazy(() => import('./PlinkoRace')), + }, +]; diff --git a/apps/platform/src/hooks/useToast.ts b/apps/platform/src/hooks/useToast.ts index 71a7db8d..f849a7f1 100644 --- a/apps/platform/src/hooks/useToast.ts +++ b/apps/platform/src/hooks/useToast.ts @@ -1,3 +1,4 @@ +//useToast.ts import { create } from 'zustand' export interface Toast { diff --git a/apps/platform/src/hooks/useUserStore.ts b/apps/platform/src/hooks/useUserStore.ts index 2ad8887e..0b0de5be 100644 --- a/apps/platform/src/hooks/useUserStore.ts +++ b/apps/platform/src/hooks/useUserStore.ts @@ -1,3 +1,4 @@ +//useUserStore.ts import { StoreApi, create } from 'zustand' import { createJSONStorage, persist } from 'zustand/middleware' diff --git a/apps/platform/src/index.tsx b/apps/platform/src/index.tsx index 67763012..688a7ebf 100644 --- a/apps/platform/src/index.tsx +++ b/apps/platform/src/index.tsx @@ -1,3 +1,10 @@ +import * as ReactRoot from 'react' +console.log('🏷️ App React identity:', ReactRoot) + +import * as ReactLocal from 'react' +console.log('🏷️ useConnection React identity:', ReactLocal) + + import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react' import { WalletModalProvider } from '@solana/wallet-adapter-react-ui' import '@solana/wallet-adapter-react-ui/styles.css' diff --git a/apps/platform/src/sections/Dashboard/Dashboard.tsx b/apps/platform/src/sections/Dashboard/Dashboard.tsx index 62fdb78b..e70b6748 100644 --- a/apps/platform/src/sections/Dashboard/Dashboard.tsx +++ b/apps/platform/src/sections/Dashboard/Dashboard.tsx @@ -4,6 +4,10 @@ import { SlideSection } from '../../components/Slider' import { GAMES } from '../../games' import { GameCard } from './GameCard' import { WelcomeBanner } from './WelcomeBanner' +// src/sections/Dashboard/Dashboard.tsx +import FeaturedInlineGame from './FeaturedInlineGame' + + export function GameSlider() { return ( @@ -42,12 +46,14 @@ export function GameGrid() { ) } + export default function Dashboard() { return ( <> +

Games

) -} +} \ No newline at end of file diff --git a/apps/platform/src/sections/Dashboard/FeaturedInlineGame.tsx b/apps/platform/src/sections/Dashboard/FeaturedInlineGame.tsx new file mode 100644 index 00000000..cff19cdb --- /dev/null +++ b/apps/platform/src/sections/Dashboard/FeaturedInlineGame.tsx @@ -0,0 +1,58 @@ +// src/sections/Dashboard/FeaturedInlineGame.tsx +import React from 'react' +import styled from 'styled-components' +import { GambaUi } from 'gamba-react-ui-v2' + +import { GAMES } from '../../games' +import { FEATURED_GAME_ID, FEATURED_GAME_INLINE } from '../../constants' +import { + Container as GameContainer, + Screen as GameScreen, + Controls as GameControls, +} from '../Game/Game.styles' + +// exactly the same wrapper width as WelcomeBanner / MainWrapper +const Wrapper = styled.div` + width: 100%; + max-width: 1200px; + margin: 0 auto; + padding: 0 20px; + box-sizing: border-box; +` + +export default function FeaturedInlineGame() { + if (!FEATURED_GAME_INLINE || !FEATURED_GAME_ID) return null + const game = GAMES.find((g) => g.id === FEATURED_GAME_ID) + if (!game) return null + + return ( + + + ⚠️ Unable to load game. +

+ } + > + + {/* force the same 600px height as your regular */} + + + + {/* controls + play button */} + + + + + +
+
+ ) +} diff --git a/apps/platform/src/sections/Dashboard/GameCard.tsx b/apps/platform/src/sections/Dashboard/GameCard.tsx index ea03493a..71a0856a 100644 --- a/apps/platform/src/sections/Dashboard/GameCard.tsx +++ b/apps/platform/src/sections/Dashboard/GameCard.tsx @@ -1,120 +1,127 @@ -import { GameBundle } from 'gamba-react-ui-v2' -import React from 'react' -import { NavLink, useLocation } from 'react-router-dom' -import styled, { keyframes } from 'styled-components' +// src/components/GameCard.tsx +import React from 'react'; +import { GameBundle } from 'gamba-react-ui-v2'; +import { NavLink, useLocation } from 'react-router-dom'; +import styled, { keyframes } from 'styled-components'; const tileAnimation = keyframes` - 0% { - background-position: -100px 100px; - } - 100% { - background-position: 100px -100px; - } -` + 0% { background-position: -100px 100px; } + 100% { background-position: 100px -100px; } +`; -const StyledGameCard = styled(NavLink)<{$small: boolean, $background: string}>` - width: 100%; - - @media (min-width: 800px) { - width: 100%; - } +const StyledGameCard = styled(NavLink)<{ $small: boolean; $background: string }>` + position: relative; + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; + pointer-events: auto; /* if you need clicks */ - aspect-ratio: ${(props) => props.$small ? '1/.5' : '1/.6'}; + width: 100%; + aspect-ratio: ${({ $small }) => ($small ? '1/.5' : '1/.6')}; + background: ${({ $background }) => $background}; background-size: cover; + background-position: center; border-radius: 10px; - - color: white; text-decoration: none; + color: white; + font-weight: bold; font-size: 24px; + transition: transform 0.2s ease; - transition: transform .2s ease; - /* border-bottom: 2px solid #00000033; */ - - & > .background { + & > .background, + & > .image { position: absolute; - left: 0; top: 0; + left: 0; width: 100%; height: 100%; - background-size: 100%; - background-position: center; + transition: transform 0.2s ease, opacity 0.3s ease; + } + + & > .background { background-image: url(/stuff.png); + background-size: 100%; background-repeat: repeat; - transition: transform .2s ease, opacity .3s; animation: ${tileAnimation} 5s linear infinite; opacity: 0; } & > .image { - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; background-size: 90% auto; background-position: center; background-repeat: no-repeat; - transform: scale(.9); - transition: transform .2s ease; + transform: scale(0.9); + } + + & > .play { + position: absolute; + bottom: 5px; + right: 5px; + padding: 5px 10px; + font-size: 14px; + background: rgba(0, 0, 0, 0.4); + border-radius: 5px; + text-transform: uppercase; + opacity: 0; + backdrop-filter: blur(20px); + transition: opacity 0.2s ease; } &:hover { transform: scale(1.01); - .image { + outline: 5px solid rgba(149, 100, 255, 0.2); + outline-offset: 0; + + & > .background { + opacity: 0.35; + } + & > .image { transform: scale(1); } - - .background { - opacity: .35; + & > .play { + opacity: 1; } } +`; - position: relative; - transform: scale(1); - background: ${(props) => props.$background}; - max-height: 100%; - overflow: hidden; - display: flex; - justify-content: center; - align-items: center; - flex-grow: 0; - flex-shrink: 0; - background-size: 100% auto; - background-position: center; +// New badge for the “VS” tag (or any other tag you choose) +const Tag = styled.div` + position: absolute; + top: 8px; + left: 8px; + padding: 2px 6px; + font-size: 12px; font-weight: bold; - .play { - font-size: 14px; - border-radius: 5px; - padding: 5px 10px; - background: #00000066; - position: absolute; - right: 5px; - bottom: 5px; - opacity: 0; - text-transform: uppercase; + background: rgba(0, 0, 0, 0.6); + color: #fff; + border-radius: 4px; + text-transform: uppercase; + z-index: 2; +`; - backdrop-filter: blur(20px); - } - &:hover .play { - opacity: 1; - } - &:hover { - outline: #9564ff33 solid 5px; - outline-offset: 0px; - } -` +export function GameCard({ + game, +}: { + game: GameBundle & { meta: { tag?: string; [key: string]: any } }; +}) { + const small = useLocation().pathname !== '/'; -export function GameCard({ game }: {game: GameBundle}) { - const small = useLocation().pathname !== '/' return ( + {/* render the VS badge if present */} + {game.meta.tag && {game.meta.tag}} +
-
+
Play {game.meta.name}
- ) + ); } diff --git a/apps/platform/src/sections/Dashboard/WelcomeBanner.tsx b/apps/platform/src/sections/Dashboard/WelcomeBanner.tsx index 7115cad3..90dfdc06 100644 --- a/apps/platform/src/sections/Dashboard/WelcomeBanner.tsx +++ b/apps/platform/src/sections/Dashboard/WelcomeBanner.tsx @@ -1,136 +1,137 @@ -import { useWallet } from '@solana/wallet-adapter-react' -import { useWalletModal } from '@solana/wallet-adapter-react-ui' -import React from 'react' -import styled from 'styled-components' -import { useUserStore } from '../../hooks/useUserStore' - -const Buttons = styled.div` - overflow: hidden; +import { useWallet } from '@solana/wallet-adapter-react'; +import { useWalletModal } from '@solana/wallet-adapter-react-ui'; +import React from 'react'; +import styled from 'styled-components'; +import { useUserStore } from '../../hooks/useUserStore'; + +const WelcomeWrapper = styled.div` + /* Animations */ + @keyframes welcome-fade-in { + from { opacity: 0; } + to { opacity: 1; } + } + + @keyframes backgroundGradient { + 0% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } + 100% { background-position: 0% 50%; } + } + + /* Styling */ + background: linear-gradient(-45deg, #ffb07c, #ff3e88, #2969ff, #ef3cff, #ff3c87); + background-size: 300% 300%; + animation: welcome-fade-in 0.5s ease, backgroundGradient 30s ease infinite; + border-radius: 12px; /* Slightly larger radius for a modern look */ + padding: 24px; /* Consistent padding */ display: flex; flex-direction: column; - justify-content: space-between; - align-items: center; - gap: 10px; + gap: 24px; /* Consistent gap */ + text-align: center; + filter: drop-shadow(0 4px 3px rgba(0,0,0,.07)) drop-shadow(0 2px 2px rgba(0,0,0,.06)); + /* Desktop styles using a min-width media query */ @media (min-width: 800px) { - height: 100%; + display: grid; + grid-template-columns: 2fr 1fr; + align-items: center; + text-align: left; + padding: 40px; + gap: 40px; } +`; - @media (max-width: 800px) { - display: flex; - flex-direction: row; - justify-content: space-between; - width: 100%; - padding-top: 0!important; +const WelcomeContent = styled.div` + h1 { + font-size: 1.75rem; /* Responsive font size */ + margin: 0 0 8px 0; + color: #ffffff; } - & > button { - border: none; - width: 100%; - border-radius: 10px; - padding: 10px; - background: #ffffffdf; - transition: background-color .2s ease; - color: black; - cursor: pointer; - &:hover { - background: white; - } - } -` - -const Welcome = styled.div` - @keyframes welcome-fade-in { - from { - opacity: 0; - } - to { - opacity: 1; - } + p { + font-size: 1rem; + color: #ffffffd1; + margin: 0; } - @keyframes backgroundGradient { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; + @media (min-width: 800px) { + h1 { + font-size: 2.25rem; } - 100% { - background-position: 0% 50%; + p { + font-size: 1.125rem; } } +`; - background: linear-gradient(-45deg, #ffb07c, #ff3e88, #2969ff, #ef3cff, #ff3c87); - background-size: 300% 300%; - animation: welcome-fade-in .5s ease, backgroundGradient 30s ease infinite; - border-radius: 10px; - position: relative; - overflow: hidden; +const ButtonGroup = styled.div` display: flex; - align-items: center; - justify-content: center; - flex-direction: column; - padding: 20px; - filter: drop-shadow(0 4px 3px rgba(0,0,0,.07)) drop-shadow(0 2px 2px rgba(0,0,0,.06)); + flex-wrap: wrap; /* Allows buttons to wrap onto the next line */ + gap: 12px; /* Space between buttons */ + justify-content: center; /* Center buttons on mobile */ - & img { - animation-duration: 5s; - animation-iteration-count: infinite; - animation-timing-function: ease-in-out; - width: 100px; - height: 100px; - top: 0; - right: 0; - &:nth-child(1) {animation-delay: 0s;} - &:nth-child(2) {animation-delay: 1s;} + @media (min-width: 800px) { + flex-direction: column; + justify-content: flex-start; } +`; - & > div { - padding: 0px; - filter: drop-shadow(0 4px 3px rgba(0,0,0,.07)) drop-shadow(0 2px 2px rgba(0,0,0,.06)); +const ActionButton = styled.button` + /* Base styles */ + border: none; + border-radius: 10px; + padding: 12px 20px; + font-size: 0.9rem; + font-weight: 600; + background: #ffffffdf; + color: black; + cursor: pointer; + transition: background-color 0.2s ease, transform 0.2s ease; + flex-grow: 1; /* Allows buttons to share space on mobile */ + text-align: center; + + &:hover { + background: white; + transform: translateY(-2px); /* Subtle hover effect */ } + /* On desktop, buttons take full width of their container */ @media (min-width: 800px) { - display: grid; - grid-template-columns: 2fr 1fr; - padding: 0; - & > div { - padding: 40px; - } + width: 100%; + flex-grow: 0; /* Reset flex-grow */ } -` +`; export function WelcomeBanner() { - const wallet = useWallet() - const walletModal = useWalletModal() - const store = useUserStore() - const copyInvite = () => { - store.set({ userModal: true }) + const wallet = useWallet(); + const walletModal = useWalletModal(); + const { set: setUserModal } = useUserStore(); // Destructure for cleaner access + + const handleCopyInvite = () => { + setUserModal({ userModal: true }); if (!wallet.connected) { - walletModal.setVisible(true) + walletModal.setVisible(true); } - } + }; + + const openLink = (url) => () => window.open(url, '_blank', 'noopener,noreferrer'); return ( - -
+ +

Welcome to Gamba v2 👋

-

- A fair, simple and decentralized casino on Solana. -

-
- - - - - -
- ) -} + + + + ); +} \ No newline at end of file diff --git a/apps/platform/src/sections/Game/Game.styles.ts b/apps/platform/src/sections/Game/Game.styles.ts index e9ed77ce..bae4b8e4 100644 --- a/apps/platform/src/sections/Game/Game.styles.ts +++ b/apps/platform/src/sections/Game/Game.styles.ts @@ -1,15 +1,10 @@ +// src/sections/Game/Game.styles.ts import styled, { css, keyframes } from 'styled-components' const splashAnimation = keyframes` - 0% { - opacity: 1; - } - 30%, 75% { - opacity: 1; - } - 100% { - opacity: 0; - } + 0% { opacity: 1; } + 30%, 75% { opacity: 1; } + 100% { opacity: 0; } ` export const loadingAnimation = keyframes` @@ -32,19 +27,15 @@ export const SettingControls = styled.div` transition: opacity .2s; padding: 5px; text-shadow: 0 0 1px #00000066; - &:hover { - opacity: 1; - } + &:hover { opacity: 1; } } ` export const Splash = styled.div` pointer-events: none; position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; + left: 0; top: 0; + width: 100%; height: 100%; opacity: 0; animation: ${splashAnimation} .75s ease; display: flex; @@ -64,28 +55,21 @@ export const Screen = styled.div` overflow: hidden; transition: height .2s ease; height: 600px; - @media (max-width: 700px) { - height: 600px; - } + @media (max-width: 700px) { height: 600px; } ` export const IconButton = styled.button` background: none; border: none; - padding: 0; - width: 50px; - padding: 10px; - justify-content: center; - align-items: center; + padding: 0 10px; display: flex; - margin: 0; + align-items: center; + justify-content: center; cursor: pointer; font-size: 16px; border-radius: 10px; color: white; - &:hover { - background: #ffffff22; - } + &:hover { background: #ffffff22; } ` export const StyledLoadingIndicator = styled.div<{$active: boolean}>` @@ -97,15 +81,12 @@ export const StyledLoadingIndicator = styled.div<{$active: boolean}>` &:after { content: " "; position: absolute; - width: 25%; - height: 100%; + width: 25%; height: 100%; animation: ${loadingAnimation} ease infinite .5s; opacity: 0; background: #9564ff; transition: opacity .5s; - ${(props) => props.$active && css` - opacity: 1; - `} + ${(props) => props.$active && css`opacity: 1;`} } ` @@ -117,39 +98,35 @@ export const Controls = styled.div` border-radius: 10px; z-index: 6; + display: flex; + justify-content: center; + align-items: center; + gap: 20px; + @media (max-width: 800px) { - padding: 10px; - display: flex; flex-direction: column; gap: 10px; + padding: 10px; } @media (min-width: 800px) { - display: flex; - gap: 20px; - align-items: center; height: 80px; } ` export const MetaControls = styled.div` position: absolute; - bottom: 0; - left: 0; + bottom: 0; left: 0; width: 100%; padding: 10px; display: flex; - justify-content: left; - align-items: left; + align-items: center; + gap: 10px; z-index: 6; ` export const spinnerAnimation = keyframes` - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } ` export const Spinner = styled.div<{$small?: boolean}>` @@ -158,13 +135,12 @@ export const Spinner = styled.div<{$small?: boolean}>` --color: white; animation: ${spinnerAnimation} 1s ease infinite; transform: translateZ(0); - border-top: var(--spinner-border) solid var(--color); border-right: var(--spinner-border) solid var(--color); border-bottom: var(--spinner-border) solid var(--color); border-left: var(--spinner-border) solid transparent; background: transparent; height: var(--spinner-size); - aspect-ratio: 1 / 1; + aspect-ratio: 1/1; border-radius: 50%; ` diff --git a/apps/platform/src/sections/Game/Game.tsx b/apps/platform/src/sections/Game/Game.tsx index 34b57569..8a265825 100644 --- a/apps/platform/src/sections/Game/Game.tsx +++ b/apps/platform/src/sections/Game/Game.tsx @@ -1,6 +1,9 @@ -import { GambaUi, useSoundStore } from 'gamba-react-ui-v2' +// src/sections/Game/Game.tsx import React from 'react' import { useParams } from 'react-router-dom' +import { GambaUi, useSoundStore } from 'gamba-react-ui-v2' +import { useTransactionError } from 'gamba-react-v2' + import { Icon } from '../../components/Icon' import { Modal } from '../../components/Modal' import { GAMES } from '../../games' @@ -13,111 +16,82 @@ import { TransactionModal } from './TransactionModal' function CustomError() { return ( - <> - - -

😭 Oh no!

-

Something went wrong

-
-
- + + +

😭 Oh no!

+

Something went wrong

+
+
) } -/** - * A renderer component to display the contents of the loaded GambaUi.Game - * Screen - * Controls - */ function CustomRenderer() { const { game } = GambaUi.useGame() const [info, setInfo] = React.useState(false) const [provablyFair, setProvablyFair] = React.useState(false) const soundStore = useSoundStore() - const firstTimePlaying = useUserStore((state) => !state.gamesPlayed.includes(game.id)) - const markGameAsPlayed = useUserStore((state) => () => state.markGameAsPlayed(game.id, true)) + const firstTimePlaying = useUserStore(s => !s.gamesPlayed.includes(game.id)) + const markGameAsPlayed = useUserStore(s => () => s.markGameAsPlayed(game.id, true)) const [ready, setReady] = React.useState(false) const [txModal, setTxModal] = React.useState(false) - // const loading = useLoadingState() + const loading = useLoadingState() - React.useEffect( - () => { - const timeout = setTimeout(() => { - setReady(true) - }, 750) - return () => clearTimeout(timeout) - }, - [], - ) + React.useEffect(() => { + const t = setTimeout(() => setReady(true), 750) + return () => clearTimeout(t) + }, []) - React.useEffect( - () => { - const timeout = setTimeout(() => { - setInfo(firstTimePlaying) - }, 1000) - return () => clearTimeout(timeout) - }, - [firstTimePlaying], - ) + React.useEffect(() => { + const t = setTimeout(() => setInfo(firstTimePlaying), 1000) + return () => clearTimeout(t) + }, [firstTimePlaying]) const closeInfo = () => { markGameAsPlayed() setInfo(false) } + // global transaction errors + useTransactionError(err => { + if (err.message === 'NOT_CONNECTED') return + // you might want to show a toast here + }) + return ( <> {info && (

- +

{game.meta.description}

- - Play - + Play
)} - {provablyFair && ( - setProvablyFair(false)} /> - )} - {txModal && ( - setTxModal(false)} /> - )} + {provablyFair && setProvablyFair(false)} />} + {txModal && setTxModal(false)} />} + - - - + {ready && } + - {/*
- setTxModal(true)}> - {loading === -1 ? ( - - ) : ( - - )} - -
*/} - setInfo(true)}> - - - setProvablyFair(true)}> - - + setInfo(true)}> + setProvablyFair(true)}> soundStore.set(soundStore.volume ? 0 : .5)}> {soundStore.volume ? : }
+ + + {/* ← No inner wrapper—controls & play buttons are centered by Controls */} -
- - -
+ +
@@ -126,16 +100,12 @@ function CustomRenderer() { export default function Game() { const { gameId } = useParams() - const game = GAMES.find((x) => x.id === gameId) + const game = GAMES.find(g => g.id === gameId) return ( <> {game ? ( - } - children={} - /> + } children={} /> ) : (

Game not found! 👎

)} diff --git a/apps/platform/src/sections/Game/LoadingBar.tsx b/apps/platform/src/sections/Game/LoadingBar.tsx index dba1260b..cf0f9a98 100644 --- a/apps/platform/src/sections/Game/LoadingBar.tsx +++ b/apps/platform/src/sections/Game/LoadingBar.tsx @@ -1,95 +1,88 @@ import { decodeGame, getGameAddress } from 'gamba-core-v2' import { useAccount, useTransactionStore, useWalletAddress } from 'gamba-react-v2' -import React from 'react' +import React, { useMemo } from 'react' import styled, { css, keyframes } from 'styled-components' -const StyledLoadingThingy = styled.div` - position: relative; +const Container = styled.div` display: flex; width: 100%; gap: 5px; ` -export const loadingAnimation = keyframes` - 0%, 100% { opacity: .6 } - 50% { opacity: .8 } +const pulse = keyframes` + 0%, 100% { opacity: 0.6 } + 50% { opacity: 0.8 } ` -const StyledLoadingBar = styled.div<{$state: 'finished' | 'loading' | 'none'}>` - position: relative; - width: 100%; - border-radius: 10px; +const Bar = styled.div<{$state: 'none' | 'loading' | 'finished'}>` flex-grow: 1; - background: var(--gamba-ui-primary-color); - color: black; - padding: 0 10px; - font-size: 12px; height: 6px; - font-weight: bold; - opacity: .2; - ${(props) => props.$state === 'loading' && css` - animation: ${loadingAnimation} ease infinite 1s; - `} - ${(props) => props.$state === 'finished' && css` - opacity: .8; - `} - &:after { - content: " "; - position: absolute; - width: 25%; - height: 100%; - transition: opacity .5s; - } + border-radius: 10px; + background: var(--gamba-ui-primary-color); + opacity: 0.2; + + ${({ $state }) => + $state === 'loading' && + css` + animation: ${pulse} 1s ease infinite; + `} + + ${({ $state }) => + $state === 'finished' && + css` + opacity: 0.8; + `} ` -const steps = [ - 'Signing', - 'Sending', - 'Settling', -] - -export function useLoadingState() { - const userAddress = useWalletAddress() - const game = useAccount(getGameAddress(userAddress), decodeGame) - const txStore = useTransactionStore() - const step = ( - () => { - if (txStore.label !== 'play') { - return -1 - } - if (game?.status.resultRequested) { - return 2 - } - if (txStore.state === 'processing' || txStore.state === 'sending') { - return 1 - } - if (txStore.state === 'simulating' || txStore.state === 'signing') { - return 0 - } - return -1 - } - )() - - return step +const steps = ['Signing', 'Sending', 'Settling'] as const + +export function useLoadingState(): Array<'none' | 'loading' | 'finished'> { + const user = useWalletAddress() + const tx = useTransactionStore() + const game = useAccount(getGameAddress(user), decodeGame) + + const status = useMemo( + () => (game?.status ? Object.keys(game.status)[0] : null), + [game?.status] + ) + + const states: Array<'none' | 'loading' | 'finished'> = ['none', 'none', 'none'] + + if (tx.label !== 'play') return states + + if (tx.state === 'simulating' || tx.state === 'signing') { + states[0] = 'loading' + return states + } + + if (tx.state === 'processing' || tx.state === 'sending') { + states[0] = 'finished' + states[1] = 'loading' + return states + } + + if (tx.state === 'confirming' || status === 'ResultRequested') { + states[0] = 'finished' + states[1] = 'finished' + states[2] = 'loading' + return states + } + + if (status === 'Ready') { + return states + } + + return states } export function LoadingBar() { - const step = useLoadingState() + const states = useLoadingState() return ( -
-
- - {steps - .map((__, i) => ( - i ? 'finished' : 'none'} - /> - ), - )} - -
-
+ + {states.map((state, i) => ( + + ))} + ) } diff --git a/apps/platform/src/sections/RecentPlays/RecentPlays.tsx b/apps/platform/src/sections/RecentPlays/RecentPlays.tsx index 70d1a553..9503e7ed 100644 --- a/apps/platform/src/sections/RecentPlays/RecentPlays.tsx +++ b/apps/platform/src/sections/RecentPlays/RecentPlays.tsx @@ -1,30 +1,27 @@ +// apps/platform/src/sections/RecentPlays/RecentPlays.tsx +import React from 'react' import { BPS_PER_WHOLE, GambaTransaction } from 'gamba-core-v2' import { GambaUi, TokenValue, useTokenMeta } from 'gamba-react-ui-v2' -import React from 'react' -import { EXPLORER_URL, PLATFORM_CREATOR_ADDRESS } from '../../constants' import { useMediaQuery } from '../../hooks/useMediaQuery' import { extractMetadata } from '../../utils' +import { EXPLORER_URL, PLATFORM_CREATOR_ADDRESS } from '../../constants' import { Container, Jackpot, Profit, Recent, Skeleton } from './RecentPlays.styles' import { ShareModal } from './ShareModal' import { useRecentPlays } from './useRecentPlays' -function TimeDiff({ time, suffix = 'ago' }: {time: number, suffix?: string}) { - const diff = (Date.now() - time) +function TimeDiff({ time, suffix = 'ago' }: { time: number; suffix?: string }) { + const diff = Date.now() - time return React.useMemo(() => { - const seconds = Math.floor(diff / 1000) - const minutes = Math.floor(seconds / 60) - const hours = Math.floor(minutes / 60) - if (hours >= 1) { - return hours + 'h ' + suffix - } - if (minutes >= 1) { - return minutes + 'm ' + suffix - } + const sec = Math.floor(diff / 1000) + const min = Math.floor(sec / 60) + const hrs = Math.floor(min / 60) + if (hrs >= 1) return `${hrs}h ${suffix}` + if (min >= 1) return `${min}m ${suffix}` return 'Just now' - }, [diff]) + }, [diff, suffix]) } -function RecentPlay({ event }: {event: GambaTransaction<'GameSettled'>}) { +function RecentPlay({ event }: { event: GambaTransaction<'GameSettled'> }) { const data = event.data const token = useTokenMeta(data.tokenMint) const md = useMediaQuery('md') @@ -40,28 +37,18 @@ function RecentPlay({ event }: {event: GambaTransaction<'GameSettled'>}) { <>
- {data.user.toBase58().substring(0, 4)}... + {data.user.toBase58().slice(0, 4)}…
{md && (profit >= 0 ? ' won ' : ' lost ')} 0}> - {/* {(token.usdPrice * profit / (10 ** token.decimals)).toLocaleString()} USD */} - - {md && ( - <> - {profit > 0 && ( -
- ({multiplier.toFixed(2)}x) -
- )} - {data.jackpotPayoutToUser.toNumber() > 0 && ( - - + - - )} - + {md && profit > 0 &&
({multiplier.toFixed(2)}x)
} + {md && data.jackpotPayoutToUser.toNumber() > 0 && ( + + + + )} ) @@ -77,20 +64,21 @@ export default function RecentPlays() { {selectedGame && ( setSelectedGame(undefined)} /> )} - {!events.length && Array.from({ length: 10 }).map((_, i) => ( - + {!events.length && Array.from({ length: 10 }).map((_, i) => )} + {events.map((tx) => ( + setSelectedGame(tx)}> +
+ +
+ +
))} - {events.map( - (tx) => ( - setSelectedGame(tx)}> -
- -
- -
- ), - )} - window.open(`${EXPLORER_URL}/platform/${PLATFORM_CREATOR_ADDRESS.toString()}`)}> + + window.open(`${EXPLORER_URL}/platform/${PLATFORM_CREATOR_ADDRESS.toString()}`) + } + > 🚀 Explorer diff --git a/apps/platform/src/sections/RecentPlays/ShareModal.tsx b/apps/platform/src/sections/RecentPlays/ShareModal.tsx index 2554942f..440db3c8 100644 --- a/apps/platform/src/sections/RecentPlays/ShareModal.tsx +++ b/apps/platform/src/sections/RecentPlays/ShareModal.tsx @@ -76,4 +76,4 @@ export function ShareModal({ event, onClose }: {event: GambaTransaction<'GameSet ) -} +} \ No newline at end of file diff --git a/apps/platform/src/sections/RecentPlays/useRecentPlays.ts b/apps/platform/src/sections/RecentPlays/useRecentPlays.ts index a75ade1f..31b7ad17 100644 --- a/apps/platform/src/sections/RecentPlays/useRecentPlays.ts +++ b/apps/platform/src/sections/RecentPlays/useRecentPlays.ts @@ -1,5 +1,11 @@ +// apps/platform/src/sections/RecentPlays/useRecentPlays.ts + import { GambaTransaction } from 'gamba-core-v2' -import { useGambaEventListener, useGambaEvents, useWalletAddress } from 'gamba-react-v2' +import { + useWalletAddress, + useGambaEvents, + useGambaEventListener, +} from 'gamba-react-v2' import React from 'react' import { useLocation } from 'react-router-dom' import { PLATFORM_CREATOR_ADDRESS } from '../../constants' @@ -10,40 +16,71 @@ interface Params { export function useRecentPlays(params: Params = {}) { const { showAllPlatforms = false } = params - const location = useLocation() + const location = useLocation() const userAddress = useWalletAddress() - // Fetch previous events - const previousEvents = useGambaEvents( + // 1) Historical events via lightweight fetchRecentLogs under the hood + const previousEvents = useGambaEvents<'GameSettled'>( 'GameSettled', - { address: !showAllPlatforms ? PLATFORM_CREATOR_ADDRESS : undefined }, + { + address: !showAllPlatforms + ? PLATFORM_CREATOR_ADDRESS + : undefined, + signatureLimit: 30, + }, ) - const [newEvents, setEvents] = React.useState[]>([]) + // 2) State for live events + const [liveEvents, setLiveEvents] = React.useState< + GambaTransaction<'GameSettled'>[] + >([]) - // Listen for new events - useGambaEventListener( + // 3) Refs for up-to-date filter values + const showAllRef = React.useRef(showAllPlatforms) + const userRef = React.useRef(userAddress) + const pathRef = React.useRef(location.pathname) + React.useEffect(() => { + showAllRef.current = showAllPlatforms + }, [showAllPlatforms]) + React.useEffect(() => { + userRef.current = userAddress + }, [userAddress]) + React.useEffect(() => { + pathRef.current = location.pathname + }, [location.pathname]) + + // 4) Live subscription via the single‐argument callback signature + useGambaEventListener<'GameSettled'>( 'GameSettled', - (event) => { - // Ignore events that occured on another platform - if (!showAllPlatforms && !event.data.creator.equals(PLATFORM_CREATOR_ADDRESS)) return - // Set a delay on games with suspenseful reveal - const delay = event.data.user.equals(userAddress) && ['plinko', 'slots'].some((x) => location.pathname.includes(x)) ? 3000 : 1 - setTimeout( - () => { - setEvents((events) => [event, ...events]) - }, - delay, + (evt) => { + const { data, signature } = evt + + // Platform filter + if ( + !showAllRef.current && + !data.creator.equals(PLATFORM_CREATOR_ADDRESS) + ) { + return + } + + // Optional suspense delay for user’s own plays + const isUserGame = data.user.equals(userRef.current) + const inSuspense = ['plinko', 'slots'].some((p) => + pathRef.current.includes(p), ) + const delay = isUserGame && inSuspense ? 3000 : 1 + + setTimeout(() => { + setLiveEvents((all) => [evt, ...all]) + }, delay) }, - [location.pathname, userAddress, showAllPlatforms], + // re-subscribe whenever these change + [showAllPlatforms, userAddress, location.pathname], ) - // Merge previous & new events + // 5) Merge & return return React.useMemo( - () => { - return [...newEvents, ...previousEvents] - }, - [newEvents, previousEvents], + () => [...liveEvents, ...previousEvents], + [liveEvents, previousEvents], ) } diff --git a/apps/platform/src/sections/TokenSelect.tsx b/apps/platform/src/sections/TokenSelect.tsx index 14a3cc3d..46e3ad98 100644 --- a/apps/platform/src/sections/TokenSelect.tsx +++ b/apps/platform/src/sections/TokenSelect.tsx @@ -58,6 +58,8 @@ function TokenSelectItem({ mint }: {mint: PublicKey}) { export default function TokenSelect() { const [visible, setVisible] = React.useState(false) const [warning, setWarning] = React.useState(false) + // Allow real plays override via query param/localStorage for deployed testing + const [allowRealPlays, setAllowRealPlays] = React.useState(false) const context = React.useContext(GambaPlatformContext) const selectedToken = useCurrentToken() const userStore = useUserStore() @@ -70,13 +72,25 @@ export default function TokenSelect() { } }, []) + // Read real-play override – enables SOL selection on deployed builds when needed + useEffect(() => { + try { + const params = new URLSearchParams(window.location.search) + const q = params.get('allowReal') || params.get('real') || params.get('realplays') + if (q != null) { + const v = q === '1' || q === 'true' + localStorage.setItem('allowRealPlays', v ? '1' : '0') + } + const saved = localStorage.getItem('allowRealPlays') + setAllowRealPlays(saved === '1') + } catch {} + }, []) + const selectPool = (pool: PoolToken) => { setVisible(false) // Check if platform has real plays disabled - if ( - import.meta.env.VITE_REAL_PLAYS_DISABLED && - !pool.token.equals(FAKE_TOKEN_MINT) - ) { + const realDisabled = Boolean(import.meta.env.VITE_REAL_PLAYS_DISABLED) && !allowRealPlays + if (realDisabled && !pool.token.equals(FAKE_TOKEN_MINT)) { setWarning(true) return } @@ -120,7 +134,8 @@ export default function TokenSelect() { )} - {POOLS.map((pool, i) => ( + {/* Mount balances for list items only when dropdown is visible to avoid unnecessary watchers */} + {visible && POOLS.map((pool, i) => ( selectPool(pool)} key={i}> diff --git a/apps/platform/vite.config.ts b/apps/platform/vite.config.ts index 25da10bb..8dc8c87f 100644 --- a/apps/platform/vite.config.ts +++ b/apps/platform/vite.config.ts @@ -1,15 +1,25 @@ -import react from '@vitejs/plugin-react' +// vite.config.ts import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import path from 'path' -const ENV_PREFIX = ['VITE_'] - -export default defineConfig(() => ({ - envPrefix: ENV_PREFIX, +export default defineConfig({ server: { port: 4001, host: false }, + envPrefix: ['VITE_'], assetsInclude: ['**/*.glb'], define: { 'process.env.ANCHOR_BROWSER': true }, - resolve: { alias: { crypto: 'crypto-browserify' } }, + resolve: { + alias: { + // === POINT TO THE PACKAGE FOLDERS, NOT entry files === + react: path.resolve(__dirname, 'node_modules/react'), + 'react-dom': path.resolve(__dirname, 'node_modules/react-dom'), + + // your other alias stays the same + crypto: 'crypto-browserify', + }, + dedupe: ['react', 'react-dom'] + }, plugins: [ react({ jsxRuntime: 'classic' }), ], -})) +}) diff --git a/apps/website/next.config.js b/apps/website/next.config.js index 7a721617..28e3cdcd 100644 --- a/apps/website/next.config.js +++ b/apps/website/next.config.js @@ -1,8 +1,9 @@ -const withNextra = require('nextra')({ +const nextraPkg = require('nextra'); +const withNextra = (nextraPkg.default || nextraPkg)({ theme: 'nextra-theme-docs', themeConfig: './theme.config.tsx', defaultShowCopyCode: true, -}) +}); -module.exports = withNextra() +module.exports = withNextra(); diff --git a/apps/website/package.json b/apps/website/package.json index 8851d744..834b5e13 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -23,8 +23,8 @@ "bright": "^0.8.2", "clsx": "^2.1.1", "next": "^13.0.6", - "nextra": "latest", - "nextra-theme-docs": "latest", + "nextra": "^3.3.1", + "nextra-theme-docs": "^3.3.1", "react": "^18.3.1", "react-code-blocks": "0.0.9-0", "react-dom": "^18.3.1", @@ -35,6 +35,6 @@ }, "devDependencies": { "@types/node": "18.11.10", - "@types/react": "18.2.21" + "@types/react": "^18.2.13" } } diff --git a/apps/website/pages/_app.mdx b/apps/website/pages/_app.tsx similarity index 100% rename from apps/website/pages/_app.mdx rename to apps/website/pages/_app.tsx diff --git a/apps/website/pages/_meta.json b/apps/website/pages/_meta.json deleted file mode 100644 index a2cedcdc..00000000 --- a/apps/website/pages/_meta.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "games": { - "type": "page", - "title": "Built on Gamba", - "display": "hidden", - "theme": { - "layout": "raw" - } - }, - "index": { - "type": "page", - "title": "Gamba", - "display": "hidden", - "theme": { - "layout": "raw" - } - }, - "docs": { - "type": "page", - "title": "Documentation" - } -} diff --git a/apps/website/pages/_meta.ts b/apps/website/pages/_meta.ts new file mode 100644 index 00000000..e8938e7f --- /dev/null +++ b/apps/website/pages/_meta.ts @@ -0,0 +1,23 @@ +// apps/website/pages/_meta.ts +export default { + games: { + type: 'page', + title: 'Built on Gamba', + display: 'hidden', + theme: { + layout: 'raw' + } + }, + index: { + type: 'page', + title: 'Gamba', + display: 'hidden', + theme: { + layout: 'raw' + } + }, + docs: { + type: 'page', + title: 'Documentation' + } +} diff --git a/apps/website/pages/docs/_meta.json b/apps/website/pages/docs/_meta.json deleted file mode 100644 index 4577d420..00000000 --- a/apps/website/pages/docs/_meta.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "index": "👋 Welcome", - "-- Introduction": { - "type": "separator", - "title": "Introduction" - }, - "apps": "🎰 Apps", - "pools": "🏦 Pools", - "games": "🎲 Games", - "explorer": "🔎 Explorer", - "examples": "👨‍💻 Examples", - "dao": "🏛️ DAO (Soon)", - "-- Development": { - "type": "separator", - "title": "Development" - }, - "get-started": "🚀 Get Started", - "packages": "📦 Packages", - "-- SDK Docs": { - "type": "separator", - "title": "SDK Docs" - }, - "gamba-core-v2": "🔧 gamba-core-v2", - "gamba-react-v2": "🧩 gamba-react-v2", - "gamba-react-ui-v2": "🎨 gamba-react-ui-v2" -} diff --git a/apps/website/pages/docs/_meta.ts b/apps/website/pages/docs/_meta.ts new file mode 100644 index 00000000..b5012c8b --- /dev/null +++ b/apps/website/pages/docs/_meta.ts @@ -0,0 +1,26 @@ +export default { + index: '👋 Welcome', + '-- Introduction': { + type: 'separator', + title: 'Introduction' + }, + apps: '🎰 Apps', + pools: '🏦 Pools', + games: '🎲 Games', + multiplayer: '🤝 Multiplayer', + explorer: '🔎 Explorer', + examples: '👨‍💻 Examples', + '-- Development': { + type: 'separator', + title: 'Development' + }, + 'get-started': '🚀 Get Started', + packages: '📦 Packages', + '-- SDK Docs': { + type: 'separator', + title: 'SDK Docs' + }, + 'gamba-core-v2': '🔧 gamba-core-v2', + 'gamba-react-v2': '🧩 gamba-react-v2', + 'gamba-react-ui-v2': '🎨 gamba-react-ui-v2' +} \ No newline at end of file diff --git a/apps/website/pages/docs/apps.mdx b/apps/website/pages/docs/apps.mdx index 567fddaa..b9c506f2 100644 --- a/apps/website/pages/docs/apps.mdx +++ b/apps/website/pages/docs/apps.mdx @@ -1,4 +1,4 @@ -import { Card, Cards } from "nextra-theme-docs"; +import { Cards } from "nextra/components"; # 🎰 Apps @@ -15,5 +15,5 @@ Explore our list of [Apps](https://explorer.gamba.so/platforms) or discover how If you're interested in hosting your own App, visit our templates page to kickstart your journey. - + diff --git a/apps/website/pages/docs/examples.mdx b/apps/website/pages/docs/examples.mdx index f8ea34b3..234c65e2 100644 --- a/apps/website/pages/docs/examples.mdx +++ b/apps/website/pages/docs/examples.mdx @@ -1,4 +1,4 @@ -import { Card, Cards } from "nextra-theme-docs"; +import { Cards } from "nextra/components"; import { projects } from "../../components/projects"; # 👨‍💻 Examples @@ -6,71 +6,11 @@ import { projects } from "../../components/projects"; Projects built on Gamba - {projects.map(({ name, link, thumbnail }) => ( - -
{ - e.target.style.backgroundColor = "rgba(0, 0, 0, 0)"; - }} - onMouseLeave={(e) => { - e.target.style.backgroundColor = "rgba(0, 0, 0, 0.5)"; - }} - /> -
- + {projects.map(({ name, link }) => ( + ))} - - + + + diff --git a/apps/website/pages/docs/explorer.mdx b/apps/website/pages/docs/explorer.mdx index f876d88a..bdea78b0 100644 --- a/apps/website/pages/docs/explorer.mdx +++ b/apps/website/pages/docs/explorer.mdx @@ -1,5 +1,4 @@ -import { Card, Cards } from "nextra-theme-docs"; -import { Tabs } from "nextra/components"; +import { Tabs, Cards } from "nextra/components"; # 🔎 Explorer @@ -10,9 +9,9 @@ The [explorer](https://explorer.gamba.so) is a comprehensive dashboard and manag - **DAO**: Manage the DAO and distribution of fees - - - + + + ## Transactions @@ -45,7 +44,7 @@ Provides access to detailed logs of all transaction events for audit and analysi - + ## Pools @@ -57,8 +56,8 @@ The "Pools" section enables users to monitor and manage liquidity pools effectiv - **Metrics & Data**: View all pools and their stats/activity. - - + + ## Platforms @@ -67,5 +66,5 @@ The "Platforms" section enables users to view gamba transactions: - **Metrics & Data**: View detailed analytics for each gaming platform and their stats/activity. - + diff --git a/apps/website/pages/docs/gamba-core-v2.mdx b/apps/website/pages/docs/gamba-core-v2.mdx index 0e464c46..dbf5fbd9 100644 --- a/apps/website/pages/docs/gamba-core-v2.mdx +++ b/apps/website/pages/docs/gamba-core-v2.mdx @@ -1,5 +1,5 @@ -import { Callout } from "nextra-theme-docs"; +import { Callout } from "nextra/components"; # 🔧 `gamba-core-v2` diff --git a/apps/website/pages/docs/gamba-core-v2/_meta.json b/apps/website/pages/docs/gamba-core-v2/_meta.json deleted file mode 100644 index ef0bbfee..00000000 --- a/apps/website/pages/docs/gamba-core-v2/_meta.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "gamba-providers": { - "title": "🛠️ GambaProviders", - "href": "/docs/gamba-core-v2#gambaproviders" - }, - "transaction-utilities": { - "title": "🔄 Transaction Utilities", - "href": "/docs/gamba-core-v2#transactionutilities" - }, - "decoding-helpers": { - "title": "🧩 Decoding Helpers", - "href": "/docs/gamba-core-v2#decodinghelpers" - }, - "game-logic": { - "title": "🎮 Game Logic", - "href": "/docs/gamba-core-v2#gamelogic" - }, - "constants": { - "title": "🔑 Constants", - "href": "/docs/gamba-core-v2#constants" - }, - "seed-address-generators": { - "title": "🗺️ Seed Address Generators", - "href": "/docs/gamba-core-v2#seedaddressgenerators" - }, - "utilities": { - "title": "📊 Utilities", - "href": "/docs/gamba-core-v2#utilities" - }, - "types": { - "title": "📜 Types", - "href": "/docs/gamba-core-v2#types" - } - } - \ No newline at end of file diff --git a/apps/website/pages/docs/gamba-core-v2/_meta.ts b/apps/website/pages/docs/gamba-core-v2/_meta.ts new file mode 100644 index 00000000..59fe006d --- /dev/null +++ b/apps/website/pages/docs/gamba-core-v2/_meta.ts @@ -0,0 +1,34 @@ +export default { + 'gamba-providers': { + title: '🛠️ GambaProviders', + href: '/docs/gamba-core-v2#gambaproviders' + }, + 'transaction-utilities': { + title: '🔄 Transaction Utilities', + href: '/docs/gamba-core-v2#transactionutilities' + }, + 'decoding-helpers': { + title: '🧩 Decoding Helpers', + href: '/docs/gamba-core-v2#decodinghelpers' + }, + 'game-logic': { + title: '🎮 Game Logic', + href: '/docs/gamba-core-v2#gamelogic' + }, + constants: { + title: '🔑 Constants', + href: '/docs/gamba-core-v2#constants' + }, + 'seed-address-generators': { + title: '🗺️ Seed Address Generators', + href: '/docs/gamba-core-v2#seedaddressgenerators' + }, + utilities: { + title: '📊 Utilities', + href: '/docs/gamba-core-v2#utilities' + }, + types: { + title: '📜 Types', + href: '/docs/gamba-core-v2#types' + } +} diff --git a/apps/website/pages/docs/gamba-core-v2/constants/_meta.json b/apps/website/pages/docs/gamba-core-v2/constants/_meta.json deleted file mode 100644 index ee71054e..00000000 --- a/apps/website/pages/docs/gamba-core-v2/constants/_meta.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "program-id": { - "title": "PROGRAM_ID", - "href": "/docs/gamba-core-v2#programid" - }, - "system-program": { - "title": "SYSTEM_PROGRAM", - "href": "/docs/gamba-core-v2#systemprogram" - }, - "gamba-state-seed": { - "title": "GAMBA_STATE_SEED", - "href": "/docs/gamba-core-v2#gambastateseed" - }, - "game-seed": { - "title": "GAME_SEED", - "href": "/docs/gamba-core-v2#gameseed" - }, - "player-seed": { - "title": "PLAYER_SEED", - "href": "/docs/gamba-core-v2#playerseed" - }, - "pool-seed": { - "title": "POOL_SEED", - "href": "/docs/gamba-core-v2#poolseed" - }, - "pool-ata-seed": { - "title": "POOL_ATA_SEED", - "href": "/docs/gamba-core-v2#poolataseed" - }, - "pool-jackpot-seed": { - "title": "POOL_JACKPOT_SEED", - "href": "/docs/gamba-core-v2#pooljackpotseed" - }, - "pool-bonus-underlying-ta-seed": { - "title": "POOL_BONUS_UNDERLYING_TA_SEED", - "href": "/docs/gamba-core-v2#poolbonusunderlyingtaseed" - }, - "pool-bonus-mint-seed": { - "title": "POOL_BONUS_MINT_SEED", - "href": "/docs/gamba-core-v2#poolbonusmintseed" - }, - "pool-lp-mint-seed": { - "title": "POOL_LP_MINT_SEED", - "href": "/docs/gamba-core-v2#poollp-mintseed" - }, - "bps-per-whole": { - "title": "BPS_PER_WHOLE", - "href": "/docs/gamba-core-v2#bpsperwhole" - } - } - \ No newline at end of file diff --git a/apps/website/pages/docs/gamba-core-v2/constants/_meta.ts b/apps/website/pages/docs/gamba-core-v2/constants/_meta.ts new file mode 100644 index 00000000..a0401ee3 --- /dev/null +++ b/apps/website/pages/docs/gamba-core-v2/constants/_meta.ts @@ -0,0 +1,50 @@ +export default { + 'program-id': { + title: 'PROGRAM_ID', + href: '/docs/gamba-core-v2#programid' + }, + 'system-program': { + title: 'SYSTEM_PROGRAM', + href: '/docs/gamba-core-v2#systemprogram' + }, + 'gamba-state-seed': { + title: 'GAMBA_STATE_SEED', + href: '/docs/gamba-core-v2#gambastateseed' + }, + 'game-seed': { + title: 'GAME_SEED', + href: '/docs/gamba-core-v2#gameseed' + }, + 'player-seed': { + title: 'PLAYER_SEED', + href: '/docs/gamba-core-v2#playerseed' + }, + 'pool-seed': { + title: 'POOL_SEED', + href: '/docs/gamba-core-v2#poolseed' + }, + 'pool-ata-seed': { + title: 'POOL_ATA_SEED', + href: '/docs/gamba-core-v2#poolataseed' + }, + 'pool-jackpot-seed': { + title: 'POOL_JACKPOT_SEED', + href: '/docs/gamba-core-v2#pooljackpotseed' + }, + 'pool-bonus-underlying-ta-seed': { + title: 'POOL_BONUS_UNDERLYING_TA_SEED', + href: '/docs/gamba-core-v2#poolbonusunderlyingtaseed' + }, + 'pool-bonus-mint-seed': { + title: 'POOL_BONUS_MINT_SEED', + href: '/docs/gamba-core-v2#poolbonusmintseed' + }, + 'pool-lp-mint-seed': { + title: 'POOL_LP_MINT_SEED', + href: '/docs/gamba-core-v2#poollp-mintseed' + }, + 'bps-per-whole': { + title: 'BPS_PER_WHOLE', + href: '/docs/gamba-core-v2#bpsperwhole' + } +} \ No newline at end of file diff --git a/apps/website/pages/docs/gamba-core-v2/decoding-helpers/_meta.json b/apps/website/pages/docs/gamba-core-v2/decoding-helpers/_meta.json deleted file mode 100644 index 10760028..00000000 --- a/apps/website/pages/docs/gamba-core-v2/decoding-helpers/_meta.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "decode-ata": { - "title": "decodeAta", - "href": "/docs/gamba-core-v2#decodeata" - }, - "decode-player": { - "title": "decodePlayer", - "href": "/docs/gamba-core-v2#decodeplayer" - }, - "decode-game": { - "title": "decodeGame", - "href": "/docs/gamba-core-v2#decodegame" - }, - "decode-pool": { - "title": "decodePool", - "href": "/docs/gamba-core-v2#decodepool" - }, - "decode-gamba-state": { - "title": "decodeGambaState", - "href": "/docs/gamba-core-v2#decodegambastate" - } - } - \ No newline at end of file diff --git a/apps/website/pages/docs/gamba-core-v2/decoding-helpers/_meta.ts b/apps/website/pages/docs/gamba-core-v2/decoding-helpers/_meta.ts new file mode 100644 index 00000000..ad8a38fb --- /dev/null +++ b/apps/website/pages/docs/gamba-core-v2/decoding-helpers/_meta.ts @@ -0,0 +1,22 @@ +export default { + 'decode-ata': { + title: 'decodeAta', + href: '/docs/gamba-core-v2#decodeata' + }, + 'decode-player': { + title: 'decodePlayer', + href: '/docs/gamba-core-v2#decodeplayer' + }, + 'decode-game': { + title: 'decodeGame', + href: '/docs/gamba-core-v2#decodegame' + }, + 'decode-pool': { + title: 'decodePool', + href: '/docs/gamba-core-v2#decodepool' + }, + 'decode-gamba-state': { + title: 'decodeGambaState', + href: '/docs/gamba-core-v2#decodegambastate' + } +} \ No newline at end of file diff --git a/apps/website/pages/docs/gamba-core-v2/gamba-providers/_meta.json b/apps/website/pages/docs/gamba-core-v2/gamba-providers/_meta.json deleted file mode 100644 index 1f096857..00000000 --- a/apps/website/pages/docs/gamba-core-v2/gamba-providers/_meta.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "gamba-provider": { - "title": "GambaProvider", - "href": "/docs/gamba-core-v2#gambaprovider" - }, - "gamba-provider-wallet": { - "title": "GambaProviderWallet", - "href": "/docs/gamba-core-v2#gambaproviderwallet" - } - } - \ No newline at end of file diff --git a/apps/website/pages/docs/gamba-core-v2/gamba-providers/_meta.ts b/apps/website/pages/docs/gamba-core-v2/gamba-providers/_meta.ts new file mode 100644 index 00000000..a7bf421b --- /dev/null +++ b/apps/website/pages/docs/gamba-core-v2/gamba-providers/_meta.ts @@ -0,0 +1,10 @@ +export default { + 'gamba-provider': { + title: 'GambaProvider', + href: '/docs/gamba-core-v2#gambaprovider' + }, + 'gamba-provider-wallet': { + title: 'GambaProviderWallet', + href: '/docs/gamba-core-v2#gambaproviderwallet' + } +} diff --git a/apps/website/pages/docs/gamba-core-v2/game-logic/_meta.json b/apps/website/pages/docs/gamba-core-v2/game-logic/_meta.json deleted file mode 100644 index 1c94cb57..00000000 --- a/apps/website/pages/docs/gamba-core-v2/game-logic/_meta.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "get-game-hash": { - "title": "getGameHash", - "href": "/docs/gamba-core-v2#getgamehash" - }, - "get-result-number": { - "title": "getResultNumber", - "href": "/docs/gamba-core-v2#getresultnumber" - }, - "parse-result": { - "title": "parseResult", - "href": "/docs/gamba-core-v2#parseresult" - }, - "get-next-result": { - "title": "getNextResult", - "href": "/docs/gamba-core-v2#getnextresult" - }, - "hmac256": { - "title": "hmac256", - "href": "/docs/gamba-core-v2#hmac256" - } - } - \ No newline at end of file diff --git a/apps/website/pages/docs/gamba-core-v2/game-logic/_meta.ts b/apps/website/pages/docs/gamba-core-v2/game-logic/_meta.ts new file mode 100644 index 00000000..6a01ba05 --- /dev/null +++ b/apps/website/pages/docs/gamba-core-v2/game-logic/_meta.ts @@ -0,0 +1,22 @@ +export default { + 'get-game-hash': { + title: 'getGameHash', + href: '/docs/gamba-core-v2#getgamehash' + }, + 'get-result-number': { + title: 'getResultNumber', + href: '/docs/gamba-core-v2#getresultnumber' + }, + 'parse-result': { + title: 'parseResult', + href: '/docs/gamba-core-v2#parseresult' + }, + 'get-next-result': { + title: 'getNextResult', + href: '/docs/gamba-core-v2#getnextresult' + }, + 'hmac256': { + title: 'hmac256', + href: '/docs/gamba-core-v2#hmac256' + } +} diff --git a/apps/website/pages/docs/gamba-core-v2/seed-address-generators/_meta.json b/apps/website/pages/docs/gamba-core-v2/seed-address-generators/_meta.json deleted file mode 100644 index ae6f1af9..00000000 --- a/apps/website/pages/docs/gamba-core-v2/seed-address-generators/_meta.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "get-pda-address": { - "title": "getPdaAddress", - "href": "/docs/gamba-core-v2#getpdaaddress" - }, - "get-pool-address": { - "title": "getPoolAddress", - "href": "/docs/gamba-core-v2#getpooladdress" - }, - "get-gamba-state-address": { - "title": "getGambaStateAddress", - "href": "/docs/gamba-core-v2#getgambastateaddress" - }, - "get-player-address": { - "title": "getPlayerAddress", - "href": "/docs/gamba-core-v2#getplayeraddress" - }, - "get-game-address": { - "title": "getGameAddress", - "href": "/docs/gamba-core-v2#getgameaddress" - }, - "get-pool-lp-address": { - "title": "getPoolLpAddress", - "href": "/docs/gamba-core-v2#getpoollpaddress" - }, - "get-pool-bonus-address": { - "title": "getPoolBonusAddress", - "href": "/docs/gamba-core-v2#getpoolbonusaddress" - }, - "get-pool-underlying-token-account-address": { - "title": "getPoolUnderlyingTokenAccountAddress", - "href": "/docs/gamba-core-v2#getpoolunderlyingtokenaccountaddress" - }, - "get-pool-jackpot-token-account-address": { - "title": "getPoolJackpotTokenAccountAddress", - "href": "/docs/gamba-core-v2#getpooljackpottokenaccountaddress" - }, - "get-pool-bonus-underlying-token-account-address": { - "title": "getPoolBonusUnderlyingTokenAccountAddress", - "href": "/docs/gamba-core-v2#getpoolbonusunderlyingtokenaccountaddress" - }, - "get-user-underlying-ata": { - "title": "getUserUnderlyingAta", - "href": "/docs/gamba-core-v2#getuserunderlyingata" - }, - "get-player-underlying-ata": { - "title": "getPlayerUnderlyingAta", - "href": "/docs/gamba-core-v2#getplayerunderlyingata" - }, - "get-user-bonus-ata-for-pool": { - "title": "getUserBonusAtaForPool", - "href": "/docs/gamba-core-v2#getuserbonusataforpool" - }, - "get-user-lp-ata-for-pool": { - "title": "getUserLpAtaForPool", - "href": "/docs/gamba-core-v2#getuserlpataforpool" - }, - "get-player-bonus-ata-for-pool": { - "title": "getPlayerBonusAtaForPool", - "href": "/docs/gamba-core-v2#getplayerbonusataforpool" - }, - "get-user-wsol-account": { - "title": "getUserWsolAccount", - "href": "/docs/gamba-core-v2#getuserwsolaccount" - } - } \ No newline at end of file diff --git a/apps/website/pages/docs/gamba-core-v2/seed-address-generators/_meta.ts b/apps/website/pages/docs/gamba-core-v2/seed-address-generators/_meta.ts new file mode 100644 index 00000000..32e6f5db --- /dev/null +++ b/apps/website/pages/docs/gamba-core-v2/seed-address-generators/_meta.ts @@ -0,0 +1,67 @@ +// apps/website/pages/docs/gamba-core-v2/seed-address-generators/_meta.ts +export default { + 'get-pda-address': { + title: 'getPdaAddress', + href: '/docs/gamba-core-v2#getpdaaddress' + }, + 'get-pool-address': { + title: 'getPoolAddress', + href: '/docs/gamba-core-v2#getpooladdress' + }, + 'get-gamba-state-address': { + title: 'getGambaStateAddress', + href: '/docs/gamba-core-v2#getgambastateaddress' + }, + 'get-player-address': { + title: 'getPlayerAddress', + href: '/docs/gamba-core-v2#getplayeraddress' + }, + 'get-game-address': { + title: 'getGameAddress', + href: '/docs/gamba-core-v2#getgameaddress' + }, + 'get-pool-lp-address': { + title: 'getPoolLpAddress', + href: '/docs/gamba-core-v2#getpoollpaddress' + }, + 'get-pool-bonus-address': { + title: 'getPoolBonusAddress', + href: '/docs/gamba-core-v2#getpoolbonusaddress' + }, + 'get-pool-underlying-token-account-address': { + title: 'getPoolUnderlyingTokenAccountAddress', + href: '/docs/gamba-core-v2#getpoolunderlyingtokenaccountaddress' + }, + 'get-pool-jackpot-token-account-address': { + title: 'getPoolJackpotTokenAccountAddress', + href: '/docs/gamba-core-v2#getpooljackpottokenaccountaddress' + }, + 'get-pool-bonus-underlying-token-account-address': { + title: 'getPoolBonusUnderlyingTokenAccountAddress', + href: '/docs/gamba-core-v2#getpoolbonusunderlyingtokenaccountaddress' + }, + 'get-user-underlying-ata': { + title: 'getUserUnderlyingAta', + href: '/docs/gamba-core-v2#getuserunderlyingata' + }, + 'get-player-underlying-ata': { + title: 'getPlayerUnderlyingAta', + href: '/docs/gamba-core-v2#getplayerunderlyingata' + }, + 'get-user-bonus-ata-for-pool': { + title: 'getUserBonusAtaForPool', + href: '/docs/gamba-core-v2#getuserbonusataforpool' + }, + 'get-user-lp-ata-for-pool': { + title: 'getUserLpAtaForPool', + href: '/docs/gamba-core-v2#getuserlpataforpool' + }, + 'get-player-bonus-ata-for-pool': { + title: 'getPlayerBonusAtaForPool', + href: '/docs/gamba-core-v2#getplayerbonusataforpool' + }, + 'get-user-wsol-account': { + title: 'getUserWsolAccount', + href: '/docs/gamba-core-v2#getuserwsolaccount' + } +} diff --git a/apps/website/pages/docs/gamba-core-v2/transaction-utilities/_meta.json b/apps/website/pages/docs/gamba-core-v2/transaction-utilities/_meta.json deleted file mode 100644 index 66c7788b..00000000 --- a/apps/website/pages/docs/gamba-core-v2/transaction-utilities/_meta.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "parse-transaction-events": { - "title": "parseTransactionEvents", - "href": "/docs/gamba-core-v2#parsetransactionevents" - }, - "parse-gamba-transaction": { - "title": "parseGambaTransaction", - "href": "/docs/gamba-core-v2#parsegambatransaction" - }, - "fetch-gamba-transactions-from-signatures": { - "title": "fetchGambaTransactionsFromSignatures", - "href": "/docs/gamba-core-v2#fetchgambatransactionsfromsignatures" - }, - "fetch-gamba-transactions": { - "title": "fetchGambaTransactions", - "href": "/docs/gamba-core-v2#fetchgambatransactions" - } - } - \ No newline at end of file diff --git a/apps/website/pages/docs/gamba-core-v2/transaction-utilities/_meta.ts b/apps/website/pages/docs/gamba-core-v2/transaction-utilities/_meta.ts new file mode 100644 index 00000000..936a750b --- /dev/null +++ b/apps/website/pages/docs/gamba-core-v2/transaction-utilities/_meta.ts @@ -0,0 +1,18 @@ +export default { + 'parse-transaction-events': { + title: 'parseTransactionEvents', + href: '/docs/gamba-core-v2#parsetransactionevents' + }, + 'parse-gamba-transaction': { + title: 'parseGambaTransaction', + href: '/docs/gamba-core-v2#parsegambatransaction' + }, + 'fetch-gamba-transactions-from-signatures': { + title: 'fetchGambaTransactionsFromSignatures', + href: '/docs/gamba-core-v2#fetchgambatransactionsfromsignatures' + }, + 'fetch-gamba-transactions': { + title: 'fetchGambaTransactions', + href: '/docs/gamba-core-v2#fetchgambatransactions' + } +} \ No newline at end of file diff --git a/apps/website/pages/docs/gamba-core-v2/types/_meta.json b/apps/website/pages/docs/gamba-core-v2/types/_meta.json deleted file mode 100644 index eeff9514..00000000 --- a/apps/website/pages/docs/gamba-core-v2/types/_meta.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "gamba": { - "title": "Gamba", - "href": "/docs/gamba-core-v2#gamba" - }, - "gamba-event-type": { - "title": "GambaEventType", - "href": "/docs/gamba-core-v2#gambaeventtype" - }, - "gamba-event": { - "title": "GambaEvent", - "href": "/docs/gamba-core-v2#gambaevent" - }, - "any-gamba-event": { - "title": "AnyGambaEvent", - "href": "/docs/gamba-core-v2#anygambaevent" - }, - "gamba-transaction": { - "title": "GambaTransaction", - "href": "/docs/gamba-core-v2#gambatransaction" - }, - "gamba-state": { - "title": "GambaState", - "href": "/docs/gamba-core-v2#gambastate" - }, - "game-state": { - "title": "GameState", - "href": "/docs/gamba-core-v2#gamestate" - }, - "player-state": { - "title": "PlayerState", - "href": "/docs/gamba-core-v2#playerstate" - }, - "pool-state": { - "title": "PoolState", - "href": "/docs/gamba-core-v2#poolstate" - }, - "game-result": { - "title": "GameResult", - "href": "/docs/gamba-core-v2#gameresult" - } - } - \ No newline at end of file diff --git a/apps/website/pages/docs/gamba-core-v2/types/_meta.ts b/apps/website/pages/docs/gamba-core-v2/types/_meta.ts new file mode 100644 index 00000000..19abd82c --- /dev/null +++ b/apps/website/pages/docs/gamba-core-v2/types/_meta.ts @@ -0,0 +1,43 @@ +// apps/website/pages/docs/gamba-core-v2/_sidebar.ts +export default { + gamba: { + title: 'Gamba', + href: '/docs/gamba-core-v2#gamba' + }, + 'gamba-event-type': { + title: 'GambaEventType', + href: '/docs/gamba-core-v2#gambaeventtype' + }, + 'gamba-event': { + title: 'GambaEvent', + href: '/docs/gamba-core-v2#gambaevent' + }, + 'any-gamba-event': { + title: 'AnyGambaEvent', + href: '/docs/gamba-core-v2#anygambaevent' + }, + 'gamba-transaction': { + title: 'GambaTransaction', + href: '/docs/gamba-core-v2#gambatransaction' + }, + 'gamba-state': { + title: 'GambaState', + href: '/docs/gamba-core-v2#gambastate' + }, + 'game-state': { + title: 'GameState', + href: '/docs/gamba-core-v2#gamestate' + }, + 'player-state': { + title: 'PlayerState', + href: '/docs/gamba-core-v2#playerstate' + }, + 'pool-state': { + title: 'PoolState', + href: '/docs/gamba-core-v2#poolstate' + }, + 'game-result': { + title: 'GameResult', + href: '/docs/gamba-core-v2#gameresult' + } +} diff --git a/apps/website/pages/docs/gamba-core-v2/utilities/_meta.json b/apps/website/pages/docs/gamba-core-v2/utilities/_meta.json deleted file mode 100644 index ecb217a8..00000000 --- a/apps/website/pages/docs/gamba-core-v2/utilities/_meta.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "basis-points": { - "title": "basisPoints", - "href": "/docs/gamba-core-v2#basispoints" - }, - "wrap-sol": { - "title": "wrapSol", - "href": "/docs/gamba-core-v2#wrapsol" - }, - "unwrap-sol": { - "title": "unwrapSol", - "href": "/docs/gamba-core-v2#unwrapsol" - }, - "is-native-mint": { - "title": "isNativeMint", - "href": "/docs/gamba-core-v2#isnativemint" - } - } - \ No newline at end of file diff --git a/apps/website/pages/docs/gamba-core-v2/utilities/_meta.ts b/apps/website/pages/docs/gamba-core-v2/utilities/_meta.ts new file mode 100644 index 00000000..45085d4e --- /dev/null +++ b/apps/website/pages/docs/gamba-core-v2/utilities/_meta.ts @@ -0,0 +1,19 @@ +// apps/website/pages/docs/gamba-core-v2/utilities/_meta.ts +export default { + 'basis-points': { + title: 'basisPoints', + href: '/docs/gamba-core-v2#basispoints' + }, + 'wrap-sol': { + title: 'wrapSol', + href: '/docs/gamba-core-v2#wrapsol' + }, + 'unwrap-sol': { + title: 'unwrapSol', + href: '/docs/gamba-core-v2#unwrapsol' + }, + 'is-native-mint': { + title: 'isNativeMint', + href: '/docs/gamba-core-v2#isnativemint' + } +} diff --git a/apps/website/pages/docs/gamba-react-ui-v2.mdx b/apps/website/pages/docs/gamba-react-ui-v2.mdx index e705ea68..178a8540 100644 --- a/apps/website/pages/docs/gamba-react-ui-v2.mdx +++ b/apps/website/pages/docs/gamba-react-ui-v2.mdx @@ -1,4 +1,4 @@ -import { Callout } from "nextra-theme-docs"; +import { Callout } from "nextra/components"; # 🎨 `gamba-react-ui-v2` diff --git a/apps/website/pages/docs/gamba-react-ui-v2/_meta.json b/apps/website/pages/docs/gamba-react-ui-v2/_meta.json deleted file mode 100644 index 8a34e031..00000000 --- a/apps/website/pages/docs/gamba-react-ui-v2/_meta.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "components": { - "title": "🖥️ Components", - "href": "/docs/gamba-react-ui-v2#components" - }, - "utilities-and-hooks": { - "title": "🛠️ Utilities and Hooks", - "href": "/docs/gamba-react-ui-v2#utilities-and-hooks" - }, - "contexts": { - "title": "🔄 Contexts", - "href": "/docs/gamba-react-ui-v2#contexts" - }, - "hooks": { - "title": "🏷️ Token Metadata", - "href": "/docs/gamba-react-ui-v2#token-metadata" - } -} diff --git a/apps/website/pages/docs/gamba-react-ui-v2/_meta.ts b/apps/website/pages/docs/gamba-react-ui-v2/_meta.ts new file mode 100644 index 00000000..1ac9e26f --- /dev/null +++ b/apps/website/pages/docs/gamba-react-ui-v2/_meta.ts @@ -0,0 +1,18 @@ +export default { + components: { + title: '🖥️ Components', + href: '/docs/gamba-react-ui-v2#components' + }, + 'utilities-and-hooks': { + title: '🛠️ Utilities and Hooks', + href: '/docs/gamba-react-ui-v2#utilities-and-hooks' + }, + contexts: { + title: '🔄 Contexts', + href: '/docs/gamba-react-ui-v2#contexts' + }, + hooks: { + title: '🏷️ Token Metadata', + href: '/docs/gamba-react-ui-v2#token-metadata' + } +} diff --git a/apps/website/pages/docs/gamba-react-ui-v2/components/_meta.json b/apps/website/pages/docs/gamba-react-ui-v2/components/_meta.json deleted file mode 100644 index bbc3032a..00000000 --- a/apps/website/pages/docs/gamba-react-ui-v2/components/_meta.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "gambauibutton": { - "title": "GambaUi.Button", - "href": "/docs/gamba-react-ui-v2#gambauibutton" - }, - "gambauiplaybutton": { - "title": "GambaUi.PlayButton", - "href": "/docs/gamba-react-ui-v2#gambauiplaybutton" - }, - "gambauiwagerinput": { - "title": "GambaUi.WagerInput", - "href": "/docs/gamba-react-ui-v2#gambauiwagerinput" - }, - "gambauiwagerselect": { - "title": "GambaUi.WagerSelect", - "href": "/docs/gamba-react-ui-v2#gambauiwagerselect" - }, - "gambauiswitch": { - "title": "GambaUi.Switch", - "href": "/docs/gamba-react-ui-v2#gambauiswitch" - }, - "gambauiselect": { - "title": "GambaUi.Select", - "href": "/docs/gamba-react-ui-v2#gambauiselect" - }, - "gambauitextinput": { - "title": "GambaUi.TextInput", - "href": "/docs/gamba-react-ui-v2#gambauitextinput" - }, - "gambauieffecttest": { - "title": "GambaUi.EffectTest", - "href": "/docs/gamba-react-ui-v2#gambauieffecttest" - }, - "gambauiresponsivesize": { - "title": "GambaUi.ResponsiveSize", - "href": "/docs/gamba-react-ui-v2#gambauiresponsivesize" - }, - "gambauiportal": { - "title": "GambaUi.Portal", - "href": "/docs/gamba-react-ui-v2#gambauiportal" - }, - "gambauiportaltarget": { - "title": "GambaUi.PortalTarget", - "href": "/docs/gamba-react-ui-v2#gambauiportaltarget" - }, - "gambauigame": { - "title": "GambaUi.Game", - "href": "/docs/gamba-react-ui-v2#gambauigame" - }, - "gambauicanvas": { - "title": "GambaUi.Canvas", - "href": "/docs/gamba-react-ui-v2#gambauicanvas" - }, - "gambauitokenvalue": { - "title": "GambaUi.TokenValue", - "href": "/docs/gamba-react-ui-v2#gambauitokenvalue" - } - } \ No newline at end of file diff --git a/apps/website/pages/docs/gamba-react-ui-v2/components/_meta.ts b/apps/website/pages/docs/gamba-react-ui-v2/components/_meta.ts new file mode 100644 index 00000000..8eb74af2 --- /dev/null +++ b/apps/website/pages/docs/gamba-react-ui-v2/components/_meta.ts @@ -0,0 +1,58 @@ +export default { + gambauibutton: { + title: 'GambaUi.Button', + href: '/docs/gamba-react-ui-v2#gambauibutton' + }, + gambauiplaybutton: { + title: 'GambaUi.PlayButton', + href: '/docs/gamba-react-ui-v2#gambauiplaybutton' + }, + gambauiwagerinput: { + title: 'GambaUi.WagerInput', + href: '/docs/gamba-react-ui-v2#gambauiwagerinput' + }, + gambauiwagerselect: { + title: 'GambaUi.WagerSelect', + href: '/docs/gamba-react-ui-v2#gambauiwagerselect' + }, + gambauiswitch: { + title: 'GambaUi.Switch', + href: '/docs/gamba-react-ui-v2#gambauiswitch' + }, + gambauiselect: { + title: 'GambaUi.Select', + href: '/docs/gamba-react-ui-v2#gambauiselect' + }, + gambauitextinput: { + title: 'GambaUi.TextInput', + href: '/docs/gamba-react-ui-v2#gambauitextinput' + }, + gambauieffecttest: { + title: 'GambaUi.EffectTest', + href: '/docs/gamba-react-ui-v2#gambauieffecttest' + }, + gambauiresponsivesize: { + title: 'GambaUi.ResponsiveSize', + href: '/docs/gamba-react-ui-v2#gambauiresponsivesize' + }, + gambauiportal: { + title: 'GambaUi.Portal', + href: '/docs/gamba-react-ui-v2#gambauiportal' + }, + gambauiportaltarget: { + title: 'GambaUi.PortalTarget', + href: '/docs/gamba-react-ui-v2#gambauiportaltarget' + }, + gambauigame: { + title: 'GambaUi.Game', + href: '/docs/gamba-react-ui-v2#gambauigame' + }, + gambauicanvas: { + title: 'GambaUi.Canvas', + href: '/docs/gamba-react-ui-v2#gambauicanvas' + }, + gambauitokenvalue: { + title: 'GambaUi.TokenValue', + href: '/docs/gamba-react-ui-v2#gambauitokenvalue' + } +} \ No newline at end of file diff --git a/apps/website/pages/docs/gamba-react-ui-v2/contexts/_meta.json b/apps/website/pages/docs/gamba-react-ui-v2/contexts/_meta.json deleted file mode 100644 index 5094d898..00000000 --- a/apps/website/pages/docs/gamba-react-ui-v2/contexts/_meta.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "gambaplatformcontext": { - "title": "GambaPlatformContext", - "href": "/docs/gamba-react-ui-v2#gambaplatformcontext" - }, - "gamecontext": { - "title": "GameContext", - "href": "/docs/gamba-react-ui-v2#gamecontext" - } - } - \ No newline at end of file diff --git a/apps/website/pages/docs/gamba-react-ui-v2/contexts/_meta.ts b/apps/website/pages/docs/gamba-react-ui-v2/contexts/_meta.ts new file mode 100644 index 00000000..f6f0b6ac --- /dev/null +++ b/apps/website/pages/docs/gamba-react-ui-v2/contexts/_meta.ts @@ -0,0 +1,10 @@ +export default { + gambaplatformcontext: { + title: 'GambaPlatformContext', + href: '/docs/gamba-react-ui-v2#gambaplatformcontext' + }, + gamecontext: { + title: 'GameContext', + href: '/docs/gamba-react-ui-v2#gamecontext' + } +} \ No newline at end of file diff --git a/apps/website/pages/docs/gamba-react-ui-v2/hooks/_meta.json b/apps/website/pages/docs/gamba-react-ui-v2/hooks/_meta.json deleted file mode 100644 index bf041066..00000000 --- a/apps/website/pages/docs/gamba-react-ui-v2/hooks/_meta.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "tokenmeta": { - "title": "TokenMeta", - "href": "/docs/gamba-react-ui-v2#tokenmeta" - }, - "makeheliustokenfetcher": { - "title": "makeHeliusTokenFetcher", - "href": "/docs/gamba-react-ui-v2#makeheliustokenfetcher" - } - } - \ No newline at end of file diff --git a/apps/website/pages/docs/gamba-react-ui-v2/hooks/_meta.ts b/apps/website/pages/docs/gamba-react-ui-v2/hooks/_meta.ts new file mode 100644 index 00000000..e9fc9c4b --- /dev/null +++ b/apps/website/pages/docs/gamba-react-ui-v2/hooks/_meta.ts @@ -0,0 +1,10 @@ +export default { + tokenmeta: { + title: 'TokenMeta', + href: '/docs/gamba-react-ui-v2#tokenmeta' + }, + makeheliustokenfetcher: { + title: 'makeHeliusTokenFetcher', + href: '/docs/gamba-react-ui-v2#makeheliustokenfetcher' + } +} \ No newline at end of file diff --git a/apps/website/pages/docs/gamba-react-ui-v2/utilities-and-hooks/_meta.json b/apps/website/pages/docs/gamba-react-ui-v2/utilities-and-hooks/_meta.json deleted file mode 100644 index d158c323..00000000 --- a/apps/website/pages/docs/gamba-react-ui-v2/utilities-and-hooks/_meta.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "usegame": { - "title": "useGame", - "href": "/docs/gamba-react-ui-v2#usegame" - }, - "usegambaplatformcontext": { - "title": "useGambaPlatformContext", - "href": "/docs/gamba-react-ui-v2#usegambaplatformcontext" - }, - "usesound": { - "title": "useSound", - "href": "/docs/gamba-react-ui-v2#usesound" - }, - "usecurrenttoken": { - "title": "useCurrentToken", - "href": "/docs/gamba-react-ui-v2#usecurrenttoken" - }, - "usetokenbalance": { - "title": "useTokenBalance", - "href": "/docs/gamba-react-ui-v2#usetokenbalance" - }, - "useuserbalance": { - "title": "useUserBalance", - "href": "/docs/gamba-react-ui-v2#useuserbalance" - }, - "usewagerinput": { - "title": "useWagerInput", - "href": "/docs/gamba-react-ui-v2#usewagerinput" - }, - "usenextfakeresult": { - "title": "useNextFakeResult", - "href": "/docs/gamba-react-ui-v2#usenextfakeresult" - }, - "usefakeaccountstore": { - "title": "useFakeAccountStore", - "href": "/docs/gamba-react-ui-v2#usefakeaccountstore" - }, - "usegambaaudiostore": { - "title": "useGambaAudioStore", - "href": "/docs/gamba-react-ui-v2#usegambaaudiostore" - }, - "usetokenlist": { - "title": "useTokenList", - "href": "/docs/gamba-react-ui-v2#usetokenlist" - }, - "gambastandardtokens": { - "title": "GambaStandardTokens", - "href": "/docs/gamba-react-ui-v2#gambastandardtokens" - }, - "usecurrentpool": { - "title": "useCurrentPool", - "href": "/docs/gamba-react-ui-v2#usecurrentpool" - } - } - \ No newline at end of file diff --git a/apps/website/pages/docs/gamba-react-ui-v2/utilities-and-hooks/_meta.ts b/apps/website/pages/docs/gamba-react-ui-v2/utilities-and-hooks/_meta.ts new file mode 100644 index 00000000..375aedbe --- /dev/null +++ b/apps/website/pages/docs/gamba-react-ui-v2/utilities-and-hooks/_meta.ts @@ -0,0 +1,54 @@ +export default { + usegame: { + title: 'useGame', + href: '/docs/gamba-react-ui-v2#usegame' + }, + usegambaplatformcontext: { + title: 'useGambaPlatformContext', + href: '/docs/gamba-react-ui-v2#usegambaplatformcontext' + }, + usesound: { + title: 'useSound', + href: '/docs/gamba-react-ui-v2#usesound' + }, + usecurrenttoken: { + title: 'useCurrentToken', + href: '/docs/gamba-react-ui-v2#usecurrenttoken' + }, + usetokenbalance: { + title: 'useTokenBalance', + href: '/docs/gamba-react-ui-v2#usetokenbalance' + }, + useuserbalance: { + title: 'useUserBalance', + href: '/docs/gamba-react-ui-v2#useuserbalance' + }, + usewagerinput: { + title: 'useWagerInput', + href: '/docs/gamba-react-ui-v2#usewagerinput' + }, + usenextfakeresult: { + title: 'useNextFakeResult', + href: '/docs/gamba-react-ui-v2#usenextfakeresult' + }, + usefakeaccountstore: { + title: 'useFakeAccountStore', + href: '/docs/gamba-react-ui-v2#usefakeaccountstore' + }, + usegambaaudiostore: { + title: 'useGambaAudioStore', + href: '/docs/gamba-react-ui-v2#usegambaaudiostore' + }, + usetokenlist: { + title: 'useTokenList', + href: '/docs/gamba-react-ui-v2#usetokenlist' + }, + gambastandardtokens: { + title: 'GambaStandardTokens', + href: '/docs/gamba-react-ui-v2#gambastandardtokens' + }, + usecurrentpool: { + title: 'useCurrentPool', + href: '/docs/gamba-react-ui-v2#usecurrentpool' + } +} diff --git a/apps/website/pages/docs/gamba-react-v2.mdx b/apps/website/pages/docs/gamba-react-v2.mdx index 926051af..35374797 100644 --- a/apps/website/pages/docs/gamba-react-v2.mdx +++ b/apps/website/pages/docs/gamba-react-v2.mdx @@ -1,4 +1,4 @@ -import { Callout } from "nextra-theme-docs"; +import { Callout } from "nextra/components"; # 🧩 `gamba-react-v2` diff --git a/apps/website/pages/docs/gamba-react-v2/_meta.json b/apps/website/pages/docs/gamba-react-v2/_meta.json deleted file mode 100644 index 9d0bd1be..00000000 --- a/apps/website/pages/docs/gamba-react-v2/_meta.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "contexts": { - "title": "🔄 Contexts", - "href": "/docs/gamba-react-v2#contexts" - }, - "types": { - "title": "📜 Types", - "href": "/docs/gamba-react-v2#types" - }, - "methodsandhooks": { - "title": "🔍 Methods and Hooks", - "href": "/docs/gamba-react-v2#methodsandhooks" - }, - "utilitiesandhooks": { - "title": "⚙️ Utilities and Functions", - "href": "/docs/gamba-react-v2#utilitiesandhooks" - } - } - \ No newline at end of file diff --git a/apps/website/pages/docs/gamba-react-v2/_meta.ts b/apps/website/pages/docs/gamba-react-v2/_meta.ts new file mode 100644 index 00000000..122ebd71 --- /dev/null +++ b/apps/website/pages/docs/gamba-react-v2/_meta.ts @@ -0,0 +1,19 @@ +// apps/website/pages/docs/gamba-react-v2/_sidebar.ts +export default { + contexts: { + title: '🔄 Contexts', + href: '/docs/gamba-react-v2#contexts' + }, + types: { + title: '📜 Types', + href: '/docs/gamba-react-v2#types' + }, + methodsandhooks: { + title: '🔍 Methods and Hooks', + href: '/docs/gamba-react-v2#methodsandhooks' + }, + utilitiesandhooks: { + title: '⚙️ Utilities and Functions', + href: '/docs/gamba-react-v2#utilitiesandhooks' + } +} diff --git a/apps/website/pages/docs/gamba-react-v2/contexts/_meta.json b/apps/website/pages/docs/gamba-react-v2/contexts/_meta.json deleted file mode 100644 index 92b98493..00000000 --- a/apps/website/pages/docs/gamba-react-v2/contexts/_meta.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "sendtransactioncontext": { - "title": "SendTransactionContext", - "href": "/docs/gamba-react-v2#sendtransactioncontext" - }, - "gambacontext": { - "title": "GambaContext", - "href": "/docs/gamba-react-v2#gambacontext" - }, - "sendtransactionprovider": { - "title": "SendTransactionProvider", - "href": "/docs/gamba-react-v2#sendtransactionprovider" - }, - "gambaprovider": { - "title": "GambaProvider", - "href": "/docs/gamba-react-v2#gambaprovider" - } - } - \ No newline at end of file diff --git a/apps/website/pages/docs/gamba-react-v2/contexts/_meta.ts b/apps/website/pages/docs/gamba-react-v2/contexts/_meta.ts new file mode 100644 index 00000000..073ab8dc --- /dev/null +++ b/apps/website/pages/docs/gamba-react-v2/contexts/_meta.ts @@ -0,0 +1,19 @@ + +export default { + sendtransactioncontext: { + title: 'SendTransactionContext', + href: '/docs/gamba-react-v2#sendtransactioncontext' + }, + gambacontext: { + title: 'GambaContext', + href: '/docs/gamba-react-v2#gambacontext' + }, + sendtransactionprovider: { + title: 'SendTransactionProvider', + href: '/docs/gamba-react-v2#sendtransactionprovider' + }, + gambaprovider: { + title: 'GambaProvider', + href: '/docs/gamba-react-v2#gambaprovider' + } +} diff --git a/apps/website/pages/docs/gamba-react-v2/methodsandhooks/_meta.json b/apps/website/pages/docs/gamba-react-v2/methodsandhooks/_meta.json deleted file mode 100644 index 8ba8de58..00000000 --- a/apps/website/pages/docs/gamba-react-v2/methodsandhooks/_meta.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "useaccount": { - "title": "useAccount", - "href": "/docs/gamba-react-v2#useaccount" - }, - "usewalletaddress": { - "title": "useWalletAddress", - "href": "/docs/gamba-react-v2#usewalletaddress" - }, - "usebalance": { - "title": "useBalance", - "href": "/docs/gamba-react-v2#usebalance" - }, - "usetransactionerror": { - "title": "useTransactionError", - "href": "/docs/gamba-react-v2#usetransactionerror" - }, - "usesendtransaction": { - "title": "useSendTransaction", - "href": "/docs/gamba-react-v2#usesendtransaction" - }, - "usegambaplay": { - "title": "useGambaPlay", - "href": "/docs/gamba-react-v2#usegambaplay" - }, - "usenextresult": { - "title": "useNextResult", - "href": "/docs/gamba-react-v2#usenextresult" - }, - "usegamba": { - "title": "useGamba", - "href": "/docs/gamba-react-v2#usegamba" - }, - "usegambaeventlistener": { - "title": "useGambaEventListener", - "href": "/docs/gamba-react-v2#usegambaeventlistener" - }, - "usegambaevents": { - "title": "useGambaEvents", - "href": "/docs/gamba-react-v2#usegambaevents" - }, - "usepool": { - "title": "usePool", - "href": "/docs/gamba-react-v2#usepool" - }, - "usetransactionstore": { - "title": "useTransactionStore", - "href": "/docs/gamba-react-v2#usetransactionstore" - }, - "usegambaprovider": { - "title": "useGambaProvider", - "href": "/docs/gamba-react-v2#usegambaprovider" - }, - "usegambaprogram": { - "title": "useGambaProgram", - "href": "/docs/gamba-react-v2#usegambaprogram" - }, - "usegambacomponent": { - "title": "useGambaComponent", - "href": "/docs/gamba-react-v2#usegambacomponent" - } - } \ No newline at end of file diff --git a/apps/website/pages/docs/gamba-react-v2/methodsandhooks/_meta.ts b/apps/website/pages/docs/gamba-react-v2/methodsandhooks/_meta.ts new file mode 100644 index 00000000..d8720145 --- /dev/null +++ b/apps/website/pages/docs/gamba-react-v2/methodsandhooks/_meta.ts @@ -0,0 +1,63 @@ + +export default { + useaccount: { + title: 'useAccount', + href: '/docs/gamba-react-v2#useaccount' + }, + usewalletaddress: { + title: 'useWalletAddress', + href: '/docs/gamba-react-v2#usewalletaddress' + }, + usebalance: { + title: 'useBalance', + href: '/docs/gamba-react-v2#usebalance' + }, + usetransactionerror: { + title: 'useTransactionError', + href: '/docs/gamba-react-v2#usetransactionerror' + }, + usesendtransaction: { + title: 'useSendTransaction', + href: '/docs/gamba-react-v2#usesendtransaction' + }, + usegambaplay: { + title: 'useGambaPlay', + href: '/docs/gamba-react-v2#usegambaplay' + }, + usenextresult: { + title: 'useNextResult', + href: '/docs/gamba-react-v2#usenextresult' + }, + usegamba: { + title: 'useGamba', + href: '/docs/gamba-react-v2#usegamba' + }, + usegambaeventlistener: { + title: 'useGambaEventListener', + href: '/docs/gamba-react-v2#usegambaeventlistener' + }, + usegambaevents: { + title: 'useGambaEvents', + href: '/docs/gamba-react-v2#usegambaevents' + }, + usepool: { + title: 'usePool', + href: '/docs/gamba-react-v2#usepool' + }, + usetransactionstore: { + title: 'useTransactionStore', + href: '/docs/gamba-react-v2#usetransactionstore' + }, + usegambaprovider: { + title: 'useGambaProvider', + href: '/docs/gamba-react-v2#usegambaprovider' + }, + usegambaprogram: { + title: 'useGambaProgram', + href: '/docs/gamba-react-v2#usegambaprogram' + }, + usegambacomponent: { + title: 'useGambaComponent', + href: '/docs/gamba-react-v2#usegambacomponent' + } +} diff --git a/apps/website/pages/docs/gamba-react-v2/types/_meta.json b/apps/website/pages/docs/gamba-react-v2/types/_meta.json deleted file mode 100644 index a533a9ff..00000000 --- a/apps/website/pages/docs/gamba-react-v2/types/_meta.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "gambaeventtype": { - "title": "GambaEventType", - "href": "/docs/gamba-react-v2#gambaeventtype" - }, - "gambatransaction": { - "title": "GambaTransaction", - "href": "/docs/gamba-react-v2#gambatransaction" - }, - "gameresult": { - "title": "GameResult", - "href": "/docs/gamba-react-v2#gameresult" - }, - "gambaplayinput": { - "title": "GambaPlayInput", - "href": "/docs/gamba-react-v2#gambaplayinput" - }, - "sendtransactionoptions": { - "title": "SendTransactionOptions", - "href": "/docs/gamba-react-v2#sendtransactionoptions" - }, - "sendtransactionprops": { - "title": "SendTransactionProps", - "href": "/docs/gamba-react-v2#sendtransactionprops" - }, - "gambaproviderprops": { - "title": "GambaProviderProps", - "href": "/docs/gamba-react-v2#gambaproviderprops" - }, - "transactionstore": { - "title": "TransactionStore", - "href": "/docs/gamba-react-v2#transactionstore" - }, - "usegambaeventsparams": { - "title": "UseGambaEventsParams", - "href": "/docs/gamba-react-v2#usegambaeventsparams" - }, - "uipoolstate": { - "title": "UiPoolState", - "href": "/docs/gamba-react-v2#uipoolstate" - }, - "gambaplugininput": { - "title": "GambaPluginInput", - "href": "/docs/gamba-react-v2#gambaplugininput" - } - } \ No newline at end of file diff --git a/apps/website/pages/docs/gamba-react-v2/types/_meta.ts b/apps/website/pages/docs/gamba-react-v2/types/_meta.ts new file mode 100644 index 00000000..59a787a7 --- /dev/null +++ b/apps/website/pages/docs/gamba-react-v2/types/_meta.ts @@ -0,0 +1,47 @@ +// apps/website/pages/docs/gamba-react-v2/types/_meta.ts +export default { + gambaeventtype: { + title: 'GambaEventType', + href: '/docs/gamba-react-v2#gambaeventtype' + }, + gambatransaction: { + title: 'GambaTransaction', + href: '/docs/gamba-react-v2#gambatransaction' + }, + gameresult: { + title: 'GameResult', + href: '/docs/gamba-react-v2#gameresult' + }, + gambaplayinput: { + title: 'GambaPlayInput', + href: '/docs/gamba-react-v2#gambaplayinput' + }, + sendtransactionoptions: { + title: 'SendTransactionOptions', + href: '/docs/gamba-react-v2#sendtransactionoptions' + }, + sendtransactionprops: { + title: 'SendTransactionProps', + href: '/docs/gamba-react-v2#sendtransactionprops' + }, + gambaproviderprops: { + title: 'GambaProviderProps', + href: '/docs/gamba-react-v2#gambaproviderprops' + }, + transactionstore: { + title: 'TransactionStore', + href: '/docs/gamba-react-v2#transactionstore' + }, + usegambaeventsparams: { + title: 'UseGambaEventsParams', + href: '/docs/gamba-react-v2#usegambaeventsparams' + }, + uipoolstate: { + title: 'UiPoolState', + href: '/docs/gamba-react-v2#uipoolstate' + }, + gambaplugininput: { + title: 'GambaPluginInput', + href: '/docs/gamba-react-v2#gambaplugininput' + } +} diff --git a/apps/website/pages/docs/gamba-react-v2/utilitiesandhooks/_meta.json b/apps/website/pages/docs/gamba-react-v2/utilitiesandhooks/_meta.json deleted file mode 100644 index 867f83d9..00000000 --- a/apps/website/pages/docs/gamba-react-v2/utilitiesandhooks/_meta.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "throwtransactionerror": { - "title": "throwTransactionError", - "href": "/docs/gamba-react-v2#throwtransactionerror" - }, - "createcustomfeeplugin": { - "title": "createCustomFeePlugin", - "href": "/docs/gamba-react-v2#createcustomfeeplugin" - } - } \ No newline at end of file diff --git a/apps/website/pages/docs/gamba-react-v2/utilitiesandhooks/_meta.ts b/apps/website/pages/docs/gamba-react-v2/utilitiesandhooks/_meta.ts new file mode 100644 index 00000000..3e0ebdea --- /dev/null +++ b/apps/website/pages/docs/gamba-react-v2/utilitiesandhooks/_meta.ts @@ -0,0 +1,11 @@ + +export default { + throwtransactionerror: { + title: 'throwTransactionError', + href: '/docs/gamba-react-v2#throwtransactionerror' + }, + createcustomfeeplugin: { + title: 'createCustomFeePlugin', + href: '/docs/gamba-react-v2#createcustomfeeplugin' + } +} diff --git a/apps/website/pages/docs/games.mdx b/apps/website/pages/docs/games.mdx index 8e3a507a..79b59f33 100644 --- a/apps/website/pages/docs/games.mdx +++ b/apps/website/pages/docs/games.mdx @@ -1,5 +1,5 @@ import Simulator from "../../components/docs/games-simulator"; -import { Callout } from "nextra-theme-docs"; +import { Callout } from "nextra/components"; import { Steps } from "nextra/components"; import { Tabs } from "nextra/components"; diff --git a/apps/website/pages/docs/get-started.mdx b/apps/website/pages/docs/get-started.mdx index d4ec448a..d2175116 100644 --- a/apps/website/pages/docs/get-started.mdx +++ b/apps/website/pages/docs/get-started.mdx @@ -1,63 +1,18 @@ -import { Callout, Card, Cards } from "nextra-theme-docs"; -import { Steps } from "nextra/components"; +import { Cards } from "nextra/components"; # 🚀 Get Started Get started with Gamba by using a template for quick setup - - <> - ![templates card](/demo.png) -
- VITE -
- -
- - <> - ![templates card](/demo2.png) -
- NEXT.ᴊs -
- -
-
+ + +
Start scratch - - - + + + \ No newline at end of file diff --git a/apps/website/pages/docs/get-started/_meta.json b/apps/website/pages/docs/get-started/_meta.json deleted file mode 100644 index b93cb90d..00000000 --- a/apps/website/pages/docs/get-started/_meta.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "vite": { - "title": "⚡ Vite", - "href": "/docs/get-started/vite" - }, - "nextjs": { - "title": "⚛️ NEXT.ᴊs", - "href": "/docs/get-started/nextjs" - }, - "manual": { - "title": "📖 Manual", - "href": "/docs/get-started/manual" - } -} diff --git a/apps/website/pages/docs/get-started/_meta.ts b/apps/website/pages/docs/get-started/_meta.ts new file mode 100644 index 00000000..c5876326 --- /dev/null +++ b/apps/website/pages/docs/get-started/_meta.ts @@ -0,0 +1,15 @@ +// apps/website/pages/docs/get-started/_sidebar.ts +export default { + vite: { + title: '⚡ Vite', + href: '/docs/get-started/vite' + }, + nextjs: { + title: '⚛️ NEXT.ᴊs', + href: '/docs/get-started/nextjs' + }, + manual: { + title: '📖 Manual', + href: '/docs/get-started/manual' + } +} diff --git a/apps/website/pages/docs/get-started/manual.mdx b/apps/website/pages/docs/get-started/manual.mdx index 1eab14e2..1e7bc8dc 100644 --- a/apps/website/pages/docs/get-started/manual.mdx +++ b/apps/website/pages/docs/get-started/manual.mdx @@ -1,4 +1,4 @@ -import { Callout, Card, Cards } from "nextra-theme-docs"; +import { Callout } from "nextra/components"; import { Steps } from "nextra/components"; # 📖 Manual diff --git a/apps/website/pages/docs/get-started/vite.mdx b/apps/website/pages/docs/get-started/vite.mdx index dd5f39db..d8635e93 100644 --- a/apps/website/pages/docs/get-started/vite.mdx +++ b/apps/website/pages/docs/get-started/vite.mdx @@ -1,5 +1,5 @@ import { Steps } from "nextra/components"; -import { Callout } from "nextra-theme-docs"; +import { Callout } from "nextra/components"; # ⚡ Vite diff --git a/apps/website/pages/docs/index.mdx b/apps/website/pages/docs/index.mdx index 248303f8..e1bd6549 100644 --- a/apps/website/pages/docs/index.mdx +++ b/apps/website/pages/docs/index.mdx @@ -1,15 +1,11 @@ -import { Callout, Card, Cards } from 'nextra-theme-docs'; - # 👋 Welcome to Gamba Gamba is a decentralized gambleFi protocol for on-chain degeneracy on Solana. ## Quickstart -Skip to the good stuff +Skip to the good stuff: + +- [Get Started](/docs/get-started) - - - <>![templates card](/demo.png) - - +templates card diff --git a/apps/website/pages/docs/multiplayer.mdx b/apps/website/pages/docs/multiplayer.mdx new file mode 100644 index 00000000..8487e7bb --- /dev/null +++ b/apps/website/pages/docs/multiplayer.mdx @@ -0,0 +1,132 @@ +# 🤝 Multiplayer + +Build multiplayer games on Gamba using the Multiplayer SDK. This page covers how to create a game and how players join, with the exact parameters you need. + +## Install + +```bash +pnpm add @gamba-labs/multiplayer-sdk @coral-xyz/anchor @solana/web3.js @solana/spl-token +``` + +## Create Game + +Function: `createGameIx(provider, params)` + +- Automatically chooses native SOL vs SPL flow based on `params.accounts.mint` (`WRAPPED_SOL_MINT` = native) +- Returns a `TransactionInstruction` + +### Params + +```ts +type CreateGameParams = { + preAllocPlayers: number + maxPlayers: number + numTeams: number + winnersTarget: number + wagerType: number + payoutType: number + wager: BN | number + softDuration: BN | number + hardDuration: BN | number + gameSeed: BN | number + minBet: BN | number + maxBet: BN | number + accounts: { + gameMaker: web3.PublicKey + mint: web3.PublicKey + } +} +``` + +Wager types: `sameWager`, `customWager`, `betRange` (as numeric enum in the IDL). If using `betRange`, UI should clamp inputs between `minBet` and `maxBet`. + +Payout types: `same`, `exponentialDecay` (as numeric enum in the IDL). + +### Example + +```ts +import { AnchorProvider, BN, web3 } from '@coral-xyz/anchor' +import { createGameIx, WRAPPED_SOL_MINT } from '@gamba-labs/multiplayer-sdk' + +const provider = AnchorProvider.env() +const walletPubkey = provider.wallet.publicKey + +const ix = await createGameIx(provider, { + preAllocPlayers: 100, + maxPlayers: 1000, + numTeams: 1, + winnersTarget: 1, + wagerType: 0, // e.g. sameWager + payoutType: 0, // e.g. same + wager: new BN(1_000_000), // 0.001 in token base units + softDuration: new BN(60), // seconds + hardDuration: new BN(300), // seconds + gameSeed: new BN(Date.now()), + minBet: new BN(1_000_000), + maxBet: new BN(1_000_000), + accounts: { + gameMaker: walletPubkey, + mint: WRAPPED_SOL_MINT, // or any SPL mint + }, +}) + +const tx = new web3.Transaction().add(ix) +await provider.sendAndConfirm(tx) +``` + +## Join Game + +Function: `joinGameIx(provider, params)` + +- Handles native SOL or SPL automatically +- Returns a `TransactionInstruction` + +### Params + +```ts +type JoinGameParams = { + creatorFeeBps: number + wager: BN | number + team?: number + playerMeta?: Buffer | Uint8Array + accounts: { + gameAccount: web3.PublicKey + mint: web3.PublicKey + playerAccount: web3.PublicKey + creatorAddress: web3.PublicKey + } +} +``` + +### Example + +```ts +import { AnchorProvider, BN, web3 } from '@coral-xyz/anchor' +import { joinGameIx, WRAPPED_SOL_MINT } from '@gamba-labs/multiplayer-sdk' + +const provider = AnchorProvider.env() +const walletPubkey = provider.wallet.publicKey + +const ix = await joinGameIx(provider, { + creatorFeeBps: 200, // 2% + wager: new BN(1_000_000), + team: 0, // optional + // playerMeta: new Uint8Array(32), // optional 32 bytes + accounts: { + gameAccount: new web3.PublicKey(''), + mint: WRAPPED_SOL_MINT, // or the SPL mint used by the game + playerAccount: walletPubkey, + creatorAddress: walletPubkey, // referral / creator + }, +}) + +const tx = new web3.Transaction().add(ix) +await provider.sendAndConfirm(tx) +``` + +## Notes + +- For SPL mints, associated token accounts are derived lazily by the SDK and created if missing. +- `gameSeed` should be unique per game; a timestamp or counter works well. +- For `betRange`, ensure your UI clamps any wager changes within `[minBet, maxBet]`. + diff --git a/apps/website/pages/docs/pools.mdx b/apps/website/pages/docs/pools.mdx index 3a971a27..48fb15b9 100644 --- a/apps/website/pages/docs/pools.mdx +++ b/apps/website/pages/docs/pools.mdx @@ -1,20 +1,20 @@ -import { Card, Cards } from "nextra-theme-docs"; +import { Cards } from "nextra/components"; # 🏦 Pools Gamba liquidity pools act as the foundation for bets placed by players through various [frontend apps](/docs/apps), resembling the house in a traditional casino. However, unlike a traditional setup, Gamba offers the advantage of inclusivity, allowing anyone to participate and share in the profits. - + + + ## Creating a Pool Creating a Gamba pool for any custom Solana token is a straightforward process, accessible via the explorer UI or by depositing into an existing pool. Moreover, pools can be effortlessly managed and interacted with through the Gamba SDK. - - - + ## LP Token diff --git a/apps/website/pages/docs/templates.mdx b/apps/website/pages/docs/templates.mdx index 59aba8b4..c8f2b647 100644 --- a/apps/website/pages/docs/templates.mdx +++ b/apps/website/pages/docs/templates.mdx @@ -1,11 +1,11 @@ -import { Callout, Card, Cards } from "nextra-theme-docs"; +import { Callout, Cards } from "nextra/components"; # 📄 Templates Gamba starter templates - VITE
-
- + NEXT.ᴊs
- + diff --git a/apps/website/theme.config.tsx b/apps/website/theme.config.tsx index fcf3a5a1..67cea8d5 100644 --- a/apps/website/theme.config.tsx +++ b/apps/website/theme.config.tsx @@ -1,30 +1,47 @@ -import { DocsThemeConfig, useConfig } from 'nextra-theme-docs' +// apps/website/theme.config.tsx +import type { DocsThemeConfig } from 'nextra-theme-docs' +import { useConfig } from 'nextra-theme-docs' import { useRouter } from 'next/router' const config: DocsThemeConfig = { - logo: (<>Gamba Logo), + logo: ( + <> + Gamba Logo + + ), + project: { link: 'https://github.com/gamba-labs' }, chat: { link: 'https://discord.gg/xjBsW3e8fK' }, - docsRepositoryBase: 'https://github.com/gamba-labs/gamba/tree/docs/apps/website', + + docsRepositoryBase: + 'https://github.com/gamba-labs/gamba/tree/docs/apps/website', + nextThemes: { defaultTheme: 'dark', forcedTheme: 'dark', }, + themeSwitch: { component: () => null }, - footer: { text: 'Gamba ©©©©©©©©' }, + + footer: { + content: `Gamba © ${new Date().getFullYear()}`, + }, + head: () => { - const { asPath, pathname } = useRouter() + const { asPath } = useRouter() const { frontMatter } = useConfig() + const ogConfig = { title: 'Gamba', - description: 'Build your own web3 games with Gamba, a decentralized betting platform on Solana', + description: + 'Build your own web3 games with Gamba, a decentralized betting platform on Solana', author: { twitter: 'gambalabs' }, favicon: '/gamba.svg', } - const favicon = String(ogConfig.favicon) - const title = String(frontMatter.title || ogConfig.title) - const description = String(frontMatter.description || ogConfig.description) + + const title = frontMatter.title ?? ogConfig.title + const description = frontMatter.description ?? ogConfig.description const canonical = new URL(asPath, 'https://gamba.so').toString() const image = 'https://www.gamba.so/og.png' @@ -35,13 +52,16 @@ const config: DocsThemeConfig = { + - + + - - + + + @@ -57,19 +77,17 @@ const config: DocsThemeConfig = { ) }, - sidebar: { toggleButton: true, defaultMenuCollapseLevel: 1 }, - useNextSeoProps() { - const { asPath } = useRouter() - - if (['/', '/docs'].includes(asPath)) { - return { titleTemplate: 'Gamba' } - } - return { titleTemplate: '%s | Gamba' } + sidebar: { + toggleButton: true, + defaultMenuCollapseLevel: 1, }, - primaryHue: { - light: 270, - dark: 204, + + color: { + hue: { + light: 270, + dark: 204, + }, }, } diff --git a/apps/website/tsconfig.json b/apps/website/tsconfig.json index 1563f3e8..620349f3 100644 --- a/apps/website/tsconfig.json +++ b/apps/website/tsconfig.json @@ -15,6 +15,6 @@ "isolatedModules": true, "jsx": "preserve" }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "pages/index.mdx", "pages/games.mdx"], "exclude": ["node_modules"] } diff --git a/package.json b/package.json index 8eeb2dd0..3d79ccd4 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,8 @@ { "private": true, + "engines": { + "node": ">=22 <23" + }, "scripts": { "build": "turbo run build", "dev": "turbo run dev --filter=!gamba-api --no-cache --continue", @@ -17,7 +20,11 @@ "packageManager": "pnpm@10.12.4", "pnpm": { "overrides": { + "react": "18.3.1", + "react-dom": "18.3.1", + "@solana/web3.js": "1.98.2", + "@solana/spl-token": "0.4.13", "@solana/wallet-adapter-react": "^0.15.39" } } -} +} \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json index 3313a0f7..9dc69d75 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,38 +1,37 @@ { "name": "gamba-core-v2", - "private": false, "version": "0.4.1", + "private": false, "main": "dist/index.js", "module": "dist/index.mjs", "types": "dist/index.d.ts", "sideEffects": false, - "files": [ - "dist/**" - ], - "publishConfig": { - "access": "public" - }, + "files": ["dist/**"], "scripts": { - "dev": "tsup src/index.ts --watch --format cjs,esm --dts", + "dev": "tsup src/index.ts --watch --format cjs,esm --dts", "build": "tsup src/index.ts --format cjs,esm --dts", - "lint": "tsc", - "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist" + "lint": "tsc", + "clean": "rm -rf .turbo node_modules dist" }, "dependencies": { - "@coral-xyz/anchor": "^0.27.0", - "@solana/spl-token": "^0.3.8", - "@solana/web3.js": "^1.93.0" + "@coral-xyz/anchor": "^0.31.1", + "@solana/spl-token": "^0.4.13", + "@solana/web3.js": "^1.98.2", + "buffer": "^6.0.3" + }, + "peerDependencies": { + "@coral-xyz/anchor": "^0.31.1", + "@solana/spl-token": "^0.4.13", + "@solana/web3.js": "^1.98.2" }, "devDependencies": { - "eslint": "^8.48.0", - "tsup": "^7.2.0", - "typescript": "^5.2.2" + "@types/node": "^24.0.10", + "eslint": "^9.30.1", + "tsup": "^8.5.0", + "typescript": "^5.2.2" }, - "peerDependencies": { - "@coral-xyz/anchor": "^0.27.0", - "@solana/web3.js": "^1.93.0" + "publishConfig": { + "access": "public" }, - "keywords": [], - "author": "", "license": "MIT" } diff --git a/packages/core/src/GambaProvider.ts b/packages/core/src/GambaProvider.ts index 9003f180..ba04a65a 100644 --- a/packages/core/src/GambaProvider.ts +++ b/packages/core/src/GambaProvider.ts @@ -1,15 +1,16 @@ import * as anchor from '@coral-xyz/anchor' +import { Buffer } from 'buffer' import NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet' import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync } from '@solana/spl-token' import { AddressLookupTableProgram, ConfirmOptions, Connection, Keypair, PublicKey, SYSVAR_RENT_PUBKEY, SystemProgram } from '@solana/web3.js' import { PROGRAM_ID } from './constants' -import { Gamba as GambaIdl, IDL } from './idl' -import { getGambaStateAddress, getGameAddress, getPlayerAddress, getPoolAddress, getPoolBonusAddress, getPoolLpAddress, getPoolUnderlyingTokenAccountAddress } from './pdas' +import { Gamba, IDL } from './idl' +import { getGambaStateAddress, getGameAddress, getPlayerAddress, getPoolAddress, getPoolBonusAddress, getPoolLpAddress, getPoolUnderlyingTokenAccountAddress, getPoolBonusUnderlyingTokenAccountAddress, getPoolJackpotTokenAccountAddress } from './pdas' import { GambaProviderWallet } from './types' import { basisPoints } from './utils' export class GambaProvider { - gambaProgram: anchor.Program + gambaProgram: anchor.Program anchorProvider: anchor.AnchorProvider wallet: GambaProviderWallet @@ -25,7 +26,7 @@ export class GambaProvider { wallet, opts, ) - this.gambaProgram = new anchor.Program(IDL, PROGRAM_ID, this.anchorProvider) + this.gambaProgram = new anchor.Program(IDL, this.anchorProvider) this.wallet = wallet } @@ -51,192 +52,242 @@ export class GambaProvider { * @param slot The slot to use for the lookup table instruction * @returns Multiple TransactionInstruction in an array */ - createPool(underlyingTokenMint: PublicKey, authority: PublicKey, slot: number) { - const gambaStateAta = getAssociatedTokenAddressSync( - underlyingTokenMint, - getGambaStateAddress(), - true, + createPool( + underlyingTokenMint: PublicKey, + authority: PublicKey, + slot: number, + ) { + // … compute all your PDAs exactly as before … + const pool = getPoolAddress(underlyingTokenMint, authority) + const poolUnderlyingTA = getPoolUnderlyingTokenAccountAddress(pool) + const [poolBonusUnderlyingTA] = PublicKey.findProgramAddressSync( + [Buffer.from('POOL_BONUS_UNDERLYING_TA'), pool.toBuffer()], + PROGRAM_ID, + ) + const gamba_state = getGambaStateAddress() + const gambaStateAta = getAssociatedTokenAddressSync(underlyingTokenMint, gamba_state, true) + const poolJackpotTA = PublicKey.findProgramAddressSync( + [Buffer.from('POOL_JACKPOT'), pool.toBuffer()], + PROGRAM_ID, + )[0] + const lpMint = getPoolLpAddress(pool) + const bonusMint = getPoolBonusAddress(pool) + const TOKEN_METADATA = new PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s') + const METADATA_SEED = 'metadata' + const [lpMintMetadata] = PublicKey.findProgramAddressSync( + [Buffer.from(METADATA_SEED), TOKEN_METADATA.toBuffer(), lpMint.toBuffer()], + TOKEN_METADATA, + ) + const [bonusMintMetadata] = PublicKey.findProgramAddressSync( + [Buffer.from(METADATA_SEED), TOKEN_METADATA.toBuffer(), bonusMint.toBuffer()], + TOKEN_METADATA, ) - const METADATA_SEED = 'metadata' - const TOKEN_METADATA_PROGRAM_ID = new PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s') - const pool = getPoolAddress(underlyingTokenMint, authority) - const lpMint = getPoolLpAddress(pool) - const bonusMint = getPoolBonusAddress(pool) - const poolUnderlyingTokenAccount = getPoolUnderlyingTokenAccountAddress(pool) - const [poolBonusUnderlyingTokenAccount] = PublicKey.findProgramAddressSync([Buffer.from('POOL_BONUS_UNDERLYING_TA'), pool.toBuffer()], PROGRAM_ID) - const [lpMintMetadata] = PublicKey.findProgramAddressSync([Buffer.from(METADATA_SEED), TOKEN_METADATA_PROGRAM_ID.toBuffer(), lpMint.toBuffer()], TOKEN_METADATA_PROGRAM_ID) - const [bonusMintMetadata] = PublicKey.findProgramAddressSync([Buffer.from(METADATA_SEED), TOKEN_METADATA_PROGRAM_ID.toBuffer(), bonusMint.toBuffer()], TOKEN_METADATA_PROGRAM_ID) - - //more addresses for lookup table - const gamba_state = getGambaStateAddress() - const poolJackpotTokenAccount = PublicKey.findProgramAddressSync([Buffer.from('POOL_JACKPOT'), pool.toBuffer()], PROGRAM_ID)[0] - const [lookupTableInst, lookupTableAddress] = AddressLookupTableProgram.createLookupTable({ - authority: this.wallet.publicKey, - payer: this.wallet.publicKey, + const [lutCreateIx, lutAddress] = AddressLookupTableProgram.createLookupTable({ + authority: this.user, + payer: this.user, recentSlot: slot - 1, }) - const addAddressesInstruction = AddressLookupTableProgram.extendLookupTable({ - payer: this.wallet.publicKey, - authority: this.wallet.publicKey, - lookupTable: lookupTableAddress, + const lutExtendIx = AddressLookupTableProgram.extendLookupTable({ + payer: this.user, + authority: this.user, + lookupTable: lutAddress, addresses: [ - pool, - underlyingTokenMint, - poolUnderlyingTokenAccount, - poolBonusUnderlyingTokenAccount, - gamba_state, - gambaStateAta, - bonusMint, - poolJackpotTokenAccount, + pool, underlyingTokenMint, poolUnderlyingTA, + poolBonusUnderlyingTA, gamba_state, gambaStateAta, + bonusMint, poolJackpotTA, ], }) - const freezeInstruction = AddressLookupTableProgram.freezeLookupTable({ - authority: this.wallet.publicKey, - lookupTable: lookupTableAddress, + const lutFreezeIx = AddressLookupTableProgram.freezeLookupTable({ + authority: this.user, + lookupTable: lutAddress, }) - const createPoolInstruction = this.gambaProgram.methods - .poolInitialize(authority, lookupTableAddress) - .accounts({ - initializer: this.wallet.publicKey, - gambaState: getGambaStateAddress(), - underlyingTokenMint: underlyingTokenMint, - pool, - poolUnderlyingTokenAccount, - poolBonusUnderlyingTokenAccount, - gambaStateAta, - lpMint, - lpMintMetadata, - bonusMint, - bonusMintMetadata, - associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, - tokenProgram: TOKEN_PROGRAM_ID, - systemProgram: SystemProgram.programId, - rent: SYSVAR_RENT_PUBKEY, - tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID, - }) + // ——— HERE is the switch to accountsPartial ——— + const accs: Record = { + initializer: this.user, + gambaState: gamba_state, + underlyingTokenMint, + pool, + poolUnderlyingTokenAccount: poolUnderlyingTA, + poolBonusUnderlyingTokenAccount: poolBonusUnderlyingTA, + gambaStateAta, + lpMint, + lpMintMetadata, + bonusMint, + bonusMintMetadata, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + tokenProgram: TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + rent: SYSVAR_RENT_PUBKEY, + tokenMetadataProgram: TOKEN_METADATA, + } + + const createPoolIx = this.gambaProgram.methods + .poolInitialize(authority, lutAddress) + .accountsPartial(accs as any) .instruction() - return [lookupTableInst, addAddressesInstruction, freezeInstruction, createPoolInstruction] + return [lutCreateIx, lutExtendIx, lutFreezeIx, createPoolIx] } /** - * - * @param pool The pool to deposit to - * @param underlyingTokenMint Token to deposit (Has to be the same as pool.underlyingTokenMint) - * @param amount Amount of tokens to deposit - */ + * + * @param pool The pool to deposit to + * @param underlyingTokenMint Token to deposit (Has to be the same as pool.underlyingTokenMint) + * @param amount Amount of tokens to deposit + */ depositToPool( pool: PublicKey, underlyingTokenMint: PublicKey, - amount: bigint | number, + amount: number | bigint, ) { + // PDAs const poolUnderlyingTokenAccount = getPoolUnderlyingTokenAccountAddress(pool) - const poolLpMint = getPoolLpAddress(pool) + const poolLpMint = getPoolLpAddress(pool) + const gambaState = getGambaStateAddress() + // User ATAs const userUnderlyingAta = getAssociatedTokenAddressSync( underlyingTokenMint, this.wallet.publicKey, ) - const userLpAta = getAssociatedTokenAddressSync( poolLpMint, this.wallet.publicKey, ) + // build a loose map of all accounts + const accs: Record = { + user: this.wallet.publicKey, + gambaState, + pool, + underlyingTokenMint, + poolUnderlyingTokenAccount, + lpMint: poolLpMint, + userUnderlyingAta, + userLpAta, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + tokenProgram: TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + } + return this.gambaProgram.methods .poolDeposit(new anchor.BN(amount)) - .accounts({ - pool, - underlyingTokenMint, - poolUnderlyingTokenAccount, - userUnderlyingAta, - userLpAta, - }) + .accountsPartial(accs as any) .instruction() } /** - * - * @param pool The pool to withdraw from - * @param underlyingTokenMint Token to withdraw (Has to be the same as pool.underlyingTokenMint) - * @param amount Amount of tokens to withdraw - */ + * + * @param pool The pool to withdraw from + * @param underlyingTokenMint Token to withdraw (Has to be the same as pool.underlyingTokenMint) + * @param amount Amount of tokens to withdraw + */ withdrawFromPool( pool: PublicKey, underlyingTokenMint: PublicKey, - amount: bigint | number, + amount: number | bigint, ) { - const poolUnderlyingTokenAccount = getPoolUnderlyingTokenAccountAddress(pool) - const poolLpMint = getPoolLpAddress(pool) - + const poolUnderlyingTA = getPoolUnderlyingTokenAccountAddress(pool) + const poolLpMint = getPoolLpAddress(pool) + const gambaState = getGambaStateAddress() const userUnderlyingAta = getAssociatedTokenAddressSync( underlyingTokenMint, this.wallet.publicKey, ) - const userLpAta = getAssociatedTokenAddressSync( poolLpMint, this.wallet.publicKey, ) + const accs: Record = { + user: this.wallet.publicKey, + gambaState, + pool, + underlyingTokenMint, + poolUnderlyingTokenAccount: poolUnderlyingTA, + lpMint: poolLpMint, + userUnderlyingAta, + userLpAta, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + tokenProgram: TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + } + return this.gambaProgram.methods .poolWithdraw(new anchor.BN(amount)) - .accounts({ - pool, - underlyingTokenMint, - poolUnderlyingTokenAccount, - userUnderlyingAta, - userLpAta, - }) + .accountsPartial(accs as any) .instruction() } /** * Mints bonus tokens that can be used as free plays in the pool * @param pool Pool to mint bonus tokens for - * @param underlyingTokenMint Token to mint bonus tokens for (Has to be equal to pool.underlyingTokenMint) + * @param underlyingTokenMint Token to mint bonus tokens for * @param amount Amount of bonus tokens to mint */ mintBonusTokens( pool: PublicKey, underlyingTokenMint: PublicKey, - amount: bigint | number, + amount: number | bigint, ) { - const poolBonusMint = getPoolBonusAddress(pool) - + const bonusMint = getPoolBonusAddress(pool) + const gambaState = getGambaStateAddress() const userUnderlyingAta = getAssociatedTokenAddressSync( underlyingTokenMint, this.wallet.publicKey, ) - - const userBonusAta = getAssociatedTokenAddressSync( - poolBonusMint, + const userBonusAta = getAssociatedTokenAddressSync( + bonusMint, this.wallet.publicKey, ) + const poolBonusUnderlyingTA = getPoolBonusUnderlyingTokenAccountAddress(pool) + const poolJackpotTA = getPoolJackpotTokenAccountAddress(pool) + + const accs: Record = { + user: this.wallet.publicKey, + gambaState: gambaState, + pool: pool, + underlyingTokenMint: underlyingTokenMint, + bonusMint: bonusMint, + userUnderlyingAta: userUnderlyingAta, + userBonusAta: userBonusAta, + poolBonusUnderlyingTokenAccount: poolBonusUnderlyingTA, + poolJackpotTokenAccount: poolJackpotTA, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + tokenProgram: TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + rent: SYSVAR_RENT_PUBKEY, + } + return this.gambaProgram.methods .poolMintBonusTokens(new anchor.BN(amount)) - .accounts({ - pool, - user: this.wallet.publicKey, - underlyingTokenMint, - userUnderlyingAta, - userBonusAta, - }) + .accountsPartial(accs as any) .instruction() } + /** * Initializes an associated Player account for the connected wallet */ createPlayer() { + const player = getPlayerAddress(this.wallet.publicKey) + const game = getGameAddress(this.wallet.publicKey) + + const accs: Record = { + player, + game, + user: this.wallet.publicKey, + systemProgram: SystemProgram.programId, + } + return this.gambaProgram.methods .playerInitialize() - .accounts({}) + .accountsPartial(accs as any) .instruction() } @@ -244,10 +295,18 @@ export class GambaProvider { * Closes the associated Player account for the connected wallet */ closePlayer() { - const gameAddress = getGameAddress(this.user) + const player = getPlayerAddress(this.wallet.publicKey) + const game = getGameAddress(this.wallet.publicKey) + + const accs = { + player, + game, + user: this.wallet.publicKey, + } + return this.gambaProgram.methods .playerClose() - .accounts({ game: gameAddress }) + .accountsPartial(accs as any) .instruction() } @@ -261,20 +320,20 @@ export class GambaProvider { creatorFee: number, jackpotFee: number, metadata: string, - useBonus: boolean, + useBonus = false, ) { - const player = getPlayerAddress(this.user) + const player = getPlayerAddress(this.wallet.publicKey) + const game = getGameAddress(this.wallet.publicKey) + const gambaState = getGambaStateAddress() const userUnderlyingAta = getAssociatedTokenAddressSync( underlyingTokenMint, - this.user, + this.wallet.publicKey, ) - const creatorAta = getAssociatedTokenAddressSync( underlyingTokenMint, creator, ) - const playerAta = getAssociatedTokenAddressSync( underlyingTokenMint, player, @@ -284,15 +343,39 @@ export class GambaProvider { const bonusMint = getPoolBonusAddress(pool) const userBonusAta = getAssociatedTokenAddressSync( bonusMint, - this.user, + this.wallet.publicKey, ) - const playerBonusAta = getAssociatedTokenAddressSync( bonusMint, - getPlayerAddress(this.user), + player, true, ) + const poolJackpotTA = PublicKey.findProgramAddressSync( + [Buffer.from('POOL_JACKPOT'), pool.toBuffer()], + PROGRAM_ID, + )[0] + + const accs: Record = { + user: this.wallet.publicKey, + player, + game, + gambaState, + pool, + underlyingTokenMint, + bonusTokenMint: bonusMint, + userUnderlyingAta, + creator, + creatorAta, + playerAta, + playerBonusAta: useBonus ? playerBonusAta : null, + userBonusAta: useBonus ? userBonusAta : null, + poolJackpotTokenAccount: poolJackpotTA, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + } + return this.gambaProgram.methods .playGame( new anchor.BN(wager), @@ -302,16 +385,7 @@ export class GambaProvider { basisPoints(jackpotFee), metadata, ) - .accounts({ - pool, - userUnderlyingAta, - underlyingTokenMint, - creator, - creatorAta, - playerAta, - playerBonusAta: useBonus ? playerBonusAta : null, - userBonusAta: useBonus ? userBonusAta : null, - }) + .accountsPartial(accs as any) .instruction() } } diff --git a/packages/core/src/decoders.ts b/packages/core/src/decoders.ts index 75ff1032..cb833aae 100644 --- a/packages/core/src/decoders.ts +++ b/packages/core/src/decoders.ts @@ -1,31 +1,56 @@ import { BorshAccountsCoder, IdlAccounts } from '@coral-xyz/anchor' import { AccountLayout } from '@solana/spl-token' -import { AccountInfo } from '@solana/web3.js' -import { GambaIdl } from '.' +import type { AccountInfo } from '@solana/web3.js' +import type { GambaIdl } from '.' import { IDL } from './idl' const accountsCoder = new BorshAccountsCoder(IDL) -const decodeAccount = (accountName: string, info: AccountInfo | null) => { - if (!info?.data?.length) - return null +/** Decode any account by its Anchor name, returning `T | null` */ +function decodeAccount( + accountName: string, + info: AccountInfo | null, +): T | null { + if (!info?.data?.length) return null return accountsCoder.decode(accountName, info.data) } -export const decodeAta = (acc: AccountInfo | null) => { +/** Standard SPL‐Token ATA decoder */ +export function decodeAta( + acc: AccountInfo | null +): ReturnType | null { if (!acc) return null return AccountLayout.decode(acc.data) } type GambaAccounts = IdlAccounts -const makeDecoder = (accountName: N) => { - return (info: AccountInfo | null) => { - return decodeAccount(accountName, info) as GambaAccounts[N] | null - } +/** Factory for a strongly-typed Anchor account decoder */ +function makeDecoder< + N extends Extract +>( + accountName: N +): (info: AccountInfo | null) => GambaAccounts[N] | null { + return (info) => decodeAccount(accountName, info) } -export const decodePlayer = makeDecoder('player') -export const decodeGame = makeDecoder('game') -export const decodePool = makeDecoder('pool') -export const decodeGambaState = makeDecoder('gambaState') +// ─── THESE NAMES MUST MATCH YOUR IDL ACCOUNTS[].name ────────────────────────── +export const decodePlayer: ( + info: AccountInfo | null +) => GambaAccounts['Player'] | null = + makeDecoder('Player') + +export const decodeGame: ( + info: AccountInfo | null +) => GambaAccounts['Game'] | null = + makeDecoder('Game') + +export const decodePool: ( + info: AccountInfo | null +) => GambaAccounts['Pool'] | null = + makeDecoder('Pool') + +export const decodeGambaState: ( + info: AccountInfo | null +) => GambaAccounts['GambaState'] | null = + makeDecoder('GambaState') diff --git a/packages/core/src/events.ts b/packages/core/src/events.ts index 50705930..7af9ddbe 100644 --- a/packages/core/src/events.ts +++ b/packages/core/src/events.ts @@ -1,6 +1,20 @@ +// packages/core/src/events.ts + import { BorshCoder, EventParser } from '@coral-xyz/anchor' -import { Connection, ParsedTransactionWithMeta, PublicKey, SignaturesForAddressOptions } from '@solana/web3.js' -import { AnyGambaEvent, GambaEvent, GambaEventType, IDL, PROGRAM_ID } from '.' +import { + Connection, + ParsedTransactionWithMeta, + PublicKey, + ConfirmedSignatureInfo, + SignaturesForAddressOptions, +} from '@solana/web3.js' +import { + AnyGambaEvent, + GambaEvent, + GambaEventType, + IDL, + PROGRAM_ID, +} from '.' export type GambaTransaction = { signature: string @@ -9,72 +23,142 @@ export type GambaTransaction = { data: GambaEvent['data'] } -const eventParser = new EventParser(PROGRAM_ID, new BorshCoder(IDL)) +const coder = new BorshCoder(IDL) +const parser = new EventParser(PROGRAM_ID, coder) -/** - * Extracts events from transaction logs - */ -export const parseTransactionEvents = (logs: string[]) => { +/** Extract Anchor events from raw logs */ +export function parseTransactionEvents(logs: string[]): AnyGambaEvent[] { try { - const parsedEvents: AnyGambaEvent[] = [] - const events = eventParser.parseLogs(logs) as any as AnyGambaEvent[] - for (const event of events) { - parsedEvents.push(event) - } - return parsedEvents + return parser.parseLogs(logs) as any as AnyGambaEvent[] } catch { return [] } } -/** - * Extracts events from a transaction +/** + * 🔴 @deprecated — use `fetchRecentLogs` for real blockTime timestamps */ -export const parseGambaTransaction = ( +export function parseGambaTransaction( transaction: ParsedTransactionWithMeta, -) => { +): GambaTransaction[] { + const blockTime = transaction.blockTime ?? 0 const logs = transaction.meta?.logMessages ?? [] - const events = parseTransactionEvents(logs) - - return events.map((event) => { - return { - signature: transaction.transaction.signatures[0], - time: (transaction.blockTime ?? 0) * 1000, - name: event.name, - data: event.data, - } as GambaTransaction<'GameSettled'> | GambaTransaction<'PoolChange'> - }) + const evts = parseTransactionEvents(logs) + return evts.map((ev) => ({ + signature: transaction.transaction.signatures[0], + time: blockTime * 1000, + // cast string → literal type + name: ev.name as GambaEventType, + data: ev.data, + })) } +/** 🔴 @deprecated — replaced by `fetchRecentLogs` */ export async function fetchGambaTransactionsFromSignatures( connection: Connection, signatures: string[], -) { - const transactions = (await connection.getParsedTransactions( +): Promise[]> { + const txns = await connection.getParsedTransactions( signatures, - { - maxSupportedTransactionVersion: 0, - commitment: 'confirmed', - }, - )).flatMap((x) => x ? [x] : []) - - return transactions.flatMap(parseGambaTransaction) + { maxSupportedTransactionVersion: 0, commitment: 'confirmed' }, + ) + return txns.flatMap((tx) => (tx ? parseGambaTransaction(tx) : [])) } -/** - * Fetches recent Gamba events - */ +/** 🔴 @deprecated — replaced by `fetchRecentLogs` */ export async function fetchGambaTransactions( connection: Connection, address: PublicKey, options: SignaturesForAddressOptions, -) { - const signatureInfo = await connection.getSignaturesForAddress( +): Promise[]> { + const sigInfos = await connection.getSignaturesForAddress( address, options, 'confirmed', ) - const events = await fetchGambaTransactionsFromSignatures(connection, signatureInfo.map((x) => x.signature)) + const sigs = sigInfos.map((s) => s.signature) + return fetchGambaTransactionsFromSignatures(connection, sigs) +} + +// ─── NEW, lightweight history API ────────────────────────────────────── - return events +/** + * Fetch recent events using only program logs, but still pull full transactions + * so we can surface their on‐chain blockTime accurately. + */ +export async function fetchRecentLogs( + connection: Connection, + address: PublicKey = PROGRAM_ID, + limit = 30, +): Promise[]> { + // 1) get latest signatures + const infos: ConfirmedSignatureInfo[] = await connection.getSignaturesForAddress( + address, + { limit }, + 'confirmed', + ) + const sigs = infos.map((i) => i.signature) + + // 2) fetch full parsed transactions + const txns = await connection.getParsedTransactions(sigs, { + maxSupportedTransactionVersion: 0, + commitment: 'confirmed', + }) + + // 3) extract events with real timestamps + const out: GambaTransaction[] = [] + for (const tx of txns) { + if (!tx) continue + const ts = (tx.blockTime ?? Math.floor(Date.now() / 1000)) * 1000 + const evts = parseTransactionEvents(tx.meta?.logMessages ?? []) + for (const ev of evts) { + out.push({ + signature: tx.transaction.signatures[0], + time: ts, + name: ev.name as GambaEventType, + data: ev.data as any, + }) + } + } + return out +} + +/** + * Subscribe to raw program logs, parse Anchor events, and invoke callback + */ +export function subscribeGambaLogs( + connection: Connection, + address: PublicKey = PROGRAM_ID, + callback: (evt: GambaTransaction) => void, +): number { + const connAny = connection as any + const subId: number = connAny.onLogs( + address, + (logInfo: { signature?: string; logs: string[] }) => { + if (!logInfo.signature) return + const now = Date.now() + const evts = parseTransactionEvents(logInfo.logs) + for (const ev of evts) { + callback({ + signature: logInfo.signature!, + time: now, + name: ev.name as GambaEventType, + data: ev.data as any, + }) + } + }, + 'confirmed', + ) + return subId +} + +/** Unsubscribe from `subscribeGambaLogs` */ +export function unsubscribeGambaLogs( + connection: Connection, + subId: number, +) { + const connAny = connection as any + if (typeof connAny.removeOnLogsListener === 'function') { + connAny.removeOnLogsListener(subId) + } } diff --git a/packages/core/src/idl.ts b/packages/core/src/idl.ts index 1198cd09..2ce18379 100644 --- a/packages/core/src/idl.ts +++ b/packages/core/src/idl.ts @@ -1,55 +1,101 @@ export type Gamba = { - 'version': '0.1.0', - 'name': 'gamba', + 'address': 'Gamba2hK6KV3quKq854B3sQG1WMdq3zgQLPKqyK4qS18', + 'metadata': { + 'name': 'gamba', + 'version': '0.1.0', + 'spec': '0.1.0' + }, 'instructions': [ { 'name': 'gambaInitialize', + 'discriminator': [ + 255, + 140, + 190, + 102, + 152, + 30, + 179, + 112 + ], 'accounts': [ { 'name': 'initializer', - 'isMut': true, - 'isSigner': true + 'writable': true, + 'signer': true }, { 'name': 'gambaState', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'GAMBA_STATE' + 'value': [ + 34, + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69, + 34 + ] } ] } }, { - 'name': 'systemProgram', - 'isMut': false, - 'isSigner': false + 'name': 'systemProgram' } ], 'args': [] }, { 'name': 'gambaSetAuthority', + 'discriminator': [ + 60, + 11, + 159, + 59, + 150, + 12, + 106, + 78 + ], 'accounts': [ { 'name': 'authority', - 'isMut': true, - 'isSigner': true + 'writable': true, + 'signer': true }, { 'name': 'gambaState', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'GAMBA_STATE' + 'value': [ + 34, + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69, + 34 + ] } ] }, @@ -61,28 +107,50 @@ export type Gamba = { 'args': [ { 'name': 'authority', - 'type': 'publicKey' + 'type': 'pubkey' } ] }, { 'name': 'gambaSetConfig', + 'discriminator': [ + 205, + 11, + 209, + 24, + 204, + 47, + 25, + 186 + ], 'accounts': [ { 'name': 'authority', - 'isMut': true, - 'isSigner': true + 'writable': true, + 'signer': true }, { 'name': 'gambaState', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'GAMBA_STATE' + 'value': [ + 34, + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69, + 34 + ] } ] }, @@ -94,7 +162,7 @@ export type Gamba = { 'args': [ { 'name': 'rngAddress', - 'type': 'publicKey' + 'type': 'pubkey' }, { 'name': 'gambaFee', @@ -166,57 +234,80 @@ export type Gamba = { }, { 'name': 'distributionRecipient', - 'type': 'publicKey' + 'type': 'pubkey' } ] }, { 'name': 'poolInitialize', + 'discriminator': [ + 37, + 10, + 195, + 69, + 4, + 213, + 88, + 173 + ], 'accounts': [ { 'name': 'initializer', - 'isMut': true, - 'isSigner': true + 'writable': true, + 'signer': true }, { 'name': 'gambaState', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'GAMBA_STATE' + 'value': [ + 34, + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69, + 34 + ] } ] } }, { - 'name': 'underlyingTokenMint', - 'isMut': false, - 'isSigner': false + 'name': 'underlyingTokenMint' }, { 'name': 'pool', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'POOL' + 'value': [ + 34, + 80, + 79, + 79, + 76, + 34 + ] }, { 'kind': 'account', - 'type': 'publicKey', - 'account': 'Mint', - 'path': 'underlying_token_mint' + 'path': 'underlying_token_mint', + 'account': 'Mint' }, { 'kind': 'arg', - 'type': 'publicKey', 'path': 'pool_authority' } ] @@ -224,252 +315,340 @@ export type Gamba = { }, { 'name': 'poolUnderlyingTokenAccount', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'POOL_ATA' + 'value': [ + 34, + 80, + 79, + 79, + 76, + 95, + 65, + 84, + 65, + 34 + ] }, { 'kind': 'account', - 'type': 'publicKey', - 'account': 'Pool', - 'path': 'pool' + 'path': 'pool', + 'account': 'Pool' } ] } }, { 'name': 'poolBonusUnderlyingTokenAccount', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'POOL_BONUS_UNDERLYING_TA' + 'value': [ + 34, + 80, + 79, + 79, + 76, + 95, + 66, + 79, + 78, + 85, + 83, + 95, + 85, + 78, + 68, + 69, + 82, + 76, + 89, + 73, + 78, + 71, + 95, + 84, + 65, + 34 + ] }, { 'kind': 'account', - 'type': 'publicKey', - 'account': 'Pool', - 'path': 'pool' + 'path': 'pool', + 'account': 'Pool' } ] } }, { 'name': 'poolJackpotTokenAccount', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'POOL_JACKPOT' + 'value': [ + 34, + 80, + 79, + 79, + 76, + 95, + 74, + 65, + 67, + 75, + 80, + 79, + 84, + 34 + ] }, { 'kind': 'account', - 'type': 'publicKey', - 'account': 'Pool', - 'path': 'pool' + 'path': 'pool', + 'account': 'Pool' } ] } }, { 'name': 'gambaStateAta', - 'isMut': true, - 'isSigner': false + 'writable': true }, { 'name': 'lpMint', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'POOL_LP_MINT' + 'value': [ + 34, + 80, + 79, + 79, + 76, + 95, + 76, + 80, + 95, + 77, + 73, + 78, + 84, + 34 + ] }, { 'kind': 'account', - 'type': 'publicKey', - 'account': 'Pool', - 'path': 'pool' + 'path': 'pool', + 'account': 'Pool' } ] } }, { 'name': 'lpMintMetadata', - 'isMut': true, - 'isSigner': false + 'writable': true }, { 'name': 'bonusMint', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'POOL_BONUS_MINT' + 'value': [ + 34, + 80, + 79, + 79, + 76, + 95, + 66, + 79, + 78, + 85, + 83, + 95, + 77, + 73, + 78, + 84, + 34 + ] }, { 'kind': 'account', - 'type': 'publicKey', - 'account': 'Pool', - 'path': 'pool' + 'path': 'pool', + 'account': 'Pool' } ] } }, { 'name': 'bonusMintMetadata', - 'isMut': true, - 'isSigner': false + 'writable': true }, { - 'name': 'associatedTokenProgram', - 'isMut': false, - 'isSigner': false + 'name': 'associatedTokenProgram' }, { - 'name': 'tokenProgram', - 'isMut': false, - 'isSigner': false + 'name': 'tokenProgram' }, { - 'name': 'systemProgram', - 'isMut': false, - 'isSigner': false + 'name': 'systemProgram' }, { - 'name': 'rent', - 'isMut': false, - 'isSigner': false + 'name': 'rent' }, { - 'name': 'tokenMetadataProgram', - 'isMut': false, - 'isSigner': false + 'name': 'tokenMetadataProgram' } ], 'args': [ { 'name': 'poolAuthority', - 'type': 'publicKey' + 'type': 'pubkey' }, { 'name': 'lookupAddress', - 'type': 'publicKey' + 'type': 'pubkey' } ] }, { 'name': 'poolDeposit', + 'discriminator': [ + 26, + 109, + 164, + 79, + 207, + 145, + 204, + 217 + ], 'accounts': [ { 'name': 'user', - 'isMut': true, - 'isSigner': true + 'writable': true, + 'signer': true }, { 'name': 'gambaState', - 'isMut': false, - 'isSigner': false, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'GAMBA_STATE' + 'value': [ + 34, + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69, + 34 + ] } ] } }, { 'name': 'pool', - 'isMut': true, - 'isSigner': false + 'writable': true }, { 'name': 'poolUnderlyingTokenAccount', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'POOL_ATA' + 'value': [ + 34, + 80, + 79, + 79, + 76, + 95, + 65, + 84, + 65, + 34 + ] }, { 'kind': 'account', - 'type': 'publicKey', - 'account': 'Pool', - 'path': 'pool' + 'path': 'pool', + 'account': 'Pool' } ] } }, { 'name': 'lpMint', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'POOL_LP_MINT' + 'value': [ + 34, + 80, + 79, + 79, + 76, + 95, + 76, + 80, + 95, + 77, + 73, + 78, + 84, + 34 + ] }, { 'kind': 'account', - 'type': 'publicKey', - 'account': 'Pool', - 'path': 'pool' + 'path': 'pool', + 'account': 'Pool' } ] } }, { - 'name': 'underlyingTokenMint', - 'isMut': false, - 'isSigner': false + 'name': 'underlyingTokenMint' }, { 'name': 'userUnderlyingAta', - 'isMut': true, - 'isSigner': false + 'writable': true }, { 'name': 'userLpAta', - 'isMut': true, - 'isSigner': false + 'writable': true }, { - 'name': 'associatedTokenProgram', - 'isMut': false, - 'isSigner': false + 'name': 'associatedTokenProgram' }, { - 'name': 'tokenProgram', - 'isMut': false, - 'isSigner': false + 'name': 'tokenProgram' }, { - 'name': 'systemProgram', - 'isMut': false, - 'isSigner': false + 'name': 'systemProgram' } ], 'args': [ @@ -481,100 +660,130 @@ export type Gamba = { }, { 'name': 'poolWithdraw', + 'discriminator': [ + 50, + 1, + 23, + 25, + 135, + 221, + 159, + 182 + ], 'accounts': [ { 'name': 'user', - 'isMut': true, - 'isSigner': true + 'writable': true, + 'signer': true }, { 'name': 'pool', - 'isMut': true, - 'isSigner': false + 'writable': true }, { 'name': 'poolUnderlyingTokenAccount', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'POOL_ATA' + 'value': [ + 34, + 80, + 79, + 79, + 76, + 95, + 65, + 84, + 65, + 34 + ] }, { 'kind': 'account', - 'type': 'publicKey', - 'account': 'Pool', - 'path': 'pool' + 'path': 'pool', + 'account': 'Pool' } ] } }, { 'name': 'lpMint', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'POOL_LP_MINT' + 'value': [ + 34, + 80, + 79, + 79, + 76, + 95, + 76, + 80, + 95, + 77, + 73, + 78, + 84, + 34 + ] }, { 'kind': 'account', - 'type': 'publicKey', - 'account': 'Pool', - 'path': 'pool' + 'path': 'pool', + 'account': 'Pool' } ] } }, { - 'name': 'underlyingTokenMint', - 'isMut': false, - 'isSigner': false + 'name': 'underlyingTokenMint' }, { 'name': 'userUnderlyingAta', - 'isMut': true, - 'isSigner': false + 'writable': true }, { 'name': 'userLpAta', - 'isMut': true, - 'isSigner': false + 'writable': true }, { 'name': 'gambaState', - 'isMut': false, - 'isSigner': false, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'GAMBA_STATE' + 'value': [ + 34, + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69, + 34 + ] } ] } }, { - 'name': 'associatedTokenProgram', - 'isMut': false, - 'isSigner': false + 'name': 'associatedTokenProgram' }, { - 'name': 'tokenProgram', - 'isMut': false, - 'isSigner': false + 'name': 'tokenProgram' }, { - 'name': 'systemProgram', - 'isMut': false, - 'isSigner': false + 'name': 'systemProgram' } ], 'args': [ @@ -586,120 +795,180 @@ export type Gamba = { }, { 'name': 'poolMintBonusTokens', + 'discriminator': [ + 105, + 130, + 72, + 25, + 88, + 185, + 100, + 55 + ], 'accounts': [ { 'name': 'user', - 'isMut': true, - 'isSigner': true + 'writable': true, + 'signer': true }, { - 'name': 'pool', - 'isMut': false, - 'isSigner': false + 'name': 'pool' }, { 'name': 'gambaState', - 'isMut': false, - 'isSigner': false, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'GAMBA_STATE' + 'value': [ + 34, + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69, + 34 + ] } ] } }, { - 'name': 'underlyingTokenMint', - 'isMut': false, - 'isSigner': false + 'name': 'underlyingTokenMint' }, { 'name': 'poolBonusUnderlyingTokenAccount', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'POOL_BONUS_UNDERLYING_TA' + 'value': [ + 34, + 80, + 79, + 79, + 76, + 95, + 66, + 79, + 78, + 85, + 83, + 95, + 85, + 78, + 68, + 69, + 82, + 76, + 89, + 73, + 78, + 71, + 95, + 84, + 65, + 34 + ] }, { 'kind': 'account', - 'type': 'publicKey', - 'account': 'Pool', - 'path': 'pool' + 'path': 'pool', + 'account': 'Pool' } ] } }, { 'name': 'bonusMint', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'POOL_BONUS_MINT' + 'value': [ + 34, + 80, + 79, + 79, + 76, + 95, + 66, + 79, + 78, + 85, + 83, + 95, + 77, + 73, + 78, + 84, + 34 + ] }, { 'kind': 'account', - 'type': 'publicKey', - 'account': 'Pool', - 'path': 'pool' + 'path': 'pool', + 'account': 'Pool' } ] } }, { 'name': 'poolJackpotTokenAccount', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'POOL_JACKPOT' + 'value': [ + 34, + 80, + 79, + 79, + 76, + 95, + 74, + 65, + 67, + 75, + 80, + 79, + 84, + 34 + ] }, { 'kind': 'account', - 'type': 'publicKey', - 'account': 'Pool', - 'path': 'pool' + 'path': 'pool', + 'account': 'Pool' } ] } }, { 'name': 'userUnderlyingAta', - 'isMut': true, - 'isSigner': false + 'writable': true }, { 'name': 'userBonusAta', - 'isMut': true, - 'isSigner': false + 'writable': true }, { - 'name': 'associatedTokenProgram', - 'isMut': false, - 'isSigner': false + 'name': 'associatedTokenProgram' }, { - 'name': 'tokenProgram', - 'isMut': false, - 'isSigner': false + 'name': 'tokenProgram' }, { - 'name': 'systemProgram', - 'isMut': false, - 'isSigner': false + 'name': 'systemProgram' } ], 'args': [ @@ -711,30 +980,50 @@ export type Gamba = { }, { 'name': 'poolAuthorityConfig', + 'discriminator': [ + 58, + 12, + 184, + 118, + 14, + 99, + 110, + 17 + ], 'accounts': [ { 'name': 'user', - 'isMut': true, - 'isSigner': true + 'writable': true, + 'signer': true }, { 'name': 'gambaState', - 'isMut': false, - 'isSigner': false, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'GAMBA_STATE' + 'value': [ + 34, + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69, + 34 + ] } ] } }, { 'name': 'pool', - 'isMut': true, - 'isSigner': false + 'writable': true } ], 'args': [ @@ -772,36 +1061,56 @@ export type Gamba = { }, { 'name': 'depositWhitelistAddress', - 'type': 'publicKey' + 'type': 'pubkey' } ] }, { 'name': 'poolGambaConfig', + 'discriminator': [ + 197, + 177, + 234, + 111, + 246, + 248, + 20, + 155 + ], 'accounts': [ { 'name': 'user', - 'isMut': true, - 'isSigner': true + 'writable': true, + 'signer': true }, { 'name': 'gambaState', - 'isMut': false, - 'isSigner': false, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'GAMBA_STATE' + 'value': [ + 34, + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69, + 34 + ] } ] } }, { 'name': 'pool', - 'isMut': true, - 'isSigner': false + 'writable': true } ], 'args': [ @@ -821,21 +1130,37 @@ export type Gamba = { }, { 'name': 'playerInitialize', + 'discriminator': [ + 213, + 160, + 145, + 88, + 197, + 68, + 63, + 150 + ], 'accounts': [ { 'name': 'player', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'PLAYER' + 'value': [ + 34, + 80, + 76, + 65, + 89, + 69, + 82, + 34 + ] }, { 'kind': 'account', - 'type': 'publicKey', 'path': 'user' } ] @@ -843,18 +1168,22 @@ export type Gamba = { }, { 'name': 'game', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'GAME' + 'value': [ + 34, + 71, + 65, + 77, + 69, + 34 + ] }, { 'kind': 'account', - 'type': 'publicKey', 'path': 'user' } ] @@ -862,39 +1191,53 @@ export type Gamba = { }, { 'name': 'user', - 'isMut': true, - 'isSigner': true + 'writable': true, + 'signer': true }, { - 'name': 'systemProgram', - 'isMut': false, - 'isSigner': false + 'name': 'systemProgram' } ], 'args': [] }, { 'name': 'playGame', + 'discriminator': [ + 37, + 88, + 207, + 85, + 42, + 144, + 122, + 197 + ], 'accounts': [ { 'name': 'user', - 'isMut': true, - 'isSigner': true + 'writable': true, + 'signer': true }, { 'name': 'player', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'PLAYER' + 'value': [ + 34, + 80, + 76, + 65, + 89, + 69, + 82, + 34 + ] }, { 'kind': 'account', - 'type': 'publicKey', 'path': 'user' } ] @@ -902,18 +1245,22 @@ export type Gamba = { }, { 'name': 'game', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'GAME' + 'value': [ + 34, + 71, + 65, + 77, + 69, + 34 + ] }, { 'kind': 'account', - 'type': 'publicKey', 'path': 'user' } ] @@ -921,114 +1268,136 @@ export type Gamba = { }, { 'name': 'pool', - 'isMut': true, - 'isSigner': false + 'writable': true }, { - 'name': 'underlyingTokenMint', - 'isMut': false, - 'isSigner': false + 'name': 'underlyingTokenMint' }, { 'name': 'bonusTokenMint', - 'isMut': false, - 'isSigner': false, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'POOL_BONUS_MINT' + 'value': [ + 34, + 80, + 79, + 79, + 76, + 95, + 66, + 79, + 78, + 85, + 83, + 95, + 77, + 73, + 78, + 84, + 34 + ] }, { 'kind': 'account', - 'type': 'publicKey', - 'account': 'Pool', - 'path': 'pool' + 'path': 'pool', + 'account': 'Pool' } ] } }, { 'name': 'userUnderlyingAta', - 'isMut': true, - 'isSigner': false + 'writable': true }, { - 'name': 'creator', - 'isMut': false, - 'isSigner': false + 'name': 'creator' }, { 'name': 'creatorAta', - 'isMut': true, - 'isSigner': false + 'writable': true }, { 'name': 'playerAta', - 'isMut': true, - 'isSigner': false + 'writable': true }, { 'name': 'playerBonusAta', - 'isMut': true, - 'isSigner': false, - 'isOptional': true + 'writable': true, + 'optional': true }, { 'name': 'userBonusAta', - 'isMut': true, - 'isSigner': false, - 'isOptional': true + 'writable': true, + 'optional': true }, { 'name': 'gambaState', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'GAMBA_STATE' + 'value': [ + 34, + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69, + 34 + ] } ] } }, { 'name': 'poolJackpotTokenAccount', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'POOL_JACKPOT' + 'value': [ + 34, + 80, + 79, + 79, + 76, + 95, + 74, + 65, + 67, + 75, + 80, + 79, + 84, + 34 + ] }, { 'kind': 'account', - 'type': 'publicKey', - 'account': 'Pool', - 'path': 'pool' + 'path': 'pool', + 'account': 'Pool' } ] } }, { - 'name': 'systemProgram', - 'isMut': false, - 'isSigner': false + 'name': 'systemProgram' }, { - 'name': 'tokenProgram', - 'isMut': false, - 'isSigner': false + 'name': 'tokenProgram' }, { - 'name': 'associatedTokenProgram', - 'isMut': false, - 'isSigner': false + 'name': 'associatedTokenProgram' } ], 'args': [ @@ -1062,26 +1431,42 @@ export type Gamba = { }, { 'name': 'playerClose', + 'discriminator': [ + 26, + 155, + 61, + 179, + 53, + 157, + 80, + 30 + ], 'accounts': [ { 'name': 'user', - 'isMut': true, - 'isSigner': true + 'writable': true, + 'signer': true }, { 'name': 'player', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'PLAYER' + 'value': [ + 34, + 80, + 76, + 65, + 89, + 69, + 82, + 34 + ] }, { 'kind': 'account', - 'type': 'publicKey', 'path': 'user' } ] @@ -1089,18 +1474,22 @@ export type Gamba = { }, { 'name': 'game', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'GAME' + 'value': [ + 34, + 71, + 65, + 77, + 69, + 34 + ] }, { 'kind': 'account', - 'type': 'publicKey', 'path': 'user' } ] @@ -1111,31 +1500,45 @@ export type Gamba = { }, { 'name': 'playerClaim', + 'discriminator': [ + 188, + 220, + 237, + 31, + 181, + 18, + 85, + 45 + ], 'accounts': [ { 'name': 'user', - 'isMut': true, - 'isSigner': true + 'writable': true, + 'signer': true }, { - 'name': 'underlyingTokenMint', - 'isMut': false, - 'isSigner': false + 'name': 'underlyingTokenMint' }, { 'name': 'player', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'PLAYER' + 'value': [ + 34, + 80, + 76, + 65, + 89, + 69, + 82, + 34 + ] }, { 'kind': 'account', - 'type': 'publicKey', 'path': 'user' } ] @@ -1143,18 +1546,22 @@ export type Gamba = { }, { 'name': 'game', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'GAME' + 'value': [ + 34, + 71, + 65, + 77, + 69, + 34 + ] }, { 'kind': 'account', - 'type': 'publicKey', 'path': 'user' } ] @@ -1162,59 +1569,66 @@ export type Gamba = { }, { 'name': 'playerAta', - 'isMut': true, - 'isSigner': false + 'writable': true }, { 'name': 'userUnderlyingAta', - 'isMut': true, - 'isSigner': false + 'writable': true }, { - 'name': 'systemProgram', - 'isMut': false, - 'isSigner': false + 'name': 'systemProgram' }, { - 'name': 'tokenProgram', - 'isMut': false, - 'isSigner': false + 'name': 'tokenProgram' }, { - 'name': 'associatedTokenProgram', - 'isMut': false, - 'isSigner': false + 'name': 'associatedTokenProgram' } ], 'args': [] }, { 'name': 'rngSettle', + 'discriminator': [ + 23, + 35, + 236, + 185, + 14, + 171, + 26, + 222 + ], 'accounts': [ { 'name': 'rng', - 'isMut': true, - 'isSigner': true + 'writable': true, + 'signer': true }, { 'name': 'user', - 'isMut': true, - 'isSigner': false + 'writable': true }, { 'name': 'player', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'PLAYER' + 'value': [ + 34, + 80, + 76, + 65, + 89, + 69, + 82, + 34 + ] }, { 'kind': 'account', - 'type': 'publicKey', 'path': 'user' } ] @@ -1222,18 +1636,22 @@ export type Gamba = { }, { 'name': 'game', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'GAME' + 'value': [ + 34, + 71, + 65, + 77, + 69, + 34 + ] }, { 'kind': 'account', - 'type': 'publicKey', 'path': 'user' } ] @@ -1241,178 +1659,236 @@ export type Gamba = { }, { 'name': 'pool', - 'isMut': true, - 'isSigner': false + 'writable': true }, { - 'name': 'underlyingTokenMint', - 'isMut': false, - 'isSigner': false + 'name': 'underlyingTokenMint' }, { 'name': 'poolUnderlyingTokenAccount', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'POOL_ATA' + 'value': [ + 34, + 80, + 79, + 79, + 76, + 95, + 65, + 84, + 65, + 34 + ] }, { 'kind': 'account', - 'type': 'publicKey', - 'account': 'Pool', - 'path': 'pool' + 'path': 'pool', + 'account': 'Pool' } ] } }, { 'name': 'poolBonusUnderlyingTokenAccount', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'POOL_BONUS_UNDERLYING_TA' + 'value': [ + 34, + 80, + 79, + 79, + 76, + 95, + 66, + 79, + 78, + 85, + 83, + 95, + 85, + 78, + 68, + 69, + 82, + 76, + 89, + 73, + 78, + 71, + 95, + 84, + 65, + 34 + ] }, { 'kind': 'account', - 'type': 'publicKey', - 'account': 'Pool', - 'path': 'pool' + 'path': 'pool', + 'account': 'Pool' } ] } }, { 'name': 'playerAta', - 'isMut': true, - 'isSigner': false + 'writable': true }, { 'name': 'userUnderlyingAta', - 'isMut': true, - 'isSigner': false + 'writable': true }, { 'name': 'gambaState', - 'isMut': false, - 'isSigner': false, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'GAMBA_STATE' + 'value': [ + 34, + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69, + 34 + ] } ] } }, { 'name': 'gambaStateAta', - 'isMut': true, - 'isSigner': false + 'writable': true }, { - 'name': 'creator', - 'isMut': false, - 'isSigner': false + 'name': 'creator' }, { 'name': 'creatorAta', - 'isMut': true, - 'isSigner': false + 'writable': true }, { 'name': 'bonusTokenMint', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'POOL_BONUS_MINT' + 'value': [ + 34, + 80, + 79, + 79, + 76, + 95, + 66, + 79, + 78, + 85, + 83, + 95, + 77, + 73, + 78, + 84, + 34 + ] }, { 'kind': 'account', - 'type': 'publicKey', - 'account': 'Pool', - 'path': 'pool' + 'path': 'pool', + 'account': 'Pool' } ] } }, { 'name': 'playerBonusAta', - 'isMut': true, - 'isSigner': false, - 'isOptional': true + 'writable': true, + 'optional': true }, { 'name': 'poolJackpotTokenAccount', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'POOL_JACKPOT' + 'value': [ + 34, + 80, + 79, + 79, + 76, + 95, + 74, + 65, + 67, + 75, + 80, + 79, + 84, + 34 + ] }, { 'kind': 'account', - 'type': 'publicKey', - 'account': 'Pool', - 'path': 'pool' + 'path': 'pool', + 'account': 'Pool' } ] } }, { 'name': 'escrowTokenAccount', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'ESCROW' + 'value': [ + 34, + 69, + 83, + 67, + 82, + 79, + 87, + 34 + ] }, { 'kind': 'account', - 'type': 'publicKey', - 'account': 'Player', - 'path': 'player' + 'path': 'player', + 'account': 'Player' } ] } }, { - 'name': 'systemProgram', - 'isMut': false, - 'isSigner': false + 'name': 'systemProgram' }, { - 'name': 'tokenProgram', - 'isMut': false, - 'isSigner': false + 'name': 'tokenProgram' }, { - 'name': 'associatedTokenProgram', - 'isMut': false, - 'isSigner': false + 'name': 'associatedTokenProgram' }, { - 'name': 'rent', - 'isMut': false, - 'isSigner': false + 'name': 'rent' } ], 'args': [ @@ -1428,27 +1904,48 @@ export type Gamba = { }, { 'name': 'rngProvideHashedSeed', + 'discriminator': [ + 238, + 154, + 25, + 143, + 191, + 19, + 25, + 224 + ], 'accounts': [ { 'name': 'game', - 'isMut': true, - 'isSigner': false + 'writable': true }, { 'name': 'rng', - 'isMut': true, - 'isSigner': true + 'writable': true, + 'signer': true }, { 'name': 'gambaState', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'GAMBA_STATE' + 'value': [ + 34, + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69, + 34 + ] } ] } @@ -1463,60 +1960,71 @@ export type Gamba = { }, { 'name': 'distributeFees', + 'discriminator': [ + 120, + 56, + 27, + 7, + 53, + 176, + 113, + 186 + ], 'accounts': [ { 'name': 'signer', - 'isMut': true, - 'isSigner': true + 'writable': true, + 'signer': true }, { - 'name': 'underlyingTokenMint', - 'isMut': false, - 'isSigner': false + 'name': 'underlyingTokenMint' }, { 'name': 'gambaState', - 'isMut': true, - 'isSigner': false, + 'writable': true, 'pda': { 'seeds': [ { 'kind': 'const', - 'type': 'string', - 'value': 'GAMBA_STATE' + 'value': [ + 34, + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69, + 34 + ] } ] } }, { 'name': 'gambaStateAta', - 'isMut': true, - 'isSigner': false + 'writable': true }, { 'name': 'distributionRecipient', - 'isMut': true, - 'isSigner': false + 'writable': true }, { 'name': 'distributionRecipientAta', - 'isMut': true, - 'isSigner': false + 'writable': true }, { - 'name': 'associatedTokenProgram', - 'isMut': false, - 'isSigner': false + 'name': 'associatedTokenProgram' }, { - 'name': 'tokenProgram', - 'isMut': false, - 'isSigner': false + 'name': 'tokenProgram' }, { - 'name': 'systemProgram', - 'isMut': false, - 'isSigner': false + 'name': 'systemProgram' } ], 'args': [ @@ -1529,7 +2037,236 @@ export type Gamba = { ], 'accounts': [ { - 'name': 'game', + 'name': 'Game', + 'discriminator': [ + 27, + 90, + 166, + 125, + 74, + 100, + 121, + 18 + ] + }, + { + 'name': 'Player', + 'discriminator': [ + 205, + 222, + 112, + 7, + 165, + 155, + 206, + 218 + ] + }, + { + 'name': 'Pool', + 'discriminator': [ + 241, + 154, + 109, + 4, + 17, + 177, + 109, + 188 + ] + }, + { + 'name': 'GambaState', + 'discriminator': [ + 142, + 203, + 14, + 224, + 153, + 118, + 52, + 200 + ] + } + ], + 'events': [ + { + 'name': 'GameSettled', + 'discriminator': [ + 63, + 109, + 128, + 85, + 229, + 63, + 167, + 176 + ] + }, + { + 'name': 'PoolChange', + 'discriminator': [ + 241, + 7, + 155, + 154, + 56, + 57, + 0, + 101 + ] + }, + { + 'name': 'PoolCreated', + 'discriminator': [ + 202, + 44, + 41, + 88, + 104, + 220, + 157, + 82 + ] + } + ], + 'errors': [ + { + 'code': 6000, + 'name': 'GenericError', + 'msg': 'Something went wrong' + }, + { + 'code': 6001, + 'name': 'Unauthorized', + 'msg': 'Unauthorized' + }, + { + 'code': 6002, + 'name': 'CustomPoolFeeExceedsLimit', + 'msg': 'Custom pool fee cannot exceed 100%' + }, + { + 'code': 6003, + 'name': 'CustomMaxPayoutExceedsLimit', + 'msg': 'Custom max payout cannot exceed 50%' + } + ], + 'types': [ + { + 'name': 'PlayerError', + 'type': { + 'kind': 'enum', + 'variants': [ + { + 'name': 'NotReadyToPlay' + }, + { + 'name': 'CreatorFeeTooHigh' + }, + { + 'name': 'WagerTooSmall' + }, + { + 'name': 'TooFewBetOutcomes' + }, + { + 'name': 'TooManyBetOutcomes' + }, + { + 'name': 'PlayerAdvantage' + }, + { + 'name': 'HouseAdvantageTooHigh' + }, + { + 'name': 'MaxPayoutExceeded' + } + ] + } + }, + { + 'name': 'RngError', + 'type': { + 'kind': 'enum', + 'variants': [ + { + 'name': 'Generic' + }, + { + 'name': 'InitialHashedSeedAlreadyProvided' + }, + { + 'name': 'IncorrectRngSeed' + }, + { + 'name': 'ResultNotRequested' + } + ] + } + }, + { + 'name': 'GambaStateError', + 'type': { + 'kind': 'enum', + 'variants': [ + { + 'name': 'PlaysNotAllowed' + }, + { + 'name': 'DepositNotAllowed' + }, + { + 'name': 'WithdrawalNotAllowed' + }, + { + 'name': 'PoolCreationNotAllowed' + }, + { + 'name': 'DepositLimitExceeded' + }, + { + 'name': 'DepositWhitelistRequired' + } + ] + } + }, + { + 'name': 'PoolAction', + 'type': { + 'kind': 'enum', + 'variants': [ + { + 'name': 'Deposit' + }, + { + 'name': 'Withdraw' + } + ] + } + }, + { + 'name': 'GameStatus', + 'type': { + 'kind': 'enum', + 'variants': [ + { + 'name': 'None' + }, + { + 'name': 'NotInitialized' + }, + { + 'name': 'Ready' + }, + { + 'name': 'ResultRequested' + } + ] + } + }, + { + 'name': 'Game', 'type': { 'kind': 'struct', 'fields': [ @@ -1548,20 +2285,22 @@ export type Gamba = { }, { 'name': 'user', - 'type': 'publicKey' + 'type': 'pubkey' }, { 'name': 'tokenMint', - 'type': 'publicKey' + 'type': 'pubkey' }, { 'name': 'pool', - 'type': 'publicKey' + 'type': 'pubkey' }, { 'name': 'status', 'type': { - 'defined': 'GameStatus' + 'defined': { + 'name': 'GameStatus' + } } }, { @@ -1584,7 +2323,7 @@ export type Gamba = { }, { 'name': 'creator', - 'type': 'publicKey' + 'type': 'pubkey' }, { 'name': 'creatorMeta', @@ -1650,7 +2389,7 @@ export type Gamba = { }, { 'name': 'pointsAuthority', - 'type': 'publicKey' + 'type': 'pubkey' }, { 'name': 'metadata', @@ -1660,7 +2399,7 @@ export type Gamba = { } }, { - 'name': 'player', + 'name': 'Player', 'type': { 'kind': 'struct', 'fields': [ @@ -1675,7 +2414,7 @@ export type Gamba = { }, { 'name': 'user', - 'type': 'publicKey' + 'type': 'pubkey' }, { 'name': 'nonce', @@ -1685,7 +2424,7 @@ export type Gamba = { } }, { - 'name': 'pool', + 'name': 'Pool', 'type': { 'kind': 'struct', 'fields': [ @@ -1700,15 +2439,15 @@ export type Gamba = { }, { 'name': 'lookupAddress', - 'type': 'publicKey' + 'type': 'pubkey' }, { 'name': 'poolAuthority', - 'type': 'publicKey' + 'type': 'pubkey' }, { 'name': 'underlyingTokenMint', - 'type': 'publicKey' + 'type': 'pubkey' }, { 'name': 'antiSpamFeeExempt', @@ -1760,7 +2499,7 @@ export type Gamba = { }, { 'name': 'customBonusTokenMint', - 'type': 'publicKey' + 'type': 'pubkey' }, { 'name': 'customBonusToken', @@ -1780,27 +2519,27 @@ export type Gamba = { }, { 'name': 'depositWhitelistAddress', - 'type': 'publicKey' + 'type': 'pubkey' } ] } }, { - 'name': 'gambaState', + 'name': 'GambaState', 'type': { 'kind': 'struct', 'fields': [ { 'name': 'authority', - 'type': 'publicKey' + 'type': 'pubkey' }, { 'name': 'rngAddress', - 'type': 'publicKey' + 'type': 'pubkey' }, { 'name': 'rngAddress2', - 'type': 'publicKey' + 'type': 'pubkey' }, { 'name': 'antiSpamFee', @@ -1872,7 +2611,7 @@ export type Gamba = { }, { 'name': 'distributionRecipient', - 'type': 'publicKey' + 'type': 'pubkey' }, { 'name': 'bump', @@ -1885,369 +2624,284 @@ export type Gamba = { } ] } - } - ], - 'types': [ + }, { - 'name': 'PlayerError', + 'name': 'GameSettled', 'type': { - 'kind': 'enum', - 'variants': [ + 'kind': 'struct', + 'fields': [ { - 'name': 'NotReadyToPlay' + 'name': 'user', + 'type': 'pubkey' }, { - 'name': 'CreatorFeeTooHigh' + 'name': 'pool', + 'type': 'pubkey' }, { - 'name': 'WagerTooSmall' + 'name': 'tokenMint', + 'type': 'pubkey' }, { - 'name': 'TooFewBetOutcomes' + 'name': 'creator', + 'type': 'pubkey' }, { - 'name': 'TooManyBetOutcomes' + 'name': 'creatorFee', + 'type': 'u64' }, { - 'name': 'PlayerAdvantage' + 'name': 'gambaFee', + 'type': 'u64' }, { - 'name': 'HouseAdvantageTooHigh' + 'name': 'poolFee', + 'type': 'u64' }, { - 'name': 'MaxPayoutExceeded' - } - ] - } - }, - { - 'name': 'RngError', - 'type': { - 'kind': 'enum', - 'variants': [ - { - 'name': 'Generic' + 'name': 'jackpotFee', + 'type': 'u64' }, { - 'name': 'InitialHashedSeedAlreadyProvided' + 'name': 'underlyingUsed', + 'type': 'u64' }, { - 'name': 'IncorrectRngSeed' + 'name': 'bonusUsed', + 'type': 'u64' }, { - 'name': 'ResultNotRequested' - } - ] - } - }, - { - 'name': 'GambaStateError', - 'type': { - 'kind': 'enum', - 'variants': [ - { - 'name': 'PlaysNotAllowed' + 'name': 'wager', + 'type': 'u64' }, { - 'name': 'DepositNotAllowed' + 'name': 'payout', + 'type': 'u64' }, { - 'name': 'WithdrawalNotAllowed' + 'name': 'multiplierBps', + 'type': 'u32' }, { - 'name': 'PoolCreationNotAllowed' + 'name': 'payoutFromBonusPool', + 'type': 'u64' }, { - 'name': 'DepositLimitExceeded' + 'name': 'payoutFromNormalPool', + 'type': 'u64' }, { - 'name': 'DepositWhitelistRequired' - } - ] - } - }, - { - 'name': 'PoolAction', - 'type': { - 'kind': 'enum', - 'variants': [ - { - 'name': 'Deposit' + 'name': 'jackpotProbabilityUbps', + 'type': 'u64' }, { - 'name': 'Withdraw' + 'name': 'jackpotResult', + 'type': 'u64' + }, + { + 'name': 'nonce', + 'type': 'u64' + }, + { + 'name': 'clientSeed', + 'type': 'string' + }, + { + 'name': 'resultIndex', + 'type': 'u64' + }, + { + 'name': 'bet', + 'type': { + 'vec': 'u32' + } + }, + { + 'name': 'jackpotPayoutToUser', + 'type': 'u64' + }, + { + 'name': 'poolLiquidity', + 'type': 'u64' + }, + { + 'name': 'rngSeed', + 'type': 'string' + }, + { + 'name': 'nextRngSeedHashed', + 'type': 'string' + }, + { + 'name': 'metadata', + 'type': 'string' } ] } }, { - 'name': 'GameStatus', + 'name': 'PoolChange', 'type': { - 'kind': 'enum', - 'variants': [ + 'kind': 'struct', + 'fields': [ { - 'name': 'None' + 'name': 'user', + 'type': 'pubkey' }, { - 'name': 'NotInitialized' + 'name': 'pool', + 'type': 'pubkey' }, { - 'name': 'Ready' + 'name': 'tokenMint', + 'type': 'pubkey' }, { - 'name': 'ResultRequested' + 'name': 'action', + 'type': { + 'defined': { + 'name': 'PoolAction' + } + } + }, + { + 'name': 'amount', + 'type': 'u64' + }, + { + 'name': 'postLiquidity', + 'type': 'u64' + }, + { + 'name': 'lpSupply', + 'type': 'u64' } ] } - } - ], - 'events': [ - { - 'name': 'GameSettled', - 'fields': [ - { - 'name': 'user', - 'type': 'publicKey', - 'index': false - }, - { - 'name': 'pool', - 'type': 'publicKey', - 'index': false - }, - { - 'name': 'tokenMint', - 'type': 'publicKey', - 'index': false - }, - { - 'name': 'creator', - 'type': 'publicKey', - 'index': false - }, - { - 'name': 'creatorFee', - 'type': 'u64', - 'index': false - }, - { - 'name': 'gambaFee', - 'type': 'u64', - 'index': false - }, - { - 'name': 'poolFee', - 'type': 'u64', - 'index': false - }, - { - 'name': 'jackpotFee', - 'type': 'u64', - 'index': false - }, - { - 'name': 'underlyingUsed', - 'type': 'u64', - 'index': false - }, - { - 'name': 'bonusUsed', - 'type': 'u64', - 'index': false - }, - { - 'name': 'wager', - 'type': 'u64', - 'index': false - }, - { - 'name': 'payout', - 'type': 'u64', - 'index': false - }, - { - 'name': 'multiplierBps', - 'type': 'u32', - 'index': false - }, - { - 'name': 'payoutFromBonusPool', - 'type': 'u64', - 'index': false - }, - { - 'name': 'payoutFromNormalPool', - 'type': 'u64', - 'index': false - }, - { - 'name': 'jackpotProbabilityUbps', - 'type': 'u64', - 'index': false - }, - { - 'name': 'jackpotResult', - 'type': 'u64', - 'index': false - }, - { - 'name': 'nonce', - 'type': 'u64', - 'index': false - }, - { - 'name': 'clientSeed', - 'type': 'string', - 'index': false - }, - { - 'name': 'resultIndex', - 'type': 'u64', - 'index': false - }, - { - 'name': 'bet', - 'type': { - 'vec': 'u32' - }, - 'index': false - }, - { - 'name': 'jackpotPayoutToUser', - 'type': 'u64', - 'index': false - }, - { - 'name': 'poolLiquidity', - 'type': 'u64', - 'index': false - }, - { - 'name': 'rngSeed', - 'type': 'string', - 'index': false - }, - { - 'name': 'nextRngSeedHashed', - 'type': 'string', - 'index': false - }, - { - 'name': 'metadata', - 'type': 'string', - 'index': false - } - ] }, { - 'name': 'PoolChange', - 'fields': [ - { - 'name': 'user', - 'type': 'publicKey', - 'index': false - }, - { - 'name': 'pool', - 'type': 'publicKey', - 'index': false - }, - { - 'name': 'tokenMint', - 'type': 'publicKey', - 'index': false - }, - { - 'name': 'action', - 'type': { - 'defined': 'PoolAction' + 'name': 'PoolCreated', + 'type': { + 'kind': 'struct', + 'fields': [ + { + 'name': 'user', + 'type': 'pubkey' }, - 'index': false - }, - { - 'name': 'amount', - 'type': 'u64', - 'index': false - }, - { - 'name': 'postLiquidity', - 'type': 'u64', - 'index': false - }, - { - 'name': 'lpSupply', - 'type': 'u64', - 'index': false - } - ] - } - ], - 'errors': [ - { - 'code': 6000, - 'name': 'GenericError', - 'msg': 'Something went wrong' - }, - { - 'code': 6001, - 'name': 'Unauthorized', - 'msg': 'Unauthorized' + { + 'name': 'authority', + 'type': 'pubkey' + }, + { + 'name': 'pool', + 'type': 'pubkey' + }, + { + 'name': 'tokenMint', + 'type': 'pubkey' + } + ] + } } ] -}; +} export const IDL: Gamba = { - version: '0.1.0', - name: 'gamba', + address: 'Gamba2hK6KV3quKq854B3sQG1WMdq3zgQLPKqyK4qS18', + metadata: { + name: 'gamba', + version: '0.1.0', + spec: '0.1.0', + }, instructions: [ { name: 'gambaInitialize', + discriminator: [ + 255, + 140, + 190, + 102, + 152, + 30, + 179, + 112, + ], accounts: [ { name: 'initializer', - isMut: true, - isSigner: true, + writable: true, + signer: true, }, { name: 'gambaState', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'GAMBA_STATE', + value: [ + 34, + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69, + 34, + ], }, ], }, }, - { - name: 'systemProgram', - isMut: false, - isSigner: false, - }, + { name: 'systemProgram' }, ], args: [], }, { name: 'gambaSetAuthority', + discriminator: [ + 60, + 11, + 159, + 59, + 150, + 12, + 106, + 78, + ], accounts: [ { name: 'authority', - isMut: true, - isSigner: true, + writable: true, + signer: true, }, { name: 'gambaState', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'GAMBA_STATE', + value: [ + 34, + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69, + 34, + ], }, ], }, @@ -2259,28 +2913,50 @@ export const IDL: Gamba = { args: [ { name: 'authority', - type: 'publicKey', + type: 'pubkey', }, ], }, { name: 'gambaSetConfig', + discriminator: [ + 205, + 11, + 209, + 24, + 204, + 47, + 25, + 186, + ], accounts: [ { name: 'authority', - isMut: true, - isSigner: true, + writable: true, + signer: true, }, { name: 'gambaState', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'GAMBA_STATE', + value: [ + 34, + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69, + 34, + ], }, ], }, @@ -2292,7 +2968,7 @@ export const IDL: Gamba = { args: [ { name: 'rngAddress', - type: 'publicKey', + type: 'pubkey', }, { name: 'gambaFee', @@ -2364,57 +3040,78 @@ export const IDL: Gamba = { }, { name: 'distributionRecipient', - type: 'publicKey', + type: 'pubkey', }, ], }, { name: 'poolInitialize', + discriminator: [ + 37, + 10, + 195, + 69, + 4, + 213, + 88, + 173, + ], accounts: [ { name: 'initializer', - isMut: true, - isSigner: true, + writable: true, + signer: true, }, { name: 'gambaState', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'GAMBA_STATE', + value: [ + 34, + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69, + 34, + ], }, ], }, }, - { - name: 'underlyingTokenMint', - isMut: false, - isSigner: false, - }, + { name: 'underlyingTokenMint' }, { name: 'pool', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'POOL', + value: [ + 34, + 80, + 79, + 79, + 76, + 34, + ], }, { kind: 'account', - type: 'publicKey', - account: 'Mint', path: 'underlying_token_mint', + account: 'Mint', }, { kind: 'arg', - type: 'publicKey', path: 'pool_authority', }, ], @@ -2422,253 +3119,323 @@ export const IDL: Gamba = { }, { name: 'poolUnderlyingTokenAccount', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'POOL_ATA', + value: [ + 34, + 80, + 79, + 79, + 76, + 95, + 65, + 84, + 65, + 34, + ], }, { kind: 'account', - type: 'publicKey', - account: 'Pool', path: 'pool', + account: 'Pool', }, ], }, }, { name: 'poolBonusUnderlyingTokenAccount', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'POOL_BONUS_UNDERLYING_TA', + value: [ + 34, + 80, + 79, + 79, + 76, + 95, + 66, + 79, + 78, + 85, + 83, + 95, + 85, + 78, + 68, + 69, + 82, + 76, + 89, + 73, + 78, + 71, + 95, + 84, + 65, + 34, + ], }, { kind: 'account', - type: 'publicKey', - account: 'Pool', path: 'pool', + account: 'Pool', }, ], }, }, { name: 'poolJackpotTokenAccount', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'POOL_JACKPOT', + value: [ + 34, + 80, + 79, + 79, + 76, + 95, + 74, + 65, + 67, + 75, + 80, + 79, + 84, + 34, + ], }, { kind: 'account', - type: 'publicKey', - account: 'Pool', path: 'pool', + account: 'Pool', }, ], }, }, { name: 'gambaStateAta', - isMut: true, - isSigner: false, + writable: true, }, { name: 'lpMint', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'POOL_LP_MINT', + value: [ + 34, + 80, + 79, + 79, + 76, + 95, + 76, + 80, + 95, + 77, + 73, + 78, + 84, + 34, + ], }, { kind: 'account', - type: 'publicKey', - account: 'Pool', path: 'pool', + account: 'Pool', }, ], }, }, { name: 'lpMintMetadata', - isMut: true, - isSigner: false, + writable: true, }, { name: 'bonusMint', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'POOL_BONUS_MINT', + value: [ + 34, + 80, + 79, + 79, + 76, + 95, + 66, + 79, + 78, + 85, + 83, + 95, + 77, + 73, + 78, + 84, + 34, + ], }, { kind: 'account', - type: 'publicKey', - account: 'Pool', path: 'pool', + account: 'Pool', }, ], }, }, { name: 'bonusMintMetadata', - isMut: true, - isSigner: false, - }, - { - name: 'associatedTokenProgram', - isMut: false, - isSigner: false, - }, - { - name: 'tokenProgram', - isMut: false, - isSigner: false, - }, - { - name: 'systemProgram', - isMut: false, - isSigner: false, - }, - { - name: 'rent', - isMut: false, - isSigner: false, - }, - { - name: 'tokenMetadataProgram', - isMut: false, - isSigner: false, + writable: true, }, + { name: 'associatedTokenProgram' }, + { name: 'tokenProgram' }, + { name: 'systemProgram' }, + { name: 'rent' }, + { name: 'tokenMetadataProgram' }, ], args: [ { name: 'poolAuthority', - type: 'publicKey', + type: 'pubkey', }, { name: 'lookupAddress', - type: 'publicKey', + type: 'pubkey', }, ], }, { name: 'poolDeposit', + discriminator: [ + 26, + 109, + 164, + 79, + 207, + 145, + 204, + 217, + ], accounts: [ { name: 'user', - isMut: true, - isSigner: true, + writable: true, + signer: true, }, { name: 'gambaState', - isMut: false, - isSigner: false, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'GAMBA_STATE', + value: [ + 34, + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69, + 34, + ], }, ], }, }, { name: 'pool', - isMut: true, - isSigner: false, + writable: true, }, { name: 'poolUnderlyingTokenAccount', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'POOL_ATA', + value: [ + 34, + 80, + 79, + 79, + 76, + 95, + 65, + 84, + 65, + 34, + ], }, { kind: 'account', - type: 'publicKey', - account: 'Pool', path: 'pool', + account: 'Pool', }, ], }, }, { name: 'lpMint', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'POOL_LP_MINT', + value: [ + 34, + 80, + 79, + 79, + 76, + 95, + 76, + 80, + 95, + 77, + 73, + 78, + 84, + 34, + ], }, { kind: 'account', - type: 'publicKey', - account: 'Pool', path: 'pool', + account: 'Pool', }, ], }, }, - { - name: 'underlyingTokenMint', - isMut: false, - isSigner: false, - }, + { name: 'underlyingTokenMint' }, { name: 'userUnderlyingAta', - isMut: true, - isSigner: false, + writable: true, }, { name: 'userLpAta', - isMut: true, - isSigner: false, - }, - { - name: 'associatedTokenProgram', - isMut: false, - isSigner: false, - }, - { - name: 'tokenProgram', - isMut: false, - isSigner: false, - }, - { - name: 'systemProgram', - isMut: false, - isSigner: false, + writable: true, }, + { name: 'associatedTokenProgram' }, + { name: 'tokenProgram' }, + { name: 'systemProgram' }, ], args: [ { @@ -2679,101 +3446,123 @@ export const IDL: Gamba = { }, { name: 'poolWithdraw', + discriminator: [ + 50, + 1, + 23, + 25, + 135, + 221, + 159, + 182, + ], accounts: [ { name: 'user', - isMut: true, - isSigner: true, + writable: true, + signer: true, }, { name: 'pool', - isMut: true, - isSigner: false, + writable: true, }, { name: 'poolUnderlyingTokenAccount', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'POOL_ATA', + value: [ + 34, + 80, + 79, + 79, + 76, + 95, + 65, + 84, + 65, + 34, + ], }, { kind: 'account', - type: 'publicKey', - account: 'Pool', path: 'pool', + account: 'Pool', }, ], }, }, { name: 'lpMint', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'POOL_LP_MINT', + value: [ + 34, + 80, + 79, + 79, + 76, + 95, + 76, + 80, + 95, + 77, + 73, + 78, + 84, + 34, + ], }, { kind: 'account', - type: 'publicKey', - account: 'Pool', path: 'pool', + account: 'Pool', }, ], }, }, - { - name: 'underlyingTokenMint', - isMut: false, - isSigner: false, - }, + { name: 'underlyingTokenMint' }, { name: 'userUnderlyingAta', - isMut: true, - isSigner: false, + writable: true, }, { name: 'userLpAta', - isMut: true, - isSigner: false, + writable: true, }, { name: 'gambaState', - isMut: false, - isSigner: false, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'GAMBA_STATE', + value: [ + 34, + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69, + 34, + ], }, ], }, }, - { - name: 'associatedTokenProgram', - isMut: false, - isSigner: false, - }, - { - name: 'tokenProgram', - isMut: false, - isSigner: false, - }, - { - name: 'systemProgram', - isMut: false, - isSigner: false, - }, + { name: 'associatedTokenProgram' }, + { name: 'tokenProgram' }, + { name: 'systemProgram' }, ], args: [ { @@ -2784,121 +3573,171 @@ export const IDL: Gamba = { }, { name: 'poolMintBonusTokens', + discriminator: [ + 105, + 130, + 72, + 25, + 88, + 185, + 100, + 55, + ], accounts: [ { name: 'user', - isMut: true, - isSigner: true, - }, - { - name: 'pool', - isMut: false, - isSigner: false, + writable: true, + signer: true, }, + { name: 'pool' }, { name: 'gambaState', - isMut: false, - isSigner: false, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'GAMBA_STATE', + value: [ + 34, + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69, + 34, + ], }, ], }, }, - { - name: 'underlyingTokenMint', - isMut: false, - isSigner: false, - }, + { name: 'underlyingTokenMint' }, { name: 'poolBonusUnderlyingTokenAccount', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'POOL_BONUS_UNDERLYING_TA', + value: [ + 34, + 80, + 79, + 79, + 76, + 95, + 66, + 79, + 78, + 85, + 83, + 95, + 85, + 78, + 68, + 69, + 82, + 76, + 89, + 73, + 78, + 71, + 95, + 84, + 65, + 34, + ], }, { kind: 'account', - type: 'publicKey', - account: 'Pool', path: 'pool', + account: 'Pool', }, ], }, }, { name: 'bonusMint', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'POOL_BONUS_MINT', + value: [ + 34, + 80, + 79, + 79, + 76, + 95, + 66, + 79, + 78, + 85, + 83, + 95, + 77, + 73, + 78, + 84, + 34, + ], }, { kind: 'account', - type: 'publicKey', - account: 'Pool', path: 'pool', + account: 'Pool', }, ], }, }, { name: 'poolJackpotTokenAccount', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'POOL_JACKPOT', + value: [ + 34, + 80, + 79, + 79, + 76, + 95, + 74, + 65, + 67, + 75, + 80, + 79, + 84, + 34, + ], }, { kind: 'account', - type: 'publicKey', - account: 'Pool', path: 'pool', + account: 'Pool', }, ], }, }, { name: 'userUnderlyingAta', - isMut: true, - isSigner: false, + writable: true, }, { name: 'userBonusAta', - isMut: true, - isSigner: false, - }, - { - name: 'associatedTokenProgram', - isMut: false, - isSigner: false, - }, - { - name: 'tokenProgram', - isMut: false, - isSigner: false, - }, - { - name: 'systemProgram', - isMut: false, - isSigner: false, + writable: true, }, + { name: 'associatedTokenProgram' }, + { name: 'tokenProgram' }, + { name: 'systemProgram' }, ], args: [ { @@ -2909,30 +3748,50 @@ export const IDL: Gamba = { }, { name: 'poolAuthorityConfig', + discriminator: [ + 58, + 12, + 184, + 118, + 14, + 99, + 110, + 17, + ], accounts: [ { name: 'user', - isMut: true, - isSigner: true, + writable: true, + signer: true, }, { name: 'gambaState', - isMut: false, - isSigner: false, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'GAMBA_STATE', + value: [ + 34, + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69, + 34, + ], }, ], }, }, { name: 'pool', - isMut: true, - isSigner: false, + writable: true, }, ], args: [ @@ -2970,36 +3829,56 @@ export const IDL: Gamba = { }, { name: 'depositWhitelistAddress', - type: 'publicKey', + type: 'pubkey', }, ], }, { name: 'poolGambaConfig', + discriminator: [ + 197, + 177, + 234, + 111, + 246, + 248, + 20, + 155, + ], accounts: [ { name: 'user', - isMut: true, - isSigner: true, + writable: true, + signer: true, }, { name: 'gambaState', - isMut: false, - isSigner: false, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'GAMBA_STATE', + value: [ + 34, + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69, + 34, + ], }, ], }, }, { name: 'pool', - isMut: true, - isSigner: false, + writable: true, }, ], args: [ @@ -3019,21 +3898,37 @@ export const IDL: Gamba = { }, { name: 'playerInitialize', + discriminator: [ + 213, + 160, + 145, + 88, + 197, + 68, + 63, + 150, + ], accounts: [ { name: 'player', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'PLAYER', + value: [ + 34, + 80, + 76, + 65, + 89, + 69, + 82, + 34, + ], }, { kind: 'account', - type: 'publicKey', path: 'user', }, ], @@ -3041,18 +3936,22 @@ export const IDL: Gamba = { }, { name: 'game', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'GAME', + value: [ + 34, + 71, + 65, + 77, + 69, + 34, + ], }, { kind: 'account', - type: 'publicKey', path: 'user', }, ], @@ -3060,39 +3959,51 @@ export const IDL: Gamba = { }, { name: 'user', - isMut: true, - isSigner: true, - }, - { - name: 'systemProgram', - isMut: false, - isSigner: false, + writable: true, + signer: true, }, + { name: 'systemProgram' }, ], args: [], }, { name: 'playGame', + discriminator: [ + 37, + 88, + 207, + 85, + 42, + 144, + 122, + 197, + ], accounts: [ { name: 'user', - isMut: true, - isSigner: true, + writable: true, + signer: true, }, { name: 'player', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'PLAYER', + value: [ + 34, + 80, + 76, + 65, + 89, + 69, + 82, + 34, + ], }, { kind: 'account', - type: 'publicKey', path: 'user', }, ], @@ -3100,18 +4011,22 @@ export const IDL: Gamba = { }, { name: 'game', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'GAME', + value: [ + 34, + 71, + 65, + 77, + 69, + 34, + ], }, { kind: 'account', - type: 'publicKey', path: 'user', }, ], @@ -3119,115 +4034,127 @@ export const IDL: Gamba = { }, { name: 'pool', - isMut: true, - isSigner: false, - }, - { - name: 'underlyingTokenMint', - isMut: false, - isSigner: false, + writable: true, }, + { name: 'underlyingTokenMint' }, { name: 'bonusTokenMint', - isMut: false, - isSigner: false, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'POOL_BONUS_MINT', + value: [ + 34, + 80, + 79, + 79, + 76, + 95, + 66, + 79, + 78, + 85, + 83, + 95, + 77, + 73, + 78, + 84, + 34, + ], }, { kind: 'account', - type: 'publicKey', - account: 'Pool', path: 'pool', + account: 'Pool', }, ], }, }, { name: 'userUnderlyingAta', - isMut: true, - isSigner: false, - }, - { - name: 'creator', - isMut: false, - isSigner: false, + writable: true, }, + { name: 'creator' }, { name: 'creatorAta', - isMut: true, - isSigner: false, + writable: true, }, { name: 'playerAta', - isMut: true, - isSigner: false, + writable: true, }, { name: 'playerBonusAta', - isMut: true, - isSigner: false, - isOptional: true, + writable: true, + optional: true, }, { name: 'userBonusAta', - isMut: true, - isSigner: false, - isOptional: true, + writable: true, + optional: true, }, { name: 'gambaState', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'GAMBA_STATE', + value: [ + 34, + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69, + 34, + ], }, ], }, }, { name: 'poolJackpotTokenAccount', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'POOL_JACKPOT', + value: [ + 34, + 80, + 79, + 79, + 76, + 95, + 74, + 65, + 67, + 75, + 80, + 79, + 84, + 34, + ], }, { kind: 'account', - type: 'publicKey', - account: 'Pool', path: 'pool', + account: 'Pool', }, ], }, }, - { - name: 'systemProgram', - isMut: false, - isSigner: false, - }, - { - name: 'tokenProgram', - isMut: false, - isSigner: false, - }, - { - name: 'associatedTokenProgram', - isMut: false, - isSigner: false, - }, + { name: 'systemProgram' }, + { name: 'tokenProgram' }, + { name: 'associatedTokenProgram' }, ], args: [ { @@ -3258,26 +4185,42 @@ export const IDL: Gamba = { }, { name: 'playerClose', + discriminator: [ + 26, + 155, + 61, + 179, + 53, + 157, + 80, + 30, + ], accounts: [ { name: 'user', - isMut: true, - isSigner: true, + writable: true, + signer: true, }, { name: 'player', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'PLAYER', + value: [ + 34, + 80, + 76, + 65, + 89, + 69, + 82, + 34, + ], }, { kind: 'account', - type: 'publicKey', path: 'user', }, ], @@ -3285,18 +4228,22 @@ export const IDL: Gamba = { }, { name: 'game', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'GAME', + value: [ + 34, + 71, + 65, + 77, + 69, + 34, + ], }, { kind: 'account', - type: 'publicKey', path: 'user', }, ], @@ -3307,31 +4254,43 @@ export const IDL: Gamba = { }, { name: 'playerClaim', + discriminator: [ + 188, + 220, + 237, + 31, + 181, + 18, + 85, + 45, + ], accounts: [ { name: 'user', - isMut: true, - isSigner: true, - }, - { - name: 'underlyingTokenMint', - isMut: false, - isSigner: false, + writable: true, + signer: true, }, + { name: 'underlyingTokenMint' }, { name: 'player', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'PLAYER', + value: [ + 34, + 80, + 76, + 65, + 89, + 69, + 82, + 34, + ], }, { kind: 'account', - type: 'publicKey', path: 'user', }, ], @@ -3339,18 +4298,22 @@ export const IDL: Gamba = { }, { name: 'game', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'GAME', + value: [ + 34, + 71, + 65, + 77, + 69, + 34, + ], }, { kind: 'account', - type: 'publicKey', path: 'user', }, ], @@ -3358,59 +4321,60 @@ export const IDL: Gamba = { }, { name: 'playerAta', - isMut: true, - isSigner: false, + writable: true, }, { name: 'userUnderlyingAta', - isMut: true, - isSigner: false, - }, - { - name: 'systemProgram', - isMut: false, - isSigner: false, - }, - { - name: 'tokenProgram', - isMut: false, - isSigner: false, - }, - { - name: 'associatedTokenProgram', - isMut: false, - isSigner: false, + writable: true, }, + { name: 'systemProgram' }, + { name: 'tokenProgram' }, + { name: 'associatedTokenProgram' }, ], args: [], }, { name: 'rngSettle', + discriminator: [ + 23, + 35, + 236, + 185, + 14, + 171, + 26, + 222, + ], accounts: [ { name: 'rng', - isMut: true, - isSigner: true, + writable: true, + signer: true, }, { name: 'user', - isMut: true, - isSigner: false, + writable: true, }, { name: 'player', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'PLAYER', + value: [ + 34, + 80, + 76, + 65, + 89, + 69, + 82, + 34, + ], }, { kind: 'account', - type: 'publicKey', path: 'user', }, ], @@ -3418,18 +4382,22 @@ export const IDL: Gamba = { }, { name: 'game', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'GAME', + value: [ + 34, + 71, + 65, + 77, + 69, + 34, + ], }, { kind: 'account', - type: 'publicKey', path: 'user', }, ], @@ -3437,179 +4405,225 @@ export const IDL: Gamba = { }, { name: 'pool', - isMut: true, - isSigner: false, - }, - { - name: 'underlyingTokenMint', - isMut: false, - isSigner: false, + writable: true, }, + { name: 'underlyingTokenMint' }, { name: 'poolUnderlyingTokenAccount', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'POOL_ATA', + value: [ + 34, + 80, + 79, + 79, + 76, + 95, + 65, + 84, + 65, + 34, + ], }, { kind: 'account', - type: 'publicKey', - account: 'Pool', path: 'pool', + account: 'Pool', }, ], }, }, { name: 'poolBonusUnderlyingTokenAccount', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'POOL_BONUS_UNDERLYING_TA', + value: [ + 34, + 80, + 79, + 79, + 76, + 95, + 66, + 79, + 78, + 85, + 83, + 95, + 85, + 78, + 68, + 69, + 82, + 76, + 89, + 73, + 78, + 71, + 95, + 84, + 65, + 34, + ], }, { kind: 'account', - type: 'publicKey', - account: 'Pool', path: 'pool', + account: 'Pool', }, ], }, }, { name: 'playerAta', - isMut: true, - isSigner: false, + writable: true, }, { name: 'userUnderlyingAta', - isMut: true, - isSigner: false, + writable: true, }, { name: 'gambaState', - isMut: false, - isSigner: false, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'GAMBA_STATE', + value: [ + 34, + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69, + 34, + ], }, ], }, }, { name: 'gambaStateAta', - isMut: true, - isSigner: false, - }, - { - name: 'creator', - isMut: false, - isSigner: false, + writable: true, }, + { name: 'creator' }, { name: 'creatorAta', - isMut: true, - isSigner: false, + writable: true, }, { name: 'bonusTokenMint', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'POOL_BONUS_MINT', + value: [ + 34, + 80, + 79, + 79, + 76, + 95, + 66, + 79, + 78, + 85, + 83, + 95, + 77, + 73, + 78, + 84, + 34, + ], }, { kind: 'account', - type: 'publicKey', - account: 'Pool', path: 'pool', + account: 'Pool', }, ], }, }, { name: 'playerBonusAta', - isMut: true, - isSigner: false, - isOptional: true, + writable: true, + optional: true, }, { name: 'poolJackpotTokenAccount', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'POOL_JACKPOT', + value: [ + 34, + 80, + 79, + 79, + 76, + 95, + 74, + 65, + 67, + 75, + 80, + 79, + 84, + 34, + ], }, { kind: 'account', - type: 'publicKey', - account: 'Pool', path: 'pool', + account: 'Pool', }, ], }, }, { name: 'escrowTokenAccount', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'ESCROW', + value: [ + 34, + 69, + 83, + 67, + 82, + 79, + 87, + 34, + ], }, { kind: 'account', - type: 'publicKey', - account: 'Player', path: 'player', + account: 'Player', }, ], }, }, - { - name: 'systemProgram', - isMut: false, - isSigner: false, - }, - { - name: 'tokenProgram', - isMut: false, - isSigner: false, - }, - { - name: 'associatedTokenProgram', - isMut: false, - isSigner: false, - }, - { - name: 'rent', - isMut: false, - isSigner: false, - }, + { name: 'systemProgram' }, + { name: 'tokenProgram' }, + { name: 'associatedTokenProgram' }, + { name: 'rent' }, ], args: [ { @@ -3624,27 +4638,48 @@ export const IDL: Gamba = { }, { name: 'rngProvideHashedSeed', + discriminator: [ + 238, + 154, + 25, + 143, + 191, + 19, + 25, + 224, + ], accounts: [ { name: 'game', - isMut: true, - isSigner: false, + writable: true, }, { name: 'rng', - isMut: true, - isSigner: true, + writable: true, + signer: true, }, { name: 'gambaState', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'GAMBA_STATE', + value: [ + 34, + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69, + 34, + ], }, ], }, @@ -3659,61 +4694,64 @@ export const IDL: Gamba = { }, { name: 'distributeFees', + discriminator: [ + 120, + 56, + 27, + 7, + 53, + 176, + 113, + 186, + ], accounts: [ { name: 'signer', - isMut: true, - isSigner: true, - }, - { - name: 'underlyingTokenMint', - isMut: false, - isSigner: false, + writable: true, + signer: true, }, + { name: 'underlyingTokenMint' }, { name: 'gambaState', - isMut: true, - isSigner: false, + writable: true, pda: { seeds: [ { kind: 'const', - type: 'string', - value: 'GAMBA_STATE', + value: [ + 34, + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69, + 34, + ], }, ], }, }, { name: 'gambaStateAta', - isMut: true, - isSigner: false, + writable: true, }, { name: 'distributionRecipient', - isMut: true, - isSigner: false, + writable: true, }, { name: 'distributionRecipientAta', - isMut: true, - isSigner: false, - }, - { - name: 'associatedTokenProgram', - isMut: false, - isSigner: false, - }, - { - name: 'tokenProgram', - isMut: false, - isSigner: false, - }, - { - name: 'systemProgram', - isMut: false, - isSigner: false, + writable: true, }, + { name: 'associatedTokenProgram' }, + { name: 'tokenProgram' }, + { name: 'systemProgram' }, ], args: [ { @@ -3725,7 +4763,188 @@ export const IDL: Gamba = { ], accounts: [ { - name: 'game', + name: 'Game', + discriminator: [ + 27, + 90, + 166, + 125, + 74, + 100, + 121, + 18, + ], + }, + { + name: 'Player', + discriminator: [ + 205, + 222, + 112, + 7, + 165, + 155, + 206, + 218, + ], + }, + { + name: 'Pool', + discriminator: [ + 241, + 154, + 109, + 4, + 17, + 177, + 109, + 188, + ], + }, + { + name: 'GambaState', + discriminator: [ + 142, + 203, + 14, + 224, + 153, + 118, + 52, + 200, + ], + }, + ], + events: [ + { + name: 'GameSettled', + discriminator: [ + 63, + 109, + 128, + 85, + 229, + 63, + 167, + 176, + ], + }, + { + name: 'PoolChange', + discriminator: [ + 241, + 7, + 155, + 154, + 56, + 57, + 0, + 101, + ], + }, + { + name: 'PoolCreated', + discriminator: [ + 202, + 44, + 41, + 88, + 104, + 220, + 157, + 82, + ], + }, + ], + errors: [ + { + code: 6000, + name: 'GenericError', + msg: 'Something went wrong', + }, + { + code: 6001, + name: 'Unauthorized', + msg: 'Unauthorized', + }, + { + code: 6002, + name: 'CustomPoolFeeExceedsLimit', + msg: 'Custom pool fee cannot exceed 100%', + }, + { + code: 6003, + name: 'CustomMaxPayoutExceedsLimit', + msg: 'Custom max payout cannot exceed 50%', + }, + ], + types: [ + { + name: 'PlayerError', + type: { + kind: 'enum', + variants: [ + { name: 'NotReadyToPlay' }, + { name: 'CreatorFeeTooHigh' }, + { name: 'WagerTooSmall' }, + { name: 'TooFewBetOutcomes' }, + { name: 'TooManyBetOutcomes' }, + { name: 'PlayerAdvantage' }, + { name: 'HouseAdvantageTooHigh' }, + { name: 'MaxPayoutExceeded' }, + ], + }, + }, + { + name: 'RngError', + type: { + kind: 'enum', + variants: [ + { name: 'Generic' }, + { name: 'InitialHashedSeedAlreadyProvided' }, + { name: 'IncorrectRngSeed' }, + { name: 'ResultNotRequested' }, + ], + }, + }, + { + name: 'GambaStateError', + type: { + kind: 'enum', + variants: [ + { name: 'PlaysNotAllowed' }, + { name: 'DepositNotAllowed' }, + { name: 'WithdrawalNotAllowed' }, + { name: 'PoolCreationNotAllowed' }, + { name: 'DepositLimitExceeded' }, + { name: 'DepositWhitelistRequired' }, + ], + }, + }, + { + name: 'PoolAction', + type: { + kind: 'enum', + variants: [ + { name: 'Deposit' }, + { name: 'Withdraw' }, + ], + }, + }, + { + name: 'GameStatus', + type: { + kind: 'enum', + variants: [ + { name: 'None' }, + { name: 'NotInitialized' }, + { name: 'Ready' }, + { name: 'ResultRequested' }, + ], + }, + }, + { + name: 'Game', type: { kind: 'struct', fields: [ @@ -3744,19 +4963,19 @@ export const IDL: Gamba = { }, { name: 'user', - type: 'publicKey', + type: 'pubkey', }, { name: 'tokenMint', - type: 'publicKey', + type: 'pubkey', }, { name: 'pool', - type: 'publicKey', + type: 'pubkey', }, { name: 'status', - type: { defined: 'GameStatus' }, + type: { defined: { name: 'GameStatus' } }, }, { name: 'nextRngSeedHashed', @@ -3778,7 +4997,7 @@ export const IDL: Gamba = { }, { name: 'creator', - type: 'publicKey', + type: 'pubkey', }, { name: 'creatorMeta', @@ -3842,7 +5061,7 @@ export const IDL: Gamba = { }, { name: 'pointsAuthority', - type: 'publicKey', + type: 'pubkey', }, { name: 'metadata', @@ -3852,7 +5071,7 @@ export const IDL: Gamba = { }, }, { - name: 'player', + name: 'Player', type: { kind: 'struct', fields: [ @@ -3867,7 +5086,7 @@ export const IDL: Gamba = { }, { name: 'user', - type: 'publicKey', + type: 'pubkey', }, { name: 'nonce', @@ -3877,7 +5096,7 @@ export const IDL: Gamba = { }, }, { - name: 'pool', + name: 'Pool', type: { kind: 'struct', fields: [ @@ -3892,15 +5111,15 @@ export const IDL: Gamba = { }, { name: 'lookupAddress', - type: 'publicKey', + type: 'pubkey', }, { name: 'poolAuthority', - type: 'publicKey', + type: 'pubkey', }, { name: 'underlyingTokenMint', - type: 'publicKey', + type: 'pubkey', }, { name: 'antiSpamFeeExempt', @@ -3952,7 +5171,7 @@ export const IDL: Gamba = { }, { name: 'customBonusTokenMint', - type: 'publicKey', + type: 'pubkey', }, { name: 'customBonusToken', @@ -3972,27 +5191,27 @@ export const IDL: Gamba = { }, { name: 'depositWhitelistAddress', - type: 'publicKey', + type: 'pubkey', }, ], }, }, { - name: 'gambaState', + name: 'GambaState', type: { kind: 'struct', fields: [ { name: 'authority', - type: 'publicKey', + type: 'pubkey', }, { name: 'rngAddress', - type: 'publicKey', + type: 'pubkey', }, { name: 'rngAddress2', - type: 'publicKey', + type: 'pubkey', }, { name: 'antiSpamFee', @@ -4064,7 +5283,7 @@ export const IDL: Gamba = { }, { name: 'distributionRecipient', - type: 'publicKey', + type: 'pubkey', }, { name: 'bump', @@ -4078,260 +5297,177 @@ export const IDL: Gamba = { ], }, }, - ], - types: [ { - name: 'PlayerError', - type: { - kind: 'enum', - variants: [ - { name: 'NotReadyToPlay' }, - { name: 'CreatorFeeTooHigh' }, - { name: 'WagerTooSmall' }, - { name: 'TooFewBetOutcomes' }, - { name: 'TooManyBetOutcomes' }, - { name: 'PlayerAdvantage' }, - { name: 'HouseAdvantageTooHigh' }, - { name: 'MaxPayoutExceeded' }, - ], - }, - }, - { - name: 'RngError', - type: { - kind: 'enum', - variants: [ - { name: 'Generic' }, - { name: 'InitialHashedSeedAlreadyProvided' }, - { name: 'IncorrectRngSeed' }, - { name: 'ResultNotRequested' }, - ], - }, - }, - { - name: 'GambaStateError', + name: 'GameSettled', type: { - kind: 'enum', - variants: [ - { name: 'PlaysNotAllowed' }, - { name: 'DepositNotAllowed' }, - { name: 'WithdrawalNotAllowed' }, - { name: 'PoolCreationNotAllowed' }, - { name: 'DepositLimitExceeded' }, - { name: 'DepositWhitelistRequired' }, + kind: 'struct', + fields: [ + { + name: 'user', + type: 'pubkey', + }, + { + name: 'pool', + type: 'pubkey', + }, + { + name: 'tokenMint', + type: 'pubkey', + }, + { + name: 'creator', + type: 'pubkey', + }, + { + name: 'creatorFee', + type: 'u64', + }, + { + name: 'gambaFee', + type: 'u64', + }, + { + name: 'poolFee', + type: 'u64', + }, + { + name: 'jackpotFee', + type: 'u64', + }, + { + name: 'underlyingUsed', + type: 'u64', + }, + { + name: 'bonusUsed', + type: 'u64', + }, + { + name: 'wager', + type: 'u64', + }, + { + name: 'payout', + type: 'u64', + }, + { + name: 'multiplierBps', + type: 'u32', + }, + { + name: 'payoutFromBonusPool', + type: 'u64', + }, + { + name: 'payoutFromNormalPool', + type: 'u64', + }, + { + name: 'jackpotProbabilityUbps', + type: 'u64', + }, + { + name: 'jackpotResult', + type: 'u64', + }, + { + name: 'nonce', + type: 'u64', + }, + { + name: 'clientSeed', + type: 'string', + }, + { + name: 'resultIndex', + type: 'u64', + }, + { + name: 'bet', + type: { vec: 'u32' }, + }, + { + name: 'jackpotPayoutToUser', + type: 'u64', + }, + { + name: 'poolLiquidity', + type: 'u64', + }, + { + name: 'rngSeed', + type: 'string', + }, + { + name: 'nextRngSeedHashed', + type: 'string', + }, + { + name: 'metadata', + type: 'string', + }, ], }, }, { - name: 'PoolAction', + name: 'PoolChange', type: { - kind: 'enum', - variants: [ - { name: 'Deposit' }, - { name: 'Withdraw' }, + kind: 'struct', + fields: [ + { + name: 'user', + type: 'pubkey', + }, + { + name: 'pool', + type: 'pubkey', + }, + { + name: 'tokenMint', + type: 'pubkey', + }, + { + name: 'action', + type: { defined: { name: 'PoolAction' } }, + }, + { + name: 'amount', + type: 'u64', + }, + { + name: 'postLiquidity', + type: 'u64', + }, + { + name: 'lpSupply', + type: 'u64', + }, ], }, }, { - name: 'GameStatus', + name: 'PoolCreated', type: { - kind: 'enum', - variants: [ - { name: 'None' }, - { name: 'NotInitialized' }, - { name: 'Ready' }, - { name: 'ResultRequested' }, + kind: 'struct', + fields: [ + { + name: 'user', + type: 'pubkey', + }, + { + name: 'authority', + type: 'pubkey', + }, + { + name: 'pool', + type: 'pubkey', + }, + { + name: 'tokenMint', + type: 'pubkey', + }, ], }, }, ], - events: [ - { - name: 'GameSettled', - fields: [ - { - name: 'user', - type: 'publicKey', - index: false, - }, - { - name: 'pool', - type: 'publicKey', - index: false, - }, - { - name: 'tokenMint', - type: 'publicKey', - index: false, - }, - { - name: 'creator', - type: 'publicKey', - index: false, - }, - { - name: 'creatorFee', - type: 'u64', - index: false, - }, - { - name: 'gambaFee', - type: 'u64', - index: false, - }, - { - name: 'poolFee', - type: 'u64', - index: false, - }, - { - name: 'jackpotFee', - type: 'u64', - index: false, - }, - { - name: 'underlyingUsed', - type: 'u64', - index: false, - }, - { - name: 'bonusUsed', - type: 'u64', - index: false, - }, - { - name: 'wager', - type: 'u64', - index: false, - }, - { - name: 'payout', - type: 'u64', - index: false, - }, - { - name: 'multiplierBps', - type: 'u32', - index: false, - }, - { - name: 'payoutFromBonusPool', - type: 'u64', - index: false, - }, - { - name: 'payoutFromNormalPool', - type: 'u64', - index: false, - }, - { - name: 'jackpotProbabilityUbps', - type: 'u64', - index: false, - }, - { - name: 'jackpotResult', - type: 'u64', - index: false, - }, - { - name: 'nonce', - type: 'u64', - index: false, - }, - { - name: 'clientSeed', - type: 'string', - index: false, - }, - { - name: 'resultIndex', - type: 'u64', - index: false, - }, - { - name: 'bet', - type: { vec: 'u32' }, - index: false, - }, - { - name: 'jackpotPayoutToUser', - type: 'u64', - index: false, - }, - { - name: 'poolLiquidity', - type: 'u64', - index: false, - }, - { - name: 'rngSeed', - type: 'string', - index: false, - }, - { - name: 'nextRngSeedHashed', - type: 'string', - index: false, - }, - { - name: 'metadata', - type: 'string', - index: false, - }, - ], - }, - { - name: 'PoolChange', - fields: [ - { - name: 'user', - type: 'publicKey', - index: false, - }, - { - name: 'pool', - type: 'publicKey', - index: false, - }, - { - name: 'tokenMint', - type: 'publicKey', - index: false, - }, - { - name: 'action', - type: { defined: 'PoolAction' }, - index: false, - }, - { - name: 'amount', - type: 'u64', - index: false, - }, - { - name: 'postLiquidity', - type: 'u64', - index: false, - }, - { - name: 'lpSupply', - type: 'u64', - index: false, - }, - ], - }, - ], - errors: [ - { - code: 6000, - name: 'GenericError', - msg: 'Something went wrong', - }, - { - code: 6001, - name: 'Unauthorized', - msg: 'Unauthorized', - }, - ], } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index ab1de751..0e0d6d2d 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -9,8 +9,8 @@ export type GambaEvent = {name: string, data: IdlEvent export type AnyGambaEvent = GambaEvent<'GameSettled'> | GambaEvent<'PoolChange'> -export type GambaState = IdlAccounts['gambaState'] -export type PlayerState = IdlAccounts['player'] -export type GameState = IdlAccounts['game'] -export type PoolState = IdlAccounts['pool'] +export type GambaState = IdlAccounts['GambaState'] +export type PlayerState = IdlAccounts['Player'] +export type GameState = IdlAccounts['Game'] +export type PoolState = IdlAccounts['Pool'] export type GambaProviderWallet = Omit & {payer?: Keypair} diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 8e8bcd02..a9baf7a1 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -1,47 +1,77 @@ import { NATIVE_MINT } from '@solana/spl-token' -import { Connection, PublicKey } from '@solana/web3.js' -import { BPS_PER_WHOLE, GameState } from '.' +import { Connection, PublicKey, AccountInfo } from '@solana/web3.js' +import { BPS_PER_WHOLE } from '.' import { decodeGame } from './decoders' import { getGameAddress } from './pdas' +import { GameState } from './types' -export const basisPoints = (percent: number) => { - return Math.round(percent * BPS_PER_WHOLE) -} +export const basisPoints = (percent: number) => + Math.round(percent * BPS_PER_WHOLE) -export const isNativeMint = (pubkey: PublicKey) => NATIVE_MINT.equals(pubkey) +export const isNativeMint = (pubkey: PublicKey) => + NATIVE_MINT.equals(pubkey) -export const hmac256 = async (secretKey: string, message: string) => { - const encoder = new TextEncoder +export const hmac256 = async ( + secretKey: string, + message: string, +): Promise => { + const encoder = new TextEncoder() const messageUint8Array = encoder.encode(message) const keyUint8Array = encoder.encode(secretKey) - const cryptoKey = await crypto.subtle.importKey('raw', keyUint8Array, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']) + const cryptoKey = await crypto.subtle.importKey( + 'raw', + keyUint8Array, + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign'], + ) const signature = await crypto.subtle.sign('HMAC', cryptoKey, messageUint8Array) - return Array.from(new Uint8Array(signature)).map((b) => b.toString(16).padStart(2, '0')).join('') + return Array.from(new Uint8Array(signature)) + .map((b) => b.toString(16).padStart(2, '0')) + .join('') } -export const getGameHash = (rngSeed: string, clientSeed: string, nonce: number) => { - return hmac256(rngSeed, [clientSeed, nonce].join('-')) -} +export const getGameHash = (rngSeed: string, clientSeed: string, nonce: number) => + hmac256(rngSeed, [clientSeed, nonce].join('-')) -export const getResultNumber = async (rngSeed: string, clientSeed: string, nonce: number) => { +export const getResultNumber = async ( + rngSeed: string, + clientSeed: string, + nonce: number, +) => { const hash = await getGameHash(rngSeed, clientSeed, nonce) return parseInt(hash.substring(0, 5), 16) } -export type GameResult = ReturnType +// ─── Explicit, portable return type ───────────────────────────────────────── +export interface GameResult { + creator: PublicKey + user: PublicKey + rngSeed: string + clientSeed: string + nonce: number + bet: number[] + resultIndex: number + wager: number + payout: number + profit: number + multiplier: number + token: PublicKey + bonusUsed: number + jackpotWin: number +} -export const parseResult = ( - state: GameState, -) => { - const clientSeed = state.clientSeed - const bet = state.bet.map((x) => x / BPS_PER_WHOLE) - const nonce = state.nonce.toNumber() - 1 - const rngSeed = state.rngSeed +/** Parses a `GameState` into a plain object */ +export const parseResult = (state: GameState): GameResult => { + const clientSeed = state.clientSeed + const bet = state.bet.map((x) => x / BPS_PER_WHOLE) + const nonce = state.nonce.toNumber() - 1 + const rngSeed = state.rngSeed const resultIndex = state.result.toNumber() - const multiplier = bet[resultIndex] - const wager = state.wager.toNumber() - const payout = (wager * multiplier) - const profit = (payout - wager) + const multiplier = bet[resultIndex] + const wager = state.wager.toNumber() + const payout = wager * multiplier + const profit = payout - wager return { creator: state.creator, @@ -61,15 +91,16 @@ export const parseResult = ( } } +/** Waits for the next game‐account change and resolves with its parsed result */ export async function getNextResult( connection: Connection, user: PublicKey, prevNonce: number, -) { - return new Promise((resolve, reject) => { +): Promise { + return new Promise((resolve, reject) => { const listener = connection.onAccountChange( getGameAddress(user), - async (account) => { + async (account: AccountInfo) => { const current = decodeGame(account) if (!current) { connection.removeAccountChangeListener(listener) @@ -77,8 +108,8 @@ export async function getNextResult( } if (current.nonce.toNumber() === prevNonce + 1) { connection.removeAccountChangeListener(listener) - const result = await parseResult(current) - return resolve(result) + const result = parseResult(current) + resolve(result) } }, ) diff --git a/packages/multiplayer/package.json b/packages/multiplayer/package.json new file mode 100644 index 00000000..19d0c231 --- /dev/null +++ b/packages/multiplayer/package.json @@ -0,0 +1,42 @@ +{ + "name": "@gamba-labs/multiplayer-sdk", + "version": "0.1.2", + "private": false, + "description": "Gamba Multiplayer on-chain game helper layer (Anchor 0.31.1)", + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "files": [ + "dist/**", + "idl/**" + ], + "sideEffects": false, + "scripts": { + "dev": "tsup src/index.ts --watch --format cjs,esm --dts", + "build": "tsup src/index.ts --format cjs,esm --dts", + "lint": "tsc --noEmit", + "test": "vitest run", + "clean": "rm -rf .turbo node_modules dist" + }, + "peerDependencies": { + "@coral-xyz/anchor": "^0.31.1", + "@solana/web3.js": "^1.98.2", + "@solana/spl-token": "^0.4.13" + }, + "devDependencies": { + "@types/node": "^24.0.10", + "tsup": "^8.5.0", + "typescript": "^5.2.2", + "vitest": "^1.6.0" + }, + "publishConfig": { + "access": "public" + }, + "keywords": [ + "solana", + "anchor", + "gamba", + "multiplayer" + ], + "license": "MIT" +} diff --git a/packages/multiplayer/src/constants.ts b/packages/multiplayer/src/constants.ts new file mode 100644 index 00000000..8472f3c1 --- /dev/null +++ b/packages/multiplayer/src/constants.ts @@ -0,0 +1,19 @@ +import { AnchorProvider, Program, utils as anchorUtils, web3 } from "@coral-xyz/anchor"; +import rawIdl from "./idl/multiplayer.json" with { type: "json" }; +import type { Multiplayer } from "./types/multiplayer.js"; + +export const IDL = rawIdl as unknown as Multiplayer; +export const PROGRAM_ID = new web3.PublicKey(IDL.address); + +export const WRAPPED_SOL_MINT = new web3.PublicKey( + "So11111111111111111111111111111111111111112", +); + +export const getProgram = (p: AnchorProvider) => new Program(IDL, p); + +export function pda(seed: Uint8Array | Buffer | string[]) { + return web3.PublicKey.findProgramAddressSync( + Array.isArray(seed) ? seed.map(s => typeof s === "string" ? anchorUtils.bytes.utf8.encode(s) : s) : [seed], + PROGRAM_ID, + )[0]; +} diff --git a/packages/multiplayer/src/errors.ts b/packages/multiplayer/src/errors.ts new file mode 100644 index 00000000..427c36eb --- /dev/null +++ b/packages/multiplayer/src/errors.ts @@ -0,0 +1,27 @@ +import rawIdl from "./idl/multiplayer.json" with { type: "json" }; + +type IdlError = { code: number; name: string; msg: string }; + +export const ERROR_MAP = new Map( + // @ts-ignore: IDL shape includes errors + (rawIdl as any).errors.map((e: IdlError) => [e.code, e]), +); + +export function decodeAnchorError(err: unknown): string | null { + if (err && typeof err === "object" && "code" in err) { + const maybe = ERROR_MAP.get(Number((err as any).code)); + if (maybe) return `${maybe.name} (${maybe.code}): ${maybe.msg}`; + } + + if (typeof err === "string") { + const m = err.match(/custom program error: 0x([0-9a-f]+)/i); + if (m) { + const code = parseInt(m[1], 16); + const maybe = ERROR_MAP.get(code); + if (maybe) return `${maybe.name} (${maybe.code}): ${maybe.msg}`; + } + } + + return null; +} + \ No newline at end of file diff --git a/packages/multiplayer/src/events.ts b/packages/multiplayer/src/events.ts new file mode 100644 index 00000000..e8a645aa --- /dev/null +++ b/packages/multiplayer/src/events.ts @@ -0,0 +1,112 @@ +import { + AnchorProvider, + EventParser, + IdlEvents, + utils as anchorUtils, +} from "@coral-xyz/anchor"; +import type { IdlAccounts } from "@coral-xyz/anchor"; +import { PublicKey, Finality } from "@solana/web3.js"; + +import { Multiplayer } from "./types/multiplayer.js"; +import { PROGRAM_ID, getProgram } from "./index.js"; + +type AllEvents = IdlEvents; // union of all event names +export type EventName = keyof AllEvents; +export type EventData = AllEvents[N]; + +export interface ParsedEvent { + data : EventData; + signature : string; + slot : number; + blockTime : number | null; +} + +const FINALITY: Finality = "confirmed"; + +export async function fetchRecentEvents( + provider: AnchorProvider, + name: N, + howMany = 5, +): Promise[]> { + const { connection } = provider; + const program = getProgram(provider); + + const sigs = await connection.getSignaturesForAddress( + PROGRAM_ID, + { limit: howMany * 10 }, + FINALITY, + ); + + const txs = await connection.getTransactions( + sigs.map(s => s.signature), + { + maxSupportedTransactionVersion: 0, + commitment: FINALITY, + }, + ); + + const parser = new EventParser(PROGRAM_ID, program.coder); + const out: ParsedEvent[] = []; + + txs.forEach((tx, i) => { + const logs = tx?.meta?.logMessages; + if (!logs) return; + try { + for (const ev of parser.parseLogs(logs)) { + if (ev.name === name) { + out.push({ + data: ev.data as EventData, + signature: sigs[i].signature, + slot: sigs[i].slot, + blockTime: sigs[i].blockTime ?? null, + }); + } + } + } catch {} + }); + + return out + .sort((a, b) => b.slot - a.slot) // newest first + .slice(0, howMany); +} + +export const fetchRecentGameCreated = (p: AnchorProvider, n = 5) => + fetchRecentEvents(p, "gameCreated", n); + +export const fetchRecentPlayerJoined = (p: AnchorProvider, n = 5) => + fetchRecentEvents(p, "playerJoined", n); + +export const fetchRecentPlayerLeft = (p: AnchorProvider, n = 5) => + fetchRecentEvents(p, "playerLeft", n); + +export const fetchRecentGameSettledPartial = (p: AnchorProvider, n = 5) => + fetchRecentEvents(p, "gameSettledPartial", n); + +export const fetchRecentWinnersSelected = (p: AnchorProvider, n = 5) => + fetchRecentEvents(p, "winnersSelected", n); + +export async function fetchRecentSpecificWinners( + provider : AnchorProvider, + creator : PublicKey, + maxPlayers : number, + howMany = 8, +): Promise[]> { + const raw = await fetchRecentEvents(provider, "winnersSelected", howMany * 5); + if (!raw.length) return []; + + return raw + .filter(ev => + ev.data.gameMaker.equals(creator) && + ev.data.maxPlayers === maxPlayers + ) + .slice(0, howMany); +} + +export function getEventDiscriminator(name: N): Uint8Array { + const hex = anchorUtils.sha256.hash(`event:${name}`); + const bytes = new Uint8Array(8); + for (let i = 0; i < 8; i++) { + bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16); + } + return bytes; +} diff --git a/packages/multiplayer/src/fetch.ts b/packages/multiplayer/src/fetch.ts new file mode 100644 index 00000000..ac3ea13a --- /dev/null +++ b/packages/multiplayer/src/fetch.ts @@ -0,0 +1,137 @@ +/* ------------------------------------------------------------------ + fetch.ts – tiny helpers around Anchor’s account APIs + Anchor v0.31.1 • Solana web3.js v1.98.2 +------------------------------------------------------------------- */ +import type { AnchorProvider, IdlAccounts } from "@coral-xyz/anchor"; +import { BN, utils as anchorUtils, web3 } from "@coral-xyz/anchor"; +import { PublicKey } from "@solana/web3.js"; + +import type { Multiplayer } from "./types/multiplayer.js"; +import { getProgram, PROGRAM_ID } from "./constants.js"; +import { + deriveGamePdaFromSeed, + deriveMetadataPda, +} from "./utils/pda.js"; + +/** Full shape of a fetched Game account + its public key */ +export type GameAccountFull = { + publicKey: PublicKey; + account: IdlAccounts["game"]; +}; + +/** + * Fetch all Multiplayer game accounts. + */ +export const fetchGames = async ( + provider: AnchorProvider, + filter?: (g: GameAccountFull) => boolean, +): Promise => { + const program = getProgram(provider); + const games = await program.account.game.all(); + return filter ? games.filter(filter) : games; +}; + +/** Optional filters for narrowing the Multiplayer games list */ +export type SpecificGameFilters = { + creator?: PublicKey; + maxPlayers?: number; + wagerType?: number; // enum repr as u8 + payoutType?: number; // enum repr as u8 + winnersTarget?: number; + mint?: PublicKey; +}; + +/** + * Fetch games by optional filters. Backwards compatible with the old + * (provider, creator, maxPlayers) signature. + */ +export function fetchSpecificGames( + provider: AnchorProvider, + filters: SpecificGameFilters, +): Promise; +export function fetchSpecificGames( + provider: AnchorProvider, + creator: PublicKey, + maxPlayers: number, +): Promise; +export async function fetchSpecificGames( + provider: AnchorProvider, + arg1: SpecificGameFilters | PublicKey, + arg2?: number, +): Promise { + const filters: SpecificGameFilters = arg1 instanceof PublicKey + ? { creator: arg1, maxPlayers: arg2 } + : (arg1 ?? {}); + + return fetchGames(provider, (g) => { + const a = g.account as IdlAccounts["game"] & Record; + if (filters.creator && !a.gameMaker.equals(filters.creator)) return false; + if (filters.maxPlayers != null && a.maxPlayers !== filters.maxPlayers) return false; + if (filters.wagerType != null && Number(a.wagerType) !== Number(filters.wagerType)) return false; + if (filters.payoutType != null && Number(a.payoutType) !== Number(filters.payoutType)) return false; + if (filters.winnersTarget != null && Number(a.winnersTarget) !== Number(filters.winnersTarget)) return false; + if (filters.mint != null && !a.mint.equals(filters.mint)) return false; + return true; + }); +} + +/** Shape of the GambaState PDA */ +export type GambaStateFull = { + publicKey: PublicKey; + account: IdlAccounts["gambaState"]; +}; + +/** + * Fetch the global GambaState PDA. + */ +export const fetchGambaState = async ( + provider: AnchorProvider, +): Promise => { + const program = getProgram(provider); + const [gambaStatePk] = PublicKey.findProgramAddressSync( + [anchorUtils.bytes.utf8.encode("GAMBA_STATE")], + PROGRAM_ID, + ); + const account = await program.account.gambaState.fetch(gambaStatePk); + return { publicKey: gambaStatePk, account }; +}; + +/** Shape of the PlayerMetadataAccount PDA */ +export type MetadataAccountFull = { + publicKey: PublicKey; + account: IdlAccounts["playerMetadataAccount"]; +}; + +/** + * Fetch the on-chain metadata for all players in a game. + */ +export const fetchPlayerMetadata = async ( + provider: AnchorProvider, + gameSeed: BN | number, +): Promise> => { + const program = getProgram(provider); + + // derive the PDAs + const gamePda = deriveGamePdaFromSeed(gameSeed); + const metaPda = deriveMetadataPda(gamePda); + + // fetch & cast explicitly (parenthesized to avoid JSX/generic parse issues) + const metaAccount = (await program + .account + .playerMetadataAccount + .fetch(metaPda)) as IdlAccounts["playerMetadataAccount"]; + + const out: Record = {}; + const entries = (metaAccount as any).entries as Array<{ player: PublicKey; meta: number[] }>; + + for (const { player, meta } of entries) { + const buf = Uint8Array.from(meta); + // trim trailing zeros + let len = buf.length; + while (len > 0 && buf[len - 1] === 0) len--; + const str = new TextDecoder().decode(buf.slice(0, len)); + out[player.toBase58()] = str; + } + + return out; +}; diff --git a/packages/multiplayer/src/idl/multiplayer.json b/packages/multiplayer/src/idl/multiplayer.json new file mode 100644 index 00000000..a3146dfd --- /dev/null +++ b/packages/multiplayer/src/idl/multiplayer.json @@ -0,0 +1,2153 @@ +{ + "address": "GambaMyTW8C1NSeFrv2c3KfmX1MSDBF6YbxDeb7dBPxM", + "metadata": { + "name": "multiplayer", + "version": "0.1.0", + "spec": "0.1.0", + "description": "Created with Anchor" + }, + "instructions": [ + { + "name": "create_game_native", + "discriminator": [ + 14, + 237, + 122, + 202, + 102, + 88, + 48, + 75 + ], + "accounts": [ + { + "name": "game_account", + "docs": [ + "The on‐chain Game account, PDA = [b\"GAME\", game_seed]." + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 71, + 65, + 77, + 69 + ] + }, + { + "kind": "arg", + "path": "game_seed" + } + ] + } + }, + { + "name": "metadata_account", + "docs": [ + "Always-present metadata PDA, PDA = [b\"METADATA\", game_account.key()]" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 77, + 69, + 84, + 65, + 68, + 65, + 84, + 65 + ] + }, + { + "kind": "account", + "path": "game_account" + } + ] + } + }, + { + "name": "mint", + "docs": [ + "always SOL (placeholder mint)" + ] + }, + { + "name": "game_maker", + "docs": [ + "creator of the game, pays rent on init" + ], + "writable": true, + "signer": true + }, + { + "name": "gamba_state", + "docs": [ + "global config PDA" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69 + ] + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "pre_alloc_players", + "type": "u16" + }, + { + "name": "max_players", + "type": "u16" + }, + { + "name": "num_teams", + "type": "u8" + }, + { + "name": "winners_target", + "type": "u16" + }, + { + "name": "wager_type", + "type": "u8" + }, + { + "name": "payout_type", + "type": "u8" + }, + { + "name": "wager", + "type": "u64" + }, + { + "name": "soft_duration", + "type": "i64" + }, + { + "name": "hard_duration", + "type": "i64" + }, + { + "name": "game_seed", + "type": "u64" + }, + { + "name": "min_bet", + "type": "u64" + }, + { + "name": "max_bet", + "type": "u64" + } + ] + }, + { + "name": "create_game_spl", + "discriminator": [ + 80, + 235, + 44, + 243, + 14, + 15, + 248, + 207 + ], + "accounts": [ + { + "name": "game_account", + "docs": [ + "The on-chain Game account, PDA = [b\"GAME\", game_seed]." + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 71, + 65, + 77, + 69 + ] + }, + { + "kind": "arg", + "path": "game_seed" + } + ] + } + }, + { + "name": "metadata_account", + "docs": [ + "Always-present metadata PDA, PDA = [b\"METADATA\", game_account.key()]" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 77, + 69, + 84, + 65, + 68, + 65, + 84, + 65 + ] + }, + { + "kind": "account", + "path": "game_account" + } + ] + } + }, + { + "name": "mint", + "docs": [ + "the SPL mint for wagers" + ] + }, + { + "name": "game_account_ta_account", + "docs": [ + "escrow token account for this game" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "game_account" + } + ] + } + }, + { + "name": "game_maker", + "writable": true, + "signer": true + }, + { + "name": "gamba_state", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69 + ] + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "token_program", + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + } + ], + "args": [ + { + "name": "pre_alloc_players", + "type": "u16" + }, + { + "name": "max_players", + "type": "u16" + }, + { + "name": "num_teams", + "type": "u8" + }, + { + "name": "winners_target", + "type": "u16" + }, + { + "name": "wager_type", + "type": "u8" + }, + { + "name": "payout_type", + "type": "u8" + }, + { + "name": "wager", + "type": "u64" + }, + { + "name": "soft_duration", + "type": "i64" + }, + { + "name": "hard_duration", + "type": "i64" + }, + { + "name": "game_seed", + "type": "u64" + }, + { + "name": "min_bet", + "type": "u64" + }, + { + "name": "max_bet", + "type": "u64" + } + ] + }, + { + "name": "distribute_native", + "discriminator": [ + 32, + 200, + 172, + 49, + 57, + 234, + 137, + 89 + ], + "accounts": [ + { + "name": "payer", + "docs": [ + "Tx signer (fees & optional PDA‑close refund go here)" + ], + "signer": true + }, + { + "name": "gamba_state", + "docs": [ + "Global configuration – for fee vault + RNG auth" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69 + ] + } + ] + } + }, + { + "name": "game_account", + "docs": [ + "Game account (large PDA, parsed manually)" + ], + "writable": true + }, + { + "name": "game_maker", + "docs": [ + "Game‑maker – receives any closing lamports" + ], + "writable": true + }, + { + "name": "gamba_fee_address", + "docs": [ + "Protocol fee vault" + ], + "writable": true + }, + { + "name": "metadata_account", + "docs": [ + "Metadata PDA – to be closed at settlement" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 77, + 69, + 84, + 65, + 68, + 65, + 84, + 65 + ] + }, + { + "kind": "account", + "path": "game_account" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, + { + "name": "distribute_spl", + "discriminator": [ + 93, + 24, + 158, + 238, + 198, + 173, + 215, + 228 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "gamba_state", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69 + ] + } + ] + } + }, + { + "name": "game_account", + "docs": [ + "Raw Game PDA (parsed by offsets)" + ], + "writable": true + }, + { + "name": "metadata_account", + "docs": [ + "Always-present metadata PDA (to be closed on final settlement)" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 77, + 69, + 84, + 65, + 68, + 65, + 84, + 65 + ] + }, + { + "kind": "account", + "path": "game_account" + } + ] + } + }, + { + "name": "game_account_ta", + "docs": [ + "Game escrow ATA (PDA = [game_account])" + ], + "writable": true + }, + { + "name": "mint" + }, + { + "name": "gamba_fee_ata", + "docs": [ + "Protocol fee vault ATA for this mint" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "gamba_fee_address" + }, + { + "kind": "const", + "value": [ + 6, + 221, + 246, + 225, + 215, + 101, + 161, + 147, + 217, + 203, + 225, + 70, + 206, + 235, + 121, + 172, + 28, + 180, + 133, + 237, + 95, + 91, + 55, + 145, + 58, + 140, + 245, + 133, + 126, + 255, + 0, + 169 + ] + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "gamba_fee_address", + "docs": [ + "Protocol fee vault (owner of `gamba_fee_ata`)" + ], + "writable": true + }, + { + "name": "game_maker", + "docs": [ + "Game‑maker (receives escrow close lamports)" + ], + "writable": true + }, + { + "name": "creator_ata_0", + "writable": true, + "optional": true + }, + { + "name": "creator_ata_1", + "writable": true, + "optional": true + }, + { + "name": "creator_ata_2", + "writable": true, + "optional": true + }, + { + "name": "creator_ata_3", + "writable": true, + "optional": true + }, + { + "name": "creator_ata_4", + "writable": true, + "optional": true + }, + { + "name": "winner_ata_0", + "writable": true, + "optional": true + }, + { + "name": "winner_ata_1", + "writable": true, + "optional": true + }, + { + "name": "winner_ata_2", + "writable": true, + "optional": true + }, + { + "name": "winner_ata_3", + "writable": true, + "optional": true + }, + { + "name": "winner_ata_4", + "writable": true, + "optional": true + }, + { + "name": "winner_ata_5", + "writable": true, + "optional": true + }, + { + "name": "winner_ata_6", + "writable": true, + "optional": true + }, + { + "name": "winner_ata_7", + "writable": true, + "optional": true + }, + { + "name": "winner_ata_8", + "writable": true, + "optional": true + }, + { + "name": "winner_ata_9", + "writable": true, + "optional": true + }, + { + "name": "winner_ata_10", + "writable": true, + "optional": true + }, + { + "name": "winner_ata_11", + "writable": true, + "optional": true + }, + { + "name": "winner_ata_12", + "writable": true, + "optional": true + }, + { + "name": "winner_ata_13", + "writable": true, + "optional": true + }, + { + "name": "winner_ata_14", + "writable": true, + "optional": true + }, + { + "name": "token_program", + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, + { + "name": "gamba_config", + "discriminator": [ + 232, + 208, + 249, + 92, + 159, + 187, + 21, + 254 + ], + "accounts": [ + { + "name": "gamba_state", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69 + ] + } + ] + } + }, + { + "name": "authority", + "writable": true, + "signer": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "fee_vault", + "type": "pubkey" + }, + { + "name": "fee_bps", + "type": "u32" + }, + { + "name": "rng", + "type": "pubkey" + }, + { + "name": "authority", + "type": "pubkey" + } + ] + }, + { + "name": "join_game", + "discriminator": [ + 107, + 112, + 18, + 38, + 56, + 173, + 60, + 128 + ], + "accounts": [ + { + "name": "game_account", + "docs": [ + "Game account as raw bytes" + ], + "writable": true + }, + { + "name": "gamba_state", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69 + ] + } + ] + } + }, + { + "name": "game_account_ta", + "docs": [ + "Optional escrow token account (only for SPL games)" + ], + "writable": true, + "optional": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "game_account" + } + ] + } + }, + { + "name": "mint", + "docs": [ + "Mint used for wagers (native SOL or SPL)" + ] + }, + { + "name": "player_account", + "docs": [ + "Player joining (payer + signer)" + ], + "writable": true, + "signer": true + }, + { + "name": "player_ata", + "docs": [ + "Optional player ATA if SPL wager" + ], + "writable": true, + "optional": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "player_account" + }, + { + "kind": "const", + "value": [ + 6, + 221, + 246, + 225, + 215, + 101, + 161, + 147, + 217, + 203, + 225, + 70, + 206, + 235, + 121, + 172, + 28, + 180, + 133, + 237, + 95, + 91, + 55, + 145, + 58, + 140, + 245, + 133, + 126, + 255, + 0, + 169 + ] + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "creator_address", + "docs": [ + "Creator/referrer collecting a fee" + ] + }, + { + "name": "creator_ata", + "docs": [ + "Creator’s ATA (created lazily)" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "creator_address" + }, + { + "kind": "const", + "value": [ + 6, + 221, + 246, + 225, + 215, + 101, + 161, + 147, + 217, + 203, + 225, + 70, + 206, + 235, + 121, + 172, + 28, + 180, + 133, + 237, + 95, + 91, + 55, + 145, + 58, + 140, + 245, + 133, + 126, + 255, + 0, + 169 + ] + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "metadata_account", + "docs": [ + "Always-present metadata PDA" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 77, + 69, + 84, + 65, + 68, + 65, + 84, + 65 + ] + }, + { + "kind": "account", + "path": "game_account" + } + ] + } + }, + { + "name": "system_program", + "docs": [ + "Programs" + ], + "address": "11111111111111111111111111111111" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "token_program", + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "args": [ + { + "name": "creator_fee_bps", + "type": "u32" + }, + { + "name": "wager", + "type": "u64" + }, + { + "name": "team", + "type": "u8" + }, + { + "name": "player_meta", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + }, + { + "name": "leave_game", + "discriminator": [ + 218, + 226, + 6, + 0, + 243, + 34, + 125, + 201 + ], + "accounts": [ + { + "name": "game_account", + "docs": [ + "Game PDA – parsed manually" + ], + "writable": true + }, + { + "name": "mint" + }, + { + "name": "player_account", + "writable": true, + "signer": true + }, + { + "name": "player_ata", + "writable": true, + "optional": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "player_account" + }, + { + "kind": "const", + "value": [ + 6, + 221, + 246, + 225, + 215, + 101, + 161, + 147, + 217, + 203, + 225, + 70, + 206, + 235, + 121, + 172, + 28, + 180, + 133, + 237, + 95, + 91, + 55, + 145, + 58, + 140, + 245, + 133, + 126, + 255, + 0, + 169 + ] + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "game_account_ta", + "writable": true, + "optional": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "game_account" + } + ] + } + }, + { + "name": "metadata_account", + "docs": [ + "Always-present metadata PDA" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 77, + 69, + 84, + 65, + 68, + 65, + 84, + 65 + ] + }, + { + "kind": "account", + "path": "game_account" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "token_program", + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "args": [] + }, + { + "name": "select_winners", + "discriminator": [ + 80, + 100, + 28, + 131, + 83, + 199, + 222, + 80 + ], + "accounts": [ + { + "name": "rng", + "docs": [ + "Authorised RNG bot" + ], + "writable": true, + "signer": true + }, + { + "name": "gamba_state", + "docs": [ + "Global config – only used to enforce `rng`" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69 + ] + } + ] + } + }, + { + "name": "game_account", + "docs": [ + "Raw Game account; we parse fields by hand" + ], + "writable": true + } + ], + "args": [] + } + ], + "accounts": [ + { + "name": "GambaState", + "discriminator": [ + 142, + 203, + 14, + 224, + 153, + 118, + 52, + 200 + ] + }, + { + "name": "Game", + "discriminator": [ + 27, + 90, + 166, + 125, + 74, + 100, + 121, + 18 + ] + }, + { + "name": "PlayerMetadataAccount", + "discriminator": [ + 204, + 224, + 199, + 121, + 70, + 159, + 53, + 55 + ] + } + ], + "events": [ + { + "name": "GameCreated", + "discriminator": [ + 218, + 25, + 150, + 94, + 177, + 112, + 96, + 2 + ] + }, + { + "name": "GameSettledPartial", + "discriminator": [ + 208, + 36, + 152, + 148, + 220, + 252, + 60, + 89 + ] + }, + { + "name": "PlayerJoined", + "discriminator": [ + 39, + 144, + 49, + 106, + 108, + 210, + 183, + 38 + ] + }, + { + "name": "PlayerLeft", + "discriminator": [ + 7, + 106, + 62, + 150, + 175, + 170, + 96, + 84 + ] + }, + { + "name": "WinnersSelected", + "discriminator": [ + 28, + 151, + 185, + 12, + 70, + 199, + 73, + 58 + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "PlayerAlreadyInGame", + "msg": "Player is already in the game" + }, + { + "code": 6001, + "name": "PlayerNotInGame", + "msg": "Player is not in the game" + }, + { + "code": 6002, + "name": "GameInProgress", + "msg": "Game is already in progress" + }, + { + "code": 6003, + "name": "InvalidGameAccount", + "msg": "Invalid game account" + }, + { + "code": 6004, + "name": "CannotSettleYet", + "msg": "Cannot settle yet" + }, + { + "code": 6005, + "name": "AuthorityMismatch", + "msg": "Signer / authority mismatch" + }, + { + "code": 6006, + "name": "InvalidInput", + "msg": "Invalid input" + }, + { + "code": 6007, + "name": "AlreadySettled", + "msg": "Game already settled" + }, + { + "code": 6008, + "name": "NumericalOverflow", + "msg": "numerical overflow" + }, + { + "code": 6009, + "name": "CreatorMismatch", + "msg": "Creator Missmatch" + }, + { + "code": 6010, + "name": "PlayerMismatch", + "msg": "player rmismatch" + }, + { + "code": 6011, + "name": "GameFull", + "msg": "Game is Full" + } + ], + "types": [ + { + "name": "CreatorPending", + "type": { + "kind": "struct", + "fields": [ + { + "name": "address", + "type": "pubkey" + }, + { + "name": "amount", + "type": "u64" + } + ] + } + }, + { + "name": "GambaState", + "type": { + "kind": "struct", + "fields": [ + { + "name": "rng", + "type": "pubkey" + }, + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "gamba_fee_address", + "type": "pubkey" + }, + { + "name": "gamba_fee_bps", + "type": "u32" + }, + { + "name": "initialized", + "type": "bool" + }, + { + "name": "game_id", + "type": "u64" + }, + { + "name": "bump", + "type": "u8" + } + ] + } + }, + { + "name": "Game", + "type": { + "kind": "struct", + "fields": [ + { + "name": "game_maker", + "type": "pubkey" + }, + { + "name": "mint", + "type": "pubkey" + }, + { + "name": "game_type", + "type": { + "defined": { + "name": "GameType" + } + } + }, + { + "name": "wager_type", + "type": { + "defined": { + "name": "WagerType" + } + } + }, + { + "name": "payout_type", + "type": { + "defined": { + "name": "PayoutType" + } + } + }, + { + "name": "max_players", + "type": "u16" + }, + { + "name": "pre_alloc", + "type": "u16" + }, + { + "name": "num_teams", + "type": "u8" + }, + { + "name": "winners_target", + "type": "u16" + }, + { + "name": "wager", + "type": "u64" + }, + { + "name": "soft_expiration_timestamp", + "type": "i64" + }, + { + "name": "hard_expiration_timestamp", + "type": "i64" + }, + { + "name": "creation_timestamp", + "docs": [ + "New field: when the game was created (unix timestamp)" + ], + "type": "i64" + }, + { + "name": "state", + "type": { + "defined": { + "name": "GameState" + } + } + }, + { + "name": "pending_gamba_fee", + "type": "u64" + }, + { + "name": "game_id", + "type": "u64" + }, + { + "name": "game_seed", + "docs": [ + "Seeds randomness for select_winners" + ], + "type": "u64" + }, + { + "name": "min_bet", + "type": "u64" + }, + { + "name": "max_bet", + "type": "u64" + }, + { + "name": "bump", + "type": "u8" + }, + { + "name": "players", + "type": { + "vec": { + "defined": { + "name": "Player" + } + } + } + }, + { + "name": "winner_indexes", + "type": { + "vec": "u16" + } + }, + { + "name": "creators_pending", + "type": { + "vec": { + "defined": { + "name": "CreatorPending" + } + } + } + } + ] + } + }, + { + "name": "GameCreated", + "type": { + "kind": "struct", + "fields": [ + { + "name": "game_id", + "type": "u64" + }, + { + "name": "game_account", + "type": "pubkey" + }, + { + "name": "game_maker", + "type": "pubkey" + }, + { + "name": "max_players", + "type": "u16" + }, + { + "name": "num_teams", + "type": "u8" + }, + { + "name": "game_type", + "type": "u8" + }, + { + "name": "wager_type", + "type": "u8" + }, + { + "name": "payout_type", + "type": "u8" + }, + { + "name": "winners_target", + "type": "u16" + }, + { + "name": "soft_duration_seconds", + "type": "i64" + }, + { + "name": "hard_duration_seconds", + "type": "i64" + }, + { + "name": "soft_expiration_timestamp", + "type": "i64" + }, + { + "name": "hard_expiration_timestamp", + "type": "i64" + }, + { + "name": "wager", + "type": "u64" + }, + { + "name": "creation_timestamp", + "type": "i64" + }, + { + "name": "game_seed", + "type": "u64" + }, + { + "name": "min_bet", + "type": "u64" + }, + { + "name": "max_bet", + "type": "u64" + } + ] + } + }, + { + "name": "GameSettledPartial", + "type": { + "kind": "struct", + "fields": [ + { + "name": "game_id", + "type": "u64" + }, + { + "name": "game_account", + "type": "pubkey" + }, + { + "name": "creators_left", + "type": "u32" + }, + { + "name": "winners_left", + "type": "u32" + }, + { + "name": "paid_creators_this_tx", + "type": "u32" + }, + { + "name": "paid_winners_this_tx", + "type": "u32" + }, + { + "name": "amount_paid", + "type": "u64" + } + ] + } + }, + { + "name": "GameState", + "repr": { + "kind": "rust" + }, + "type": { + "kind": "enum", + "variants": [ + { + "name": "Waiting" + }, + { + "name": "Playing" + }, + { + "name": "Settled" + } + ] + } + }, + { + "name": "GameType", + "repr": { + "kind": "rust" + }, + "type": { + "kind": "enum", + "variants": [ + { + "name": "Individual" + }, + { + "name": "Team" + } + ] + } + }, + { + "name": "MetadataEntry", + "type": { + "kind": "struct", + "fields": [ + { + "name": "player", + "type": "pubkey" + }, + { + "name": "meta", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + }, + { + "name": "PayoutType", + "repr": { + "kind": "rust" + }, + "type": { + "kind": "enum", + "variants": [ + { + "name": "Same" + }, + { + "name": "ExponentialDecay" + } + ] + } + }, + { + "name": "Player", + "type": { + "kind": "struct", + "fields": [ + { + "name": "creator_address", + "type": "pubkey" + }, + { + "name": "user", + "type": "pubkey" + }, + { + "name": "creator_fee_amount", + "type": "u64" + }, + { + "name": "gamba_fee_amount", + "type": "u64" + }, + { + "name": "wager", + "type": "u64" + }, + { + "name": "pending_payout", + "type": "u64" + }, + { + "name": "team", + "type": "u8" + } + ] + } + }, + { + "name": "PlayerJoined", + "type": { + "kind": "struct", + "fields": [ + { + "name": "game_id", + "type": "u64" + }, + { + "name": "game_account", + "type": "pubkey" + }, + { + "name": "player", + "type": "pubkey" + }, + { + "name": "wager", + "type": "u64" + }, + { + "name": "creator_fee", + "type": "u64" + }, + { + "name": "mint", + "type": "pubkey" + }, + { + "name": "game_type", + "type": "u8" + }, + { + "name": "team", + "type": "u8" + } + ] + } + }, + { + "name": "PlayerLeft", + "type": { + "kind": "struct", + "fields": [ + { + "name": "game_id", + "type": "u64" + }, + { + "name": "game_account", + "type": "pubkey" + }, + { + "name": "player", + "type": "pubkey" + } + ] + } + }, + { + "name": "PlayerMetadataAccount", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "docs": [ + "bump is injected by Anchor (no need to store it yourself)" + ], + "type": "u8" + }, + { + "name": "max_entries", + "docs": [ + "static cap + capacity pointer" + ], + "type": "u16" + }, + { + "name": "pre_alloc", + "type": "u16" + }, + { + "name": "entries", + "docs": [ + "dynamic, streamed just like your players Vec" + ], + "type": { + "vec": { + "defined": { + "name": "MetadataEntry" + } + } + } + } + ] + } + }, + { + "name": "WagerType", + "repr": { + "kind": "rust" + }, + "type": { + "kind": "enum", + "variants": [ + { + "name": "SameWager" + }, + { + "name": "CustomWager" + }, + { + "name": "BetRange" + } + ] + } + }, + { + "name": "WinnersSelected", + "type": { + "kind": "struct", + "fields": [ + { + "name": "game_id", + "type": "u64" + }, + { + "name": "game_account", + "type": "pubkey" + }, + { + "name": "game_maker", + "type": "pubkey" + }, + { + "name": "max_players", + "type": "u16" + }, + { + "name": "winner_indexes", + "type": { + "vec": "u16" + } + }, + { + "name": "winner_wagers", + "type": { + "vec": "u64" + } + }, + { + "name": "payouts", + "type": { + "vec": "u64" + } + }, + { + "name": "total_wager", + "type": "u64" + }, + { + "name": "players_sample", + "type": { + "vec": "pubkey" + } + } + ] + } + } + ] +} \ No newline at end of file diff --git a/packages/multiplayer/src/index.ts b/packages/multiplayer/src/index.ts new file mode 100644 index 00000000..4e14c18a --- /dev/null +++ b/packages/multiplayer/src/index.ts @@ -0,0 +1,13 @@ +export * from "./constants.js"; +export * from "./errors.js"; +export * from "./instructions/gamba-config.js"; +export * from "./instructions/create-game.js"; +export * from "./instructions/distribute-native.js"; +export * from "./instructions/distribute-spl.js"; +export * from "./instructions/join-game.js"; +export * from "./instructions/leave-game.js"; +export * from "./instructions/select-winners.js"; +export * from "./fetch.js"; +export * from "./events.js"; +export * from "./utils/pda"; +export type { Multiplayer } from "./types/multiplayer.js"; diff --git a/packages/multiplayer/src/instructions/create-game.ts b/packages/multiplayer/src/instructions/create-game.ts new file mode 100644 index 00000000..7aedbe67 --- /dev/null +++ b/packages/multiplayer/src/instructions/create-game.ts @@ -0,0 +1,116 @@ +import { + AnchorProvider, + BN, + web3, +} from "@coral-xyz/anchor"; +import { + WRAPPED_SOL_MINT, + getProgram, +} from "../constants.js"; +import { + deriveGambaState, + deriveGamePdaFromSeed, + deriveMetadataPda, + deriveEscrowPda, +} from "../utils/pda.js"; + +export interface CreateGameParams { + preAllocPlayers: number; + maxPlayers: number; + numTeams: number; + winnersTarget: number; + wagerType: number; + payoutType: number; + wager: BN | number; + softDuration: BN | number; + hardDuration: BN | number; + gameSeed: BN | number; + minBet: BN | number; + maxBet: BN | number; + + accounts: { + gameMaker: web3.PublicKey; + mint: web3.PublicKey; + }; +} + +export const createGameNativeIx = async ( + provider: AnchorProvider, + p: CreateGameParams, +) => { + const program = getProgram(provider); + const gambaState = await deriveGambaState(); + const gamePda = deriveGamePdaFromSeed(p.gameSeed); + const metaPda = deriveMetadataPda(gamePda); + + const ix = await program.methods + .createGameNative( + p.preAllocPlayers, + p.maxPlayers, + p.numTeams, + p.winnersTarget, + p.wagerType, + p.payoutType, + new BN(p.wager), + new BN(p.softDuration), + new BN(p.hardDuration), + new BN(p.gameSeed), + new BN(p.minBet), + new BN(p.maxBet), + ) + .accounts({ + gameAccount: gamePda, + metadataACcount: metaPda, + mint: p.accounts.mint, + gameMaker: p.accounts.gameMaker, + gambaState, + } as any) + .instruction(); + + return ix; +}; + +export const createGameSplIx = async ( + provider: AnchorProvider, + p: CreateGameParams, +) => { + const program = getProgram(provider); + const gambaState = await deriveGambaState(); + const gamePda = deriveGamePdaFromSeed(p.gameSeed); + const metaPda = deriveMetadataPda(gamePda); + const escrowPda = deriveEscrowPda(gamePda); + + const ix = await program.methods + .createGameSpl( + p.preAllocPlayers, + p.maxPlayers, + p.numTeams, + p.winnersTarget, + p.wagerType, + p.payoutType, + new BN(p.wager), + new BN(p.softDuration), + new BN(p.hardDuration), + new BN(p.gameSeed), + new BN(p.minBet), + new BN(p.maxBet), + ) + .accounts({ + gameAccount: gamePda, + metadataACcount: metaPda, + mint: p.accounts.mint, + gameAccountTaAccount: escrowPda, + gameMaker: p.accounts.gameMaker, + gambaState, + } as any) + .instruction(); + + return ix; +}; +export const createGameIx = ( + provider: AnchorProvider, + p: CreateGameParams, +) => + p.accounts.mint.equals(WRAPPED_SOL_MINT) + ? createGameNativeIx(provider, p) + : createGameSplIx(provider, p); diff --git a/packages/multiplayer/src/instructions/distribute-native.ts b/packages/multiplayer/src/instructions/distribute-native.ts new file mode 100644 index 00000000..d54271da --- /dev/null +++ b/packages/multiplayer/src/instructions/distribute-native.ts @@ -0,0 +1,45 @@ +import { AnchorProvider, web3 } from "@coral-xyz/anchor"; +import { getProgram } from "../constants.js"; +import { deriveGambaState, deriveMetadataPda } from "../utils/pda.js"; + +export interface DistributeNativeParams { + accounts: { + payer: web3.PublicKey; + gameAccount: web3.PublicKey; + gambaFeeAddress: web3.PublicKey; + }; + remaining: web3.PublicKey[]; +} + +export const distributeNativeIx = async ( + provider: AnchorProvider, + p: DistributeNativeParams, +): Promise => { + const program = getProgram(provider); + const gambaStatePda = deriveGambaState(); + const metadataAccount = deriveMetadataPda(p.accounts.gameAccount); + + const rawGame = await program.account.game.fetch(p.accounts.gameAccount); + const gameMaker = (rawGame as any).gameMaker as web3.PublicKey; + + const rem = p.remaining.map((pk) => ({ + pubkey: pk, + isWritable: true, + isSigner: false, + })); + + const ix = await program.methods + .distributeNative() + .accountsPartial({ + payer: p.accounts.payer, + gambaState: gambaStatePda, + gameAccount: p.accounts.gameAccount, + metadataAccount, + gameMaker, + gambaFeeAddress: p.accounts.gambaFeeAddress, + } as any) + .remainingAccounts(rem) + .instruction(); + + return ix; +}; diff --git a/packages/multiplayer/src/instructions/distribute-spl.ts b/packages/multiplayer/src/instructions/distribute-spl.ts new file mode 100644 index 00000000..eb3a632c --- /dev/null +++ b/packages/multiplayer/src/instructions/distribute-spl.ts @@ -0,0 +1,77 @@ +import { AnchorProvider, web3 } from "@coral-xyz/anchor"; +import { getAssociatedTokenAddressSync as getAta } from "@solana/spl-token"; +import { getProgram } from "../constants.js"; +import { + deriveGambaState, + deriveMetadataPda, + deriveEscrowPda, +} from "../utils/pda.js"; + +export interface DistributeSplParams { + accounts: { + payer: web3.PublicKey; + gameAccount: web3.PublicKey; + gambaFeeAddress: web3.PublicKey; + mint: web3.PublicKey; + }; + creators?: web3.PublicKey[]; // ≤ 5 raw owner keys + winners?: web3.PublicKey[]; // ≤ 15 raw owner keys +} + +export const distributeSplIx = async ( + provider: AnchorProvider, + p: DistributeSplParams, +): Promise => { + const program = getProgram(provider); + const gambaStatePda = deriveGambaState(); + const metadataAccount = deriveMetadataPda(p.accounts.gameAccount); + const gameAccountTa = deriveEscrowPda(p.accounts.gameAccount); + const gambaFeeAta = getAta(p.accounts.mint, p.accounts.gambaFeeAddress); + + // fetch gameMaker + const rawGame = await program.account.game.fetch(p.accounts.gameAccount); + const gameMaker = (rawGame as any).gameMaker as web3.PublicKey; + + // Build the full account map. We'll explicitly set + // ALL 20 optional ATA slots—either to an ATA or to null. + const accs: Record = { + payer: p.accounts.payer, + gambaState: gambaStatePda, + gameAccount: p.accounts.gameAccount, + metadataAccount, + gameAccountTa, + mint: p.accounts.mint, + gambaFeeAta, + gambaFeeAddress: p.accounts.gambaFeeAddress, + gameMaker, + }; + + // Helper to fill an ATA slot or null + function setAtaSlot( + slot: string, + ownerKey: web3.PublicKey | undefined, + ) { + accs[slot] = ownerKey + ? getAta(p.accounts.mint, ownerKey) + : null; + } + + // creators → creatorAta0 … creatorAta4 + for (let i = 0; i < 5; i++) { + setAtaSlot(`creatorAta${i}`, p.creators?.[i]); + } + + // winners → winnerAta0 … winnerAta14 + for (let i = 0; i < 15; i++) { + setAtaSlot(`winnerAta${i}`, p.winners?.[i]); + } + + // Now that every optional slot is _present_ (maybe null), + // the builder will accept it and replace nulls with the sentinel. + const ix = await program.methods + .distributeSpl() + .accountsPartial(accs as any) + .instruction(); + + return ix; +}; diff --git a/packages/multiplayer/src/instructions/gamba-config.ts b/packages/multiplayer/src/instructions/gamba-config.ts new file mode 100644 index 00000000..1fb46772 --- /dev/null +++ b/packages/multiplayer/src/instructions/gamba-config.ts @@ -0,0 +1,25 @@ +import { AnchorProvider, web3 } from "@coral-xyz/anchor"; +import { getProgram } from "../constants.js"; + + +export interface GambaConfigParams { + gambaFeeAddress : web3.PublicKey; + gambaFeeBps : number; + rng : web3.PublicKey; + authority : web3.PublicKey; + authoritySigner : web3.PublicKey; +} + +export const gambaConfigIx = async ( + provider: AnchorProvider, + p: GambaConfigParams, +) => { + const program = getProgram(provider); + const ix = await program.methods + .gambaConfig(p.gambaFeeAddress, p.gambaFeeBps, p.rng, p.authority) + .accounts({ authority: p.authoritySigner }) + .instruction(); + + return ix; +}; + diff --git a/packages/multiplayer/src/instructions/join-game.ts b/packages/multiplayer/src/instructions/join-game.ts new file mode 100644 index 00000000..3455bb24 --- /dev/null +++ b/packages/multiplayer/src/instructions/join-game.ts @@ -0,0 +1,73 @@ +import { AnchorProvider, BN, utils as anchorUtils, web3 } from "@coral-xyz/anchor"; +import { getAssociatedTokenAddressSync as ata } from "@solana/spl-token"; +import { WRAPPED_SOL_MINT, getProgram } from "../constants.js"; +import { + deriveGambaState, + deriveMetadataPda, + deriveEscrowPda, +} from "../utils/pda.js"; + +export interface JoinGameParams { + creatorFeeBps: number; + wager: BN | number; + team?: number; + playerMeta?: Buffer | Uint8Array; + + accounts: { + gameAccount: web3.PublicKey; + mint: web3.PublicKey; + playerAccount: web3.PublicKey; + creatorAddress: web3.PublicKey; + }; +} + +export const joinGameIx = async ( + provider: AnchorProvider, + p: JoinGameParams, +) => { + const program = getProgram(provider); + const isNative = p.accounts.mint.equals(WRAPPED_SOL_MINT); + + const gambaState = deriveGambaState(); + const metaPda = deriveMetadataPda(p.accounts.gameAccount); + const gameTa = isNative + ? null + : deriveEscrowPda(p.accounts.gameAccount); + + const playerAta = isNative + ? null + : ata(p.accounts.mint, p.accounts.playerAccount, false); + + const creatorAta = ata( + p.accounts.mint, + p.accounts.creatorAddress, + true, + ); + + const teamIndex = p.team ?? 0; + const metaBytes = p.playerMeta + ? Array.from(p.playerMeta) + : []; + + const ix = await program.methods + .joinGame( + p.creatorFeeBps, + new BN(p.wager), + teamIndex, + metaBytes, // [u8;32] or empty + ) + .accountsPartial({ + gameAccount: p.accounts.gameAccount, + metadataAccount: metaPda, + gambaState, + gameAccountTa: gameTa, + mint: p.accounts.mint, + playerAccount: p.accounts.playerAccount, + playerAta, + creatorAddress: p.accounts.creatorAddress, + creatorAta, + } as any) + .instruction(); + + return ix; +}; diff --git a/packages/multiplayer/src/instructions/leave-game.ts b/packages/multiplayer/src/instructions/leave-game.ts new file mode 100644 index 00000000..bc75a8f7 --- /dev/null +++ b/packages/multiplayer/src/instructions/leave-game.ts @@ -0,0 +1,54 @@ +import { + AnchorProvider, + utils as anchorUtils, + web3, +} from "@coral-xyz/anchor"; +import { getAssociatedTokenAddressSync as ata } from "@solana/spl-token"; +import { + WRAPPED_SOL_MINT, + PROGRAM_ID, + getProgram, +} from "../constants.js"; +import { + deriveMetadataPda, + deriveEscrowPda, +} from "../utils/pda.js"; + +export interface LeaveGameParams { + accounts: { + gameAccount: web3.PublicKey; + mint: web3.PublicKey; + playerAccount: web3.PublicKey; + }; +} + +export const leaveGameIx = async ( + provider: AnchorProvider, + p: LeaveGameParams, +) => { + const program = getProgram(provider); + const isNative = p.accounts.mint.equals(WRAPPED_SOL_MINT); + + const metaPda = deriveMetadataPda(p.accounts.gameAccount); + const gameTa = isNative + ? null + : deriveEscrowPda(p.accounts.gameAccount); + + const playerAta = isNative + ? null + : ata(p.accounts.mint, p.accounts.playerAccount, false); + + const ix = await program.methods + .leaveGame() + .accountsPartial({ + gameAccount: p.accounts.gameAccount, + metadataAccount: metaPda, + gameAccountTa: gameTa, + mint: p.accounts.mint, + playerAccount: p.accounts.playerAccount, + playerAta, + } as any) + .instruction(); + + return ix; +}; diff --git a/packages/multiplayer/src/instructions/select-winners.ts b/packages/multiplayer/src/instructions/select-winners.ts new file mode 100644 index 00000000..c91a8729 --- /dev/null +++ b/packages/multiplayer/src/instructions/select-winners.ts @@ -0,0 +1,25 @@ +import { AnchorProvider, web3 } from "@coral-xyz/anchor"; +import { getProgram } from "../constants.js"; + +export interface SelectWinnersParams { + accounts: { + rng : web3.PublicKey; + gameAccount : web3.PublicKey; + }; +} + +export const selectWinnersIx = async ( + provider: AnchorProvider, + p: SelectWinnersParams, +) => { + const program = getProgram(provider); + const ix = await program.methods + .selectWinners() + .accounts({ + rng : p.accounts.rng, + gameAccount : p.accounts.gameAccount, + }) + .instruction(); + + return ix; +}; diff --git a/packages/multiplayer/src/types/multiplayer.ts b/packages/multiplayer/src/types/multiplayer.ts new file mode 100644 index 00000000..3115a7d4 --- /dev/null +++ b/packages/multiplayer/src/types/multiplayer.ts @@ -0,0 +1,2159 @@ +/** + * 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/multiplayer.json`. + */ +export type Multiplayer = { + "address": "GambaMyTW8C1NSeFrv2c3KfmX1MSDBF6YbxDeb7dBPxM", + "metadata": { + "name": "multiplayer", + "version": "0.1.0", + "spec": "0.1.0", + "description": "Created with Anchor" + }, + "instructions": [ + { + "name": "createGameNative", + "discriminator": [ + 14, + 237, + 122, + 202, + 102, + 88, + 48, + 75 + ], + "accounts": [ + { + "name": "gameAccount", + "docs": [ + "The on‐chain Game account, PDA = [b\"GAME\", game_seed]." + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 71, + 65, + 77, + 69 + ] + }, + { + "kind": "arg", + "path": "gameSeed" + } + ] + } + }, + { + "name": "metadataAccount", + "docs": [ + "Always-present metadata PDA, PDA = [b\"METADATA\", game_account.key()]" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 77, + 69, + 84, + 65, + 68, + 65, + 84, + 65 + ] + }, + { + "kind": "account", + "path": "gameAccount" + } + ] + } + }, + { + "name": "mint", + "docs": [ + "always SOL (placeholder mint)" + ] + }, + { + "name": "gameMaker", + "docs": [ + "creator of the game, pays rent on init" + ], + "writable": true, + "signer": true + }, + { + "name": "gambaState", + "docs": [ + "global config PDA" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69 + ] + } + ] + } + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "preAllocPlayers", + "type": "u16" + }, + { + "name": "maxPlayers", + "type": "u16" + }, + { + "name": "numTeams", + "type": "u8" + }, + { + "name": "winnersTarget", + "type": "u16" + }, + { + "name": "wagerType", + "type": "u8" + }, + { + "name": "payoutType", + "type": "u8" + }, + { + "name": "wager", + "type": "u64" + }, + { + "name": "softDuration", + "type": "i64" + }, + { + "name": "hardDuration", + "type": "i64" + }, + { + "name": "gameSeed", + "type": "u64" + }, + { + "name": "minBet", + "type": "u64" + }, + { + "name": "maxBet", + "type": "u64" + } + ] + }, + { + "name": "createGameSpl", + "discriminator": [ + 80, + 235, + 44, + 243, + 14, + 15, + 248, + 207 + ], + "accounts": [ + { + "name": "gameAccount", + "docs": [ + "The on-chain Game account, PDA = [b\"GAME\", game_seed]." + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 71, + 65, + 77, + 69 + ] + }, + { + "kind": "arg", + "path": "gameSeed" + } + ] + } + }, + { + "name": "metadataAccount", + "docs": [ + "Always-present metadata PDA, PDA = [b\"METADATA\", game_account.key()]" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 77, + 69, + 84, + 65, + 68, + 65, + 84, + 65 + ] + }, + { + "kind": "account", + "path": "gameAccount" + } + ] + } + }, + { + "name": "mint", + "docs": [ + "the SPL mint for wagers" + ] + }, + { + "name": "gameAccountTaAccount", + "docs": [ + "escrow token account for this game" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "gameAccount" + } + ] + } + }, + { + "name": "gameMaker", + "writable": true, + "signer": true + }, + { + "name": "gambaState", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69 + ] + } + ] + } + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111" + }, + { + "name": "tokenProgram", + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "associatedTokenProgram", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + } + ], + "args": [ + { + "name": "preAllocPlayers", + "type": "u16" + }, + { + "name": "maxPlayers", + "type": "u16" + }, + { + "name": "numTeams", + "type": "u8" + }, + { + "name": "winnersTarget", + "type": "u16" + }, + { + "name": "wagerType", + "type": "u8" + }, + { + "name": "payoutType", + "type": "u8" + }, + { + "name": "wager", + "type": "u64" + }, + { + "name": "softDuration", + "type": "i64" + }, + { + "name": "hardDuration", + "type": "i64" + }, + { + "name": "gameSeed", + "type": "u64" + }, + { + "name": "minBet", + "type": "u64" + }, + { + "name": "maxBet", + "type": "u64" + } + ] + }, + { + "name": "distributeNative", + "discriminator": [ + 32, + 200, + 172, + 49, + 57, + 234, + 137, + 89 + ], + "accounts": [ + { + "name": "payer", + "docs": [ + "Tx signer (fees & optional PDA‑close refund go here)" + ], + "signer": true + }, + { + "name": "gambaState", + "docs": [ + "Global configuration – for fee vault + RNG auth" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69 + ] + } + ] + } + }, + { + "name": "gameAccount", + "docs": [ + "Game account (large PDA, parsed manually)" + ], + "writable": true + }, + { + "name": "gameMaker", + "docs": [ + "Game‑maker – receives any closing lamports" + ], + "writable": true + }, + { + "name": "gambaFeeAddress", + "docs": [ + "Protocol fee vault" + ], + "writable": true + }, + { + "name": "metadataAccount", + "docs": [ + "Metadata PDA – to be closed at settlement" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 77, + 69, + 84, + 65, + 68, + 65, + 84, + 65 + ] + }, + { + "kind": "account", + "path": "gameAccount" + } + ] + } + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, + { + "name": "distributeSpl", + "discriminator": [ + 93, + 24, + 158, + 238, + 198, + 173, + 215, + 228 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "gambaState", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69 + ] + } + ] + } + }, + { + "name": "gameAccount", + "docs": [ + "Raw Game PDA (parsed by offsets)" + ], + "writable": true + }, + { + "name": "metadataAccount", + "docs": [ + "Always-present metadata PDA (to be closed on final settlement)" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 77, + 69, + 84, + 65, + 68, + 65, + 84, + 65 + ] + }, + { + "kind": "account", + "path": "gameAccount" + } + ] + } + }, + { + "name": "gameAccountTa", + "docs": [ + "Game escrow ATA (PDA = [game_account])" + ], + "writable": true + }, + { + "name": "mint" + }, + { + "name": "gambaFeeAta", + "docs": [ + "Protocol fee vault ATA for this mint" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "gambaFeeAddress" + }, + { + "kind": "const", + "value": [ + 6, + 221, + 246, + 225, + 215, + 101, + 161, + 147, + 217, + 203, + 225, + 70, + 206, + 235, + 121, + 172, + 28, + 180, + 133, + 237, + 95, + 91, + 55, + 145, + 58, + 140, + 245, + 133, + 126, + 255, + 0, + 169 + ] + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "gambaFeeAddress", + "docs": [ + "Protocol fee vault (owner of `gamba_fee_ata`)" + ], + "writable": true + }, + { + "name": "gameMaker", + "docs": [ + "Game‑maker (receives escrow close lamports)" + ], + "writable": true + }, + { + "name": "creatorAta0", + "writable": true, + "optional": true + }, + { + "name": "creatorAta1", + "writable": true, + "optional": true + }, + { + "name": "creatorAta2", + "writable": true, + "optional": true + }, + { + "name": "creatorAta3", + "writable": true, + "optional": true + }, + { + "name": "creatorAta4", + "writable": true, + "optional": true + }, + { + "name": "winnerAta0", + "writable": true, + "optional": true + }, + { + "name": "winnerAta1", + "writable": true, + "optional": true + }, + { + "name": "winnerAta2", + "writable": true, + "optional": true + }, + { + "name": "winnerAta3", + "writable": true, + "optional": true + }, + { + "name": "winnerAta4", + "writable": true, + "optional": true + }, + { + "name": "winnerAta5", + "writable": true, + "optional": true + }, + { + "name": "winnerAta6", + "writable": true, + "optional": true + }, + { + "name": "winnerAta7", + "writable": true, + "optional": true + }, + { + "name": "winnerAta8", + "writable": true, + "optional": true + }, + { + "name": "winnerAta9", + "writable": true, + "optional": true + }, + { + "name": "winnerAta10", + "writable": true, + "optional": true + }, + { + "name": "winnerAta11", + "writable": true, + "optional": true + }, + { + "name": "winnerAta12", + "writable": true, + "optional": true + }, + { + "name": "winnerAta13", + "writable": true, + "optional": true + }, + { + "name": "winnerAta14", + "writable": true, + "optional": true + }, + { + "name": "tokenProgram", + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "associatedTokenProgram", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, + { + "name": "gambaConfig", + "discriminator": [ + 232, + 208, + 249, + 92, + 159, + 187, + 21, + 254 + ], + "accounts": [ + { + "name": "gambaState", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69 + ] + } + ] + } + }, + { + "name": "authority", + "writable": true, + "signer": true + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "feeVault", + "type": "pubkey" + }, + { + "name": "feeBps", + "type": "u32" + }, + { + "name": "rng", + "type": "pubkey" + }, + { + "name": "authority", + "type": "pubkey" + } + ] + }, + { + "name": "joinGame", + "discriminator": [ + 107, + 112, + 18, + 38, + 56, + 173, + 60, + 128 + ], + "accounts": [ + { + "name": "gameAccount", + "docs": [ + "Game account as raw bytes" + ], + "writable": true + }, + { + "name": "gambaState", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69 + ] + } + ] + } + }, + { + "name": "gameAccountTa", + "docs": [ + "Optional escrow token account (only for SPL games)" + ], + "writable": true, + "optional": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "gameAccount" + } + ] + } + }, + { + "name": "mint", + "docs": [ + "Mint used for wagers (native SOL or SPL)" + ] + }, + { + "name": "playerAccount", + "docs": [ + "Player joining (payer + signer)" + ], + "writable": true, + "signer": true + }, + { + "name": "playerAta", + "docs": [ + "Optional player ATA if SPL wager" + ], + "writable": true, + "optional": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "playerAccount" + }, + { + "kind": "const", + "value": [ + 6, + 221, + 246, + 225, + 215, + 101, + 161, + 147, + 217, + 203, + 225, + 70, + 206, + 235, + 121, + 172, + 28, + 180, + 133, + 237, + 95, + 91, + 55, + 145, + 58, + 140, + 245, + 133, + 126, + 255, + 0, + 169 + ] + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "creatorAddress", + "docs": [ + "Creator/referrer collecting a fee" + ] + }, + { + "name": "creatorAta", + "docs": [ + "Creator’s ATA (created lazily)" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "creatorAddress" + }, + { + "kind": "const", + "value": [ + 6, + 221, + 246, + 225, + 215, + 101, + 161, + 147, + 217, + 203, + 225, + 70, + 206, + 235, + 121, + 172, + 28, + 180, + 133, + 237, + 95, + 91, + 55, + 145, + 58, + 140, + 245, + 133, + 126, + 255, + 0, + 169 + ] + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "metadataAccount", + "docs": [ + "Always-present metadata PDA" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 77, + 69, + 84, + 65, + 68, + 65, + 84, + 65 + ] + }, + { + "kind": "account", + "path": "gameAccount" + } + ] + } + }, + { + "name": "systemProgram", + "docs": [ + "Programs" + ], + "address": "11111111111111111111111111111111" + }, + { + "name": "associatedTokenProgram", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "tokenProgram", + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "args": [ + { + "name": "creatorFeeBps", + "type": "u32" + }, + { + "name": "wager", + "type": "u64" + }, + { + "name": "team", + "type": "u8" + }, + { + "name": "playerMeta", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + }, + { + "name": "leaveGame", + "discriminator": [ + 218, + 226, + 6, + 0, + 243, + 34, + 125, + 201 + ], + "accounts": [ + { + "name": "gameAccount", + "docs": [ + "Game PDA – parsed manually" + ], + "writable": true + }, + { + "name": "mint" + }, + { + "name": "playerAccount", + "writable": true, + "signer": true + }, + { + "name": "playerAta", + "writable": true, + "optional": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "playerAccount" + }, + { + "kind": "const", + "value": [ + 6, + 221, + 246, + 225, + 215, + 101, + 161, + 147, + 217, + 203, + 225, + 70, + 206, + 235, + 121, + 172, + 28, + 180, + 133, + 237, + 95, + 91, + 55, + 145, + 58, + 140, + 245, + 133, + 126, + 255, + 0, + 169 + ] + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "gameAccountTa", + "writable": true, + "optional": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "gameAccount" + } + ] + } + }, + { + "name": "metadataAccount", + "docs": [ + "Always-present metadata PDA" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 77, + 69, + 84, + 65, + 68, + 65, + 84, + 65 + ] + }, + { + "kind": "account", + "path": "gameAccount" + } + ] + } + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111" + }, + { + "name": "associatedTokenProgram", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "tokenProgram", + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "args": [] + }, + { + "name": "selectWinners", + "discriminator": [ + 80, + 100, + 28, + 131, + 83, + 199, + 222, + 80 + ], + "accounts": [ + { + "name": "rng", + "docs": [ + "Authorised RNG bot" + ], + "writable": true, + "signer": true + }, + { + "name": "gambaState", + "docs": [ + "Global config – only used to enforce `rng`" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 71, + 65, + 77, + 66, + 65, + 95, + 83, + 84, + 65, + 84, + 69 + ] + } + ] + } + }, + { + "name": "gameAccount", + "docs": [ + "Raw Game account; we parse fields by hand" + ], + "writable": true + } + ], + "args": [] + } + ], + "accounts": [ + { + "name": "gambaState", + "discriminator": [ + 142, + 203, + 14, + 224, + 153, + 118, + 52, + 200 + ] + }, + { + "name": "game", + "discriminator": [ + 27, + 90, + 166, + 125, + 74, + 100, + 121, + 18 + ] + }, + { + "name": "playerMetadataAccount", + "discriminator": [ + 204, + 224, + 199, + 121, + 70, + 159, + 53, + 55 + ] + } + ], + "events": [ + { + "name": "gameCreated", + "discriminator": [ + 218, + 25, + 150, + 94, + 177, + 112, + 96, + 2 + ] + }, + { + "name": "gameSettledPartial", + "discriminator": [ + 208, + 36, + 152, + 148, + 220, + 252, + 60, + 89 + ] + }, + { + "name": "playerJoined", + "discriminator": [ + 39, + 144, + 49, + 106, + 108, + 210, + 183, + 38 + ] + }, + { + "name": "playerLeft", + "discriminator": [ + 7, + 106, + 62, + 150, + 175, + 170, + 96, + 84 + ] + }, + { + "name": "winnersSelected", + "discriminator": [ + 28, + 151, + 185, + 12, + 70, + 199, + 73, + 58 + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "playerAlreadyInGame", + "msg": "Player is already in the game" + }, + { + "code": 6001, + "name": "playerNotInGame", + "msg": "Player is not in the game" + }, + { + "code": 6002, + "name": "gameInProgress", + "msg": "Game is already in progress" + }, + { + "code": 6003, + "name": "invalidGameAccount", + "msg": "Invalid game account" + }, + { + "code": 6004, + "name": "cannotSettleYet", + "msg": "Cannot settle yet" + }, + { + "code": 6005, + "name": "authorityMismatch", + "msg": "Signer / authority mismatch" + }, + { + "code": 6006, + "name": "invalidInput", + "msg": "Invalid input" + }, + { + "code": 6007, + "name": "alreadySettled", + "msg": "Game already settled" + }, + { + "code": 6008, + "name": "numericalOverflow", + "msg": "numerical overflow" + }, + { + "code": 6009, + "name": "creatorMismatch", + "msg": "Creator Missmatch" + }, + { + "code": 6010, + "name": "playerMismatch", + "msg": "player rmismatch" + }, + { + "code": 6011, + "name": "gameFull", + "msg": "Game is Full" + } + ], + "types": [ + { + "name": "creatorPending", + "type": { + "kind": "struct", + "fields": [ + { + "name": "address", + "type": "pubkey" + }, + { + "name": "amount", + "type": "u64" + } + ] + } + }, + { + "name": "gambaState", + "type": { + "kind": "struct", + "fields": [ + { + "name": "rng", + "type": "pubkey" + }, + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "gambaFeeAddress", + "type": "pubkey" + }, + { + "name": "gambaFeeBps", + "type": "u32" + }, + { + "name": "initialized", + "type": "bool" + }, + { + "name": "gameId", + "type": "u64" + }, + { + "name": "bump", + "type": "u8" + } + ] + } + }, + { + "name": "game", + "type": { + "kind": "struct", + "fields": [ + { + "name": "gameMaker", + "type": "pubkey" + }, + { + "name": "mint", + "type": "pubkey" + }, + { + "name": "gameType", + "type": { + "defined": { + "name": "gameType" + } + } + }, + { + "name": "wagerType", + "type": { + "defined": { + "name": "wagerType" + } + } + }, + { + "name": "payoutType", + "type": { + "defined": { + "name": "payoutType" + } + } + }, + { + "name": "maxPlayers", + "type": "u16" + }, + { + "name": "preAlloc", + "type": "u16" + }, + { + "name": "numTeams", + "type": "u8" + }, + { + "name": "winnersTarget", + "type": "u16" + }, + { + "name": "wager", + "type": "u64" + }, + { + "name": "softExpirationTimestamp", + "type": "i64" + }, + { + "name": "hardExpirationTimestamp", + "type": "i64" + }, + { + "name": "creationTimestamp", + "docs": [ + "New field: when the game was created (unix timestamp)" + ], + "type": "i64" + }, + { + "name": "state", + "type": { + "defined": { + "name": "gameState" + } + } + }, + { + "name": "pendingGambaFee", + "type": "u64" + }, + { + "name": "gameId", + "type": "u64" + }, + { + "name": "gameSeed", + "docs": [ + "Seeds randomness for select_winners" + ], + "type": "u64" + }, + { + "name": "minBet", + "type": "u64" + }, + { + "name": "maxBet", + "type": "u64" + }, + { + "name": "bump", + "type": "u8" + }, + { + "name": "players", + "type": { + "vec": { + "defined": { + "name": "player" + } + } + } + }, + { + "name": "winnerIndexes", + "type": { + "vec": "u16" + } + }, + { + "name": "creatorsPending", + "type": { + "vec": { + "defined": { + "name": "creatorPending" + } + } + } + } + ] + } + }, + { + "name": "gameCreated", + "type": { + "kind": "struct", + "fields": [ + { + "name": "gameId", + "type": "u64" + }, + { + "name": "gameAccount", + "type": "pubkey" + }, + { + "name": "gameMaker", + "type": "pubkey" + }, + { + "name": "maxPlayers", + "type": "u16" + }, + { + "name": "numTeams", + "type": "u8" + }, + { + "name": "gameType", + "type": "u8" + }, + { + "name": "wagerType", + "type": "u8" + }, + { + "name": "payoutType", + "type": "u8" + }, + { + "name": "winnersTarget", + "type": "u16" + }, + { + "name": "softDurationSeconds", + "type": "i64" + }, + { + "name": "hardDurationSeconds", + "type": "i64" + }, + { + "name": "softExpirationTimestamp", + "type": "i64" + }, + { + "name": "hardExpirationTimestamp", + "type": "i64" + }, + { + "name": "wager", + "type": "u64" + }, + { + "name": "creationTimestamp", + "type": "i64" + }, + { + "name": "gameSeed", + "type": "u64" + }, + { + "name": "minBet", + "type": "u64" + }, + { + "name": "maxBet", + "type": "u64" + } + ] + } + }, + { + "name": "gameSettledPartial", + "type": { + "kind": "struct", + "fields": [ + { + "name": "gameId", + "type": "u64" + }, + { + "name": "gameAccount", + "type": "pubkey" + }, + { + "name": "creatorsLeft", + "type": "u32" + }, + { + "name": "winnersLeft", + "type": "u32" + }, + { + "name": "paidCreatorsThisTx", + "type": "u32" + }, + { + "name": "paidWinnersThisTx", + "type": "u32" + }, + { + "name": "amountPaid", + "type": "u64" + } + ] + } + }, + { + "name": "gameState", + "repr": { + "kind": "rust" + }, + "type": { + "kind": "enum", + "variants": [ + { + "name": "waiting" + }, + { + "name": "playing" + }, + { + "name": "settled" + } + ] + } + }, + { + "name": "gameType", + "repr": { + "kind": "rust" + }, + "type": { + "kind": "enum", + "variants": [ + { + "name": "individual" + }, + { + "name": "team" + } + ] + } + }, + { + "name": "metadataEntry", + "type": { + "kind": "struct", + "fields": [ + { + "name": "player", + "type": "pubkey" + }, + { + "name": "meta", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + }, + { + "name": "payoutType", + "repr": { + "kind": "rust" + }, + "type": { + "kind": "enum", + "variants": [ + { + "name": "same" + }, + { + "name": "exponentialDecay" + } + ] + } + }, + { + "name": "player", + "type": { + "kind": "struct", + "fields": [ + { + "name": "creatorAddress", + "type": "pubkey" + }, + { + "name": "user", + "type": "pubkey" + }, + { + "name": "creatorFeeAmount", + "type": "u64" + }, + { + "name": "gambaFeeAmount", + "type": "u64" + }, + { + "name": "wager", + "type": "u64" + }, + { + "name": "pendingPayout", + "type": "u64" + }, + { + "name": "team", + "type": "u8" + } + ] + } + }, + { + "name": "playerJoined", + "type": { + "kind": "struct", + "fields": [ + { + "name": "gameId", + "type": "u64" + }, + { + "name": "gameAccount", + "type": "pubkey" + }, + { + "name": "player", + "type": "pubkey" + }, + { + "name": "wager", + "type": "u64" + }, + { + "name": "creatorFee", + "type": "u64" + }, + { + "name": "mint", + "type": "pubkey" + }, + { + "name": "gameType", + "type": "u8" + }, + { + "name": "team", + "type": "u8" + } + ] + } + }, + { + "name": "playerLeft", + "type": { + "kind": "struct", + "fields": [ + { + "name": "gameId", + "type": "u64" + }, + { + "name": "gameAccount", + "type": "pubkey" + }, + { + "name": "player", + "type": "pubkey" + } + ] + } + }, + { + "name": "playerMetadataAccount", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "docs": [ + "bump is injected by Anchor (no need to store it yourself)" + ], + "type": "u8" + }, + { + "name": "maxEntries", + "docs": [ + "static cap + capacity pointer" + ], + "type": "u16" + }, + { + "name": "preAlloc", + "type": "u16" + }, + { + "name": "entries", + "docs": [ + "dynamic, streamed just like your players Vec" + ], + "type": { + "vec": { + "defined": { + "name": "metadataEntry" + } + } + } + } + ] + } + }, + { + "name": "wagerType", + "repr": { + "kind": "rust" + }, + "type": { + "kind": "enum", + "variants": [ + { + "name": "sameWager" + }, + { + "name": "customWager" + }, + { + "name": "betRange" + } + ] + } + }, + { + "name": "winnersSelected", + "type": { + "kind": "struct", + "fields": [ + { + "name": "gameId", + "type": "u64" + }, + { + "name": "gameAccount", + "type": "pubkey" + }, + { + "name": "gameMaker", + "type": "pubkey" + }, + { + "name": "maxPlayers", + "type": "u16" + }, + { + "name": "winnerIndexes", + "type": { + "vec": "u16" + } + }, + { + "name": "winnerWagers", + "type": { + "vec": "u64" + } + }, + { + "name": "payouts", + "type": { + "vec": "u64" + } + }, + { + "name": "totalWager", + "type": "u64" + }, + { + "name": "playersSample", + "type": { + "vec": "pubkey" + } + } + ] + } + } + ] +}; diff --git a/packages/multiplayer/src/utils/pda.ts b/packages/multiplayer/src/utils/pda.ts new file mode 100644 index 00000000..378a0b98 --- /dev/null +++ b/packages/multiplayer/src/utils/pda.ts @@ -0,0 +1,54 @@ +// src/utils/pda.ts + +import { Program, utils as anchorUtils, BN } from "@coral-xyz/anchor"; +import { PublicKey } from "@solana/web3.js"; +import { PROGRAM_ID } from "../constants"; + +/** + * Derive the global config PDA for Gamba: + * seeds = [ "GAMBA_STATE" ] + */ +export function deriveGambaState(): PublicKey { + const [gambaState] = PublicKey.findProgramAddressSync( + [anchorUtils.bytes.utf8.encode("GAMBA_STATE")], + PROGRAM_ID + ); + return gambaState; +} + +/** + * Derive the game PDA from YOUR seed: + * seeds = [ "GAME", seed_le_bytes ] + */ +export function deriveGamePdaFromSeed(seed: BN | number): PublicKey { + const buf = new BN(seed).toArrayLike(Buffer, "le", 8); + const [pda] = PublicKey.findProgramAddressSync( + [anchorUtils.bytes.utf8.encode("GAME"), buf], + PROGRAM_ID + ); + return pda; +} + +/** + * Derive the metadata PDA for a given game: + * seeds = [ "METADATA", gamePda ] + */ +export function deriveMetadataPda(gamePda: PublicKey): PublicKey { + const [metaPda] = PublicKey.findProgramAddressSync( + [anchorUtils.bytes.utf8.encode("METADATA"), gamePda.toBuffer()], + PROGRAM_ID + ); + return metaPda; +} + +/** + * Derive the escrow‐ATA PDA for a given game PDA: + * seeds = [ gamePda ] + */ +export function deriveEscrowPda(gamePda: PublicKey): PublicKey { + const [escrowPda] = PublicKey.findProgramAddressSync( + [gamePda.toBuffer()], + PROGRAM_ID + ); + return escrowPda; +} \ No newline at end of file diff --git a/packages/multiplayer/tsconfig.json b/packages/multiplayer/tsconfig.json new file mode 100644 index 00000000..911a6fa1 --- /dev/null +++ b/packages/multiplayer/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "declaration": true, + "declarationMap": true, + + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + + "lib": ["ES2020", "DOM"], + "resolveJsonModule": true, + "types": ["node"], + + "strict": true, + "skipLibCheck": true + }, + "include": ["src/**/*", "idl/**/*"], + "exclude": ["dist", "test"] +} diff --git a/packages/multiplayer/tsup.config.ts b/packages/multiplayer/tsup.config.ts new file mode 100644 index 00000000..d7432269 --- /dev/null +++ b/packages/multiplayer/tsup.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["cjs", "esm"], + dts: true, + sourcemap: true, + clean: true, + treeshake: true, + external: [ + "@coral-xyz/anchor", + "@solana/web3.js" + ] +}); diff --git a/packages/react-ui/package.json b/packages/react-ui/package.json index 287a79dd..8a2f15e9 100644 --- a/packages/react-ui/package.json +++ b/packages/react-ui/package.json @@ -1,16 +1,11 @@ { "name": "gamba-react-ui-v2", - "private": false, "version": "0.7.1", + "private": false, "main": "dist/index.js", "module": "dist/index.mjs", "types": "dist/index.d.ts", - "files": [ - "dist/**" - ], - "publishConfig": { - "access": "public" - }, + "files": ["dist/**"], "scripts": { "dev": "tsup src/index.ts --watch --format cjs,esm --dts --external react --external react-dom --external @solana/web3.js --external @solana/spl-token --external @solana/wallet-adapter-react --external @coral-xyz/anchor --external styled-components --external gamba-react-v2 --external gamba-core-v2 --external @gamba-labs/multiplayer-sdk", "build": "tsup src/index.ts --format cjs,esm --dts --external react --external react-dom --external @solana/web3.js --external @solana/spl-token --external @solana/wallet-adapter-react --external @coral-xyz/anchor --external styled-components --external gamba-react-v2 --external gamba-core-v2 --external @gamba-labs/multiplayer-sdk", @@ -18,39 +13,40 @@ "clean": "rm -rf .turbo node_modules dist" }, "dependencies": { - "@coral-xyz/anchor": "^0.27.0", "@preact/signals-react": "^1.3.8", - "@solana/spl-token": "^0.3.8", - "@solana/web3.js": "^1.93.0", - "gamba-core-v2": "workspace:*", - "gamba-react-v2": "workspace:*", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "styled-components": "^6.0.8", - "tone": "^14.7.77", - "zustand": "^4.4.3" + "gamba-core-v2": "workspace:*", + "gamba-react-v2": "workspace:*", + "styled-components": "^6.0.8", + "tone": "^14.7.77", + "zustand": "^4.4.3" + }, + "peerDependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "@coral-xyz/anchor": "^0.31.1", + "@solana/wallet-adapter-react": "^0.15.39", + "@solana/web3.js": "^1.98.2", + "@solana/spl-token": "^0.4.13", + "gamba-core-v2": "*", + "gamba-react-v2": "*", + "@gamba-labs/multiplayer-sdk": "*", + "styled-components": "^6.0.8" + }, + "peerDependenciesMeta": { + "@solana/wallet-adapter-react": { "optional": true } }, "devDependencies": { "@changesets/cli": "^2.26.2", + "@types/node": "^24.0.10", "@types/react": "^18.2.13", - "@solana/wallet-adapter-react": "^0.15.35", - "@types/react-dom": "^18.0.11", - "assert": "^2.0.0", - "tsup": "^7.2.0", - "typescript": "^5.2.2" + "@types/react-dom": "^18.2.4", + "assert": "^2.0.0", + "tsup": "^8.5.0", + "typescript": "^5.2.2", + "@gamba-labs/multiplayer-sdk": "workspace:*" }, - "peerDependencies": { - "@coral-xyz/anchor": "^0.27.0", - "@solana/wallet-adapter-react": "^0.15.35", - "@solana/web3.js": "^1.93.0", - "gamba-core": "*", - "gamba-react": "*", - "react": "^18.3.1", - "react-dom": "^18.3.1" + "publishConfig": { + "access": "public" }, - "peerDependenciesMeta": { - "@solana/wallet-adapter-react": { - "optional": true - } - } + "license": "MIT" } diff --git a/packages/react-ui/src/GameContext.tsx b/packages/react-ui/src/GameContext.tsx index e19b4c92..50fa072b 100644 --- a/packages/react-ui/src/GameContext.tsx +++ b/packages/react-ui/src/GameContext.tsx @@ -1,4 +1,3 @@ -import { useGamba } from 'gamba-react-v2' import React from 'react' import { GameBundle, useGame } from '.' import { EffectTest } from './EffectTest' @@ -23,13 +22,15 @@ interface GameProps extends React.PropsWithChildren { errorFallback?: React.ReactNode } -export const GameContext = React.createContext({ game: { id: 'unknown', app: null! } }) +export const GameContext = React.createContext({ + game: { id: 'unknown', app: null! }, +}) function Game({ game, children, errorFallback }: GameProps) { return ( - }> + @@ -38,15 +39,14 @@ function Game({ game, children, errorFallback }: GameProps) { ) } +/** + * PlayButton no longer looks at gamba.isPlaying; + * it only disables if you pass `disabled={true}` yourself. + */ export function PlayButton(props: ButtonProps) { - const gamba = useGamba() return ( - @@ -57,19 +57,16 @@ export const GambaUi = { useGame, useSound, Portal, - PortalTarget: PortalTarget, + PortalTarget, Effect: EffectTest, Button, Game, Responsive: ResponsiveSize, Canvas: GambaCanvas, - WagerInput: WagerInput, - /** - * @deprecated Use WagerInput with "options" prop - */ - WagerSelect: WagerSelect, - Switch: Switch, + WagerInput, + WagerSelect, + Switch, PlayButton, - Select: Select, - TextInput: TextInput, + Select, + TextInput, } diff --git a/packages/react-ui/src/components/Button.tsx b/packages/react-ui/src/components/Button.tsx index 5b6da88b..1598f581 100644 --- a/packages/react-ui/src/components/Button.tsx +++ b/packages/react-ui/src/components/Button.tsx @@ -3,20 +3,29 @@ import styled, { css } from 'styled-components' type ButtonSize = 'small' | 'medium' | 'large' -const StyledButton = styled.button<{$main?: boolean, $size: ButtonSize}>` +const StyledButton = styled.button<{ + $main?: boolean + $size?: ButtonSize +}>` --color: var(--gamba-ui-button-default-color); --background-color: var(--gamba-ui-button-default-background); --background-color-hover: var(--gamba-ui-button-default-background-hover); - ${(props) => props.$main && css` - --background-color: var(--gamba-ui-button-main-background); - --color: var(--gamba-ui-button-main-color); - --background-color-hover: var(--gamba-ui-button-main-background-hover); - `} + ${({ $main }) => + $main && + css` + --background-color: var(--gamba-ui-button-main-background); + --color: var(--gamba-ui-button-main-color); + --background-color-hover: var(--gamba-ui-button-main-background-hover); + `} - ${(props) => css` - --padding: ${props.$size === 'small' ? '5px' : props.$size === 'medium' ? '10px' : props.$size === 'large' && '15px'}; - `} + /* default $size to "medium" */ + ${({ $size = 'medium' }) => + css` + --padding: ${ + $size === 'small' ? '5px' : $size === 'large' ? '15px' : '10px' + }; + `} background: var(--background-color); color: var(--color); @@ -28,32 +37,44 @@ const StyledButton = styled.button<{$main?: boolean, $size: ButtonSize}>` border-radius: var(--gamba-ui-border-radius); padding: var(--padding); cursor: pointer; - /* min-width: 100px; */ text-align: center; - align-items: center; &:disabled { cursor: default; - opacity: .7; + opacity: 0.7; } ` -export interface ButtonProps extends React.PropsWithChildren { +export interface ButtonProps { disabled?: boolean onClick?: () => void main?: boolean size?: ButtonSize + children?: React.ReactNode | bigint } -export function Button(props: ButtonProps) { +export function Button({ + disabled, + onClick, + main, + size, + children, +}: ButtonProps) { + // coerce bigint → string + const safeChildren = + typeof children === 'bigint' ? children.toString() : children + + // cast away the styled-component’s complex signature + const SButton: any = StyledButton + return ( - - {props.children} - + {safeChildren} + ) } diff --git a/packages/react-ui/src/components/ResponsiveSize.tsx b/packages/react-ui/src/components/ResponsiveSize.tsx index b76c6a9f..d61149bd 100644 --- a/packages/react-ui/src/components/ResponsiveSize.tsx +++ b/packages/react-ui/src/components/ResponsiveSize.tsx @@ -1,6 +1,12 @@ -import React from 'react' +import React, { + useRef, + useLayoutEffect, + PropsWithChildren, + HTMLAttributes, +} from 'react' import styled from 'styled-components' +// styled wrapper const Responsive = styled.div` justify-content: center; align-items: center; @@ -13,39 +19,61 @@ const Responsive = styled.div` top: 0; ` -interface Props extends React.PropsWithChildren> { +// Omit 'contentEditable' to avoid the type mismatch +type DivProps = Omit, 'contentEditable'> + +interface Props extends PropsWithChildren { maxScale?: number overlay?: boolean } -export default function ResponsiveSize({ children, maxScale = 1, overlay, ...props }: Props) { - const wrapper = React.useRef(null!) - const inner = React.useRef(null!) - const content = React.useRef(null!) +export default function ResponsiveSize({ + children, + maxScale = 1, + overlay, // kept for future use + ...props +}: Props) { + const wrapperRef = useRef(null) + const innerRef = useRef(null) + const contentRef = useRef(null) - React.useLayoutEffect(() => { + useLayoutEffect(() => { let timeout: NodeJS.Timeout const resize = () => { - const ww = wrapper.current.clientWidth / (content.current.scrollWidth + 40) - const hh = wrapper.current.clientHeight / (content.current.clientHeight + 80) + if ( + !wrapperRef.current || + !innerRef.current || + !contentRef.current + ) { + return + } + const ww = + wrapperRef.current.clientWidth / + (contentRef.current.scrollWidth + 40) + const hh = + wrapperRef.current.clientHeight / + (contentRef.current.clientHeight + 80) const zoom = Math.min(maxScale, ww, hh) - inner.current.style.transform = 'scale(' + zoom + ')' + innerRef.current.style.transform = `scale(${zoom})` } + // observe size changes on the wrapper const ro = new ResizeObserver(resize) + if (wrapperRef.current) { + ro.observe(wrapperRef.current) + } - ro.observe(wrapper.current) - + // also debounce window resizes const resizeHandler = () => { clearTimeout(timeout) - timeout = setTimeout(() => { - resize() - }, 250) + timeout = setTimeout(resize, 250) } - window.addEventListener('resize', resizeHandler) + // initial scale + resize() + return () => { window.removeEventListener('resize', resizeHandler) ro.disconnect() @@ -54,11 +82,9 @@ export default function ResponsiveSize({ children, maxScale = 1, overlay, ...pro }, [maxScale]) return ( - -
-
- {children} -
+ +
+
{children}
) diff --git a/packages/react-ui/src/components/TextInput.tsx b/packages/react-ui/src/components/TextInput.tsx index 7e3318bc..04e41442 100644 --- a/packages/react-ui/src/components/TextInput.tsx +++ b/packages/react-ui/src/components/TextInput.tsx @@ -17,24 +17,49 @@ const StyledTextInput = styled.input` &:disabled { cursor: default; - opacity: .7; + opacity: 0.7; } ` -export interface TextInputProps extends Omit, 'onChange'> { - disabled?: boolean - onClick?: () => void +export interface TextInputProps + // remove the problematic props + extends Omit< + React.InputHTMLAttributes, + 'children' | 'onChange' | 'formAction' | 'contentEditable' + > { + /** current input value */ value: T + /** new-value callback */ onChange?: (value: string) => void + /** click handler */ + onClick?: () => void + /** disable input */ + disabled?: boolean + /** if true, prevent editing and show value */ + locked?: boolean } -export function TextInput({ onChange, ...props }: TextInputProps) { +export function TextInput({ + value, + onChange, + locked, + ...props +}: TextInputProps) { + // cast away the styled-components overload complexity + const SInput: any = StyledTextInput + return ( - onChange && onChange(evt.target.value)} - onFocus={(evt) => evt.target.select()} + ) => + onChange?.(evt.target.value) + } + onFocus={(evt: React.FocusEvent) => + evt.target.select() + } + disabled={props.disabled || locked} /> ) } diff --git a/packages/react-ui/src/components/WagerInput.tsx b/packages/react-ui/src/components/WagerInput.tsx index ba7b1ef4..2de5db13 100644 --- a/packages/react-ui/src/components/WagerInput.tsx +++ b/packages/react-ui/src/components/WagerInput.tsx @@ -1,4 +1,4 @@ -import { useGamba } from 'gamba-react-v2' +// src/components/WagerInput.tsx import React, { useRef } from 'react' import styled, { css } from 'styled-components' import { useCurrentToken, useFees, useUserBalance } from '../hooks' @@ -59,11 +59,8 @@ const Flex = styled.button` const Input = styled.input` border: none; - border-radius: 0; margin: 0; - padding: 10px; - padding-left: 0; - padding-right: 0; + padding: 10px 0; color: var(--gamba-ui-input-color); background: var(--gamba-ui-input-background); outline: none; @@ -74,8 +71,6 @@ const Input = styled.input` -webkit-appearance: none; margin: 0; } - - /* Firefox */ &[type=number] { -moz-appearance: textfield; } @@ -83,7 +78,6 @@ const Input = styled.input` const InputButton = styled.button` border: none; - border-radius: 0; margin: 0; padding: 2px 10px; color: var(--gamba-ui-input-color); @@ -104,14 +98,11 @@ const TokenImage = styled.img` ` const WagerAmount = styled.div` - text-wrap: nowrap; padding: 10px 0; width: 40px; - @media (min-width: 600px) { width: 100px; } - opacity: .8; overflow: hidden; ` @@ -119,6 +110,11 @@ const WagerAmount = styled.div` export interface WagerInputBaseProps { value: number onChange: (value: number) => void + /** If provided, the input is locked to this exact lamports value */ + lockedValue?: number + /** Optional lower/upper bounds in lamports (for range wager games) */ + minValue?: number + maxValue?: number } export type WagerInputProps = WagerInputBaseProps & { @@ -128,94 +124,113 @@ export type WagerInputProps = WagerInputBaseProps & { } export function WagerInput(props: WagerInputProps) { - const gamba = useGamba() - const token = useCurrentToken() - const [input, setInput] = React.useState('') - const balance = useUserBalance() // useBalance(walletAddress, token.mint) - const fees = useFees() + const token = useCurrentToken() + const balance = useUserBalance() + const fees = useFees() + const ref = useRef(null!) const [isEditing, setIsEditing] = React.useState(false) - const ref = useRef(null!) + const [input, setInput] = React.useState('') - React.useEffect( - () => { - props.onChange(token.baseWager) - }, - [token.mint.toString()], - ) + const isLocked = props.lockedValue != null + const effectiveValue = isLocked ? (props.lockedValue as number) : props.value + + const clampToBounds = (n: number) => { + let x = n + if (props.minValue != null) x = Math.max(x, props.minValue) + if (props.maxValue != null) x = Math.min(x, props.maxValue) + return x + } + + // whenever the mint changes, reset back to base wager + React.useEffect(() => { + props.onChange(token.baseWager) + }, [token.mint.toString()]) useOnClickOutside(ref, () => setIsEditing(false)) const startEditInput = () => { + if (props.disabled || isLocked) return if (props.options) { setIsEditing(!isEditing) return } setIsEditing(true) - setInput(String(props.value / (10 ** token.decimals))) + setInput(String(effectiveValue / 10 ** token.decimals)) } const apply = () => { - props.onChange(Number(input) * (10 ** token.decimals)) + if (isLocked) return + const next = Number(input) * 10 ** token.decimals + props.onChange(clampToBounds(next)) setIsEditing(false) } const x2 = () => { - const availableBalance = balance.balance + balance.bonusBalance - const nextValue = Math.max(token.baseWager, (props.value * 2) || token.baseWager) - props.onChange(Math.max(0, Math.min(nextValue, availableBalance - nextValue * fees))) + if (isLocked) return + const available = balance.balance + balance.bonusBalance + const base = token.baseWager + const want = Math.max(base, effectiveValue * 2 || base) + const cappedBal = Math.min(want, available - want * fees) + const bounded = clampToBounds(cappedBal) + props.onChange(bounded) } return (
- !gamba.isPlaying && startEditInput()}> + - {(!isEditing || props.options) ? ( + {isLocked || (!isEditing || props.options) ? ( - + ) : ( setInput(evt.target.value)} - onKeyDown={(e) => e.code === 'Enter' && apply()} - onBlur={(evt) => apply()} - disabled={gamba.isPlaying} + onChange={e => setInput(e.target.value)} + onKeyDown={e => e.code === 'Enter' && apply()} + onBlur={apply} + disabled={props.disabled || isLocked} autoFocus - onFocus={(e) => e.target.select()} + onFocus={e => e.target.select()} /> )} + {!props.options && ( - props.onChange(props.value / 2)}> + props.onChange(clampToBounds(effectiveValue / 2))} + > x.5 - + 2x )} - {props.options && isEditing && ( + + {props.options && isEditing && !isLocked && ( - {props.options.map((valueInBaseWager, i) => ( + {props.options.map((opt, i) => ( ))} diff --git a/packages/react-ui/src/components/WagerSelect.tsx b/packages/react-ui/src/components/WagerSelect.tsx index cba1c3cf..d46310c0 100644 --- a/packages/react-ui/src/components/WagerSelect.tsx +++ b/packages/react-ui/src/components/WagerSelect.tsx @@ -1,4 +1,3 @@ -import { useGamba } from 'gamba-react-v2' import React from 'react' import { Select } from './Select' import { TokenValue } from './TokenValue' @@ -8,23 +7,24 @@ export interface WagerSelectProps { value: number onChange: (value: number) => void className?: string + disabled?: boolean } -/** - * @deprecated Use WagerInput with "options" prop - */ -export function WagerSelect(props: WagerSelectProps) { - const gamba = useGamba() +export function WagerSelect({ + options, + value, + onChange, + className, + disabled = false, +}: WagerSelectProps) { return (