Skip to main content

Whisky React SDK

The Whisky React SDK provides React components and hooks for seamless integration of the Whisky Gaming Protocol into frontend applications.

Overview

The React SDK serves as the UI layer, providing:
  • React Hooks: Custom hooks for protocol interaction
  • UI Components: Pre-built gaming components
  • Wallet Integration: Seamless wallet connectivity
  • State Management: Application state handling

Features

  • Unified Package: Combines UI components and React hooks in a single package
  • Whisky Protocol Integration: Built specifically for the Whisky Gaming Protocol
  • TypeScript Support: Full TypeScript support with comprehensive type definitions
  • Wallet Integration: Seamless integration with Solana wallet adapters
  • Plugin System: Extensible plugin system for custom functionality
  • Transaction Management: Built-in transaction handling and state management

Installation

npm install @whisky-gaming/ui

Quick Start

import { WhiskyProvider, useWhisky } from '@whisky-gaming/ui'

function App() {
  return (
    <WhiskyProvider>
      <YourGame />
    </WhiskyProvider>
  )
}

function YourGame() {
  const { play, isPlaying, game } = useWhisky()
  
  const handlePlay = async () => {
    await play({
      wager: 1000000, // 0.001 SOL
      bet: [50, 50], // Equal probability
      creator: 'YOUR_CREATOR_ADDRESS',
    })
  }
  
  return (
    <div>
      <button onClick={handlePlay} disabled={isPlaying}>
        {isPlaying ? 'Playing...' : 'Play'}
      </button>
    </div>
  )
}

Core Components

WhiskyProvider

The main provider that wraps your app and provides access to the Whisky protocol.
import { WhiskyProvider } from '@whisky-gaming/ui'

<WhiskyProvider plugins={[yourCustomPlugin]}>
  {children}
</WhiskyProvider>
Props:
interface WhiskyProviderProps {
  children: React.ReactNode;
  plugins?: WhiskyPlugin[];
  config?: WhiskyConfig;
}

WhiskyPlatformProvider

Provides platform-specific functionality like token selection and pool management.
import { WhiskyPlatformProvider } from '@whisky-gaming/ui'

<WhiskyPlatformProvider>
  {children}
</WhiskyPlatformProvider>

Hooks

useWhisky()

Main hook for interacting with the Whisky protocol.
const { play, result, isPlaying, game, error } = useWhisky()
Returns:
interface WhiskyHookReturn {
  play: (params: PlayParams) => Promise<void>;
  result: GameResult | null;
  isPlaying: boolean;
  game: Game | null;
  error: Error | null;
  reset: () => void;
}
Example:
function GamingComponent() {
  const { play, result, isPlaying, game, error } = useWhisky()
  
  const handlePlay = async () => {
    try {
      await play({
        wager: 1000000,
        bet: [50, 50],
        creator: 'CREATOR_ADDRESS',
        clientSeed: 'my_seed_123'
      })
    } catch (err) {
      console.error('Game failed:', err)
    }
  }
  
  return (
    <div>
      <button onClick={handlePlay} disabled={isPlaying}>
        {isPlaying ? 'Playing...' : 'Play Game'}
      </button>
      
      {result && (
        <div>
          {result.won ? 'You won!' : 'You lost!'}
          <p>Payout: {result.payout}</p>
        </div>
      )}
      
      {error && <div>Error: {error.message}</div>}
    </div>
  )
}

usePool()

Get pool information for a specific token.
const { pool, loading, error } = usePool(tokenMint, poolAuthority)
Parameters:
  • tokenMint: Token mint address
  • poolAuthority: Pool authority address
Returns:
interface PoolHookReturn {
  pool: PoolInfo | null;
  loading: boolean;
  error: Error | null;
  refetch: () => void;
}
Example:
function PoolInfo({ tokenMint, poolAuthority }) {
  const { pool, loading, error } = usePool(tokenMint, poolAuthority)
  
  if (loading) return <div>Loading pool...</div>
  if (error) return <div>Error: {error.message}</div>
  if (!pool) return <div>Pool not found</div>
  
  return (
    <div>
      <h3>Pool Information</h3>
      <p>Total Liquidity: {pool.totalLiquidity}</p>
      <p>Total Plays: {pool.totalPlays}</p>
      <p>Min Wager: {pool.minWager}</p>
    </div>
  )
}

