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 devEnvironment Configuration
# .env.local
VITE_SUPABASE_URL=your-supabase-url
VITE_SUPABASE_ANON_KEY=your-supabase-anon-key
VITE_WALLETCONNECT_PROJECT_ID=your-walletconnect-idProject 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=mainnetDebugging
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-featurePull Request Process
Ensure tests pass
Update documentation
Request review from team
Address feedback
Merge when approved
Last updated
