342 lines
9.9 KiB
Go
342 lines
9.9 KiB
Go
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.NewSimpleArbitrageService(
|
|
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.NewSimpleArbitrageService(
|
|
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
|
|
}
|