useBalance()

Get balance information for a wallet and token.
const { balance, nativeBalance } = useBalance(wallet, token)
Parameters:
  • wallet: Wallet public key
  • token: Token mint address (optional)
Returns:
interface BalanceHookReturn {
  balance: number | null;
  nativeBalance: number | null;
  bonusBalance: number | null;
  loading: boolean;
  error: Error | null;
  refetch: () => void;
}
Example:
function BalanceDisplay({ wallet, token }) {
  const { balance, nativeBalance, loading } = useBalance(wallet, token)
  
  if (loading) return <div>Loading balance...</div>
  
  return (
    <div>
      <p>Token Balance: {balance}</p>
      <p>SOL Balance: {nativeBalance}</p>
    </div>
  )
}

useWalletAddress()

Get the current wallet address.
const walletAddress = useWalletAddress()
Returns: string | null Example:
function WalletInfo() {
  const walletAddress = useWalletAddress()
  
  if (!walletAddress) {
    return <div>Please connect your wallet</div>
  }
  
  return (
    <div>
      <p>Connected: {walletAddress.substring(0, 8)}...</p>
    </div>
  )
}

UI Components

TokenValue

Display token values with proper formatting.
import { TokenValue } from '@whisky-gaming/ui'

<TokenValue amount={1000000} token={tokenMeta} />
Props:
interface TokenValueProps {
  amount: number;
  token: TokenMetadata;
  showSymbol?: boolean;
  decimals?: number;
  className?: string;
}
Example:
function TokenDisplay() {
  const usdcToken = {
    symbol: 'USDC',
    decimals: 6,
    name: 'USD Coin'
  }
  
  return (
    <div>
      <TokenValue 
        amount={1000000} 
        token={usdcToken} 
        showSymbol={true}
      />
      {/* Displays: "1.00 USDC" */}
    </div>
  )
}

Canvas

Game canvas component for rendering games.
import { Canvas } from '@whisky-gaming/ui'

<Canvas game={gameBundle} />
Props:
interface CanvasProps {
  game: GameBundle;
  width?: number;
  height?: number;
  onGameEnd?: (result: GameResult) => void;
  className?: string;
}
Example:
function GameCanvas({ gameBundle }) {
  const handleGameEnd = (result) => {
    console.log('Game ended:', result)
  }
  
  return (
    <Canvas 
      game={gameBundle}
      width={800}
      height={600}
      onGameEnd={handleGameEnd}
    />
  )
}

GameButton

Pre-styled button component for gaming actions.
import { GameButton } from '@whisky-gaming/ui'

<GameButton onClick={handlePlay} disabled={isPlaying}>
  Play Game
</GameButton>
Props:
interface GameButtonProps {
  children: React.ReactNode;
  onClick: () => void;
  disabled?: boolean;
  variant?: 'primary' | 'secondary' | 'success' | 'danger';
  size?: 'small' | 'medium' | 'large';
  loading?: boolean;
  className?: string;
}

TokenSelector

Component for selecting tokens for gaming.
import { TokenSelector } from '@whisky-gaming/ui'

<TokenSelector 
  selectedToken={selectedToken}
  onTokenSelect={setSelectedToken}
  tokens={availableTokens}
/>
Props:
interface TokenSelectorProps {
  selectedToken: TokenMetadata | null;
  onTokenSelect: (token: TokenMetadata) => void;
  tokens: TokenMetadata[];
  disabled?: boolean;
  className?: string;
}

Plugin System

Create custom plugins to extend functionality:
import { WhiskyPlugin } from '@whisky-gaming/ui'

const myPlugin: WhiskyPlugin = async (input, context) => {
  // Your custom logic here
  return [] // Return additional transaction instructions
}

<WhiskyProvider plugins={[myPlugin]}>
  {children}
</WhiskyProvider>
Plugin Interface:
interface WhiskyPlugin {
  name: string;
  version: string;
  execute: (input: any, context: PluginContext) => Promise<any[]>;
}
Example Plugin:
const referralPlugin: WhiskyPlugin = {
  name: 'referral',
  version: '1.0.0',
  execute: async (input, context) => {
    const { referralCode } = input
    
    if (referralCode) {
      // Add referral logic
      return [referralInstruction]
    }
    
    return []
  }
}

