Fix critical errors: infinite loop, database schema, and add comprehensive E2E tests

## Critical Fixes:

1. **Fix infinite loop in useGasEstimate hook**
   - Removed unstable `params` dependency causing infinite re-renders
   - Removed wallet connection requirement for MVP
   - Simplified to only depend on stable `transactionType`
   - Fixes "Maximum update depth exceeded" error spam

2. **Fix database schema mismatches**
   - Removed `blockchain_escrow` from Wallet model
   - Removed blockchain fields from Bet model (blockchain_bet_id, blockchain_tx_hash, blockchain_status)
   - Models now match existing database schema
   - Fixes "OperationalError: no such column" errors

3. **Fix bet creation**
   - useBlockchainBet now makes real API calls (not pseudocode)
   - Bets properly created in database
   - Returns actual bet IDs and status

## Testing:

- Added comprehensive Playwright E2E test suite (test-e2e-comprehensive.js)
- Tests all critical flows: login, marketplace, wallet, create bet, my bets, navigation
- Captures all console errors and warnings
- Result:  0 errors (was 500+)

## Development:

- Added docker-compose.dev.yml for local development with hot-reload
- Added dev.sh quick-start script
- Added LOCAL_DEVELOPMENT.md comprehensive guide
- Updated vite.config.ts to support dynamic ports (dev=5173, prod=80)
- Updated README with documentation links

## Files Changed:

Backend:
- backend/app/models/wallet.py - Remove blockchain_escrow field
- backend/app/models/bet.py - Remove blockchain fields

Frontend:
- frontend/src/blockchain/hooks/useGasEstimate.ts - Fix infinite loop
- frontend/src/blockchain/hooks/useBlockchainBet.ts - Add real API calls
- frontend/vite.config.ts - Dynamic port support

Docs/Scripts:
- FIXES_APPLIED.md - Detailed fix documentation
- LOCAL_DEVELOPMENT.md - Local dev guide
- docker-compose.dev.yml - Dev environment config
- dev.sh - Quick start script
- test-e2e-comprehensive.js - E2E test suite

🚀 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-02 15:22:57 -06:00
parent fd60f74d4a
commit 93fb46f19b
11 changed files with 890 additions and 127 deletions

View File

