package main import ( "context" "fmt" "math/big" "net/url" "os" "os/signal" "strings" "syscall" "time" "github.com/ethereum/go-ethereum/ethclient" "github.com/fraktal/mev-beta/internal/config" "github.com/fraktal/mev-beta/internal/logger" "github.com/fraktal/mev-beta/pkg/arbitrage" "github.com/fraktal/mev-beta/pkg/metrics" "github.com/fraktal/mev-beta/pkg/security" "github.com/urfave/cli/v2" ) func main() { app := &cli.App{ Name: "mev-bot", Usage: "An MEV bot that monitors Arbitrum sequencer for swap opportunities", Commands: []*cli.Command{ { Name: "start", Usage: "Start the MEV bot", Action: func(c *cli.Context) error { return startBot() }, }, { Name: "scan", Usage: "Scan for potential arbitrage opportunities", Action: func(c *cli.Context) error { return scanOpportunities() }, }, }, } if err := app.Run(os.Args); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } } func startBot() error { // Load configuration configFile := "config/config.yaml" if _, err := os.Stat("config/local.yaml"); err == nil { configFile = "config/local.yaml" } if _, err := os.Stat("config/arbitrum_production.yaml"); err == nil { configFile = "config/arbitrum_production.yaml" } cfg, err := config.Load(configFile) if err != nil { return fmt.Errorf("failed to load config: %w", err) } // Initialize logger log := logger.New(cfg.Log.Level, cfg.Log.Format, cfg.Log.File) log.Info(fmt.Sprintf("Starting MEV bot with Arbitrage Service - Config: %s", configFile)) log.Debug(fmt.Sprintf("RPC Endpoint: %s", cfg.Arbitrum.RPCEndpoint)) log.Debug(fmt.Sprintf("WS Endpoint: %s", cfg.Arbitrum.WSEndpoint)) log.Debug(fmt.Sprintf("Chain ID: %d", cfg.Arbitrum.ChainID)) // Initialize metrics collector metricsCollector := metrics.NewMetricsCollector(log) // Start metrics server if enabled var metricsServer *metrics.MetricsServer if os.Getenv("METRICS_ENABLED") == "true" { metricsPort := os.Getenv("METRICS_PORT") if metricsPort == "" { metricsPort = "9090" } metricsServer = metrics.NewMetricsServer(metricsCollector, log, metricsPort) go func() { if err := metricsServer.Start(); err != nil { log.Error("Metrics server error: ", err) } }() log.Info(fmt.Sprintf("Metrics server started on port %s", metricsPort)) } // Validate and create Ethereum client if err := validateRPCEndpoint(cfg.Arbitrum.RPCEndpoint); err != nil { return fmt.Errorf("invalid RPC endpoint: %w", err) } client, err := ethclient.Dial(cfg.Arbitrum.RPCEndpoint) if err != nil { return fmt.Errorf("failed to connect to Ethereum client: %w", err) } defer client.Close() // Create key manager for secure transaction signing encryptionKey := os.Getenv("MEV_BOT_ENCRYPTION_KEY") if encryptionKey == "" { return fmt.Errorf("MEV_BOT_ENCRYPTION_KEY environment variable is required for secure operations") } keyManagerConfig := &security.KeyManagerConfig{ KeystorePath: getEnvOrDefault("MEV_BOT_KEYSTORE_PATH", "keystore"), EncryptionKey: encryptionKey, KeyRotationDays: 30, MaxSigningRate: 100, SessionTimeout: time.Hour, AuditLogPath: getEnvOrDefault("MEV_BOT_AUDIT_LOG", "logs/audit.log"), BackupPath: getEnvOrDefault("MEV_BOT_BACKUP_PATH", "backups"), } keyManager, err := security.NewKeyManager(keyManagerConfig, log) if err != nil { return fmt.Errorf("failed to create key manager: %w", err) } // Create arbitrage database arbitrageDB, err := arbitrage.NewSQLiteDatabase(cfg.Database.File, log) if err != nil { return fmt.Errorf("failed to create arbitrage database: %w", err) } defer arbitrageDB.Close() // Check if arbitrage service is enabled if !cfg.Arbitrage.Enabled { log.Info("Arbitrage service is disabled in configuration") return fmt.Errorf("arbitrage service disabled - enable in config to run") } // Create arbitrage service log.Info("Creating arbitrage service...") arbitrageService, err := arbitrage.NewArbitrageService( client, log, &cfg.Arbitrage, keyManager, arbitrageDB, ) if err != nil { return fmt.Errorf("failed to create arbitrage service: %w", err) } log.Info("Arbitrage service created successfully") // Start the arbitrage service log.Info("Starting arbitrage service...") if err := arbitrageService.Start(); err != nil { return fmt.Errorf("failed to start arbitrage service: %w", err) } defer arbitrageService.Stop() log.Info("Arbitrage service started successfully") log.Info("MEV bot started successfully - monitoring for arbitrage opportunities...") // Setup graceful shutdown sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) // Wait for shutdown signal <-sigChan log.Info("Shutdown signal received, stopping MEV bot...") // Stop metrics server if running if metricsServer != nil { metricsServer.Stop() } // Get final stats stats := arbitrageService.GetStats() log.Info(fmt.Sprintf("Final Statistics - Opportunities: %d, Executions: %d, Successful: %d, Total Profit: %s ETH", stats.TotalOpportunitiesDetected, stats.TotalOpportunitiesExecuted, stats.TotalSuccessfulExecutions, formatEther(stats.TotalProfitRealized))) log.Info("MEV bot stopped gracefully") return nil } // formatEther formats wei amount to ether string func formatEther(wei *big.Int) string { if wei == nil { return "0.000000" } eth := new(big.Float).SetInt(wei) eth.Quo(eth, big.NewFloat(1e18)) return fmt.Sprintf("%.6f", eth) } // getEnvOrDefault returns environment variable value or default if not set func getEnvOrDefault(key, defaultValue string) string { if value := os.Getenv(key); value != "" { return value } return defaultValue } // validateRPCEndpoint validates RPC endpoint URL for security func validateRPCEndpoint(endpoint string) error { if endpoint == "" { return fmt.Errorf("RPC endpoint cannot be empty") } u, err := url.Parse(endpoint) if err != nil { return fmt.Errorf("invalid RPC endpoint URL: %w", err) } // Check for valid schemes switch u.Scheme { case "http", "https", "ws", "wss": // Valid schemes default: return fmt.Errorf("invalid RPC scheme: %s (must be http, https, ws, or wss)", u.Scheme) } // Check for localhost/private networks in production if strings.Contains(u.Hostname(), "localhost") || strings.Contains(u.Hostname(), "127.0.0.1") { // Allow localhost only if explicitly enabled if os.Getenv("MEV_BOT_ALLOW_LOCALHOST") != "true" { return fmt.Errorf("localhost RPC endpoints not allowed in production (set MEV_BOT_ALLOW_LOCALHOST=true to override)") } } // Validate hostname is not empty if u.Hostname() == "" { return fmt.Errorf("RPC endpoint must have a valid hostname") } return nil } func scanOpportunities() error { fmt.Println("Scanning for arbitrage opportunities...") // Load configuration configFile := "config/config.yaml" if _, err := os.Stat("config/local.yaml"); err == nil { configFile = "config/local.yaml" } if _, err := os.Stat("config/arbitrum_production.yaml"); err == nil { configFile = "config/arbitrum_production.yaml" } cfg, err := config.Load(configFile) if err != nil { return fmt.Errorf("failed to load config: %w", err) } // Initialize logger log := logger.New(cfg.Log.Level, cfg.Log.Format, cfg.Log.File) log.Info("Starting one-time arbitrage opportunity scan...") // Validate and create Ethereum client if err := validateRPCEndpoint(cfg.Arbitrum.RPCEndpoint); err != nil { return fmt.Errorf("invalid RPC endpoint: %w", err) } client, err := ethclient.Dial(cfg.Arbitrum.RPCEndpoint) if err != nil { return fmt.Errorf("failed to connect to Ethereum client: %w", err) } defer client.Close() // Create key manager (not used for scanning but needed for service) encryptionKey := os.Getenv("MEV_BOT_ENCRYPTION_KEY") if encryptionKey == "" { return fmt.Errorf("MEV_BOT_ENCRYPTION_KEY environment variable is required") } keyManagerConfig := &security.KeyManagerConfig{ KeystorePath: getEnvOrDefault("MEV_BOT_KEYSTORE_PATH", "keystore"), EncryptionKey: encryptionKey, KeyRotationDays: 30, MaxSigningRate: 100, SessionTimeout: time.Hour, AuditLogPath: getEnvOrDefault("MEV_BOT_AUDIT_LOG", "logs/audit.log"), BackupPath: getEnvOrDefault("MEV_BOT_BACKUP_PATH", "backups"), } keyManager, err := security.NewKeyManager(keyManagerConfig, log) if err != nil { return fmt.Errorf("failed to create key manager: %w", err) } // Create arbitrage database arbitrageDB, err := arbitrage.NewSQLiteDatabase(cfg.Database.File, log) if err != nil { return fmt.Errorf("failed to create arbitrage database: %w", err) } defer arbitrageDB.Close() // Create arbitrage service with scanning enabled but execution disabled scanConfig := cfg.Arbitrage scanConfig.MaxConcurrentExecutions = 0 // Disable execution for scan mode arbitrageService, err := arbitrage.NewArbitrageService( client, log, &scanConfig, keyManager, arbitrageDB, ) if err != nil { return fmt.Errorf("failed to create arbitrage service: %w", err) } // Start the service in scan mode if err := arbitrageService.Start(); err != nil { return fmt.Errorf("failed to start arbitrage service: %w", err) } defer arbitrageService.Stop() // Create context with timeout for scanning ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() log.Info("Scanning for 30 seconds...") // Wait for scan duration <-ctx.Done() // Get and display results stats := arbitrageService.GetStats() log.Info(fmt.Sprintf("Scan Results - Opportunities Detected: %d", stats.TotalOpportunitiesDetected)) // Get recent opportunities from database history, err := arbitrageDB.GetExecutionHistory(ctx, 10) if err == nil && len(history) > 0 { log.Info(fmt.Sprintf("Found %d recent opportunities in database", len(history))) } fmt.Printf("Scan completed. Found %d arbitrage opportunities.\n", stats.TotalOpportunitiesDetected) fmt.Println("Check the database and logs for detailed results.") return nil }