function App() {
  return (
    <WhiskyProvider plugins={[referralPlugin]}>
      <GamingApp />
    </WhiskyProvider>
  )
}

Referral System

Built-in referral system support:
import { ReferralProvider, useReferral } from '@whisky-gaming/ui'

<ReferralProvider>
  <YourApp />
</ReferralProvider>

// In your component
const { referralCode, setReferralCode } = useReferral()
Referral Hook:
interface ReferralHookReturn {
  referralCode: string | null;
  setReferralCode: (code: string) => void;
  clearReferralCode: () => void;
  referralStats: ReferralStats | null;
}
Example:
function ReferralSystem() {
  const { referralCode, setReferralCode, referralStats } = useReferral()
  
  const handleReferralInput = (code: string) => {
    setReferralCode(code)
  }
  
  return (
    <div>
      <input 
        placeholder="Enter referral code"
        onChange={(e) => handleReferralInput(e.target.value)}
        value={referralCode || ''}
      />
      
      {referralStats && (
        <div>
          <p>Referrals: {referralStats.totalReferrals}</p>
          <p>Earnings: {referralStats.totalEarnings}</p>
        </div>
      )}
    </div>
  )
}

Advanced Usage Examples

Complete Gaming Application

import React from 'react'
import { 
  WhiskyProvider, 
  useWhisky, 
  usePool, 
  useBalance,
  TokenValue,
  GameButton,
  TokenSelector 
} from '@whisky-gaming/ui'

function GamingApp() {
  return (
    <WhiskyProvider>
      <div className="gaming-app">
        <h1>Whisky Gaming</h1>
        <GameInterface />
      </div>
    </WhiskyProvider>
  )
}

function GameInterface() {
  const { play, isPlaying, result, error } = useWhisky()
  const [selectedToken, setSelectedToken] = useState(null)
  const [wager, setWager] = useState(1000000)
  const [bet, setBet] = useState([50, 50])
  
  const { pool } = usePool(selectedToken?.mint)
  const { balance } = useBalance(wallet, selectedToken?.mint)
  
  const handlePlay = async () => {
    if (!selectedToken || !pool) return
    
    await play({
      poolAddress: pool.address,
      wager,
      bet,
      creator: 'CREATOR_ADDRESS',
      clientSeed: `game_${Date.now()}`,
      creatorFee: 100,
      jackpotFee: 50,
      metadata: 'Custom Game'
    })
  }
  
  return (
    <div className="game-interface">
      <div className="token-section">
        <TokenSelector 
          selectedToken={selectedToken}
          onTokenSelect={setSelectedToken}
          tokens={availableTokens}
        />
        
        {balance && (
          <div className="balance">
            Balance: <TokenValue amount={balance} token={selectedToken} />
          </div>
        )}
      </div>
      
      <div className="game-controls">
        <input 
          type="number"
          value={wager}
          onChange={(e) => setWager(Number(e.target.value))}
          placeholder="Wager amount"
        />
        
        <GameButton 
          onClick={handlePlay}
          disabled={isPlaying || !selectedToken || !pool}
          loading={isPlaying}
        >
          {isPlaying ? 'Playing...' : 'Play Game'}
        </GameButton>
      </div>
      
      {result && (
        <div className="game-result">
          <h3>{result.won ? 'You Won!' : 'You Lost!'}</h3>
          <p>Payout: <TokenValue amount={result.payout} token={selectedToken} /></p>
          <p>Multiplier: {result.multiplier}x</p>
        </div>
      )}
      
      {error && (
        <div className="error">
          Error: {error.message}
        </div>
      )}
    </div>
  )
}

Multi-Game Interface

