'use client'; import { useState, useRef, useEffect } from 'react'; import { useWallet } from '@solana/wallet-adapter-react'; import { Connection, PublicKey, Transaction } from '@solana/web3.js'; import {createTransferInstruction, getAssociatedTokenAddress, createAssociatedTokenAccountInstruction } from '@solana/spl-token'; import Header, { HeaderRef } from './components/Header'; import Leaderboard from './components/Leaderboard'; import { CLUSTER_URL, ENTRY_FEE_DINO } from './shared'; import { DINO_TOKEN_ADDRESS, FEE_COLLECTOR } from './constants'; import { sendTransactionWithMobileOptimization, isMobileDevice, getMobileCommitmentLevel, validateTransactionForMobile } from './utils/mobileWalletUtils'; interface DashboardData { leaderboard: Array<{ owner: string; score: string }>; available_tx: string | null; attempts_count:number; } export default function Home() { const wallet = useWallet(); const { publicKey, sendTransaction } = wallet; const [isProcessing, setIsProcessing] = useState(false); const [txHash, setTxHash] = useState(null); const [error, setError] = useState(null); const [hasTicket, setHasTicket] = useState(false); const [ticketTxHash, setTicketTxHash] = useState(null); const [showPaymentSuccess, setShowPaymentSuccess] = useState(false); const [highscore, setHighscore] = useState(0); const [dashboardData, setDashboardData] = useState({ leaderboard: [], available_tx: null, attempts_count: 0 }); const [dashboardLoading, setDashboardLoading] = useState(true); const [dashboardError, setDashboardError] = useState(null); const headerRef = useRef(null); // Poll for available tickets every 5 seconds (only when no ticket is available) useEffect(() => { if (!publicKey) { setHasTicket(false); return; } // Don't poll if we already have a ticket if (hasTicket) { return; } const checkTicket = async () => { try { const response = await fetch(`https://vps.playpoolstudios.com/dino/api/get_available_tx.php?owner=${publicKey.toString()}`); if (response.ok) { const ticketData = await response.text(); // If we get a valid transaction hash (not "0"), we have a ticket const ticketAvailable = ticketData !== "0" && ticketData.trim().length > 0; setHasTicket(ticketAvailable); // If ticket becomes available, stop polling immediately if (ticketAvailable) { setTicketTxHash(ticketData); return; } setTicketTxHash(null); } else { setHasTicket(false); setTicketTxHash(null); } } catch (error) { console.error('Error checking ticket:', error); setHasTicket(false); } finally { } }; // Check immediately checkTicket(); // Set up polling every 5 seconds const interval = setInterval(checkTicket, 5000); // Cleanup interval on unmount, when publicKey changes, or when ticket becomes available return () => clearInterval(interval); }, [publicKey, hasTicket]); useEffect(() => { const handleMessage = (event: MessageEvent) => { if (event.origin === window.location.origin && typeof event.data === "string") { try { const message = JSON.parse(event.data); if (message?.type === "gameClose" && typeof message.status === "number") { game_close_signal(message.status); } } catch (error) { console.error("JSON parse error from Unity message:", error); } } }; window.addEventListener("message", handleMessage); return () => window.removeEventListener("message", handleMessage); }, []); // Fetch dashboard data every 5 seconds useEffect(() => { const fetchDashboardData = async () => { try { setDashboardLoading(true); const response = await fetch('https://vps.playpoolstudios.com/dino/api/get_dashboard_data.php'); if (!response.ok) { throw new Error('Failed to fetch dashboard data'); } const data: DashboardData = await response.json(); setHighscore(parseInt(data.leaderboard[0].score)); setDashboardData(data); setDashboardError(null); } catch (err) { console.error('Error fetching dashboard data:', err); setDashboardError(err instanceof Error ? err.message : 'Failed to fetch dashboard data'); } finally { setDashboardLoading(false); } }; fetchDashboardData(); // Refresh dashboard data every 5 seconds const interval = setInterval(fetchDashboardData, 5000); return () => clearInterval(interval); }, []); const game_close_signal = (status: number) => { console.log("game_close_signal", status); setHasTicket(false); setTicketTxHash(null); }; // Auto-hide payment success panel after 5 seconds useEffect(() => { console.log('useEffect triggered:', { txHash, showPaymentSuccess }); if (txHash && !showPaymentSuccess) { console.log('Starting progress animation'); setShowPaymentSuccess(true); console.log('showPaymentSuccess set to true'); } else { console.log('Condition not met:', { txHash: !!txHash, showPaymentSuccess }); } }, [txHash, showPaymentSuccess]); // Test useEffect useEffect(() => { console.log('Test useEffect - component mounted'); }, []); const handleEnterGame = async () => { if (!publicKey) { setError('Please connect your wallet first'); return; } setIsProcessing(true); setError(null); setTxHash(null); try { const connection = new Connection(CLUSTER_URL, 'confirmed'); const dinoMint = new PublicKey(DINO_TOKEN_ADDRESS); // Get the user's DINO token account const userTokenAccount = await getAssociatedTokenAddress( dinoMint, publicKey ); // Check user's token account status and balance console.log('Checking user token account status...'); const userTokenAccountInfo = await connection.getAccountInfo(userTokenAccount); if (!userTokenAccountInfo) { throw new Error('DINO token account not found. Please ensure you have DINO tokens in your wallet.'); } // Get token balance const userTokenAccounts = await connection.getParsedTokenAccountsByOwner( publicKey, { mint: dinoMint } ); if (userTokenAccounts.value.length === 0) { throw new Error('No DINO tokens found in your wallet.'); } const userBalance = userTokenAccounts.value[0].account.data.parsed.info.tokenAmount.uiAmount; console.log('User DINO balance:', userBalance); if (userBalance < ENTRY_FEE_DINO) { throw new Error(`Insufficient DINO tokens. You have ${userBalance} DINO, but need ${ENTRY_FEE_DINO} DINO to play.`); } // Get or create the fee collector's DINO token account const feeCollectorTokenAccount = await getAssociatedTokenAddress( dinoMint, FEE_COLLECTOR ); const transaction = new Transaction(); // Check if fee collector token account exists, if not create it const feeCollectorAccountInfo = await connection.getAccountInfo(feeCollectorTokenAccount); if (!feeCollectorAccountInfo) { console.log('Creating fee collector token account...'); const createAccountIx = createAssociatedTokenAccountInstruction( publicKey, feeCollectorTokenAccount, FEE_COLLECTOR, dinoMint ); transaction.add(createAccountIx); console.log('Added create account instruction'); } // Add the transfer instruction (10 DINO tokens = 10,000,000,000 lamports for 9 decimals) const transferAmount = ENTRY_FEE_DINO * 1_000_000_000; // 10 DINO tokens console.log('Adding transfer instruction for', ENTRY_FEE_DINO, 'DINO tokens (', transferAmount, 'lamports)'); const transferIx = createTransferInstruction( userTokenAccount, feeCollectorTokenAccount, publicKey, transferAmount ); // Validate the transfer instruction if (!transferIx.programId.equals(dinoMint)) { console.warn('Transfer instruction program ID mismatch'); } transaction.add(transferIx); console.log('Added transfer instruction'); // Get recent blockhash with higher commitment for mobile wallets const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('confirmed'); transaction.recentBlockhash = blockhash; transaction.lastValidBlockHeight = lastValidBlockHeight; transaction.feePayer = publicKey; // Ensure the transaction is properly formatted for mobile wallets console.log('Transaction details before preflight:'); console.log('- Recent blockhash:', transaction.recentBlockhash); console.log('- Last valid block height:', transaction.lastValidBlockHeight); console.log('- Fee payer:', transaction.feePayer?.toString()); console.log('- Number of instructions:', transaction.instructions.length); console.log('- Instructions:', transaction.instructions.map((ix, i) => `${i}: ${ix.programId.toString()}`)); // Validate transaction for mobile wallets const validationErrors = validateTransactionForMobile(transaction); if (validationErrors.length > 0) { console.warn('Transaction validation errors:', validationErrors); // For mobile wallets, we might want to be more strict if (isMobileDevice()) { throw new Error(`Transaction validation failed: ${validationErrors.join(', ')}`); } } // Preflight check for mobile wallets try { console.log('Running preflight check for transaction...'); const { value: { err, logs } } = await connection.simulateTransaction(transaction); if (err) { console.warn('Preflight check failed:', err); console.log('Preflight logs:', logs); // For mobile wallets, we'll try to fix common issues and retry if (isMobileDevice()) { console.log('Attempting to fix transaction for mobile wallet...'); // Refresh blockhash and retry preflight const { blockhash: newBlockhash, lastValidBlockHeight: newLastValidBlockHeight } = await connection.getLatestBlockhash('confirmed'); transaction.recentBlockhash = newBlockhash; transaction.lastValidBlockHeight = newLastValidBlockHeight; // Retry preflight with fresh blockhash const retryResult = await connection.simulateTransaction(transaction); if (retryResult.value.err) { console.warn('Preflight retry also failed:', retryResult.value.err); // On mobile, we might want to proceed anyway as some wallets handle this differently console.log('Proceeding with transaction despite preflight failure (mobile wallet behavior)'); } else { console.log('Preflight retry successful!'); } } else { // On desktop, be more strict about preflight failures throw new Error(`Transaction simulation failed: ${err}`); } } else { console.log('Preflight check passed successfully'); } } catch (simError) { console.warn('Preflight simulation failed:', simError); // On mobile, we might want to proceed anyway as some wallets handle this differently if (isMobileDevice()) { console.log('Proceeding with transaction despite preflight failure (mobile wallet behavior)'); } else { throw new Error('Transaction preflight check failed. Please try again.'); } } // Send the transaction with mobile-optimized options let signature: string; try { signature = await sendTransactionWithMobileOptimization( transaction, connection, wallet, { skipPreflight: false, // Try with preflight first preflightCommitment: 'confirmed', maxRetries: 3, commitment: getMobileCommitmentLevel() } ); } catch (error) { console.warn('Mobile-optimized transaction failed, trying with preflight disabled...'); // If mobile-optimized fails, try with preflight disabled for mobile wallets if (isMobileDevice()) { try { signature = await sendTransactionWithMobileOptimization( transaction, connection, wallet, { skipPreflight: true, // Disable preflight as fallback preflightCommitment: 'confirmed', maxRetries: 2, commitment: getMobileCommitmentLevel() } ); } catch (fallbackError) { console.error('Fallback transaction also failed:', fallbackError); throw fallbackError; } } else { throw error; } } // Remove the old confirmation logic since it's handled in the utility setTxHash(signature); console.log('Transaction successful! Hash:', signature); console.log('txHash state set to:', signature); // Refresh the dino balance in header after successful payment if (headerRef.current) { await headerRef.current.refreshBalance(); } // Immediately check for ticket availability after successful payment try { const ticketResponse = await fetch(`https://vps.playpoolstudios.com/dino/api/get_available_tx.php?owner=${publicKey.toString()}`); if (ticketResponse.ok) { const ticketData = await ticketResponse.text(); const ticketAvailable = ticketData !== "0" && ticketData.trim().length > 0; setHasTicket(ticketAvailable); } } catch (ticketError) { console.warn('Error checking ticket after payment:', ticketError); } // Send transaction to validator try { const validatorUrl = `https://solpay.playpoolstudios.com/tx/new?tx=${signature}&target_address=${FEE_COLLECTOR.toString()}&amount=10000000000&sender_address=${publicKey.toString()}&token_mint=${dinoMint.toString()}`; // Add leaderboard entry const leaderboardUrl = 'https://vps.playpoolstudios.com/dino/api/add_leaderboard.php'; const leaderboardData = new FormData(); leaderboardData.append('tx', signature); leaderboardData.append('owner', publicKey.toString()); leaderboardData.append('score', '0'); // Initial score of 0 try { const leaderboardResponse = await fetch(leaderboardUrl, { method: 'POST', body: leaderboardData }); if (!leaderboardResponse.ok) { console.warn('Failed to add leaderboard entry'); } } catch (leaderboardError) { console.warn('Error adding leaderboard entry:', leaderboardError); } const response = await fetch(validatorUrl); if (response.ok) { const result = await response.json(); console.log('Transaction validated:', result); } else { console.warn('Failed to validate transaction with server'); } } catch (validationError) { console.warn('Error validating transaction with server:', validationError); } } catch (err) { console.error('Error processing payment:', err); // Enhanced error handling for mobile wallets let errorMessage = 'Failed to process payment'; if (err instanceof Error) { // Log additional debugging info for mobile wallets if (isMobileDevice()) { console.log('Mobile device detected, additional debugging info:'); console.log('User agent:', navigator.userAgent); console.log('Screen size:', window.innerWidth, 'x', window.innerHeight); console.log('Wallet connected:', !!publicKey); } if (err.message.includes('signature verification failed')) { errorMessage = 'Mobile wallet signature verification failed. Please try again or check your wallet settings.'; } else if (err.message.includes('missing signature')) { errorMessage = 'Transaction signature missing. Please approve the transaction in your wallet.'; } else if (err.message.includes('User rejected')) { errorMessage = 'Transaction was rejected. Please try again.'; } else if (err.message.includes('insufficient funds')) { errorMessage = 'Insufficient DINO tokens. Please check your balance.'; } else if (err.message.includes('Transaction simulation failed')) { errorMessage = 'Transaction validation failed. Please check your wallet connection and try again.'; } else if (err.message.includes('blockhash')) { errorMessage = 'Transaction expired. Please try again.'; } else if (err.message.includes('preflight')) { errorMessage = 'Transaction preflight check failed. This usually means there\'s an issue with your wallet connection or token balance. Try the Debug Wallet button to diagnose the issue.'; } else if (err.message.includes('insufficient lamports')) { errorMessage = 'Insufficient SOL for transaction fees. Please ensure you have some SOL in your wallet for gas fees.'; } else if (err.message.includes('invalid account')) { errorMessage = 'Invalid account detected. Please refresh the page and reconnect your wallet.'; } else { errorMessage = err.message; } } setError(errorMessage); } finally { setIsProcessing(false); } }; const handleDebugWallet = async () => { if (!publicKey) { setError('Please connect your wallet first'); return; } console.log('=== WALLET DEBUG INFO ==='); console.log('Public key:', publicKey.toString()); console.log('Is mobile device:', isMobileDevice()); try { const connection = new Connection(CLUSTER_URL, 'confirmed'); const dinoMint = new PublicKey(DINO_TOKEN_ADDRESS); // Test basic connection console.log('Testing Solana connection...'); const version = await connection.getVersion(); console.log('Solana version:', version); // Test token account console.log('Testing token account...'); const userTokenAccount = await getAssociatedTokenAddress(dinoMint, publicKey); console.log('User token account:', userTokenAccount.toString()); const userTokenAccountInfo = await connection.getAccountInfo(userTokenAccount); console.log('Token account exists:', !!userTokenAccountInfo); if (userTokenAccountInfo) { const userTokenAccounts = await connection.getParsedTokenAccountsByOwner( publicKey, { mint: dinoMint } ); if (userTokenAccounts.value.length > 0) { const balance = userTokenAccounts.value[0].account.data.parsed.info.tokenAmount.uiAmount; console.log('DINO balance:', balance); } else { console.log('No DINO tokens found'); } } // Test simple transaction simulation console.log('Testing simple transaction simulation...'); const testTransaction = new Transaction(); testTransaction.recentBlockhash = (await connection.getLatestBlockhash('confirmed')).blockhash; testTransaction.feePayer = publicKey; try { const simResult = await connection.simulateTransaction(testTransaction); console.log('Simple transaction simulation result:', simResult); } catch (simError) { console.error('Simple transaction simulation failed:', simError); } console.log('=== END DEBUG INFO ==='); setError('Debug info logged to console. Check browser console for details.'); } catch (error) { console.error('Debug failed:', error); setError(`Debug failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } }; return (
{/* Game iframe - shown when user has a ticket */} {hasTicket && (