import { useState, useCallback } from 'react'; import { useDubs } from '../provider'; import { signAndSendBase64Transaction } from '../utils/transaction'; import type { CreateGameParams, MutationStatus } from '../types'; import { emitCreditsChanged } from './useCredits'; /** * Pull a short, recognisable team name from a long display name. Mirrors * the `getTeamNickname` helper the website uses when composing the * "I just placed a bet on X" share message — keeps the chat-feed text * tight ("Mariners" rather than "Seattle Mariners"). */ function teamNickname(name: string | null | undefined): string { if (!name) return 'TBD'; const parts = name.split(' '); return parts.length > 2 ? parts[parts.length - 1] : name; } export interface CreateGameMutationResult { gameId: string; gameAddress: string; signature: string; explorerUrl: string; } export function useCreateGame() { const { client, wallet, connection } = useDubs(); const [status, setStatus] = useState('idle'); const [error, setError] = useState(null); const [data, setData] = useState(null); const reset = useCallback(() => { setStatus('idle'); setError(null); setData(null); }, []); const execute = useCallback(async (params: CreateGameParams): Promise => { setStatus('building'); setError(null); setData(null); try { // 1. Build unsigned transaction console.log('[useCreateGame] Step 1: Building transaction...'); const createResult = await client.createGame(params); console.log('[useCreateGame] Step 1 done:', { gameId: createResult.gameId, gameAddress: createResult.gameAddress }); // 2. Sign and send transaction setStatus('signing'); console.log('[useCreateGame] Step 2: Signing and sending...'); const signature = await signAndSendBase64Transaction( createResult.transaction, wallet, connection, ); console.log('[useCreateGame] Step 2 done. Signature:', signature); // 3. Confirm with backend (server handles on-chain verification) setStatus('confirming'); console.log('[useCreateGame] Step 3: Confirming with backend...'); // For sponsored creates, the server forces wager to the credit // amount — use that, not whatever was in params, so the saved // game record is consistent with the on-chain bet. const wagerAmount = createResult.wagerAmount ?? params.wagerAmount; const confirmResult = await client.confirmGame({ gameId: createResult.gameId, playerWallet: params.playerWallet, signature, teamChoice: params.teamChoice, wagerAmount, role: 'creator', gameAddress: createResult.gameAddress, // Echo the credit code through so the server marks it as used // after the on-chain confirm succeeds. promoCode: createResult.promoCode, }); console.log('[useCreateGame] Step 3 done.'); const result: CreateGameMutationResult = { gameId: createResult.gameId, gameAddress: createResult.gameAddress, signature, explorerUrl: confirmResult.explorerUrl, }; setData(result); setStatus('success'); console.log('[useCreateGame] Complete!'); // Sponsored create just consumed a credit server-side — broadcast // so every mounted useCredits instance (rewards widget, streak // sheet, create sheet) refetches and the count drops in real time. if (createResult.promoCode) emitCreditsChanged(); // Auto-share to global chat. Mirrors the dubs.app website's // BetCreationModal behaviour so a bet placed from any client surfaces // in chat with a tappable game-invite chip and a "Join me!" CTA. // Fire-and-forget: a chat-broadcast failure must not surface as a // create-game error, the on-chain bet is already settled. try { const event = createResult.event; const home = event?.opponents?.[0]?.name ?? null; const away = event?.opponents?.[1]?.name ?? null; const teamLabel = params.teamChoice === 'home' ? teamNickname(home) : params.teamChoice === 'away' ? teamNickname(away) : 'Draw'; const message = `I just placed a ${wagerAmount} SOL bet on ${teamLabel} - Join me!`; // event.startTime is an ESPN-unified ISO string ("…Z"), but every // downstream chat consumer (dubs.app SPA, mobile app) does // `new Date(strTimestamp + 'Z')` and crashes on the resulting // double-Z. Strip the trailing Z so the on-the-wire shape matches // the legacy TheSportsDB format the rest of the system expects. const strTimestamp = typeof event?.startTime === 'string' ? event.startTime.replace(/Z$/, '') : null; const gameInvite = { gameId: createResult.gameId, gameAddress: createResult.gameAddress, title: event?.title, league: event?.league, gameType: 'sports', buyIn: wagerAmount, status: 'waiting', homeTeam: home, awayTeam: away, homeTeamBadge: event?.opponents?.[0]?.imageUrl ?? null, awayTeamBadge: event?.opponents?.[1]?.imageUrl ?? null, imageUrl: event?.media?.thumbnail ?? null, strThumb: event?.media?.thumbnail ?? null, strPoster: event?.media?.poster ?? null, strTimestamp, creatorTeam: params.teamChoice, creatorWallet: params.playerWallet, } as Record; await client.sendChatMessage({ message, gameInvite }); } catch (chatErr) { console.warn('[useCreateGame] Auto chat-share failed (non-fatal):', chatErr); } return result; } catch (err) { console.error('[useCreateGame] FAILED:', err); const error = err instanceof Error ? err : new Error(String(err)); setError(error); setStatus('error'); throw error; } }, [client, wallet]); return { execute, status, error, data, reset }; }