function MultiGameInterface() {
  const [selectedGame, setSelectedGame] = useState('coinflip')
  
  const games = {
    coinflip: {
      name: 'Coin Flip',
      bet: [50, 50],
      description: 'Simple 50/50 chance'
    },
    dice: {
      name: 'Dice Roll',
      bet: [16, 16, 16, 16, 16, 20],
      description: 'Six-sided dice with slight bias'
    },
    slots: {
      name: 'Slot Machine',
      bet: [500, 300, 150, 40, 9, 1],
      description: 'Complex slot machine simulation'
    }
  }
  
  const { play, isPlaying } = useWhisky()
  
  const handleGameSelect = (gameKey) => {
    setSelectedGame(gameKey)
  }
  
  const handlePlay = async () => {
    const game = games[selectedGame]
    await play({
      wager: 1000000,
      bet: game.bet,
      creator: 'CREATOR_ADDRESS',
      metadata: game.name
    })
  }
  
  return (
    <div className="multi-game">
      <div className="game-selector">
        {Object.entries(games).map(([key, game]) => (
          <button
            key={key}
            onClick={() => handleGameSelect(key)}
            className={selectedGame === key ? 'active' : ''}
          >
            {game.name}
          </button>
        ))}
      </div>
      
      <div className="game-info">
        <h3>{games[selectedGame].name}</h3>
        <p>{games[selectedGame].description}</p>
      </div>
      
      <GameButton onClick={handlePlay} disabled={isPlaying}>
        Play {games[selectedGame].name}
      </GameButton>
    </div>
  )
}

Pool Management Interface

function PoolManagement() {
  const [selectedPool, setSelectedPool] = useState(null)
  const [depositAmount, setDepositAmount] = useState('')
  
  const { pools, loading } = usePools()
  const { depositLiquidity, withdrawLiquidity } = usePoolActions()
  
  const handleDeposit = async () => {
    if (!selectedPool || !depositAmount) return
    
    await depositLiquidity(selectedPool.address, Number(depositAmount))
    setDepositAmount('')
  }
  
  const handleWithdraw = async (lpTokenAmount) => {
    if (!selectedPool) return
    
    await withdrawLiquidity(selectedPool.address, lpTokenAmount)
  }
  
  return (
    <div className="pool-management">
      <h2>Pool Management</h2>
      
      <div className="pool-list">
        {pools.map(pool => (
          <div 
            key={pool.address}
            className={`pool-item ${selectedPool?.address === pool.address ? 'selected' : ''}`}
            onClick={() => setSelectedPool(pool)}
          >
            <TokenValue amount={pool.totalLiquidity} token={pool.token} />
            <p>Total Plays: {pool.totalPlays}</p>
            <p>Fee: {pool.feeBps / 100}%</p>
          </div>
        ))}
      </div>
      
      {selectedPool && (
        <div className="pool-actions">
          <h3>Manage {selectedPool.token.symbol} Pool</h3>
          
          <div className="deposit-section">
            <input
              type="number"
              value={depositAmount}
              onChange={(e) => setDepositAmount(e.target.value)}
              placeholder="Amount to deposit"
            />
            <GameButton onClick={handleDeposit}>
              Deposit
            </GameButton>
          </div>
          
          <div className="withdraw-section">
            <p>Your LP Tokens: {selectedPool.userLpTokens}</p>
            <GameButton 
              onClick={() => handleWithdraw(selectedPool.userLpTokens)}
              variant="secondary"
            >
              Withdraw All
            </GameButton>
          </div>
        </div>
      )}
    </div>
  )
}

Styling and Theming

Custom Styling

// Custom CSS
.game-button {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  border: none;
  border-radius: 8px;
  color: white;
  padding: 12px 24px;
  font-size: 16px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s ease;
}

.game-button:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

.game-button:disabled {
  opacity: 0.6;
  cursor: not-allowed;
  transform: none;
}

Theme Provider

import { WhiskyThemeProvider } from '@whisky-gaming/ui'

const customTheme = {
  colors: {
    primary: '#667eea',
    secondary: '#764ba2',
    success: '#48bb78',
    danger: '#f56565',
    background: '#1a202c',
    text: '#ffffff'
  },
  borderRadius: '8px',
  spacing: {
    small: '8px',
    medium: '16px',
    large: '24px'
  }
}

<WhiskyThemeProvider theme={customTheme}>
  <GamingApp />
</WhiskyThemeProvider>

Error Handling

Global Error Boundary

import { WhiskyErrorBoundary } from '@whisky-gaming/ui'