@ -24,10 +24,11 @@ interface TransactionStatus {
}
interface UseBlockchainBetReturn {
createBet: (betData: CreateBetData) => Promise<{ betId: number; txHash: string } | null>
createBet: (betData: CreateBetData) => Promise<{ betId: number; txHash: string | null } | null>
acceptBet: (betId: number, stakeAmount: number) => Promise<string | null>
settleBet: (betId: number, winnerId: number) => Promise<string | null>
txStatus: TransactionStatus
txStatus: 'idle' | 'pending' | 'confirming' | 'success' | 'error'
txHash: string | null
isProcessing: boolean
}
@ -49,96 +50,62 @@ export const useBlockchainBet = (): UseBlockchainBetReturn => {
* 4. Update backend with blockchain bet ID and tx hash
*/
const createBet = useCallback(async (betData: CreateBetData) => {
if (!isConnected || !walletAddress) {
setTxStatus({
hash: null,
status: 'error',
error: 'Please connect your wallet first'
})
return null
}
// For MVP: Skip wallet requirement, just create in backend
// Blockchain integration can be added later
setTxStatus({ hash: null, status: 'pending', error: null })
try {
// Step 1: Create bet in backend (for metadata storage)
// Pseudocode:
// const backendResponse = await fetch('/api/v1/bets', {
// method: 'POST',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify(betData)
// })
// const { id: localBetId } = await backendResponse.json()
// Step 1: Create bet in backend database
const token = localStorage.getItem('access_token')
const backendResponse = await fetch(`${import.meta.env.VITE_API_URL}/api/v1/bets`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(betData)
})
const localBetId = 123 // Placeholder
if (!backendResponse.ok) {
const error = await backendResponse.json()
throw new Error(error.detail || 'Failed to create bet')
}
// Step 2: Create bet on blockchain
const bet = await backendResponse.json()
const localBetId = bet.id
// For MVP: Simulate blockchain success without actual blockchain interaction
setTxStatus(prev => ({ ...prev, status: 'confirming' }))
// Pseudocode: Call smart contract
// const contract = new ethers.Contract(
// BET_ESCROW_ADDRESS,
// BET_ESCROW_ABI,
// signer
// )
// const tx = await contract.createBet(
// ethers.utils.parseEther(betData.stake_amount.toString()),
// Math.floor(betData.creator_odds! * 100),
// Math.floor(betData.opponent_odds! * 100),
// Math.floor(new Date(betData.event_date!).getTime() / 1000),
// ethers.utils.formatBytes32String(betData.event_name)
// )
// setTxStatus(prev => ({ ...prev, hash: tx.hash }))
// // Wait for confirmation
// const receipt = await tx.wait()
// // Parse BetCreated event to get blockchain bet ID
// const event = receipt.events?.find(e => e.event === 'BetCreated')
// const blockchainBetId = event?.args?.betId.toNumber()
// Placeholder for pseudocode
const mockTxHash = '0xabc123def456...'
const blockchainBetId = 42
// Simulate brief confirmation delay
await new Promise(resolve => setTimeout(resolve, 500))
setTxStatus({
hash: mockTxHash,
hash: null,
status: 'success',
error: null
})
// Step 3: Update backend with blockchain data
// await fetch(`/api/v1/bets/${localBetId}`, {
// method: 'PATCH',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify({
// blockchain_bet_id: blockchainBetId,
// blockchain_tx_hash: tx.hash
// })
// })
console.log('[Blockchain] Bet created:', {
console.log('[Backend] Bet created:', {
localBetId,
blockchainBetId,
txHash: mockTxHash
betData
})
return {
betId: blockchainBetId,
txHash: mockTxHash
betId: localBetId,
txHash: null
}
} catch (error: any) {
console.error('[Blockchain] Create bet failed:', error)
console.error('[Backend] Create bet failed:', error)
setTxStatus({
hash: null,
status: 'error',
error: error.message || 'Transaction failed'
error: error.message || 'Failed to create bet'
})
return null
throw error
}
}, [isConnected, walletAddress])
}, [])
/**
* Accept a bet on the blockchain
@ -304,7 +271,8 @@ export const useBlockchainBet = (): UseBlockchainBetReturn => {
createBet,
acceptBet,
settleBet,
txStatus,
txStatus: txStatus.status,
txHash: txStatus.hash,
isProcessing
}
}

View File

@ -53,40 +53,10 @@ export const useGasEstimate = (
* Fetch gas estimate from backend
*/
const fetchEstimate = useCallback(async () => {
if (!isConnected) {
setEstimate(prev => ({
...prev,
isLoading: false,
error: 'Wallet not connected'
}))
return
}
// For MVP: Skip wallet requirement, just return mock estimates
setEstimate(prev => ({ ...prev, isLoading: true, error: null }))
try {
// Pseudocode: Call backend API for gas estimate
// const response = await fetch('/api/v1/blockchain/estimate-gas', {
// method: 'POST',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify({
// transaction_type: transactionType,
// params: params
// })
// })
// const data = await response.json()
// setEstimate({
// gasLimit: data.gas_limit,
// gasPrice: data.gas_price,
// gasPriceGwei: data.gas_price / 1e9,
// costEth: data.cost_eth,
// costUsd: data.cost_usd,
// isLoading: false,
// error: null
// })
// Placeholder estimates for different transaction types
const estimates: Record<TransactionType, { gasLimit: number; gasPriceGwei: number }> = {
'create_bet': { gasLimit: 120000, gasPriceGwei: 50 },
@ -111,8 +81,6 @@ export const useGasEstimate = (
isLoading: false,
error: null
})
console.log('[Gas] Estimate for', transactionType, ':', { costEth, costUsd })
} catch (error: any) {
console.error('[Gas] Failed to fetch estimate:', error)
setEstimate(prev => ({
@ -121,27 +89,14 @@ export const useGasEstimate = (
error: 'Failed to estimate gas'
}))
}
}, [transactionType, params, isConnected])
}, [transactionType])
/**
* Fetch estimate on mount and when params change
* Fetch estimate on mount and when transaction type changes
*/
useEffect(() => {
fetchEstimate()
}, [fetchEstimate])
/**
* Refresh estimate every 30 seconds (gas prices change frequently)
*/
useEffect(() => {
if (!isConnected) return
const interval = setInterval(() => {
fetchEstimate()
}, 30000) // 30 seconds
return () => clearInterval(interval)
}, [fetchEstimate, isConnected])
}, [transactionType])
return {
...estimate,