HUNCH Developer Guide

Development Setup

Prerequisites

  • Node.js 18+ and npm

  • Git

  • MetaMask browser extension

  • Code editor (VS Code recommended)

Initial Setup

# Clone the repository
git clone <repository-url>
cd hunch

# Install dependencies
npm install

# Copy environment variables
cp .env.example .env.local

# Start development server
npm run dev

Environment Configuration

# .env.local
VITE_SUPABASE_URL=your-supabase-url
VITE_SUPABASE_ANON_KEY=your-supabase-anon-key
VITE_WALLETCONNECT_PROJECT_ID=your-walletconnect-id

Project Structure

src/
├── components/          # React components
│   ├── ui/             # shadcn/ui components
│   ├── trading/        # Trading-specific components
│   └── market/         # Market-specific components
├── hooks/              # Custom React hooks
├── pages/              # Route components
├── services/           # Business logic and API calls
├── types/              # TypeScript type definitions
├── config/             # Configuration files
├── providers/          # Context providers
└── lib/                # Utility functions

contracts/              # Smart contracts
├── Market.sol
├── MarketFactory.sol
├── Oracle.sol
└── MultiOptionMarket.sol

docs/                   # Documentation
└── ...

Architecture Overview

Frontend Architecture

┌─────────────────────────────────────────┐
│                Pages                    │
│  ┌─────────┐ ┌─────────┐ ┌─────────┐   │
│  │  Index  │ │ Market  │ │Portfolio│   │
│  └─────────┘ └─────────┘ └─────────┘   │
└─────────────────┬───────────────────────┘

┌─────────────────┼───────────────────────┐
│              Components                 │
│  ┌─────────┐ ┌─────────┐ ┌─────────┐   │
│  │ Trading │ │ Market  │ │   UI    │   │
│  │  Panel  │ │  Card   │ │Elements │   │
│  └─────────┘ └─────────┘ └─────────┘   │
└─────────────────┬───────────────────────┘

┌─────────────────┼───────────────────────┐
│                Hooks                    │
│  ┌─────────┐ ┌─────────┐ ┌─────────┐   │
│  │useMarket│ │useWallet│ │useTrading│  │
│  └─────────┘ └─────────┘ └─────────┘   │
└─────────────────┬───────────────────────┘

┌─────────────────┼───────────────────────┐
│               Services                  │
│  ┌─────────┐ ┌─────────┐ ┌─────────┐   │
│  │ Trading │ │ Market  │ │ Contract│   │
│  │Service  │ │Service  │ │Service  │   │
│  └─────────┘ └─────────┘ └─────────┘   │
└─────────────────────────────────────────┘

Smart Contract Architecture

┌─────────────────┐    ┌─────────────────┐
│ MarketFactory   │    │ MultiOption     │
│                 │    │ MarketFactory   │
│ ┌─────────────┐ │    │ ┌─────────────┐ │
│ │   creates   │ │    │ │   creates   │ │
│ │             │ │    │ │             │ │
│ │   Market    │ │    │ │ MultiOption │ │
│ │ Contracts   │ │    │ │   Market    │ │
│ └─────────────┘ │    │ └─────────────┘ │
└─────────────────┘    └─────────────────┘
         │                        │
         └────────────┬───────────┘

              ┌─────────────────┐
              │     Oracle      │
              │                 │
              │ ┌─────────────┐ │
              │ │  resolves   │ │
              │ │   market    │ │
              │ │  outcomes   │ │
              │ └─────────────┘ │
              └─────────────────┘

Core Components

Market Components

MarketCard

interface MarketCardProps {
  market: Market;
  variant?: 'compact' | 'detailed';
  onTrade?: (marketId: string) => void;
}

export function MarketCard({ market, variant = 'compact', onTrade }: MarketCardProps) {
  // Component implementation
}

TradingPanel

interface TradingPanelProps {
  marketId: string;
  side: 'YES' | 'NO';
  onTradeComplete?: (trade: Trade) => void;
}

export function TradingPanel({ marketId, side, onTradeComplete }: TradingPanelProps) {
  // Trading interface implementation
}

Custom Hooks