function App() {
  return (
    <WhiskyErrorBoundary
      fallback={<div>Something went wrong with the gaming system</div>}
    >
      <WhiskyProvider>
        <GamingApp />
      </WhiskyProvider>
    </WhiskyErrorBoundary>
  )
}

Hook Error Handling

function SafeGamingComponent() {
  const { play, error, reset } = useWhisky()
  
  useEffect(() => {
    if (error) {
      // Log error to monitoring service
      console.error('Gaming error:', error)
      
      // Show user-friendly message
      toast.error('Game failed. Please try again.')
    }
  }, [error])
  
  const handlePlay = async () => {
    try {
      await play(gameParams)
    } catch (err) {
      // Handle specific errors
      if (err.code === 'INSUFFICIENT_BALANCE') {
        toast.error('Insufficient balance')
      } else if (err.code === 'POOL_NOT_FOUND') {
        toast.error('Pool not available')
      } else {
        toast.error('Unexpected error occurred')
      }
    }
  }
  
  return (
    <div>
      <GameButton onClick={handlePlay}>
        Play Game
      </GameButton>
      
      {error && (
        <button onClick={reset}>
          Reset Game State
        </button>
      )}
    </div>
  )
}

Performance Optimization

Memoization

import React, { useMemo, useCallback } from 'react'

function OptimizedGamingComponent() {
  const { play, isPlaying } = useWhisky()
  
  // Memoize expensive calculations
  const gameConfig = useMemo(() => ({
    bet: [25, 25, 25, 25],
    creatorFee: 100,
    jackpotFee: 50
  }), [])
  
  // Memoize callback
  const handlePlay = useCallback(async () => {
    await play({
      wager: 1000000,
      ...gameConfig,
      metadata: 'Optimized Game'
    })
  }, [play, gameConfig])
  
  return (
    <GameButton onClick={handlePlay} disabled={isPlaying}>
      Play Optimized Game
    </GameButton>
  )
}

Lazy Loading

import React, { lazy, Suspense } from 'react'

const GameCanvas = lazy(() => import('./GameCanvas'))
const PoolManagement = lazy(() => import('./PoolManagement'))

function LazyGamingApp() {
  return (
    <WhiskyProvider>
      <Suspense fallback={<div>Loading...</div>}>
        <GameCanvas />
        <PoolManagement />
      </Suspense>
    </WhiskyProvider>
  )
}

Testing

Component Testing

import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { WhiskyProvider } from '@whisky-gaming/ui'

function TestGamingComponent() {
  const { play, isPlaying } = useWhisky()
  
  return (
    <button onClick={() => play(gameParams)} disabled={isPlaying}>
      {isPlaying ? 'Playing...' : 'Play'}
    </button>
  )
}

test('renders play button', () => {
  render(
    <WhiskyProvider>
      <TestGamingComponent />
    </WhiskyProvider>
  )
  
  expect(screen.getByText('Play')).toBeInTheDocument()
})

test('handles play action', async () => {
  render(
    <WhiskyProvider>
      <TestGamingComponent />
    </WhiskyProvider>
  )
  
  const button = screen.getByText('Play')
  fireEvent.click(button)
  
  await waitFor(() => {
    expect(screen.getByText('Playing...')).toBeInTheDocument()
  })
})

Hook Testing

import { renderHook, act } from '@testing-library/react-hooks'
import { useWhisky } from '@whisky-gaming/ui'

test('useWhisky hook', () => {
  const { result } = renderHook(() => useWhisky(), {
    wrapper: WhiskyProvider
  })
  
  expect(result.current.isPlaying).toBe(false)
  expect(result.current.result).toBe(null)
  
  act(() => {
    result.current.play(gameParams)
  })
  
  expect(result.current.isPlaying).toBe(true)
})

Dependencies

This package requires the following peer dependencies:
  • @whisky-gaming/core: The core Whisky protocol SDK
  • @solana/wallet-adapter-react: Solana wallet integration
  • @solana/web3.js: Solana web3 utilities
  • react: React library
  • react-dom: React DOM

Development

# Install dependencies
npm install

# Build the package
npm run build

# Development mode with watch
npm run dev

# Type checking
npm run lint

Support

For React SDK support and questions: