feat(testing): add Anvil fork local testing infrastructure
Complete local testing setup with Anvil fork of Arbitrum mainnet: Infrastructure: - Docker Compose orchestration (Anvil, MEV Bot, Prometheus, Grafana) - Anvil fork configuration with 1-second blocks - Multi-stage Dockerfile for optimized builds - Health checks and auto-restart policies Configuration: - Comprehensive .env.example with all parameters - Prometheus metrics collection setup - Grafana datasource provisioning - .gitignore to prevent committing secrets Testing Scripts: - setup-local-fork.sh: Initialize fork and fund test wallet - create-test-swap.sh: Generate test swaps for bot detection - Both scripts include validation and helpful output Integration Components: - pkg/sequencer/reader.go: WebSocket reader for pending transactions - Worker pool pattern (10 workers) - <50ms processing target - Front-running capability - Auto-reconnection with exponential backoff - pkg/pools/discovery.go: Pool discovery service - UniswapV2-style pools (SushiSwap, Camelot) - UniswapV3 pools (multiple fee tiers) - Factory contract queries - Liquidity filtering Documentation: - TESTING.md: Complete testing guide - Quick start instructions - Testing scenarios - Monitoring and debugging - Performance benchmarks - Troubleshooting guide This enables safe local testing without deploying to public testnet, using real Arbitrum mainnet state forked locally with Anvil. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
147
.env.example
Normal file
147
.env.example
Normal file
@@ -0,0 +1,147 @@
|
||||
# MEV Bot V2 Configuration
|
||||
# Copy this file to .env and fill in your values
|
||||
|
||||
# ============================================================================
|
||||
# NETWORK CONFIGURATION
|
||||
# ============================================================================
|
||||
|
||||
# Arbitrum RPC URL (for forking with Anvil)
|
||||
ARBITRUM_RPC_URL=https://arb1.arbitrum.io/rpc
|
||||
|
||||
# Block number to fork from (optional, defaults to latest)
|
||||
# FORK_BLOCK_NUMBER=latest
|
||||
|
||||
# Local Anvil URLs (used by bot when running in docker-compose)
|
||||
# RPC_URL=http://anvil:8545
|
||||
# WS_URL=ws://anvil:8546
|
||||
# SEQUENCER_WS_URL=ws://anvil:8546
|
||||
|
||||
# Production Arbitrum URLs (when not using Anvil)
|
||||
# RPC_URL=https://arb1.arbitrum.io/rpc
|
||||
# WS_URL=wss://arb1.arbitrum.io/ws
|
||||
# SEQUENCER_WS_URL=wss://arb1.arbitrum.io/ws
|
||||
|
||||
# Private RPC endpoint (optional, for faster execution)
|
||||
# PRIVATE_RPC_URL=
|
||||
# USE_PRIVATE_RPC=false
|
||||
|
||||
# ============================================================================
|
||||
# WALLET CONFIGURATION
|
||||
# ============================================================================
|
||||
|
||||
# Private key for the wallet that will execute trades
|
||||
# IMPORTANT: Never commit this file with real keys!
|
||||
# Use a dedicated wallet for testing with test funds only
|
||||
PRIVATE_KEY=0000000000000000000000000000000000000000000000000000000000000000
|
||||
|
||||
# ============================================================================
|
||||
# SMART CONTRACT ADDRESSES
|
||||
# ============================================================================
|
||||
|
||||
# Flashloan executor contract (deployed on Arbitrum)
|
||||
# Leave as zero address if not deployed yet
|
||||
EXECUTOR_CONTRACT=0x0000000000000000000000000000000000000000
|
||||
|
||||
# ============================================================================
|
||||
# TRADING PARAMETERS
|
||||
# ============================================================================
|
||||
|
||||
# Minimum profit threshold (in wei, 0.01 ETH = 10000000000000000)
|
||||
MIN_PROFIT=10000000000000000
|
||||
|
||||
# Minimum ROI percentage (1% = 0.01)
|
||||
MIN_ROI=0.01
|
||||
|
||||
# Maximum slippage in basis points (200 = 2%)
|
||||
MAX_SLIPPAGE_BPS=200
|
||||
|
||||
# Minimum swap amount (in wei, 0.001 ETH = 1000000000000000)
|
||||
MIN_SWAP_AMOUNT=1000000000000000
|
||||
|
||||
# Minimum pool liquidity (in wei, 1 ETH = 1000000000000000000)
|
||||
MIN_POOL_LIQUIDITY=1000000000000000000
|
||||
|
||||
# ============================================================================
|
||||
# RISK MANAGEMENT
|
||||
# ============================================================================
|
||||
|
||||
# Maximum position size per trade (in wei, 10 ETH = 10000000000000000000)
|
||||
MAX_POSITION_SIZE=10000000000000000000
|
||||
|
||||
# Maximum daily volume (in wei, 100 ETH = 100000000000000000000)
|
||||
MAX_DAILY_VOLUME=100000000000000000000
|
||||
|
||||
# Maximum gas limit per transaction
|
||||
MAX_GAS_LIMIT=3000000
|
||||
|
||||
# Gas price strategy (fast, normal, slow)
|
||||
GAS_PRICE_STRATEGY=fast
|
||||
|
||||
# ============================================================================
|
||||
# ARBITRAGE DETECTION
|
||||
# ============================================================================
|
||||
|
||||
# Maximum hops in arbitrage path
|
||||
MAX_HOPS=3
|
||||
|
||||
# Maximum paths to explore per token
|
||||
MAX_PATHS=100
|
||||
|
||||
# Maximum concurrent detection operations
|
||||
MAX_CONCURRENT_DETECTION=10
|
||||
|
||||
# ============================================================================
|
||||
# EXECUTION SETTINGS
|
||||
# ============================================================================
|
||||
|
||||
# Enable transaction simulation before execution
|
||||
ENABLE_SIMULATION=true
|
||||
|
||||
# Enable front-running of detected opportunities
|
||||
ENABLE_FRONT_RUNNING=true
|
||||
|
||||
# Number of confirmations to wait
|
||||
CONFIRMATION_BLOCKS=1
|
||||
|
||||
# Transaction timeout (in seconds)
|
||||
TX_TIMEOUT=300
|
||||
|
||||
# Maximum retries for failed transactions
|
||||
MAX_RETRIES=3
|
||||
|
||||
# ============================================================================
|
||||
# POOL DISCOVERY
|
||||
# ============================================================================
|
||||
|
||||
# Maximum number of pools to discover
|
||||
MAX_POOLS_TO_DISCOVER=1000
|
||||
|
||||
# ============================================================================
|
||||
# PERFORMANCE TUNING
|
||||
# ============================================================================
|
||||
|
||||
# Number of worker threads for transaction processing
|
||||
WORKER_COUNT=10
|
||||
|
||||
# Transaction buffer size
|
||||
BUFFER_SIZE=1000
|
||||
|
||||
# ============================================================================
|
||||
# MONITORING
|
||||
# ============================================================================
|
||||
|
||||
# Metrics server port
|
||||
METRICS_PORT=9090
|
||||
|
||||
# Log level (debug, info, warn, error)
|
||||
LOG_LEVEL=info
|
||||
|
||||
# ============================================================================
|
||||
# TESTING CONFIGURATION
|
||||
# ============================================================================
|
||||
|
||||
# Enable dry-run mode (log opportunities but don't execute)
|
||||
# DRY_RUN=false
|
||||
|
||||
# Enable test mode with reduced thresholds
|
||||
# TEST_MODE=false
|
||||
74
.gitignore
vendored
Normal file
74
.gitignore
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
# Environment files with secrets
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Private keys and wallets
|
||||
*.key
|
||||
*.keystore
|
||||
keystore/
|
||||
wallets/
|
||||
|
||||
# Build artifacts
|
||||
bin/
|
||||
build/
|
||||
dist/
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test coverage
|
||||
*.out
|
||||
coverage.html
|
||||
coverage.txt
|
||||
|
||||
# IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Data directories
|
||||
data/
|
||||
*.db
|
||||
*.sqlite
|
||||
|
||||
# Docker volumes
|
||||
volumes/
|
||||
|
||||
# Temporary files
|
||||
tmp/
|
||||
temp/
|
||||
*.tmp
|
||||
|
||||
# Dependency directories
|
||||
vendor/
|
||||
node_modules/
|
||||
|
||||
# Compiled binaries
|
||||
mev-bot-v2
|
||||
mev-bot-v1
|
||||
|
||||
# Monitoring data
|
||||
prometheus-data/
|
||||
grafana-data/
|
||||
|
||||
# Backup files
|
||||
*.bak
|
||||
*.backup
|
||||
*~
|
||||
|
||||
# Archive files
|
||||
*.tar
|
||||
*.tar.gz
|
||||
*.zip
|
||||
55
Dockerfile
Normal file
55
Dockerfile
Normal file
@@ -0,0 +1,55 @@
|
||||
# Multi-stage build for MEV Bot V2
|
||||
|
||||
# Stage 1: Build
|
||||
FROM golang:1.21-alpine AS builder
|
||||
|
||||
# Install build dependencies
|
||||
RUN apk add --no-cache git make gcc musl-dev linux-headers
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /build
|
||||
|
||||
# Copy go mod files
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
# Download dependencies
|
||||
RUN go mod download
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the application
|
||||
RUN go build -o mev-bot-v2 -ldflags="-w -s" ./cmd/mev-bot-v2
|
||||
|
||||
# Stage 2: Runtime
|
||||
FROM alpine:latest
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apk add --no-cache ca-certificates tzdata
|
||||
|
||||
# Create non-root user
|
||||
RUN addgroup -g 1000 mevbot && \
|
||||
adduser -D -u 1000 -G mevbot mevbot
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy binary from builder
|
||||
COPY --from=builder /build/mev-bot-v2 .
|
||||
|
||||
# Create directories
|
||||
RUN mkdir -p /app/logs /app/data && \
|
||||
chown -R mevbot:mevbot /app
|
||||
|
||||
# Switch to non-root user
|
||||
USER mevbot
|
||||
|
||||
# Expose metrics port
|
||||
EXPOSE 9090
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:9090/metrics || exit 1
|
||||
|
||||
# Run the application
|
||||
ENTRYPOINT ["/app/mev-bot-v2"]
|
||||
446
TESTING.md
Normal file
446
TESTING.md
Normal file
@@ -0,0 +1,446 @@
|
||||
# MEV Bot V2 - Local Fork Testing Guide
|
||||
|
||||
This guide explains how to test the MEV Bot V2 using a local Arbitrum fork with Anvil.
|
||||
|
||||
## Overview
|
||||
|
||||
Instead of deploying to a public testnet, we use Foundry's Anvil to create a local fork of Arbitrum mainnet. This allows us to:
|
||||
|
||||
- ✅ Test with real mainnet state (pools, liquidity, etc.)
|
||||
- ✅ Execute transactions instantly (no waiting for block confirmations)
|
||||
- ✅ Use unlimited test funds
|
||||
- ✅ Reset and replay scenarios easily
|
||||
- ✅ Debug without spending real gas
|
||||
- ✅ Test front-running scenarios safely
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Docker & Docker Compose** installed
|
||||
2. **Foundry** (for cast commands) - `curl -L https://foundry.paradigm.xyz | bash && foundryup`
|
||||
3. **Test wallet with private key** (NEVER use real funds)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Initial Setup
|
||||
|
||||
```bash
|
||||
# Copy environment file
|
||||
cp .env.example .env
|
||||
|
||||
# Edit .env and set your test private key
|
||||
nano .env # Set PRIVATE_KEY to a test wallet key
|
||||
|
||||
# Set up the local fork
|
||||
./scripts/setup-local-fork.sh
|
||||
```
|
||||
|
||||
This script will:
|
||||
- Start Anvil fork of Arbitrum
|
||||
- Fund your test wallet with 100 ETH
|
||||
- Verify connection and balances
|
||||
|
||||
### 2. Start the Full Stack
|
||||
|
||||
```bash
|
||||
# Start all services (Anvil, MEV Bot, Prometheus, Grafana)
|
||||
docker-compose up -d
|
||||
|
||||
# View MEV bot logs
|
||||
docker-compose logs -f mev-bot
|
||||
|
||||
# View Anvil logs
|
||||
docker-compose logs -f anvil
|
||||
```
|
||||
|
||||
### 3. Create Test Swaps
|
||||
|
||||
```bash
|
||||
# Create a test swap for the bot to detect
|
||||
./scripts/create-test-swap.sh
|
||||
```
|
||||
|
||||
This will:
|
||||
- Wrap ETH to WETH
|
||||
- Approve SushiSwap router
|
||||
- Execute a 0.1 ETH → USDC swap
|
||||
- The MEV bot should detect this as a potential front-run opportunity
|
||||
|
||||
### 4. Monitor Activity
|
||||
|
||||
**View Metrics:**
|
||||
```bash
|
||||
# Prometheus metrics
|
||||
open http://localhost:9090
|
||||
|
||||
# Query example: mev_bot_opportunities_found
|
||||
```
|
||||
|
||||
**View Grafana Dashboard:**
|
||||
```bash
|
||||
# Grafana UI
|
||||
open http://localhost:3000
|
||||
# Login: admin / admin
|
||||
```
|
||||
|
||||
**View Bot Logs:**
|
||||
```bash
|
||||
docker-compose logs -f mev-bot | grep -i "opportunity\|profit\|execution"
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ Anvil Fork │ ← Forked Arbitrum mainnet state
|
||||
│ (Port 8545) │
|
||||
└────────┬────────┘
|
||||
│
|
||||
├─────────► Pool Discovery (queries real pools)
|
||||
│
|
||||
├─────────► Sequencer Reader (monitors pending txs)
|
||||
│
|
||||
└─────────► Executor (sends front-run txs)
|
||||
│
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ MEV Bot V2 │
|
||||
└──────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ Prometheus │ ← Metrics
|
||||
│ (Port 9090) │
|
||||
└──────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ Grafana │ ← Visualization
|
||||
│ (Port 3000) │
|
||||
└──────────────────┘
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Edit `.env` to configure the bot:
|
||||
|
||||
**Critical Settings:**
|
||||
```bash
|
||||
# Your test wallet private key
|
||||
PRIVATE_KEY=your_test_key_here
|
||||
|
||||
# Minimum profit to execute (0.01 ETH)
|
||||
MIN_PROFIT=10000000000000000
|
||||
|
||||
# Enable front-running
|
||||
ENABLE_FRONT_RUNNING=true
|
||||
|
||||
# Enable simulation before execution
|
||||
ENABLE_SIMULATION=true
|
||||
```
|
||||
|
||||
**Risk Parameters:**
|
||||
```bash
|
||||
# Maximum position size (10 ETH)
|
||||
MAX_POSITION_SIZE=10000000000000000000
|
||||
|
||||
# Maximum daily volume (100 ETH)
|
||||
MAX_DAILY_VOLUME=100000000000000000000
|
||||
```
|
||||
|
||||
**Performance Tuning:**
|
||||
```bash
|
||||
# Worker threads
|
||||
WORKER_COUNT=10
|
||||
|
||||
# Buffer size
|
||||
BUFFER_SIZE=1000
|
||||
|
||||
# Log level
|
||||
LOG_LEVEL=debug
|
||||
```
|
||||
|
||||
### Anvil Configuration
|
||||
|
||||
You can customize the fork in `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
anvil:
|
||||
command: >
|
||||
anvil
|
||||
--fork-url ${ARBITRUM_RPC_URL}
|
||||
--fork-block-number ${FORK_BLOCK_NUMBER:-latest} # Specific block or latest
|
||||
--chain-id 42161
|
||||
--accounts 10 # Test accounts
|
||||
--balance 10000 # ETH per account
|
||||
--block-time 1 # 1 second blocks
|
||||
```
|
||||
|
||||
## Testing Scenarios
|
||||
|
||||
### Scenario 1: Simple Arbitrage Detection
|
||||
|
||||
```bash
|
||||
# 1. Start the bot
|
||||
docker-compose up -d mev-bot
|
||||
|
||||
# 2. Create imbalance by swapping on one DEX
|
||||
./scripts/create-test-swap.sh
|
||||
|
||||
# 3. Check if bot detected opportunity
|
||||
docker-compose logs mev-bot | grep "opportunity"
|
||||
```
|
||||
|
||||
### Scenario 2: Front-Running Test
|
||||
|
||||
```bash
|
||||
# 1. Enable front-running in .env
|
||||
ENABLE_FRONT_RUNNING=true
|
||||
|
||||
# 2. Restart bot
|
||||
docker-compose restart mev-bot
|
||||
|
||||
# 3. Create a large swap
|
||||
./scripts/create-test-swap.sh
|
||||
|
||||
# 4. Check if bot front-ran the transaction
|
||||
docker-compose logs mev-bot | grep "front-running"
|
||||
```
|
||||
|
||||
### Scenario 3: Multi-Hop Arbitrage
|
||||
|
||||
```bash
|
||||
# Create price imbalance across multiple pools
|
||||
# Swap WETH → USDC on SushiSwap
|
||||
# Swap USDC → WBTC on Uniswap V3
|
||||
# Swap WBTC → WETH on Camelot
|
||||
|
||||
# Bot should detect triangular arbitrage opportunity
|
||||
docker-compose logs mev-bot | grep "triangular"
|
||||
```
|
||||
|
||||
### Scenario 4: Stress Test
|
||||
|
||||
```bash
|
||||
# Generate many swaps quickly
|
||||
for i in {1..100}; do
|
||||
./scripts/create-test-swap.sh &
|
||||
done
|
||||
|
||||
# Monitor processing latency
|
||||
docker-compose logs mev-bot | grep "latency"
|
||||
```
|
||||
|
||||
## Monitoring & Debugging
|
||||
|
||||
### Check Pool Discovery
|
||||
|
||||
```bash
|
||||
# View discovered pools
|
||||
docker-compose logs mev-bot | grep "discovered pool"
|
||||
|
||||
# Check total pools cached
|
||||
docker-compose logs mev-bot | grep "pools_cached"
|
||||
```
|
||||
|
||||
### Check Sequencer Connection
|
||||
|
||||
```bash
|
||||
# Verify WebSocket connection
|
||||
docker-compose logs mev-bot | grep "connected to sequencer"
|
||||
|
||||
# Check transaction processing
|
||||
docker-compose logs mev-bot | grep "tx_processed"
|
||||
```
|
||||
|
||||
### Check Opportunity Detection
|
||||
|
||||
```bash
|
||||
# View detected opportunities
|
||||
docker-compose logs mev-bot | grep "opportunities_found"
|
||||
|
||||
# View execution attempts
|
||||
docker-compose logs mev-bot | grep "executions_attempted"
|
||||
```
|
||||
|
||||
### Check Performance Metrics
|
||||
|
||||
```bash
|
||||
# Prometheus queries
|
||||
curl http://localhost:9090/api/v1/query?query=mev_bot_parse_latency_seconds
|
||||
curl http://localhost:9090/api/v1/query?query=mev_bot_detect_latency_seconds
|
||||
curl http://localhost:9090/api/v1/query?query=mev_bot_execute_latency_seconds
|
||||
```
|
||||
|
||||
### Debug Mode
|
||||
|
||||
```bash
|
||||
# Enable debug logging
|
||||
echo "LOG_LEVEL=debug" >> .env
|
||||
|
||||
# Restart with debug logs
|
||||
docker-compose restart mev-bot
|
||||
|
||||
# View detailed logs
|
||||
docker-compose logs -f mev-bot
|
||||
```
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Issue: Anvil not starting
|
||||
|
||||
```bash
|
||||
# Check if port 8545 is already in use
|
||||
lsof -i :8545
|
||||
|
||||
# Kill existing process or change port in docker-compose.yml
|
||||
```
|
||||
|
||||
### Issue: Bot not discovering pools
|
||||
|
||||
```bash
|
||||
# Check RPC connectivity
|
||||
docker-compose exec anvil cast block-number --rpc-url http://localhost:8545
|
||||
|
||||
# Check pool discovery logs
|
||||
docker-compose logs mev-bot | grep "pool discovery"
|
||||
|
||||
# Verify token addresses are correct for Arbitrum
|
||||
```
|
||||
|
||||
### Issue: No opportunities detected
|
||||
|
||||
```bash
|
||||
# 1. Verify pools were discovered
|
||||
docker-compose logs mev-bot | grep "pools_cached"
|
||||
|
||||
# 2. Check minimum thresholds
|
||||
# Lower MIN_PROFIT in .env for testing
|
||||
|
||||
# 3. Create larger price imbalances
|
||||
# Increase swap amounts in create-test-swap.sh
|
||||
```
|
||||
|
||||
### Issue: Transactions reverting
|
||||
|
||||
```bash
|
||||
# Enable simulation to catch errors before execution
|
||||
ENABLE_SIMULATION=true
|
||||
|
||||
# Check revert reasons
|
||||
docker-compose logs mev-bot | grep "revert"
|
||||
|
||||
# Verify wallet has sufficient balance
|
||||
docker-compose exec anvil cast balance <YOUR_ADDRESS> --rpc-url http://localhost:8545
|
||||
```
|
||||
|
||||
## Advanced Testing
|
||||
|
||||
### Forking from Specific Block
|
||||
|
||||
```bash
|
||||
# Set specific block in .env
|
||||
FORK_BLOCK_NUMBER=180000000
|
||||
|
||||
# Restart Anvil
|
||||
docker-compose restart anvil
|
||||
```
|
||||
|
||||
### Impersonate Accounts
|
||||
|
||||
```bash
|
||||
# Use Anvil's account impersonation to test edge cases
|
||||
docker-compose exec anvil cast rpc anvil_impersonateAccount <ADDRESS>
|
||||
```
|
||||
|
||||
### Manipulate State
|
||||
|
||||
```bash
|
||||
# Set arbitrary balances
|
||||
docker-compose exec anvil cast rpc anvil_setBalance <ADDRESS> <AMOUNT>
|
||||
|
||||
# Mine blocks
|
||||
docker-compose exec anvil cast rpc anvil_mine 100
|
||||
|
||||
# Set next block timestamp
|
||||
docker-compose exec anvil cast rpc evm_setNextBlockTimestamp <TIMESTAMP>
|
||||
```
|
||||
|
||||
### Reset Fork
|
||||
|
||||
```bash
|
||||
# Reset to original fork state
|
||||
docker-compose exec anvil cast rpc anvil_reset
|
||||
|
||||
# Re-run pool discovery
|
||||
docker-compose restart mev-bot
|
||||
```
|
||||
|
||||
## Performance Benchmarks
|
||||
|
||||
Target metrics for production readiness:
|
||||
|
||||
- ✅ **Parse Latency:** < 5ms per transaction
|
||||
- ✅ **Detect Latency:** < 10ms per opportunity scan
|
||||
- ✅ **Execute Latency:** < 30ms decision time
|
||||
- ✅ **Total Latency:** < 50ms end-to-end
|
||||
- ✅ **Throughput:** > 100 tx/second processing
|
||||
- ✅ **Memory:** < 500MB steady state
|
||||
- ✅ **CPU:** < 50% on 4 cores
|
||||
|
||||
### Benchmarking Commands
|
||||
|
||||
```bash
|
||||
# Measure parse latency
|
||||
docker-compose logs mev-bot | grep "avg_parse_latency"
|
||||
|
||||
# Measure detection latency
|
||||
docker-compose logs mev-bot | grep "avg_detect_latency"
|
||||
|
||||
# Measure execution latency
|
||||
docker-compose logs mev-bot | grep "avg_execute_latency"
|
||||
|
||||
# Check memory usage
|
||||
docker stats mev-bot-v2
|
||||
|
||||
# Load test with many concurrent swaps
|
||||
./scripts/load-test.sh
|
||||
```
|
||||
|
||||
## Cleanup
|
||||
|
||||
```bash
|
||||
# Stop all services
|
||||
docker-compose down
|
||||
|
||||
# Remove volumes (clears metrics data)
|
||||
docker-compose down -v
|
||||
|
||||
# Clean up logs
|
||||
rm -rf logs/*
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
Once local testing is successful:
|
||||
|
||||
1. **Optimize Parameters:** Tune MIN_PROFIT, MAX_SLIPPAGE, etc.
|
||||
2. **Deploy Flashloan Contract:** Deploy executor contract to Arbitrum testnet
|
||||
3. **Testnet Testing:** Test on Arbitrum Goerli with real test ETH
|
||||
4. **Mainnet Deployment:** Deploy with conservative parameters and monitoring
|
||||
|
||||
## Resources
|
||||
|
||||
- **Anvil Docs:** https://book.getfoundry.sh/anvil/
|
||||
- **Arbitrum Docs:** https://docs.arbitrum.io/
|
||||
- **Prometheus Docs:** https://prometheus.io/docs/
|
||||
- **Grafana Docs:** https://grafana.com/docs/
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
1. Check logs: `docker-compose logs mev-bot`
|
||||
2. Review configuration: `.env`
|
||||
3. Check Anvil status: `docker-compose logs anvil`
|
||||
4. Verify metrics: `http://localhost:9090`
|
||||
11
config/grafana/datasources/prometheus.yml
Normal file
11
config/grafana/datasources/prometheus.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Prometheus
|
||||
type: prometheus
|
||||
access: proxy
|
||||
url: http://prometheus:9090
|
||||
isDefault: true
|
||||
editable: true
|
||||
jsonData:
|
||||
timeInterval: 15s
|
||||
16
config/prometheus.yml
Normal file
16
config/prometheus.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
global:
|
||||
scrape_interval: 15s
|
||||
scrape_timeout: 10s
|
||||
evaluation_interval: 15s
|
||||
|
||||
scrape_configs:
|
||||
- job_name: 'mev-bot'
|
||||
static_configs:
|
||||
- targets: ['mev-bot:9090']
|
||||
labels:
|
||||
instance: 'mev-bot-v2'
|
||||
environment: 'testing'
|
||||
|
||||
- job_name: 'prometheus'
|
||||
static_configs:
|
||||
- targets: ['localhost:9090']
|
||||
104
docker-compose.yml
Normal file
104
docker-compose.yml
Normal file
@@ -0,0 +1,104 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# Anvil fork of Arbitrum for local testing
|
||||
anvil:
|
||||
image: ghcr.io/foundry-rs/foundry:latest
|
||||
container_name: mev-bot-anvil
|
||||
ports:
|
||||
- "8545:8545"
|
||||
- "8546:8546"
|
||||
command: >
|
||||
anvil
|
||||
--fork-url ${ARBITRUM_RPC_URL:-https://arb1.arbitrum.io/rpc}
|
||||
--fork-block-number ${FORK_BLOCK_NUMBER:-latest}
|
||||
--host 0.0.0.0
|
||||
--port 8545
|
||||
--chain-id 42161
|
||||
--accounts 10
|
||||
--balance 10000
|
||||
--gas-limit 30000000
|
||||
--gas-price 0
|
||||
--base-fee 0
|
||||
--no-mining
|
||||
--block-time 1
|
||||
networks:
|
||||
- mev-network
|
||||
healthcheck:
|
||||
test: ["CMD", "cast", "block-number", "--rpc-url", "http://localhost:8545"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
|
||||
# MEV Bot V2
|
||||
mev-bot:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: mev-bot-v2
|
||||
depends_on:
|
||||
anvil:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
# Override RPC URLs to point to local Anvil fork
|
||||
- RPC_URL=http://anvil:8545
|
||||
- WS_URL=ws://anvil:8546
|
||||
- SEQUENCER_WS_URL=ws://anvil:8546
|
||||
# Testing configuration
|
||||
- ENABLE_SIMULATION=true
|
||||
- ENABLE_FRONT_RUNNING=true
|
||||
- LOG_LEVEL=debug
|
||||
networks:
|
||||
- mev-network
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
- ./data:/app/data
|
||||
|
||||
# Prometheus for metrics collection
|
||||
prometheus:
|
||||
image: prom/prometheus:latest
|
||||
container_name: mev-bot-prometheus
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
- ./config/prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
- prometheus-data:/prometheus
|
||||
command:
|
||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||
- '--storage.tsdb.path=/prometheus'
|
||||
- '--web.console.libraries=/usr/share/prometheus/console_libraries'
|
||||
- '--web.console.templates=/usr/share/prometheus/consoles'
|
||||
networks:
|
||||
- mev-network
|
||||
restart: unless-stopped
|
||||
|
||||
# Grafana for metrics visualization
|
||||
grafana:
|
||||
image: grafana/grafana:latest
|
||||
container_name: mev-bot-grafana
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- GF_SECURITY_ADMIN_USER=admin
|
||||
- GF_SECURITY_ADMIN_PASSWORD=admin
|
||||
- GF_INSTALL_PLUGINS=
|
||||
volumes:
|
||||
- grafana-data:/var/lib/grafana
|
||||
- ./config/grafana/dashboards:/etc/grafana/provisioning/dashboards
|
||||
- ./config/grafana/datasources:/etc/grafana/provisioning/datasources
|
||||
networks:
|
||||
- mev-network
|
||||
depends_on:
|
||||
- prometheus
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
mev-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
prometheus-data:
|
||||
grafana-data:
|
||||
397
pkg/pools/discovery.go
Normal file
397
pkg/pools/discovery.go
Normal file
@@ -0,0 +1,397 @@
|
||||
package pools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math/big"
|
||||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
|
||||
"github.com/your-org/mev-bot/pkg/cache"
|
||||
mevtypes "github.com/your-org/mev-bot/pkg/types"
|
||||
)
|
||||
|
||||
// Known factory addresses on Arbitrum
|
||||
var (
|
||||
UniswapV2FactoryAddress = common.HexToAddress("0xf1D7CC64Fb4452F05c498126312eBE29f30Fbcf9") // SushiSwap
|
||||
UniswapV3FactoryAddress = common.HexToAddress("0x1F98431c8aD98523631AE4a59f267346ea31F984") // Uniswap V3
|
||||
CamelotFactoryAddress = common.HexToAddress("0x6EcCab422D763aC031210895C81787E87B43A652") // Camelot
|
||||
CurveRegistryAddress = common.HexToAddress("0x445FE580eF8d70FF569aB36e80c647af338db351") // Curve (mainnet, example)
|
||||
)
|
||||
|
||||
// Top traded tokens on Arbitrum
|
||||
var TopTokens = []common.Address{
|
||||
common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), // WETH
|
||||
common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"), // USDC
|
||||
common.HexToAddress("0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9"), // USDT
|
||||
common.HexToAddress("0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f"), // WBTC
|
||||
common.HexToAddress("0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"), // DAI
|
||||
common.HexToAddress("0xf97f4df75117a78c1A5a0DBb814Af92458539FB4"), // LINK
|
||||
common.HexToAddress("0xFA7F8980b0f1E64A2062791cc3b0871572f1F7f0"), // UNI
|
||||
}
|
||||
|
||||
// DiscoveryConfig contains configuration for pool discovery
|
||||
type DiscoveryConfig struct {
|
||||
// Connection
|
||||
RPCURL string
|
||||
|
||||
// Discovery parameters
|
||||
StartBlock uint64
|
||||
MaxPools int
|
||||
MinLiquidity *big.Int
|
||||
BatchSize int
|
||||
ConcurrentFetches int
|
||||
|
||||
// Token pairs to discover
|
||||
TokenPairs []TokenPair
|
||||
}
|
||||
|
||||
// TokenPair represents a pair of tokens
|
||||
type TokenPair struct {
|
||||
Token0 common.Address
|
||||
Token1 common.Address
|
||||
}
|
||||
|
||||
// DefaultDiscoveryConfig returns default configuration
|
||||
func DefaultDiscoveryConfig() *DiscoveryConfig {
|
||||
// Generate pairs from top tokens
|
||||
pairs := make([]TokenPair, 0)
|
||||
for i := 0; i < len(TopTokens); i++ {
|
||||
for j := i + 1; j < len(TopTokens); j++ {
|
||||
pairs = append(pairs, TokenPair{
|
||||
Token0: TopTokens[i],
|
||||
Token1: TopTokens[j],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return &DiscoveryConfig{
|
||||
RPCURL: "https://arb1.arbitrum.io/rpc",
|
||||
StartBlock: 0,
|
||||
MaxPools: 1000,
|
||||
MinLiquidity: big.NewInt(1e18), // 1 ETH minimum
|
||||
BatchSize: 100,
|
||||
ConcurrentFetches: 10,
|
||||
TokenPairs: pairs,
|
||||
}
|
||||
}
|
||||
|
||||
// Discovery discovers pools on Arbitrum
|
||||
type Discovery struct {
|
||||
config *DiscoveryConfig
|
||||
client *ethclient.Client
|
||||
cache *cache.PoolCache
|
||||
logger *slog.Logger
|
||||
|
||||
mu sync.Mutex
|
||||
poolsDiscovered int
|
||||
}
|
||||
|
||||
// NewDiscovery creates a new pool discovery service
|
||||
func NewDiscovery(config *DiscoveryConfig, cache *cache.PoolCache, logger *slog.Logger) (*Discovery, error) {
|
||||
if config == nil {
|
||||
config = DefaultDiscoveryConfig()
|
||||
}
|
||||
|
||||
client, err := ethclient.Dial(config.RPCURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to RPC: %w", err)
|
||||
}
|
||||
|
||||
return &Discovery{
|
||||
config: config,
|
||||
client: client,
|
||||
cache: cache,
|
||||
logger: logger.With("component", "pool_discovery"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DiscoverAll discovers all pools from known DEXes
|
||||
func (d *Discovery) DiscoverAll(ctx context.Context) error {
|
||||
d.logger.Info("starting pool discovery")
|
||||
|
||||
// Discover UniswapV2-style pools (SushiSwap, Camelot, etc.)
|
||||
if err := d.discoverUniswapV2Pools(ctx); err != nil {
|
||||
d.logger.Error("uniswap v2 discovery failed", "error", err)
|
||||
}
|
||||
|
||||
// Discover UniswapV3 pools
|
||||
if err := d.discoverUniswapV3Pools(ctx); err != nil {
|
||||
d.logger.Error("uniswap v3 discovery failed", "error", err)
|
||||
}
|
||||
|
||||
d.logger.Info("pool discovery complete", "pools_discovered", d.poolsDiscovered, "total_cached", d.cache.Count())
|
||||
return nil
|
||||
}
|
||||
|
||||
// discoverUniswapV2Pools discovers UniswapV2-style pools
|
||||
func (d *Discovery) discoverUniswapV2Pools(ctx context.Context) error {
|
||||
d.logger.Info("discovering UniswapV2-style pools")
|
||||
|
||||
factories := []struct {
|
||||
address common.Address
|
||||
protocol string
|
||||
}{
|
||||
{UniswapV2FactoryAddress, mevtypes.ProtocolUniswapV2},
|
||||
{CamelotFactoryAddress, mevtypes.ProtocolCamelot},
|
||||
}
|
||||
|
||||
for _, factory := range factories {
|
||||
d.logger.Info("querying factory", "protocol", factory.protocol, "address", factory.address.Hex())
|
||||
|
||||
// Query each token pair
|
||||
for _, pair := range d.config.TokenPairs {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
poolAddr, err := d.getUniswapV2Pool(ctx, factory.address, pair.Token0, pair.Token1)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if poolAddr == (common.Address{}) {
|
||||
continue // Pool doesn't exist
|
||||
}
|
||||
|
||||
// Fetch pool info
|
||||
poolInfo, err := d.fetchUniswapV2PoolInfo(ctx, poolAddr, pair.Token0, pair.Token1, factory.protocol)
|
||||
if err != nil {
|
||||
d.logger.Debug("failed to fetch pool info", "pool", poolAddr.Hex(), "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check minimum liquidity
|
||||
if poolInfo.LiquidityUSD.Cmp(d.config.MinLiquidity) < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Add to cache
|
||||
if err := d.cache.Add(poolInfo); err != nil {
|
||||
d.logger.Warn("failed to add pool to cache", "pool", poolAddr.Hex(), "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
d.mu.Lock()
|
||||
d.poolsDiscovered++
|
||||
d.mu.Unlock()
|
||||
|
||||
d.logger.Debug("discovered pool",
|
||||
"protocol", factory.protocol,
|
||||
"pool", poolAddr.Hex(),
|
||||
"token0", pair.Token0.Hex(),
|
||||
"token1", pair.Token1.Hex(),
|
||||
"liquidity", poolInfo.LiquidityUSD.String(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getUniswapV2Pool gets a UniswapV2 pool address for a token pair
|
||||
func (d *Discovery) getUniswapV2Pool(ctx context.Context, factory common.Address, token0, token1 common.Address) (common.Address, error) {
|
||||
// getPair(address,address) returns (address)
|
||||
// This is a simplified version - in production, use generated bindings
|
||||
calldata := append([]byte{0xe6, 0xa4, 0x39, 0x05}, // getPair selector
|
||||
append(padLeft(token0.Bytes(), 32), padLeft(token1.Bytes(), 32)...)...)
|
||||
|
||||
result, err := d.client.CallContract(ctx, map[string]interface{}{
|
||||
"to": factory,
|
||||
"data": common.Bytes2Hex(calldata),
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return common.Address{}, err
|
||||
}
|
||||
|
||||
if len(result) == 0 {
|
||||
return common.Address{}, nil
|
||||
}
|
||||
|
||||
return common.BytesToAddress(result[12:]), nil
|
||||
}
|
||||
|
||||
// fetchUniswapV2PoolInfo fetches pool information
|
||||
func (d *Discovery) fetchUniswapV2PoolInfo(ctx context.Context, poolAddr, token0, token1 common.Address, protocol string) (*mevtypes.PoolInfo, error) {
|
||||
// getReserves() returns (uint112,uint112,uint32)
|
||||
// Simplified - in production use generated bindings
|
||||
calldata := []byte{0x09, 0x02, 0xf1, 0xac} // getReserves selector
|
||||
|
||||
result, err := d.client.CallContract(ctx, map[string]interface{}{
|
||||
"to": poolAddr,
|
||||
"data": common.Bytes2Hex(calldata),
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(result) < 64 {
|
||||
return nil, fmt.Errorf("invalid reserves response")
|
||||
}
|
||||
|
||||
reserve0 := new(big.Int).SetBytes(result[0:32])
|
||||
reserve1 := new(big.Int).SetBytes(result[32:64])
|
||||
|
||||
// Estimate liquidity in USD (simplified - in production, use price oracle)
|
||||
liquidityUSD := new(big.Int).Add(reserve0, reserve1)
|
||||
|
||||
return &mevtypes.PoolInfo{
|
||||
Address: poolAddr,
|
||||
Protocol: protocol,
|
||||
Token0: token0,
|
||||
Token1: token1,
|
||||
Reserve0: reserve0,
|
||||
Reserve1: reserve1,
|
||||
Fee: 300, // 0.3% for UniswapV2
|
||||
LiquidityUSD: liquidityUSD,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// discoverUniswapV3Pools discovers UniswapV3 pools
|
||||
func (d *Discovery) discoverUniswapV3Pools(ctx context.Context) error {
|
||||
d.logger.Info("discovering UniswapV3 pools")
|
||||
|
||||
// UniswapV3 has multiple fee tiers
|
||||
feeTiers := []uint32{100, 500, 3000, 10000}
|
||||
|
||||
for _, pair := range d.config.TokenPairs {
|
||||
for _, fee := range feeTiers {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
poolAddr, err := d.getUniswapV3Pool(ctx, pair.Token0, pair.Token1, fee)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if poolAddr == (common.Address{}) {
|
||||
continue // Pool doesn't exist
|
||||
}
|
||||
|
||||
// Fetch pool info
|
||||
poolInfo, err := d.fetchUniswapV3PoolInfo(ctx, poolAddr, pair.Token0, pair.Token1, fee)
|
||||
if err != nil {
|
||||
d.logger.Debug("failed to fetch pool info", "pool", poolAddr.Hex(), "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check minimum liquidity
|
||||
if poolInfo.LiquidityUSD.Cmp(d.config.MinLiquidity) < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Add to cache
|
||||
if err := d.cache.Add(poolInfo); err != nil {
|
||||
d.logger.Warn("failed to add pool to cache", "pool", poolAddr.Hex(), "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
d.mu.Lock()
|
||||
d.poolsDiscovered++
|
||||
d.mu.Unlock()
|
||||
|
||||
d.logger.Debug("discovered pool",
|
||||
"protocol", mevtypes.ProtocolUniswapV3,
|
||||
"pool", poolAddr.Hex(),
|
||||
"token0", pair.Token0.Hex(),
|
||||
"token1", pair.Token1.Hex(),
|
||||
"fee", fee,
|
||||
"liquidity", poolInfo.LiquidityUSD.String(),
|
||||
)
|
||||
|
||||
// Check if we've reached max pools
|
||||
if d.poolsDiscovered >= d.config.MaxPools {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getUniswapV3Pool gets a UniswapV3 pool address
|
||||
func (d *Discovery) getUniswapV3Pool(ctx context.Context, token0, token1 common.Address, fee uint32) (common.Address, error) {
|
||||
// getPool(address,address,uint24) returns (address)
|
||||
// Simplified - in production use generated bindings
|
||||
feeBytes := make([]byte, 32)
|
||||
copy(feeBytes[29:], big.NewInt(int64(fee)).Bytes())
|
||||
|
||||
calldata := append([]byte{0x17, 0x79, 0x05, 0x7a}, // getPool selector
|
||||
append(append(padLeft(token0.Bytes(), 32), padLeft(token1.Bytes(), 32)...), feeBytes...)...)
|
||||
|
||||
result, err := d.client.CallContract(ctx, map[string]interface{}{
|
||||
"to": UniswapV3FactoryAddress,
|
||||
"data": common.Bytes2Hex(calldata),
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return common.Address{}, err
|
||||
}
|
||||
|
||||
if len(result) == 0 {
|
||||
return common.Address{}, nil
|
||||
}
|
||||
|
||||
return common.BytesToAddress(result[12:]), nil
|
||||
}
|
||||
|
||||
// fetchUniswapV3PoolInfo fetches UniswapV3 pool information
|
||||
func (d *Discovery) fetchUniswapV3PoolInfo(ctx context.Context, poolAddr, token0, token1 common.Address, fee uint32) (*mevtypes.PoolInfo, error) {
|
||||
// liquidity() returns (uint128)
|
||||
// Simplified - in production use generated bindings
|
||||
calldata := []byte{0x1a, 0x68, 0x65, 0x02} // liquidity selector
|
||||
|
||||
result, err := d.client.CallContract(ctx, map[string]interface{}{
|
||||
"to": poolAddr,
|
||||
"data": common.Bytes2Hex(calldata),
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(result) < 16 {
|
||||
return nil, fmt.Errorf("invalid liquidity response")
|
||||
}
|
||||
|
||||
liquidity := new(big.Int).SetBytes(result[16:32])
|
||||
|
||||
return &mevtypes.PoolInfo{
|
||||
Address: poolAddr,
|
||||
Protocol: mevtypes.ProtocolUniswapV3,
|
||||
Token0: token0,
|
||||
Token1: token1,
|
||||
Reserve0: liquidity, // Simplified
|
||||
Reserve1: liquidity,
|
||||
Fee: fee,
|
||||
LiquidityUSD: liquidity,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// padLeft pads bytes to the left with zeros
|
||||
func padLeft(data []byte, length int) []byte {
|
||||
if len(data) >= length {
|
||||
return data
|
||||
}
|
||||
padded := make([]byte, length)
|
||||
copy(padded[length-len(data):], data)
|
||||
return padded
|
||||
}
|
||||
|
||||
// GetStats returns discovery statistics
|
||||
func (d *Discovery) GetStats() map[string]interface{} {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
return map[string]interface{}{
|
||||
"pools_discovered": d.poolsDiscovered,
|
||||
"pools_cached": d.cache.Count(),
|
||||
}
|
||||
}
|
||||
461
pkg/sequencer/reader.go
Normal file
461
pkg/sequencer/reader.go
Normal file
@@ -0,0 +1,461 @@
|
||||
package sequencer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math/big"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"github.com/your-org/mev-bot/pkg/arbitrage"
|
||||
"github.com/your-org/mev-bot/pkg/cache"
|
||||
"github.com/your-org/mev-bot/pkg/execution"
|
||||
"github.com/your-org/mev-bot/pkg/parsers"
|
||||
"github.com/your-org/mev-bot/pkg/validation"
|
||||
)
|
||||
|
||||
// ReaderConfig contains configuration for the sequencer reader
|
||||
type ReaderConfig struct {
|
||||
// WebSocket connection
|
||||
WSURL string
|
||||
ReconnectDelay time.Duration
|
||||
MaxReconnectDelay time.Duration
|
||||
PingInterval time.Duration
|
||||
|
||||
// RPC for fetching full transactions
|
||||
RPCURL string
|
||||
|
||||
// Processing
|
||||
WorkerCount int
|
||||
BufferSize int
|
||||
|
||||
// Filtering
|
||||
MinProfit *big.Int
|
||||
EnableFrontRunning bool
|
||||
|
||||
// Performance
|
||||
MaxProcessingTime time.Duration
|
||||
}
|
||||
|
||||
// DefaultReaderConfig returns default configuration
|
||||
func DefaultReaderConfig() *ReaderConfig {
|
||||
return &ReaderConfig{
|
||||
WSURL: "wss://arb1.arbitrum.io/ws",
|
||||
ReconnectDelay: 1 * time.Second,
|
||||
MaxReconnectDelay: 60 * time.Second,
|
||||
PingInterval: 30 * time.Second,
|
||||
RPCURL: "https://arb1.arbitrum.io/rpc",
|
||||
WorkerCount: 10,
|
||||
BufferSize: 1000,
|
||||
MinProfit: big.NewInt(0.01e18), // 0.01 ETH
|
||||
EnableFrontRunning: true,
|
||||
MaxProcessingTime: 50 * time.Millisecond,
|
||||
}
|
||||
}
|
||||
|
||||
// Reader reads pending transactions from the Arbitrum sequencer
|
||||
type Reader struct {
|
||||
config *ReaderConfig
|
||||
logger *slog.Logger
|
||||
|
||||
// Components
|
||||
parsers *parsers.Factory
|
||||
validator *validation.Validator
|
||||
poolCache *cache.PoolCache
|
||||
detector *arbitrage.Detector
|
||||
executor *execution.Executor
|
||||
|
||||
// Connections
|
||||
wsConn *websocket.Conn
|
||||
rpcClient *ethclient.Client
|
||||
|
||||
// Channels
|
||||
txHashes chan string
|
||||
stopCh chan struct{}
|
||||
wg sync.WaitGroup
|
||||
|
||||
// State
|
||||
mu sync.RWMutex
|
||||
connected bool
|
||||
lastProcessed time.Time
|
||||
processedCount uint64
|
||||
opportunityCount uint64
|
||||
executionCount uint64
|
||||
|
||||
// Metrics (placeholders for actual metrics)
|
||||
txReceived uint64
|
||||
txProcessed uint64
|
||||
parseErrors uint64
|
||||
validationErrors uint64
|
||||
opportunitiesFound uint64
|
||||
executionsAttempted uint64
|
||||
avgParseLatency time.Duration
|
||||
avgDetectLatency time.Duration
|
||||
avgExecuteLatency time.Duration
|
||||
}
|
||||
|
||||
// NewReader creates a new sequencer reader
|
||||
func NewReader(
|
||||
config *ReaderConfig,
|
||||
parsers *parsers.Factory,
|
||||
validator *validation.Validator,
|
||||
poolCache *cache.PoolCache,
|
||||
detector *arbitrage.Detector,
|
||||
executor *execution.Executor,
|
||||
logger *slog.Logger,
|
||||
) (*Reader, error) {
|
||||
if config == nil {
|
||||
config = DefaultReaderConfig()
|
||||
}
|
||||
|
||||
// Connect to RPC for fetching full transactions
|
||||
rpcClient, err := ethclient.Dial(config.RPCURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to RPC: %w", err)
|
||||
}
|
||||
|
||||
return &Reader{
|
||||
config: config,
|
||||
logger: logger.With("component", "sequencer_reader"),
|
||||
parsers: parsers,
|
||||
validator: validator,
|
||||
poolCache: poolCache,
|
||||
detector: detector,
|
||||
executor: executor,
|
||||
rpcClient: rpcClient,
|
||||
txHashes: make(chan string, config.BufferSize),
|
||||
stopCh: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Start starts the sequencer reader
|
||||
func (r *Reader) Start(ctx context.Context) error {
|
||||
r.logger.Info("starting sequencer reader")
|
||||
|
||||
// Start workers
|
||||
for i := 0; i < r.config.WorkerCount; i++ {
|
||||
r.wg.Add(1)
|
||||
go r.worker(ctx, i)
|
||||
}
|
||||
|
||||
// Start connection manager
|
||||
r.wg.Add(1)
|
||||
go r.maintainConnection(ctx)
|
||||
|
||||
// Wait for context cancellation
|
||||
<-ctx.Done()
|
||||
|
||||
r.logger.Info("stopping sequencer reader")
|
||||
close(r.stopCh)
|
||||
r.wg.Wait()
|
||||
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
// maintainConnection maintains the WebSocket connection with automatic reconnection
|
||||
func (r *Reader) maintainConnection(ctx context.Context) {
|
||||
defer r.wg.Done()
|
||||
|
||||
reconnectDelay := r.config.ReconnectDelay
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// Connect to sequencer
|
||||
conn, err := r.connect(ctx)
|
||||
if err != nil {
|
||||
r.logger.Error("connection failed", "error", err, "retry_in", reconnectDelay)
|
||||
time.Sleep(reconnectDelay)
|
||||
|
||||
// Exponential backoff
|
||||
reconnectDelay *= 2
|
||||
if reconnectDelay > r.config.MaxReconnectDelay {
|
||||
reconnectDelay = r.config.MaxReconnectDelay
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Reset backoff on successful connection
|
||||
reconnectDelay = r.config.ReconnectDelay
|
||||
r.wsConn = conn
|
||||
|
||||
r.mu.Lock()
|
||||
r.connected = true
|
||||
r.mu.Unlock()
|
||||
|
||||
r.logger.Info("connected to sequencer")
|
||||
|
||||
// Subscribe to pending transactions
|
||||
if err := r.subscribe(ctx, conn); err != nil {
|
||||
r.logger.Error("subscription failed", "error", err)
|
||||
conn.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
// Read messages until connection fails
|
||||
if err := r.readMessages(ctx, conn); err != nil {
|
||||
r.logger.Error("connection lost", "error", err)
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
r.connected = false
|
||||
r.mu.Unlock()
|
||||
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// connect establishes a WebSocket connection
|
||||
func (r *Reader) connect(ctx context.Context) (*websocket.Conn, error) {
|
||||
dialer := websocket.DefaultDialer
|
||||
dialer.HandshakeTimeout = 10 * time.Second
|
||||
|
||||
conn, _, err := dialer.DialContext(ctx, r.config.WSURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dial failed: %w", err)
|
||||
}
|
||||
|
||||
// Set read/write deadlines
|
||||
conn.SetReadDeadline(time.Now().Add(r.config.PingInterval * 2))
|
||||
conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// subscribe subscribes to pending transactions
|
||||
func (r *Reader) subscribe(ctx context.Context, conn *websocket.Conn) error {
|
||||
// Subscribe to newPendingTransactions
|
||||
sub := map[string]interface{}{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "eth_subscribe",
|
||||
"params": []interface{}{"newPendingTransactions"},
|
||||
}
|
||||
|
||||
if err := conn.WriteJSON(sub); err != nil {
|
||||
return fmt.Errorf("subscription write failed: %w", err)
|
||||
}
|
||||
|
||||
// Read subscription response
|
||||
var resp map[string]interface{}
|
||||
if err := conn.ReadJSON(&resp); err != nil {
|
||||
return fmt.Errorf("subscription response failed: %w", err)
|
||||
}
|
||||
|
||||
r.logger.Info("subscribed to pending transactions", "response", resp)
|
||||
return nil
|
||||
}
|
||||
|
||||
// readMessages reads messages from the WebSocket connection
|
||||
func (r *Reader) readMessages(ctx context.Context, conn *websocket.Conn) error {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-r.stopCh:
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
|
||||
// Set read deadline
|
||||
conn.SetReadDeadline(time.Now().Add(r.config.PingInterval * 2))
|
||||
|
||||
var msg map[string]interface{}
|
||||
if err := conn.ReadJSON(&msg); err != nil {
|
||||
return fmt.Errorf("read failed: %w", err)
|
||||
}
|
||||
|
||||
// Extract transaction hash from notification
|
||||
if params, ok := msg["params"].(map[string]interface{}); ok {
|
||||
if result, ok := params["result"].(string); ok {
|
||||
// Send to worker pool
|
||||
select {
|
||||
case r.txHashes <- result:
|
||||
r.txReceived++
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
r.logger.Warn("tx buffer full, dropping tx")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// worker processes transaction hashes
|
||||
func (r *Reader) worker(ctx context.Context, id int) {
|
||||
defer r.wg.Done()
|
||||
|
||||
logger := r.logger.With("worker", id)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-r.stopCh:
|
||||
return
|
||||
case txHash := <-r.txHashes:
|
||||
if err := r.processTxHash(ctx, txHash); err != nil {
|
||||
logger.Debug("processing error", "tx", txHash, "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processTxHash processes a transaction hash
|
||||
func (r *Reader) processTxHash(ctx context.Context, txHash string) error {
|
||||
startTime := time.Now()
|
||||
|
||||
// Enforce max processing time
|
||||
procCtx, cancel := context.WithTimeout(ctx, r.config.MaxProcessingTime)
|
||||
defer cancel()
|
||||
|
||||
// Fetch full transaction
|
||||
tx, isPending, err := r.rpcClient.TransactionByHash(procCtx, common.HexToHash(txHash))
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch tx failed: %w", err)
|
||||
}
|
||||
|
||||
if !isPending {
|
||||
return nil // Skip already mined transactions
|
||||
}
|
||||
|
||||
parseStart := time.Now()
|
||||
|
||||
// Parse transaction events
|
||||
events, err := r.parsers.ParseTransaction(tx)
|
||||
if err != nil {
|
||||
r.parseErrors++
|
||||
return fmt.Errorf("parse failed: %w", err)
|
||||
}
|
||||
|
||||
if len(events) == 0 {
|
||||
return nil // No swap events
|
||||
}
|
||||
|
||||
r.avgParseLatency = time.Since(parseStart)
|
||||
|
||||
// Validate events
|
||||
validEvents := r.validator.FilterValid(events)
|
||||
if len(validEvents) == 0 {
|
||||
r.validationErrors++
|
||||
return nil
|
||||
}
|
||||
|
||||
detectStart := time.Now()
|
||||
|
||||
// Detect arbitrage opportunities for each swap
|
||||
for _, event := range validEvents {
|
||||
// Get input token from the swap
|
||||
inputToken := event.GetInputToken()
|
||||
|
||||
// Detect opportunities starting with this token
|
||||
opportunities, err := r.detector.DetectOpportunities(procCtx, inputToken)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
r.avgDetectLatency = time.Since(detectStart)
|
||||
|
||||
// Execute profitable opportunities
|
||||
for _, opp := range opportunities {
|
||||
if opp.NetProfit.Cmp(r.config.MinProfit) > 0 {
|
||||
r.opportunitiesFound++
|
||||
r.opportunityCount++
|
||||
|
||||
if r.config.EnableFrontRunning {
|
||||
execStart := time.Now()
|
||||
go r.executeFrontRun(ctx, opp, tx)
|
||||
r.avgExecuteLatency = time.Since(execStart)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
r.txProcessed++
|
||||
r.processedCount++
|
||||
r.lastProcessed = time.Now()
|
||||
|
||||
totalLatency := time.Since(startTime)
|
||||
if totalLatency > r.config.MaxProcessingTime {
|
||||
r.logger.Warn("processing too slow", "latency", totalLatency, "target", r.config.MaxProcessingTime)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// executeFrontRun executes a front-running transaction
|
||||
func (r *Reader) executeFrontRun(ctx context.Context, opp *arbitrage.Opportunity, targetTx *types.Transaction) {
|
||||
r.executionsAttempted++
|
||||
r.executionCount++
|
||||
|
||||
r.logger.Info("front-running opportunity",
|
||||
"opportunity_id", opp.ID,
|
||||
"type", opp.Type,
|
||||
"profit", opp.NetProfit.String(),
|
||||
"roi", fmt.Sprintf("%.2f%%", opp.ROI*100),
|
||||
"target_tx", targetTx.Hash().Hex(),
|
||||
)
|
||||
|
||||
// Execute the arbitrage
|
||||
result, err := r.executor.Execute(ctx, opp)
|
||||
if err != nil {
|
||||
r.logger.Error("execution failed",
|
||||
"opportunity_id", opp.ID,
|
||||
"error", err,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if result.Success {
|
||||
r.logger.Info("execution succeeded",
|
||||
"opportunity_id", opp.ID,
|
||||
"tx_hash", result.TxHash.Hex(),
|
||||
"actual_profit", result.ActualProfit.String(),
|
||||
"gas_cost", result.GasCost.String(),
|
||||
"duration", result.Duration,
|
||||
)
|
||||
} else {
|
||||
r.logger.Warn("execution failed",
|
||||
"opportunity_id", opp.ID,
|
||||
"tx_hash", result.TxHash.Hex(),
|
||||
"error", result.Error,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// GetStats returns current statistics
|
||||
func (r *Reader) GetStats() map[string]interface{} {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
return map[string]interface{}{
|
||||
"connected": r.connected,
|
||||
"tx_received": r.txReceived,
|
||||
"tx_processed": r.txProcessed,
|
||||
"parse_errors": r.parseErrors,
|
||||
"validation_errors": r.validationErrors,
|
||||
"opportunities_found": r.opportunitiesFound,
|
||||
"executions_attempted": r.executionsAttempted,
|
||||
"avg_parse_latency": r.avgParseLatency.String(),
|
||||
"avg_detect_latency": r.avgDetectLatency.String(),
|
||||
"avg_execute_latency": r.avgExecuteLatency.String(),
|
||||
"last_processed": r.lastProcessed.Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
|
||||
// Stop stops the sequencer reader
|
||||
func (r *Reader) Stop() {
|
||||
close(r.stopCh)
|
||||
}
|
||||
105
scripts/create-test-swap.sh
Executable file
105
scripts/create-test-swap.sh
Executable file
@@ -0,0 +1,105 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# MEV Bot V2 - Create Test Swap Script
|
||||
# This script creates a test swap on the Anvil fork for the bot to detect
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo "🔄 Creating test swap on Anvil fork..."
|
||||
|
||||
# Load environment variables
|
||||
if [ ! -f .env ]; then
|
||||
echo -e "${RED}❌ Error: .env file not found${NC}"
|
||||
exit 1
|
||||
fi
|
||||
source .env
|
||||
|
||||
# Token addresses on Arbitrum
|
||||
WETH="0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"
|
||||
USDC="0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"
|
||||
|
||||
# SushiSwap Router on Arbitrum
|
||||
SUSHISWAP_ROUTER="0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506"
|
||||
|
||||
# Amount to swap (0.1 ETH)
|
||||
AMOUNT="100000000000000000"
|
||||
|
||||
echo -e "${YELLOW}Token In: WETH ($WETH)${NC}"
|
||||
echo -e "${YELLOW}Token Out: USDC ($USDC)${NC}"
|
||||
echo -e "${YELLOW}Amount: 0.1 ETH${NC}"
|
||||
|
||||
# Step 1: Wrap ETH to WETH
|
||||
echo ""
|
||||
echo "📦 Step 1: Wrapping ETH to WETH..."
|
||||
docker-compose exec -T anvil cast send $WETH \
|
||||
"deposit()" \
|
||||
--value 0.1ether \
|
||||
--private-key $PRIVATE_KEY \
|
||||
--rpc-url http://localhost:8545
|
||||
|
||||
WETH_BALANCE=$(docker-compose exec -T anvil cast call $WETH \
|
||||
"balanceOf(address)(uint256)" \
|
||||
$(docker-compose exec -T anvil cast wallet address $PRIVATE_KEY) \
|
||||
--rpc-url http://localhost:8545)
|
||||
echo -e "${GREEN}✅ WETH Balance: $WETH_BALANCE${NC}"
|
||||
|
||||
# Step 2: Approve router to spend WETH
|
||||
echo ""
|
||||
echo "✅ Step 2: Approving SushiSwap router..."
|
||||
docker-compose exec -T anvil cast send $WETH \
|
||||
"approve(address,uint256)" \
|
||||
$SUSHISWAP_ROUTER \
|
||||
"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" \
|
||||
--private-key $PRIVATE_KEY \
|
||||
--rpc-url http://localhost:8545
|
||||
|
||||
echo -e "${GREEN}✅ Approval confirmed${NC}"
|
||||
|
||||
# Step 3: Execute swap
|
||||
echo ""
|
||||
echo "🔄 Step 3: Executing swap on SushiSwap..."
|
||||
echo -e "${YELLOW}This swap should be detected by the MEV bot!${NC}"
|
||||
|
||||
# Build swap calldata
|
||||
# swapExactTokensForTokens(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline)
|
||||
DEADLINE=$(($(date +%s) + 3600))
|
||||
TO=$(docker-compose exec -T anvil cast wallet address $PRIVATE_KEY)
|
||||
|
||||
docker-compose exec -T anvil cast send $SUSHISWAP_ROUTER \
|
||||
"swapExactTokensForTokens(uint256,uint256,address[],address,uint256)" \
|
||||
$AMOUNT \
|
||||
"0" \
|
||||
"[$WETH,$USDC]" \
|
||||
$TO \
|
||||
$DEADLINE \
|
||||
--private-key $PRIVATE_KEY \
|
||||
--rpc-url http://localhost:8545
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}✅ Test swap executed!${NC}"
|
||||
|
||||
# Check balances
|
||||
echo ""
|
||||
echo "📊 Final balances:"
|
||||
WETH_BALANCE=$(docker-compose exec -T anvil cast call $WETH \
|
||||
"balanceOf(address)(uint256)" \
|
||||
$TO \
|
||||
--rpc-url http://localhost:8545)
|
||||
echo -e " WETH: $WETH_BALANCE"
|
||||
|
||||
USDC_BALANCE=$(docker-compose exec -T anvil cast call $USDC \
|
||||
"balanceOf(address)(uint256)" \
|
||||
$TO \
|
||||
--rpc-url http://localhost:8545)
|
||||
echo -e " USDC: $USDC_BALANCE"
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}✨ Test swap complete!${NC}"
|
||||
echo ""
|
||||
echo "Check the MEV bot logs to see if it detected the opportunity:"
|
||||
echo " docker-compose logs -f mev-bot"
|
||||
86
scripts/setup-local-fork.sh
Executable file
86
scripts/setup-local-fork.sh
Executable file
@@ -0,0 +1,86 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# MEV Bot V2 - Local Fork Setup Script
|
||||
# This script sets up the Anvil fork with test liquidity and scenarios
|
||||
|
||||
echo "🚀 Setting up MEV Bot V2 local testing environment..."
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Check if .env exists
|
||||
if [ ! -f .env ]; then
|
||||
echo -e "${YELLOW}⚠️ No .env file found. Copying from .env.example...${NC}"
|
||||
cp .env.example .env
|
||||
echo -e "${RED}⚠️ IMPORTANT: Edit .env and set your PRIVATE_KEY before continuing!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Load environment variables
|
||||
source .env
|
||||
|
||||
# Check if private key is set
|
||||
if [ "$PRIVATE_KEY" == "0000000000000000000000000000000000000000000000000000000000000000" ]; then
|
||||
echo -e "${RED}❌ Error: PRIVATE_KEY not set in .env file${NC}"
|
||||
echo -e "${YELLOW}Please set a test private key in .env${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✅ Configuration loaded${NC}"
|
||||
|
||||
# Start Anvil fork
|
||||
echo "🔧 Starting Anvil fork of Arbitrum..."
|
||||
docker-compose up -d anvil
|
||||
|
||||
# Wait for Anvil to be ready
|
||||
echo "⏳ Waiting for Anvil to be ready..."
|
||||
max_attempts=30
|
||||
attempt=0
|
||||
while [ $attempt -lt $max_attempts ]; do
|
||||
if docker-compose exec -T anvil cast block-number --rpc-url http://localhost:8545 &>/dev/null; then
|
||||
echo -e "${GREEN}✅ Anvil is ready${NC}"
|
||||
break
|
||||
fi
|
||||
attempt=$((attempt + 1))
|
||||
sleep 2
|
||||
done
|
||||
|
||||
if [ $attempt -eq $max_attempts ]; then
|
||||
echo -e "${RED}❌ Anvil failed to start${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get current block number
|
||||
BLOCK_NUMBER=$(docker-compose exec -T anvil cast block-number --rpc-url http://localhost:8545)
|
||||
echo -e "${GREEN}📦 Forked at block: $BLOCK_NUMBER${NC}"
|
||||
|
||||
# Get wallet address from private key
|
||||
WALLET_ADDRESS=$(docker-compose exec -T anvil cast wallet address $PRIVATE_KEY)
|
||||
echo -e "${GREEN}💼 Wallet address: $WALLET_ADDRESS${NC}"
|
||||
|
||||
# Fund the wallet with test ETH
|
||||
echo "💰 Funding wallet with 100 test ETH..."
|
||||
docker-compose exec -T anvil cast send --value 100ether --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 $WALLET_ADDRESS --rpc-url http://localhost:8545
|
||||
|
||||
# Get wallet balance
|
||||
BALANCE=$(docker-compose exec -T anvil cast balance $WALLET_ADDRESS --rpc-url http://localhost:8545)
|
||||
echo -e "${GREEN}💵 Wallet balance: $(echo "scale=4; $BALANCE / 1000000000000000000" | bc) ETH${NC}"
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}✨ Local fork setup complete!${NC}"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Start the full stack: docker-compose up -d"
|
||||
echo " 2. View logs: docker-compose logs -f mev-bot"
|
||||
echo " 3. View metrics: http://localhost:9090"
|
||||
echo " 4. View Grafana: http://localhost:3000 (admin/admin)"
|
||||
echo ""
|
||||
echo "Testing commands:"
|
||||
echo " - Check Anvil: docker-compose exec anvil cast block-number --rpc-url http://localhost:8545"
|
||||
echo " - Get balance: docker-compose exec anvil cast balance $WALLET_ADDRESS --rpc-url http://localhost:8545"
|
||||
echo " - Create test swap: ./scripts/create-test-swap.sh"
|
||||
echo ""
|
||||
Reference in New Issue
Block a user