useMarkets

export function useMarkets(filters?: MarketFilters) {
  const { data: markets, isLoading, error } = useQuery({
    queryKey: ['markets', filters],
    queryFn: () => fetchMarkets(filters),
    staleTime: 30000, // 30 seconds
  });

  return { markets, isLoading, error };
}

useTrading

export function useTrading(marketId: string) {
  const { data: account } = useAccount();
  
  const buyShares = useMutation({
    mutationFn: async ({ side, amount }: BuySharesParams) => {
      return tradingService.buyShares(marketId, side, amount);
    },
    onSuccess: () => {
      // Invalidate relevant queries
      queryClient.invalidateQueries(['portfolio', account.address]);
    },
  });

  return { buyShares };
}

Services

Trading Service

class TradingService {
  async buyShares(
    marketId: string, 
    side: 'YES' | 'NO', 
    amount: bigint
  ): Promise<string> {
    const market = new ethers.Contract(marketId, MARKET_ABI, signer);
    const tx = await market.buyShares(side === 'YES' ? 1 : 0, amount, {
      value: await this.calculateCost(marketId, side, amount)
    });
    return tx.hash;
  }

  async calculateCost(
    marketId: string,
    side: 'YES' | 'NO', 
    shares: bigint
  ): Promise<bigint> {
    const market = new ethers.Contract(marketId, MARKET_ABI, provider);
    return await market.calculateCost(side === 'YES' ? 1 : 0, shares);
  }
}

State Management

React Query Configuration

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 30000, // 30 seconds
      gcTime: 300000, // 5 minutes
      retry: 3,
      refetchOnWindowFocus: false,
    },
    mutations: {
      retry: 1,
    },
  },
});

Global State

  • Wallet State: Managed by Wagmi

  • Market Data: Cached via React Query

  • UI State: Local component state

  • Theme: React Context

Web3 Integration

Wagmi Configuration

import { createConfig, http } from 'wagmi';
import { mainnet, polygon, sepolia } from 'wagmi/chains';
import { metaMask, walletConnect } from 'wagmi/connectors';

export const wagmiConfig = createConfig({
  chains: [mainnet, polygon, sepolia],
  connectors: [
    metaMask(),
    walletConnect({ projectId: 'your-project-id' }),
  ],
  transports: {
    [mainnet.id]: http(),
    [polygon.id]: http(),
    [sepolia.id]: http(),
  },
});

Contract Interaction

import { useWriteContract, useReadContract } from 'wagmi';

function useMarketFactory() {
  const { writeContract } = useWriteContract();
  
  const createMarket = async (params: CreateMarketParams) => {
    return writeContract({
      address: MARKET_FACTORY_ADDRESS,
      abi: MARKET_FACTORY_ABI,
      functionName: 'createMarket',
      args: [params.question, params.description, params.endTime, params.category],
    });
  };

  return { createMarket };
}

Database Integration

Supabase Setup

import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.VITE_SUPABASE_URL!,
  process.env.VITE_SUPABASE_ANON_KEY!
);

export { supabase };

Type-Safe Database Queries

import { Database } from './types/database';

type Market = Database['public']['Tables']['markets_enhanced']['Row'];

async function fetchMarkets(): Promise<Market[]> {
  const { data, error } = await supabase
    .from('markets_enhanced')
    .select('*')
    .eq('resolved', false)
    .order('created_at', { ascending: false });

  if (error) throw error;
  return data;
}

Styling System

Tailwind Configuration

// tailwind.config.ts
module.exports = {
  content: ['./src/**/*.{ts,tsx}'],
  theme: {
    extend: {
      colors: {
        border: 'hsl(var(--border))',
        background: 'hsl(var(--background))',
        foreground: 'hsl(var(--foreground))',
        primary: {
          DEFAULT: 'hsl(var(--primary))',
          foreground: 'hsl(var(--primary-foreground))',
        },
        // ... semantic color tokens
      },
    },
  },
  plugins: [require('tailwindcss-animate')],
};

Component Variants

import { cva } from 'class-variance-authority';

const buttonVariants = cva(
  'inline-flex items-center justify-center rounded-md font-medium transition-colors',
  {
    variants: {
      variant: {
        default: 'bg-primary text-primary-foreground hover:bg-primary/90',
        destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
        outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
      },
      size: {
        default: 'h-10 px-4 py-2',
        sm: 'h-9 rounded-md px-3',
        lg: 'h-11 rounded-md px-8',
      },
    },
    defaultVariants: {
      variant: 'default',
      size: 'default',
    },
  }
);

Testing

Test Setup

// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
  },
});

Component Testing

import { render, screen } from '@testing-library/react';
import { vi } from 'vitest';
import { MarketCard } from '../MarketCard';

const mockMarket = {
  id: '1',
  question: 'Test market?',
  // ... other properties
};

test('renders market card', () => {
  render(<MarketCard market={mockMarket} />);
  expect(screen.getByText('Test market?')).toBeInTheDocument();
});

Hook Testing

import { renderHook, waitFor } from '@testing-library/react';
import { useMarkets } from '../useMarkets';

test('fetches markets', async () => {
  const { result } = renderHook(() => useMarkets());
  
  await waitFor(() => {
    expect(result.current.markets).toBeDefined();
  });
});

Performance Optimization

Code Splitting

import { lazy, Suspense } from 'react';

const MarketDetail = lazy(() => import('./pages/MarketDetail'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <MarketDetail />
    </Suspense>
  );
}

Memoization

import { memo, useMemo } from 'react';

const MarketCard = memo(({ market }: MarketCardProps) => {
  const formattedPrice = useMemo(() => 
    formatPrice(market.price), [market.price]
  );

  return <div>{formattedPrice}</div>;
});

React Query Optimization

// Prefetch critical data
function useMarketPrefetch() {
  const queryClient = useQueryClient();
  
  const prefetchMarket = useCallback((marketId: string) => {
    queryClient.prefetchQuery({
      queryKey: ['market', marketId],
      queryFn: () => fetchMarket(marketId),
      staleTime: 60000,
    });
  }, [queryClient]);

  return { prefetchMarket };
}

Security Considerations

Input Validation

import { z } from 'zod';

const CreateMarketSchema = z.object({
  question: z.string().min(10).max(200),
  description: z.string().max(1000),
  endTime: z.date().min(new Date()),
  category: z.string().min(1).max(50),
});

function validateMarket(data: unknown) {
  return CreateMarketSchema.parse(data);
}

Safe Contract Interactions

async function safeContractCall<T>(
  contractCall: () => Promise<T>
): Promise<{ data?: T; error?: string }> {
  try {
    const data = await contractCall();
    return { data };
  } catch (error) {
    console.error('Contract call failed:', error);
    return { error: error.message };
  }
}

Deployment

Build Configuration

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  build: {
    outDir: 'dist',
    sourcemap: true,
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          web3: ['wagmi', 'viem', 'ethers'],
          ui: ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
        },
      },
    },
  },
});

Environment Variables

# Production
VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_ANON_KEY=your-anon-key
VITE_WALLETCONNECT_PROJECT_ID=your-project-id
VITE_NETWORK=mainnet

Debugging

Development Tools

// Debug logging
const debug = {
  log: (message: string, data?: any) => {
    if (process.env.NODE_ENV === 'development') {
      console.log(`[HUNCH] ${message}`, data);
    }
  },
  error: (message: string, error?: any) => {
    console.error(`[HUNCH ERROR] ${message}`, error);
  },
};

Error Boundaries

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    console.error('Error caught by boundary:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return <ErrorFallback />;
    }

    return this.props.children;
  }
}

Contributing

Code Style

  • Use TypeScript for all new code

  • Follow ESLint configuration

  • Use Prettier for formatting

  • Write tests for new features

  • Document complex logic

Git Workflow

# Create feature branch
git checkout -b feature/new-feature

# Make changes and commit
git add .
git commit -m "feat: add new feature"

# Push and create PR
git push origin feature/new-feature

Pull Request Process

  1. Ensure tests pass

  2. Update documentation

  3. Request review from team

  4. Address feedback

  5. Merge when approved

Last updated