package arbitrage import ( "context" "encoding/binary" "fmt" stdmath "math" "math/big" "os" "sync" "sync/atomic" "time" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" "github.com/holiman/uint256" "github.com/fraktal/mev-beta/internal/config" "github.com/fraktal/mev-beta/internal/logger" "github.com/fraktal/mev-beta/internal/ratelimit" "github.com/fraktal/mev-beta/pkg/arbitrum" parser "github.com/fraktal/mev-beta/pkg/arbitrum/parser" "github.com/fraktal/mev-beta/pkg/contracts" "github.com/fraktal/mev-beta/pkg/exchanges" "github.com/fraktal/mev-beta/pkg/market" "github.com/fraktal/mev-beta/pkg/marketmanager" "github.com/fraktal/mev-beta/pkg/math" "github.com/fraktal/mev-beta/pkg/monitor" "github.com/fraktal/mev-beta/pkg/pools" "github.com/fraktal/mev-beta/pkg/scanner" "github.com/fraktal/mev-beta/pkg/security" "github.com/fraktal/mev-beta/pkg/tokens" pkgtypes "github.com/fraktal/mev-beta/pkg/types" ) // safeConvertUint32ToInt32 safely converts a uint32 to int32, capping at MaxInt32 if overflow would occur func safeConvertUint32ToInt32(v uint32) int32 { if v > stdmath.MaxInt32 { return stdmath.MaxInt32 } return int32(v) } // TokenPair is defined in executor.go to avoid duplication // Use the canonical ArbitrageOpportunity from types package // ArbitrageStats contains service statistics with atomic counters for thread safety type ArbitrageStats struct { // Atomic counters for thread-safe access without locks TotalOpportunitiesDetected int64 TotalOpportunitiesExecuted int64 TotalSuccessfulExecutions int64 TotalExecutionTimeNanos int64 ExecutionCount int64 // Protected by mutex for complex operations TotalProfitRealized *big.Int TotalGasSpent *big.Int AverageExecutionTime time.Duration LastExecutionTime time.Time } // ArbitrageDatabase interface for persistence type ArbitrageDatabase interface { SaveOpportunity(ctx context.Context, opportunity *pkgtypes.ArbitrageOpportunity) error SaveExecution(ctx context.Context, result *ExecutionResult) error GetExecutionHistory(ctx context.Context, limit int) ([]*ExecutionResult, error) SavePoolData(ctx context.Context, poolData *SimplePoolData) error GetPoolData(ctx context.Context, poolAddress common.Address) (*SimplePoolData, error) } // ArbitrageService is a sophisticated arbitrage service with comprehensive MEV detection // Now integrated with the complete MEV bot architecture type ArbitrageService struct { client *ethclient.Client logger *logger.Logger config *config.ArbitrageConfig keyManager *security.KeyManager // Core components (legacy) multiHopScanner *MultiHopScanner executor *ArbitrageExecutor // NEW: Comprehensive MEV architecture components exchangeRegistry *exchanges.ExchangeRegistry arbitrageCalculator *math.ArbitrageCalculator detectionEngine *ArbitrageDetectionEngine flashExecutor *FlashSwapExecutor liveFramework *LiveExecutionFramework // Market management marketManager *market.MarketManager marketDataManager *marketmanager.MarketManager // Pool discovery and token cache (NEW: integrated from infrastructure) poolDiscovery *pools.PoolDiscovery tokenMetadataCache *tokens.MetadataCache // Token cache for pool addresses (legacy) tokenCache map[common.Address]TokenPair tokenCacheMutex sync.RWMutex // Opportunity path cache for execution opportunityPathCache map[string]*ArbitragePath opportunityPathMutex sync.RWMutex // State management isRunning bool liveMode bool // NEW: Whether to use comprehensive live framework monitoringOnly bool // NEW: Whether to run in monitoring-only mode runMutex sync.RWMutex ctx context.Context cancel context.CancelFunc // Metrics and monitoring stats *ArbitrageStats statsMutex sync.RWMutex // Database integration database ArbitrageDatabase } // SimpleSwapEvent represents a swap event for arbitrage detection type SimpleSwapEvent struct { TxHash common.Hash PoolAddress common.Address Token0 common.Address Token1 common.Address Amount0 *big.Int Amount1 *big.Int SqrtPriceX96 *big.Int Liquidity *big.Int Tick int32 BlockNumber uint64 LogIndex uint Timestamp time.Time } // SimplePoolData represents basic pool information type SimplePoolData struct { Address common.Address Token0 common.Address Token1 common.Address Fee int64 Liquidity *big.Int SqrtPriceX96 *big.Int Tick int32 BlockNumber uint64 TxHash common.Hash LogIndex uint LastUpdated time.Time } // NewArbitrageService creates a new sophisticated arbitrage service func NewArbitrageService( ctx context.Context, client *ethclient.Client, logger *logger.Logger, cfg *config.ArbitrageConfig, keyManager *security.KeyManager, database ArbitrageDatabase, poolDiscovery *pools.PoolDiscovery, tokenCache *tokens.MetadataCache, ) (*ArbitrageService, error) { serviceCtx, cancel := context.WithCancel(ctx) // Create multi-hop scanner with simple market manager multiHopScanner := NewMultiHopScanner(logger, client, nil) // Create arbitrage executor executor, err := NewArbitrageExecutor( client, logger, keyManager, common.HexToAddress(cfg.ArbitrageContractAddress), common.HexToAddress(cfg.FlashSwapContractAddress), ) if err != nil { cancel() return nil, fmt.Errorf("failed to create arbitrage executor: %w", err) } // Initialize comprehensive MEV architecture components logger.Info("🚀 Initializing comprehensive MEV bot architecture...") // NEW: Initialize exchange registry for all Arbitrum DEXs exchangeRegistry := exchanges.NewExchangeRegistry(client, logger) if err := exchangeRegistry.LoadArbitrumExchanges(); err != nil { logger.Warn(fmt.Sprintf("Failed to load some exchanges: %v", err)) } logger.Info("✅ Exchange registry initialized with all Arbitrum DEXs") // NEW: Create arbitrage calculator with gas estimator arbitrumClient := &arbitrum.ArbitrumClient{ Client: client, Logger: logger, ChainID: nil, } gasEstimator := arbitrum.NewL2GasEstimator(arbitrumClient, logger) arbitrageCalculator := math.NewArbitrageCalculator(gasEstimator) logger.Info("✅ Universal arbitrage calculator initialized") // NEW: Create detection engine // Create minimal detection config detectionConfig := DetectionConfig{ ScanInterval: time.Second * 5, // 5 seconds scan interval MaxConcurrentScans: 5, // 5 concurrent scans MaxConcurrentPaths: 10, // 10 concurrent path checks MinProfitThreshold: nil, // Will be set in the function MaxPriceImpact: nil, // Will be set in the function MaxHops: 3, // Max 3 hops in path HighPriorityTokens: []common.Address{}, // Empty for now EnabledExchanges: []math.ExchangeType{}, // Empty for now ExchangeWeights: map[math.ExchangeType]float64{}, // Empty for now CachePoolData: true, CacheTTL: 5 * time.Minute, BatchSize: 100, RequiredConfidence: 0.7, } detectionEngine := NewArbitrageDetectionEngine(exchangeRegistry, gasEstimator, logger, detectionConfig) logger.Info("✅ Real-time detection engine initialized") // NEW: Create flash swap executor // Create minimal execution config executionConfig := ExecutionConfig{ MaxSlippage: nil, // Will be set in the function MinProfitThreshold: nil, // Will be set in the function MaxPositionSize: nil, // Will be set in the function MaxDailyVolume: nil, // Will be set in the function GasLimitMultiplier: 1.2, // 20% buffer MaxGasPrice: nil, // Will be set in the function PriorityFeeStrategy: "competitive", // Competitive strategy ExecutionTimeout: 30 * time.Second, // 30 seconds timeout ConfirmationBlocks: 1, // 1 confirmation block RetryAttempts: 3, // 3 retry attempts RetryDelay: time.Second, // 1 second delay between retries EnableMEVProtection: true, // Enable MEV protection PrivateMempool: false, // Not using private mempool FlashbotsRelay: "", // Empty for now EnableDetailedLogs: true, // Enable detailed logs TrackPerformance: true, // Track performance } // SECURITY FIX (Phase 3): Pass real KeyManager and contract addresses instead of nil/zero values // Get contract addresses from config (with environment variable fallback) arbitrageContractAddr := common.HexToAddress(cfg.ArbitrageContractAddress) flashSwapContractAddr := common.HexToAddress(cfg.FlashSwapContractAddress) // If config addresses are zero, try environment variables as fallback if arbitrageContractAddr == (common.Address{}) { if envAddr := os.Getenv("CONTRACT_ARBITRAGE_EXECUTOR"); envAddr != "" { arbitrageContractAddr = common.HexToAddress(envAddr) } } if flashSwapContractAddr == (common.Address{}) { if envAddr := os.Getenv("CONTRACT_FLASH_SWAPPER"); envAddr != "" { flashSwapContractAddr = common.HexToAddress(envAddr) } } // SECURITY FIX (Phase 3): Validate dependencies before creating executors if keyManager == nil { logger.Warn("⚠️ KeyManager is nil - live execution will be disabled") } if arbitrageContractAddr == (common.Address{}) { logger.Warn("⚠️ Arbitrage contract address not configured - live execution will be disabled") } if flashSwapContractAddr == (common.Address{}) { logger.Warn("⚠️ Flash swap contract address not configured - live execution will be disabled") } // Pass real KeyManager from function parameter (not nil) flashExecutor := NewFlashSwapExecutor(client, logger, keyManager, gasEstimator, arbitrageContractAddr, flashSwapContractAddr, executionConfig) logger.Info("✅ Flash swap executor initialized with KeyManager and contract addresses") // NEW: Create live execution framework var liveFramework *LiveExecutionFramework if detectionEngine != nil && flashExecutor != nil { // Create minimal framework config frameworkConfig := FrameworkConfig{ DetectionConfig: DetectionConfig{}, // Will be initialized properly ExecutionConfig: ExecutionConfig{}, // Will be initialized properly MaxConcurrentExecutions: 5, // 5 concurrent executions DailyProfitTarget: nil, // Will be set in the function DailyLossLimit: nil, // Will be set in the function MaxPositionSize: nil, // Will be set in the function WorkerPoolSize: 10, // 10 worker pool size OpportunityQueueSize: 1000, // 1000 opportunity queue size ExecutionQueueSize: 100, // 100 execution queue size EmergencyStopEnabled: true, // Emergency stop enabled CircuitBreakerEnabled: true, // Circuit breaker enabled MaxFailureRate: 0.1, // 10% max failure rate HealthCheckInterval: 30 * time.Second, // 30 second health check interval } // SECURITY FIX (Phase 3): Pass real KeyManager and contract addresses // Use the same contract addresses as flash executor var err error // Validate critical dependencies for live mode if keyManager == nil || arbitrageContractAddr == (common.Address{}) || flashSwapContractAddr == (common.Address{}) { logger.Warn("⚠️ Missing dependencies for live framework - disabling live mode") logger.Info(" Required: KeyManager, arbitrage contract address, flash swap contract address") liveFramework = nil } else { liveFramework, err = NewLiveExecutionFramework(client, logger, keyManager, gasEstimator, arbitrageContractAddr, flashSwapContractAddr, frameworkConfig) if err != nil { logger.Warn(fmt.Sprintf("Failed to create live framework: %v", err)) liveFramework = nil } else { logger.Info("✅ Live execution framework initialized with KeyManager and contract addresses") } } } // Initialize legacy market manager with nil config for now var marketManager *market.MarketManager = nil logger.Info("Legacy market manager initialization deferred") // Initialize new market manager marketDataManagerConfig := &marketmanager.MarketManagerConfig{ VerificationWindow: 500 * time.Millisecond, MaxMarkets: 10000, } marketDataManager := marketmanager.NewMarketManager(marketDataManagerConfig) // Initialize stats stats := &ArbitrageStats{ TotalProfitRealized: big.NewInt(0), TotalGasSpent: big.NewInt(0), } service := &ArbitrageService{ client: client, logger: logger, config: cfg, keyManager: keyManager, multiHopScanner: multiHopScanner, executor: executor, exchangeRegistry: exchangeRegistry, arbitrageCalculator: arbitrageCalculator, detectionEngine: detectionEngine, flashExecutor: flashExecutor, liveFramework: liveFramework, marketManager: marketManager, marketDataManager: marketDataManager, poolDiscovery: poolDiscovery, // NEW: Pool discovery integration tokenMetadataCache: tokenCache, // NEW: Token metadata cache integration ctx: serviceCtx, cancel: cancel, stats: stats, database: database, tokenCache: make(map[common.Address]TokenPair), opportunityPathCache: make(map[string]*ArbitragePath), liveMode: liveFramework != nil, monitoringOnly: false, } detectionEngine.SetOpportunityHandler(service.handleDetectedOpportunity) return service, nil } // convertPoolDataToMarket converts existing PoolData to marketmanager.Market func (sas *ArbitrageService) convertPoolDataToMarket(poolData *market.PoolData, protocol string) *marketmanager.Market { // Create raw ticker from token addresses rawTicker := fmt.Sprintf("%s_%s", poolData.Token0.Hex(), poolData.Token1.Hex()) // Create ticker (using token symbols would require token registry) ticker := fmt.Sprintf("TOKEN0_TOKEN1") // Placeholder - would need token symbol lookup in real implementation // Convert uint256 values to big.Int/big.Float liquidity := new(big.Int) if poolData.Liquidity != nil { liquidity.Set(poolData.Liquidity.ToBig()) } sqrtPriceX96 := new(big.Int) if poolData.SqrtPriceX96 != nil { sqrtPriceX96.Set(poolData.SqrtPriceX96.ToBig()) } // Calculate approximate price from sqrtPriceX96 price := big.NewFloat(0) if sqrtPriceX96.Sign() > 0 { // Price = (sqrtPriceX96 / 2^96)^2 // Convert to big.Float for precision sqrtPriceFloat := new(big.Float).SetInt(sqrtPriceX96) q96 := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(2), big.NewInt(96), nil)) ratio := new(big.Float).Quo(sqrtPriceFloat, q96) price.Mul(ratio, ratio) } // Create market with converted data marketObj := marketmanager.NewMarket( common.HexToAddress("0x1F98431c8aD98523631AE4a59f267346ea31F984"), // Uniswap V3 Factory poolData.Address, poolData.Token0, poolData.Token1, uint32(poolData.Fee), ticker, rawTicker, protocol, ) // Update price and liquidity data marketObj.UpdatePriceData( price, liquidity, sqrtPriceX96, int32(poolData.Tick), ) // Update metadata marketObj.UpdateMetadata( time.Now().Unix(), 0, // Block number would need to be fetched common.Hash{}, // TxHash would need to be fetched marketmanager.StatusConfirmed, ) return marketObj } // convertMarketToPoolData converts marketmanager.Market to PoolData func (sas *ArbitrageService) convertMarketToPoolData(marketObj *marketmanager.Market) *market.PoolData { // Convert big.Int to uint256.Int liquidity := uint256.NewInt(0) if marketObj.Liquidity != nil { liquidity.SetFromBig(marketObj.Liquidity) } sqrtPriceX96 := uint256.NewInt(0) if marketObj.SqrtPriceX96 != nil { sqrtPriceX96.SetFromBig(marketObj.SqrtPriceX96) } // Create PoolData with converted values return &market.PoolData{ Address: marketObj.PoolAddress, Token0: marketObj.Token0, Token1: marketObj.Token1, Fee: int64(marketObj.Fee), Liquidity: liquidity, SqrtPriceX96: sqrtPriceX96, Tick: int(marketObj.Tick), TickSpacing: 60, // Default for 0.3% fee tier LastUpdated: time.Now(), } } // syncMarketData synchronizes market data between the two market managers // marketDataSyncer periodically syncs market data between managers func (sas *ArbitrageService) marketDataSyncer() { sas.logger.Info("Starting market data syncer...") ticker := time.NewTicker(10 * time.Second) // Sync every 10 seconds defer ticker.Stop() for { select { case <-sas.ctx.Done(): sas.logger.Info("Market data syncer stopped") return case <-ticker.C: sas.syncMarketData() // Example of how to use the new market manager for arbitrage detection // This would be integrated with the existing arbitrage detection logic sas.performAdvancedArbitrageDetection() } } } // performAdvancedArbitrageDetection uses the new market manager for enhanced arbitrage detection func (sas *ArbitrageService) performAdvancedArbitrageDetection() { // This would use the marketmanager's arbitrage detection capabilities // For example: // 1. Get markets from the new manager // 2. Use the marketmanager's arbitrage detector // 3. Convert results to the existing format // Example placeholder: sas.logger.Debug("Performing advanced arbitrage detection with new market manager") // In a real implementation, you would: // 1. Get relevant markets from marketDataManager // 2. Use marketmanager.NewArbitrageDetector() to create detector // 3. Call detector.DetectArbitrageOpportunities() with markets // 4. Convert opportunities to the existing format // 5. Process them with the existing execution logic } // Start begins the simplified arbitrage service func (sas *ArbitrageService) Start() error { sas.runMutex.Lock() defer sas.runMutex.Unlock() if sas.isRunning { return fmt.Errorf("arbitrage service is already running") } sas.logger.Info("Starting simplified arbitrage service...") // Start worker goroutines go sas.statsUpdater() go sas.blockchainMonitor() go sas.marketDataSyncer() // Start market data synchronization sas.isRunning = true sas.logger.Info("Simplified arbitrage service started successfully") return nil } // Stop stops the arbitrage service func (sas *ArbitrageService) Stop() error { sas.runMutex.Lock() defer sas.runMutex.Unlock() if !sas.isRunning { return nil } sas.logger.Info("Stopping simplified arbitrage service...") // Cancel context to stop all workers sas.cancel() sas.isRunning = false sas.logger.Info("Simplified arbitrage service stopped") return nil } // ProcessSwapEvent processes a swap event for arbitrage opportunities func (sas *ArbitrageService) ProcessSwapEvent(event *SimpleSwapEvent) error { sas.logger.Debug(fmt.Sprintf("Processing swap event: token0=%s, token1=%s, amount0=%s, amount1=%s", event.Token0.Hex(), event.Token1.Hex(), event.Amount0.String(), event.Amount1.String())) // Check if this swap is large enough to potentially move prices if !sas.isSignificantSwap(event) { return nil } // Scan for arbitrage opportunities return sas.detectArbitrageOpportunities(event) } // isSignificantSwap checks if a swap is large enough to create arbitrage opportunities func (sas *ArbitrageService) isSignificantSwap(event *SimpleSwapEvent) bool { // Convert amounts to absolute values for comparison amount0Abs := new(big.Int).Abs(event.Amount0) amount1Abs := new(big.Int).Abs(event.Amount1) // Check if either amount is above our threshold minSwapSize := big.NewInt(sas.config.MinSignificantSwapSize) return amount0Abs.Cmp(minSwapSize) > 0 || amount1Abs.Cmp(minSwapSize) > 0 } // detectArbitrageOpportunities scans for arbitrage opportunities triggered by an event func (sas *ArbitrageService) detectArbitrageOpportunities(event *SimpleSwapEvent) error { start := time.Now() // Determine the tokens involved in potential arbitrage tokens := []common.Address{event.Token0, event.Token1} var allOpportunities []*pkgtypes.ArbitrageOpportunity // Scan for opportunities starting with each token for _, token := range tokens { // Determine appropriate amount to use for scanning scanAmount := sas.calculateScanAmount(event, token) // Use multi-hop scanner to find arbitrage paths paths, err := sas.multiHopScanner.ScanForArbitrage(sas.ctx, token, scanAmount) if err != nil { sas.logger.Debug(fmt.Sprintf("Arbitrage scan failed for token %s: %v", token.Hex(), err)) continue } // Convert paths to opportunities for _, path := range paths { if !sas.isValidOpportunity(path) { continue } if len(path.Tokens) == 0 { continue } pathTokens := make([]string, len(path.Tokens)) for i, tokenAddr := range path.Tokens { pathTokens[i] = tokenAddr.Hex() } poolAddresses := make([]string, len(path.Pools)) for i, poolInfo := range path.Pools { poolAddresses[i] = poolInfo.Address.Hex() } opportunityID := sas.generateOpportunityID(path, event) amountCopy := new(big.Int).Set(scanAmount) estimatedProfit := new(big.Int).Set(path.NetProfit) opportunity := &pkgtypes.ArbitrageOpportunity{ ID: opportunityID, Path: pathTokens, Pools: poolAddresses, AmountIn: amountCopy, RequiredAmount: amountCopy, Profit: estimatedProfit, NetProfit: new(big.Int).Set(path.NetProfit), EstimatedProfit: estimatedProfit, DetectedAt: time.Now(), ExpiresAt: time.Now().Add(sas.config.OpportunityTTL), Timestamp: time.Now().Unix(), Urgency: sas.calculateUrgency(path), ROI: path.ROI, TokenIn: path.Tokens[0], TokenOut: path.Tokens[len(path.Tokens)-1], Confidence: 0.7, } sas.storeOpportunityPath(opportunityID, path) allOpportunities = append(allOpportunities, opportunity) } } // Sort opportunities by urgency and profit sas.rankOpportunities(allOpportunities) // Process top opportunities maxOpportunities := sas.config.MaxOpportunitiesPerEvent for i, opportunity := range allOpportunities { if i >= maxOpportunities { break } // Update stats // Atomic increment for thread safety - no lock needed atomic.AddInt64(&sas.stats.TotalOpportunitiesDetected, 1) // Save to database if err := sas.database.SaveOpportunity(sas.ctx, opportunity); err != nil { sas.logger.Warn(fmt.Sprintf("Failed to save opportunity to database: %v", err)) } // Execute if execution is enabled if sas.config.MaxConcurrentExecutions > 0 { go sas.executeOpportunity(opportunity) } } elapsed := time.Since(start) sas.logger.Debug(fmt.Sprintf("Arbitrage detection completed in %v: found %d opportunities", elapsed, len(allOpportunities))) return nil } // executeOpportunity executes a single arbitrage opportunity func (sas *ArbitrageService) handleDetectedOpportunity(opportunity *pkgtypes.ArbitrageOpportunity) { if opportunity == nil { return } go sas.executeOpportunity(opportunity) } func (sas *ArbitrageService) executeOpportunity(opportunity *pkgtypes.ArbitrageOpportunity) { // Check if opportunity is still valid if time.Now().After(opportunity.ExpiresAt) { sas.logger.Debug(fmt.Sprintf("Opportunity %s expired", opportunity.ID)) return } // Update stats // Atomic increment for thread safety - no lock needed atomic.AddInt64(&sas.stats.TotalOpportunitiesExecuted, 1) // Resolve the execution path associated with this opportunity executionPath := sas.getOpportunityPath(opportunity.ID) if executionPath == nil { executionPath = sas.fallbackPathFromOpportunity(opportunity) } if executionPath == nil { sas.logger.Warn(fmt.Sprintf("No execution path available for opportunity %s", opportunity.ID)) return } inputAmount := opportunity.RequiredAmount sas.deleteOpportunityPath(opportunity.ID) if inputAmount != nil { inputAmount = new(big.Int).Set(inputAmount) } // Prepare execution parameters params := &ArbitrageParams{ Path: executionPath, InputAmount: inputAmount, MinOutputAmount: sas.calculateMinOutput(opportunity), Deadline: big.NewInt(time.Now().Add(5 * time.Minute).Unix()), FlashSwapData: []byte{}, // Additional data if needed } var estimatedProfitDecimal *math.UniversalDecimal if opportunity.Quantities != nil { if gross, err := decimalAmountToUniversal(opportunity.Quantities.GrossProfit); err == nil { estimatedProfitDecimal = gross } } profitDisplay := ethAmountString(nil, estimatedProfitDecimal, opportunity.EstimatedProfit) sas.logger.Info(fmt.Sprintf("Executing arbitrage opportunity %s with estimated profit %s ETH", opportunity.ID, profitDisplay)) // Execute the arbitrage result, err := sas.executor.ExecuteArbitrage(sas.ctx, params) if err != nil { sas.logger.Error(fmt.Sprintf("Arbitrage execution failed for opportunity %s: %v", opportunity.ID, err)) return } // Process execution results sas.processExecutionResult(result) } // Helper methods from the original service func (sas *ArbitrageService) isValidOpportunity(path *ArbitragePath) bool { minProfit := big.NewInt(sas.config.MinProfitWei) if path.NetProfit.Cmp(minProfit) < 0 { return false } if path.ROI < sas.config.MinROIPercent { return false } if time.Since(path.LastUpdated) > sas.config.MaxPathAge { return false } currentGasPrice, err := sas.client.SuggestGasPrice(sas.ctx) if err != nil { currentGasPrice = big.NewInt(sas.config.MaxGasPriceWei) } return sas.executor.IsProfitableAfterGas(path, currentGasPrice) } func (sas *ArbitrageService) calculateScanAmount(event *SimpleSwapEvent, token common.Address) *big.Int { var swapAmount *big.Int if token == event.Token0 { swapAmount = new(big.Int).Abs(event.Amount0) } else { swapAmount = new(big.Int).Abs(event.Amount1) } scanAmount := new(big.Int).Div(swapAmount, big.NewInt(10)) minAmount := big.NewInt(sas.config.MinScanAmountWei) if scanAmount.Cmp(minAmount) < 0 { scanAmount = minAmount } maxAmount := big.NewInt(sas.config.MaxScanAmountWei) if scanAmount.Cmp(maxAmount) > 0 { scanAmount = maxAmount } return scanAmount } func (sas *ArbitrageService) calculateUrgency(path *ArbitragePath) int { urgency := int(path.ROI / 2) profitETH := new(big.Float).SetInt(path.NetProfit) profitETH.Quo(profitETH, big.NewFloat(1e18)) profitFloat, _ := profitETH.Float64() if profitFloat > 1.0 { urgency += 5 } else if profitFloat > 0.1 { urgency += 2 } if urgency < 1 { urgency = 1 } if urgency > 10 { urgency = 10 } return urgency } func (sas *ArbitrageService) storeOpportunityPath(id string, path *ArbitragePath) { if id == "" || path == nil { return } sas.opportunityPathMutex.Lock() sas.opportunityPathCache[id] = path sas.opportunityPathMutex.Unlock() } func (sas *ArbitrageService) getOpportunityPath(id string) *ArbitragePath { sas.opportunityPathMutex.RLock() defer sas.opportunityPathMutex.RUnlock() return sas.opportunityPathCache[id] } func (sas *ArbitrageService) deleteOpportunityPath(id string) { if id == "" { return } sas.opportunityPathMutex.Lock() delete(sas.opportunityPathCache, id) sas.opportunityPathMutex.Unlock() } func decimalAmountToUniversal(dec pkgtypes.DecimalAmount) (*math.UniversalDecimal, error) { if dec.Value == "" { return nil, fmt.Errorf("decimal amount empty") } value, ok := new(big.Int).SetString(dec.Value, 10) if !ok { return nil, fmt.Errorf("invalid decimal amount %s", dec.Value) } return math.NewUniversalDecimal(value, dec.Decimals, dec.Symbol) } func (sas *ArbitrageService) fallbackPathFromOpportunity(opportunity *pkgtypes.ArbitrageOpportunity) *ArbitragePath { if opportunity == nil { return nil } path := &ArbitragePath{ Tokens: make([]common.Address, len(opportunity.Path)), Pools: make([]*PoolInfo, 0, len(opportunity.Pools)), Protocols: make([]string, 0), Fees: make([]int64, 0), EstimatedGas: big.NewInt(0), NetProfit: big.NewInt(0), ROI: opportunity.ROI, LastUpdated: time.Now(), } if opportunity.NetProfit != nil { path.NetProfit = new(big.Int).Set(opportunity.NetProfit) } if opportunity.Quantities != nil { if net, err := decimalAmountToUniversal(opportunity.Quantities.NetProfit); err == nil { path.NetProfitDecimal = net } if gas, err := decimalAmountToUniversal(opportunity.Quantities.GasCost); err == nil { path.EstimatedGasDecimal = gas } if amt, err := decimalAmountToUniversal(opportunity.Quantities.AmountIn); err == nil { path.InputAmountDecimal = amt } } for i, tokenStr := range opportunity.Path { path.Tokens[i] = common.HexToAddress(tokenStr) } for _, poolStr := range opportunity.Pools { path.Pools = append(path.Pools, &PoolInfo{Address: common.HexToAddress(poolStr)}) } return path } func (sas *ArbitrageService) rankOpportunities(opportunities []*pkgtypes.ArbitrageOpportunity) { for i := 0; i < len(opportunities); i++ { for j := i + 1; j < len(opportunities); j++ { iOpp := opportunities[i] jOpp := opportunities[j] if jOpp.Urgency > iOpp.Urgency { opportunities[i], opportunities[j] = opportunities[j], opportunities[i] } else if jOpp.Urgency == iOpp.Urgency { if jOpp.EstimatedProfit.Cmp(iOpp.EstimatedProfit) > 0 { opportunities[i], opportunities[j] = opportunities[j], opportunities[i] } } } } } func (sas *ArbitrageService) calculateMinOutput(opportunity *pkgtypes.ArbitrageOpportunity) *big.Int { expectedOutput := new(big.Int).Add(opportunity.RequiredAmount, opportunity.EstimatedProfit) slippageTolerance := sas.config.SlippageTolerance slippageMultiplier := big.NewFloat(1.0 - slippageTolerance) expectedFloat := new(big.Float).SetInt(expectedOutput) minOutputFloat := new(big.Float).Mul(expectedFloat, slippageMultiplier) minOutput := new(big.Int) minOutputFloat.Int(minOutput) return minOutput } func (sas *ArbitrageService) processExecutionResult(result *ExecutionResult) { // Update statistics with proper synchronization if result.Success { // Atomic increment for success count - no lock needed atomic.AddInt64(&sas.stats.TotalSuccessfulExecutions, 1) } // Track execution time atomically if available if result.ExecutionTime > 0 { atomic.AddInt64(&sas.stats.TotalExecutionTimeNanos, result.ExecutionTime.Nanoseconds()) atomic.AddInt64(&sas.stats.ExecutionCount, 1) } // Update monetary stats with mutex protection (big.Int operations are not atomic) sas.statsMutex.Lock() if result.Success && result.ProfitRealized != nil { sas.stats.TotalProfitRealized.Add(sas.stats.TotalProfitRealized, result.ProfitRealized) } gasUsedBigInt := new(big.Int).SetUint64(result.GasUsed) gasCost := new(big.Int).Mul(result.GasPrice, gasUsedBigInt) sas.stats.TotalGasSpent.Add(sas.stats.TotalGasSpent, gasCost) sas.stats.LastExecutionTime = time.Now() // Calculate average execution time using atomic values executionCount := atomic.LoadInt64(&sas.stats.ExecutionCount) if executionCount > 0 { totalNanos := atomic.LoadInt64(&sas.stats.TotalExecutionTimeNanos) sas.stats.AverageExecutionTime = time.Duration(totalNanos / executionCount) } sas.statsMutex.Unlock() if err := sas.database.SaveExecution(sas.ctx, result); err != nil { sas.logger.Warn(fmt.Sprintf("Failed to save execution result to database: %v", err)) } if result.Success { profitDisplay := ethAmountString(nil, nil, result.ProfitRealized) sas.logger.Info(fmt.Sprintf("Arbitrage execution successful: TX %s, Profit: %s ETH, Gas: %d", result.TransactionHash.Hex(), profitDisplay, result.GasUsed)) } else { sas.logger.Error(fmt.Sprintf("Arbitrage execution failed: TX %s, Error: %v", result.TransactionHash.Hex(), result.Error)) } } func (sas *ArbitrageService) statsUpdater() { defer sas.logger.Info("Stats updater stopped") ticker := time.NewTicker(sas.config.StatsUpdateInterval) defer ticker.Stop() for { select { case <-sas.ctx.Done(): return case <-ticker.C: sas.logStats() } } } func (sas *ArbitrageService) logStats() { // Read atomic counters without locks detected := atomic.LoadInt64(&sas.stats.TotalOpportunitiesDetected) executed := atomic.LoadInt64(&sas.stats.TotalOpportunitiesExecuted) successful := atomic.LoadInt64(&sas.stats.TotalSuccessfulExecutions) // Read monetary stats with mutex protection sas.statsMutex.RLock() totalProfit := new(big.Int).Set(sas.stats.TotalProfitRealized) totalGas := new(big.Int).Set(sas.stats.TotalGasSpent) avgExecutionTime := sas.stats.AverageExecutionTime lastExecution := sas.stats.LastExecutionTime sas.statsMutex.RUnlock() // Calculate success rate successRate := 0.0 if executed > 0 { successRate = float64(successful) / float64(executed) * 100 } // Log comprehensive stats profitDisplay := ethAmountString(nil, nil, totalProfit) gasDisplay := ethAmountString(nil, nil, totalGas) sas.logger.Info(fmt.Sprintf("Arbitrage Service Stats - Detected: %d, Executed: %d, Successful: %d, "+ "Success Rate: %.2f%%, Total Profit: %s ETH, Total Gas: %s ETH, Avg Execution: %v, Last: %v", detected, executed, successful, successRate, profitDisplay, gasDisplay, avgExecutionTime, lastExecution.Format("15:04:05"))) } func (sas *ArbitrageService) generateOpportunityID(path *ArbitragePath, event *SimpleSwapEvent) string { return fmt.Sprintf("%s_%s_%d", event.TxHash.Hex()[:10], path.Tokens[0].Hex()[:8], time.Now().UnixNano()) } func (sas *ArbitrageService) GetStats() *ArbitrageStats { // Read atomic counters without locks detected := atomic.LoadInt64(&sas.stats.TotalOpportunitiesDetected) executed := atomic.LoadInt64(&sas.stats.TotalOpportunitiesExecuted) successful := atomic.LoadInt64(&sas.stats.TotalSuccessfulExecutions) totalNanos := atomic.LoadInt64(&sas.stats.TotalExecutionTimeNanos) executionCount := atomic.LoadInt64(&sas.stats.ExecutionCount) // Read monetary stats with mutex protection and create safe copy sas.statsMutex.RLock() statsCopy := &ArbitrageStats{ TotalOpportunitiesDetected: detected, TotalOpportunitiesExecuted: executed, TotalSuccessfulExecutions: successful, TotalExecutionTimeNanos: totalNanos, ExecutionCount: executionCount, TotalProfitRealized: new(big.Int).Set(sas.stats.TotalProfitRealized), TotalGasSpent: new(big.Int).Set(sas.stats.TotalGasSpent), AverageExecutionTime: sas.stats.AverageExecutionTime, LastExecutionTime: sas.stats.LastExecutionTime, } sas.statsMutex.RUnlock() return statsCopy } func (sas *ArbitrageService) IsRunning() bool { sas.runMutex.RLock() defer sas.runMutex.RUnlock() return sas.isRunning } // blockchainMonitor monitors the Arbitrum sequencer using the ORIGINAL ArbitrumMonitor with ArbitrumL2Parser func (sas *ArbitrageService) blockchainMonitor() { defer sas.logger.Info("💀 ARBITRUM SEQUENCER MONITOR STOPPED - Full sequencer reading terminated") sas.logger.Info("🚀 STARTING ARBITRUM SEQUENCER MONITOR FOR MEV OPPORTUNITIES") sas.logger.Info("🔧 Initializing complete Arbitrum L2 parser for FULL transaction analysis") sas.logger.Info("🎯 This is the ORIGINAL sequencer reader architecture - NOT simplified!") sas.logger.Info("📊 Full DEX transaction parsing, arbitrage detection, and market analysis enabled") // Create the proper Arbitrum monitor with sequencer reader using ORIGINAL architecture monitor, err := sas.createArbitrumMonitor() if err != nil { sas.logger.Error(fmt.Sprintf("❌ CRITICAL: Failed to create Arbitrum monitor: %v", err)) sas.logger.Error("❌ FALLBACK: Using basic block polling instead of proper sequencer reader") // Fallback to basic block monitoring sas.fallbackBlockPolling() return } sas.logger.Info("✅ ARBITRUM SEQUENCER MONITOR CREATED SUCCESSFULLY") sas.logger.Info("🔄 Starting to monitor Arbitrum sequencer feed for LIVE transactions...") sas.logger.Info("📡 Full L2 transaction parsing, DEX detection, and arbitrage scanning active") // Start the monitor with full logging if err := monitor.Start(sas.ctx); err != nil { sas.logger.Error(fmt.Sprintf("❌ CRITICAL: Failed to start Arbitrum monitor: %v", err)) sas.logger.Error("❌ EMERGENCY FALLBACK: Switching to basic block polling") sas.fallbackBlockPolling() return } sas.logger.Info("🎉 ARBITRUM SEQUENCER MONITORING STARTED - PROCESSING LIVE TRANSACTIONS") sas.logger.Info("📈 Real-time DEX transaction detection, arbitrage opportunities, and profit analysis active") sas.logger.Info("⚡ Full market pipeline, scanner, and MEV coordinator operational") // Keep the monitor running with status logging ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() for { select { case <-sas.ctx.Done(): sas.logger.Info("🛑 Context cancelled - stopping Arbitrum sequencer monitor...") return case <-ticker.C: sas.logger.Info("💓 Arbitrum sequencer monitor heartbeat - actively scanning for MEV opportunities") sas.logger.Info("📊 Monitor status: ACTIVE | Sequencer: CONNECTED | Parser: OPERATIONAL") } } } // fallbackBlockPolling provides fallback block monitoring through polling with EXTENSIVE LOGGING func (sas *ArbitrageService) fallbackBlockPolling() { sas.logger.Info("⚠️ USING FALLBACK BLOCK POLLING - This is NOT the proper sequencer reader!") sas.logger.Info("⚠️ This fallback method has limited transaction analysis capabilities") sas.logger.Info("⚠️ For full MEV detection, the proper ArbitrumMonitor with L2Parser should be used") ticker := time.NewTicker(3 * time.Second) // Poll every 3 seconds defer ticker.Stop() var lastBlock uint64 processedBlocks := 0 foundSwaps := 0 for { select { case <-sas.ctx.Done(): sas.logger.Info(fmt.Sprintf("🛑 Fallback block polling stopped. Processed %d blocks, found %d swaps", processedBlocks, foundSwaps)) return case <-ticker.C: header, err := sas.client.HeaderByNumber(sas.ctx, nil) if err != nil { sas.logger.Error(fmt.Sprintf("❌ Failed to get latest block: %v", err)) continue } if header.Number.Uint64() > lastBlock { lastBlock = header.Number.Uint64() processedBlocks++ sas.logger.Info(fmt.Sprintf("📦 Processing block %d (fallback mode) - total processed: %d", lastBlock, processedBlocks)) swapsFound := sas.processNewBlock(header) if swapsFound > 0 { foundSwaps += swapsFound sas.logger.Info(fmt.Sprintf("💰 Found %d swaps in block %d - total swaps found: %d", swapsFound, lastBlock, foundSwaps)) } } } } } // processNewBlock processes a new block looking for swap events with EXTENSIVE LOGGING func (sas *ArbitrageService) processNewBlock(header *types.Header) int { blockNumber := header.Number.Uint64() // Skip processing if block has no transactions if header.TxHash == (common.Hash{}) { sas.logger.Info(fmt.Sprintf("📦 Block %d: EMPTY BLOCK - no transactions to process", blockNumber)) return 0 } sas.logger.Info(fmt.Sprintf("🔍 PROCESSING BLOCK %d FOR UNISWAP V3 SWAP EVENTS", blockNumber)) sas.logger.Info(fmt.Sprintf("📊 Block %d: Hash=%s, Timestamp=%d", blockNumber, header.Hash().Hex(), header.Time)) // Instead of getting full block (which fails with unsupported tx types), // we'll scan the block's logs directly for Uniswap V3 Swap events swapEvents := sas.getSwapEventsFromBlock(blockNumber) if len(swapEvents) > 0 { sas.logger.Info(fmt.Sprintf("💰 FOUND %d SWAP EVENTS IN BLOCK %d - PROCESSING FOR ARBITRAGE", len(swapEvents), blockNumber)) // Process each swap event with detailed logging for i, event := range swapEvents { sas.logger.Info(fmt.Sprintf("🔄 Processing swap event %d/%d from block %d", i+1, len(swapEvents), blockNumber)) sas.logger.Info(fmt.Sprintf("💱 Swap details: Pool=%s, Amount0=%s, Amount1=%s", event.PoolAddress.Hex(), event.Amount0.String(), event.Amount1.String())) go func(e *SimpleSwapEvent, index int) { sas.logger.Info(fmt.Sprintf("⚡ Starting arbitrage analysis for swap %d from block %d", index+1, blockNumber)) if err := sas.ProcessSwapEvent(e); err != nil { sas.logger.Error(fmt.Sprintf("❌ Failed to process swap event %d: %v", index+1, err)) } else { sas.logger.Info(fmt.Sprintf("✅ Successfully processed swap event %d for arbitrage opportunities", index+1)) } }(event, i) } } else { sas.logger.Info(fmt.Sprintf("📦 Block %d: NO SWAP EVENTS FOUND - continuing to monitor", blockNumber)) } return len(swapEvents) } // processTransaction analyzes a transaction for swap events func (sas *ArbitrageService) processTransaction(tx *types.Transaction, blockNumber uint64) bool { // Get transaction receipt to access logs receipt, err := sas.client.TransactionReceipt(sas.ctx, tx.Hash()) if err != nil { return false // Skip if we can't get receipt } swapFound := false // Look for Uniswap V3 Swap events for _, log := range receipt.Logs { event := sas.parseSwapLog(log, tx, blockNumber) if event != nil { swapFound = true sas.logger.Info(fmt.Sprintf("Found swap event: %s/%s, amounts: %s/%s", event.Token0.Hex()[:10], event.Token1.Hex()[:10], event.Amount0.String(), event.Amount1.String())) // Process the swap event asynchronously to avoid blocking go func(e *SimpleSwapEvent) { if err := sas.ProcessSwapEvent(e); err != nil { sas.logger.Debug(fmt.Sprintf("Failed to process swap event: %v", err)) } }(event) } } return swapFound } // parseSwapLog attempts to parse a log as a Uniswap V3 Swap event func (sas *ArbitrageService) parseSwapLog(log *types.Log, tx *types.Transaction, blockNumber uint64) *SimpleSwapEvent { // Uniswap V3 Pool Swap event signature // Swap(indexed address sender, indexed address recipient, int256 amount0, int256 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick) swapEventSig := common.HexToHash("0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67") if len(log.Topics) == 0 || log.Topics[0] != swapEventSig { return nil } // Parse the event data if len(log.Topics) < 3 || len(log.Data) < 160 { // 5 * 32 bytes for amount0, amount1, sqrtPriceX96, liquidity, tick return nil } // Extract indexed parameters (sender, recipient) // sender := common.BytesToAddress(log.Topics[1].Bytes()) // recipient := common.BytesToAddress(log.Topics[2].Bytes()) // Extract non-indexed parameters from data // Parse signed amounts correctly (amount0 and amount1 are int256) amount0, err := sas.parseSignedInt256(log.Data[0:32]) if err != nil { return nil } amount1, err := sas.parseSignedInt256(log.Data[32:64]) if err != nil { return nil } sqrtPriceX96 := new(big.Int).SetBytes(log.Data[64:96]) liquidity := new(big.Int).SetBytes(log.Data[96:128]) // Extract tick (int24, but stored as int256) tick, err := sas.parseSignedInt24(log.Data[128:160]) if err != nil { return nil } // CRITICAL FIX: Validate pool address is not zero before processing if log.Address == (common.Address{}) { return nil // Skip events with zero pool addresses } // Get pool tokens by querying the actual pool contract token0, token1, err := sas.getPoolTokens(log.Address) if err != nil { return nil // Skip if we can't get pool tokens } // DEBUG: Log details about this swap event creation if log.Address == (common.Address{}) { sas.logger.Error(fmt.Sprintf("ZERO ADDRESS DEBUG [ARBITRAGE-1]: Creating SimpleSwapEvent with zero address - TxHash: %s, LogIndex: %d, BlockNumber: %d, LogTopics: %d, LogData: %d bytes", tx.Hash().Hex(), log.Index, log.BlockNumber, len(log.Topics), len(log.Data))) } return &SimpleSwapEvent{ TxHash: tx.Hash(), PoolAddress: log.Address, Token0: token0, Token1: token1, Amount0: amount0, Amount1: amount1, SqrtPriceX96: sqrtPriceX96, Liquidity: liquidity, Tick: tick, BlockNumber: blockNumber, LogIndex: log.Index, Timestamp: time.Now(), } } // parseSignedInt256 correctly parses a signed 256-bit integer from bytes func (sas *ArbitrageService) parseSignedInt256(data []byte) (*big.Int, error) { if len(data) != 32 { return nil, fmt.Errorf("invalid data length for int256: got %d, need 32", len(data)) } value := new(big.Int).SetBytes(data) // Check if the value is negative (MSB set) if len(data) > 0 && data[0]&0x80 != 0 { // Convert from two's complement // Subtract 2^256 to get the negative value maxUint256 := new(big.Int) maxUint256.Lsh(big.NewInt(1), 256) value.Sub(value, maxUint256) } return value, nil } // parseSignedInt24 correctly parses a signed 24-bit integer stored in a 32-byte field func (sas *ArbitrageService) parseSignedInt24(data []byte) (int32, error) { if len(data) != 32 { return 0, fmt.Errorf("invalid data length for int24: got %d, need 32", len(data)) } signByte := data[28] if signByte != 0x00 && signByte != 0xFF { return 0, fmt.Errorf("invalid sign extension byte 0x%02x for int24", signByte) } if signByte == 0x00 && data[29]&0x80 != 0 { return 0, fmt.Errorf("value uses more than 23 bits for positive int24") } if signByte == 0xFF && data[29]&0x80 == 0 { return 0, fmt.Errorf("value uses more than 23 bits for negative int24") } // Extract the last 4 bytes (since int24 is stored as int256) value := binary.BigEndian.Uint32(data[28:32]) // Convert to int24 by masking and sign-extending int24Value := int32(safeConvertUint32ToInt32(value & 0xFFFFFF)) // Mask to 24 bits // Check if negative (bit 23 set) if int24Value&0x800000 != 0 { // Sign extend to int32 int24Value |= ^0xFFFFFF // Set all bits above bit 23 to 1 for negative numbers } // Validate range for int24 if int24Value < -8388608 || int24Value > 8388607 { return 0, fmt.Errorf("value %d out of range for int24", int24Value) } return int24Value, nil } // getPoolTokens retrieves token addresses for a Uniswap V3 pool with caching func (sas *ArbitrageService) getPoolTokens(poolAddress common.Address) (token0, token1 common.Address, err error) { // Check cache first sas.tokenCacheMutex.RLock() if cached, exists := sas.tokenCache[poolAddress]; exists { sas.tokenCacheMutex.RUnlock() return cached.TokenA, cached.TokenB, nil } sas.tokenCacheMutex.RUnlock() // Create timeout context for contract calls ctx, cancel := context.WithTimeout(sas.ctx, 5*time.Second) defer cancel() // Pre-computed function selectors for token0() and token1() token0Selector := []byte{0x0d, 0xfe, 0x16, 0x81} // token0() token1Selector := []byte{0xd2, 0x1c, 0xec, 0xd4} // token1() // Call token0() function token0Data, err := sas.client.CallContract(ctx, ethereum.CallMsg{ To: &poolAddress, Data: token0Selector, }, nil) if err != nil { return common.Address{}, common.Address{}, fmt.Errorf("failed to call token0(): %w", err) } // Call token1() function token1Data, err := sas.client.CallContract(ctx, ethereum.CallMsg{ To: &poolAddress, Data: token1Selector, }, nil) if err != nil { return common.Address{}, common.Address{}, fmt.Errorf("failed to call token1(): %w", err) } // Parse the results if len(token0Data) < 32 || len(token1Data) < 32 { return common.Address{}, common.Address{}, fmt.Errorf("invalid token data length") } token0 = common.BytesToAddress(token0Data[12:32]) token1 = common.BytesToAddress(token1Data[12:32]) // Cache the result sas.tokenCacheMutex.Lock() sas.tokenCache[poolAddress] = TokenPair{TokenA: token0, TokenB: token1} sas.tokenCacheMutex.Unlock() return token0, token1, nil } // getSwapEventsFromBlock retrieves Uniswap V3 swap events from a specific block using log filtering func (sas *ArbitrageService) getSwapEventsFromBlock(blockNumber uint64) []*SimpleSwapEvent { // Uniswap V3 Pool Swap event signature swapEventSig := common.HexToHash("0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67") // Create filter query for this specific block blockNumberBigInt := new(big.Int).SetUint64(blockNumber) query := ethereum.FilterQuery{ FromBlock: blockNumberBigInt, ToBlock: blockNumberBigInt, Topics: [][]common.Hash{{swapEventSig}}, } // Get logs for this block logs, err := sas.client.FilterLogs(sas.ctx, query) if err != nil { sas.logger.Debug(fmt.Sprintf("Failed to get logs for block %d: %v", blockNumber, err)) return nil } // Debug: Log how many logs we found for this block if len(logs) > 0 { sas.logger.Info(fmt.Sprintf("Found %d potential swap logs in block %d", len(logs), blockNumber)) } var swapEvents []*SimpleSwapEvent // Parse each log into a swap event for _, log := range logs { event := sas.parseSwapEvent(log, blockNumber) if event != nil { swapEvents = append(swapEvents, event) sas.logger.Info(fmt.Sprintf("Successfully parsed swap event: pool=%s, amount0=%s, amount1=%s", event.PoolAddress.Hex(), event.Amount0.String(), event.Amount1.String())) } else { sas.logger.Debug(fmt.Sprintf("Failed to parse swap log from pool %s", log.Address.Hex())) } } return swapEvents } // parseSwapEvent parses a log entry into a SimpleSwapEvent // createArbitrumMonitor creates the ORIGINAL ArbitrumMonitor with full sequencer reading capabilities func (sas *ArbitrageService) createArbitrumMonitor() (*monitor.ArbitrumMonitor, error) { sas.logger.Info("🏗️ CREATING ORIGINAL ARBITRUM MONITOR WITH FULL SEQUENCER READER") sas.logger.Info("🔧 This will use ArbitrumL2Parser for proper transaction analysis") sas.logger.Info("📡 Full MEV detection, market analysis, and arbitrage scanning enabled") // Get RPC endpoints from environment variables rpcEndpoint := os.Getenv("ARBITRUM_RPC_ENDPOINT") if rpcEndpoint == "" { return nil, fmt.Errorf("ARBITRUM_RPC_ENDPOINT environment variable is required") } wsEndpoint := os.Getenv("ARBITRUM_WS_ENDPOINT") if wsEndpoint == "" { wsEndpoint = rpcEndpoint // Fallback to RPC endpoint } // Create Arbitrum configuration from environment arbConfig := &config.ArbitrumConfig{ RPCEndpoint: rpcEndpoint, WSEndpoint: wsEndpoint, ChainID: 42161, RateLimit: config.RateLimitConfig{ RequestsPerSecond: 100, Burst: 200, }, } // Create bot configuration botConfig := &config.BotConfig{ PollingInterval: 1, // 1 second polling MaxWorkers: 10, ChannelBufferSize: 100, RPCTimeout: 30, MinProfitThreshold: 0.01, // 1% minimum profit GasPriceMultiplier: 1.2, // 20% gas price premium Enabled: true, } sas.logger.Info(fmt.Sprintf("📊 Arbitrum config: RPC=%s, ChainID=%d", arbConfig.RPCEndpoint, arbConfig.ChainID)) // Create rate limiter manager rateLimiter := ratelimit.NewLimiterManager(arbConfig) sas.logger.Info("⚡ Rate limiter manager created for RPC throttling") // Price oracle will be added later when needed sas.logger.Info("💰 Price oracle support ready") // Create market manager for pool management uniswapConfig := &config.UniswapConfig{ FactoryAddress: "0x1F98431c8aD98523631AE4a59f267346ea31F984", // Uniswap V3 Factory PositionManagerAddress: "0xC36442b4a4522E871399CD717aBDD847Ab11FE88", // Uniswap V3 Position Manager FeeTiers: []int64{100, 500, 3000, 10000}, // 0.01%, 0.05%, 0.3%, 1% Cache: config.CacheConfig{ Enabled: true, Expiration: 300, // 5 minutes MaxSize: 1000, }, } marketManager := market.NewMarketManager(uniswapConfig, sas.logger) sas.logger.Info("🏪 Market manager created for pool data management") // Create a proper config.Config for ContractExecutor cfg := &config.Config{ Arbitrum: *arbConfig, Contracts: config.ContractsConfig{ ArbitrageExecutor: sas.config.ArbitrageContractAddress, FlashSwapper: sas.config.FlashSwapContractAddress, }, } // Create ContractExecutor contractExecutor, err := contracts.NewContractExecutor(cfg, sas.logger, sas.keyManager) if err != nil { return nil, fmt.Errorf("failed to create contract executor: %w", err) } // Create market scanner for arbitrage detection marketScanner := scanner.NewScanner(botConfig, sas.logger, contractExecutor, nil, nil) // No reserve cache in basic service sas.logger.Info("🔍 Market scanner created for arbitrage opportunity detection") // ✅ CRITICAL FIX: Set opportunity forwarder to route opportunities through multi-hop scanner bridgeExecutor := parser.NewExecutor(sas, sas.logger) marketScanner.GetMarketScanner().SetOpportunityForwarder(bridgeExecutor) sas.logger.Info("✅ Market scanner configured to forward opportunities to multi-hop arbitrage service") // Create the ORIGINAL ArbitrumMonitor sas.logger.Info("🚀 Creating ArbitrumMonitor with full sequencer reading capabilities...") monitor, err := monitor.NewArbitrumMonitor( arbConfig, botConfig, sas.logger, rateLimiter, marketManager, marketScanner, ) if err != nil { return nil, fmt.Errorf("failed to create ArbitrumMonitor: %w", err) } monitor.SetOpportunityExecutor(bridgeExecutor) sas.logger.Info("✅ ORIGINAL ARBITRUM MONITOR CREATED SUCCESSFULLY") sas.logger.Info("🎯 Full sequencer reader with ArbitrumL2Parser operational") sas.logger.Info("💡 DEX transaction parsing, MEV coordinator, and market pipeline active") sas.logger.Info("📈 Real-time arbitrage detection and profit analysis enabled") return monitor, nil } func (sas *ArbitrageService) parseSwapEvent(log types.Log, blockNumber uint64) *SimpleSwapEvent { // Validate log structure if len(log.Topics) < 3 || len(log.Data) < 160 { // 5 * 32 bytes for amount0, amount1, sqrtPriceX96, liquidity, tick sas.logger.Debug(fmt.Sprintf("Invalid log structure: topics=%d, data_len=%d", len(log.Topics), len(log.Data))) return nil } // Extract non-indexed parameters from data // Parse signed amounts correctly (amount0 and amount1 are int256) amount0, err := sas.parseSignedInt256(log.Data[0:32]) if err != nil { sas.logger.Debug(fmt.Sprintf("Failed to parse amount0: %v", err)) return nil } amount1, err := sas.parseSignedInt256(log.Data[32:64]) if err != nil { sas.logger.Debug(fmt.Sprintf("Failed to parse amount1: %v", err)) return nil } sqrtPriceX96 := new(big.Int).SetBytes(log.Data[64:96]) liquidity := new(big.Int).SetBytes(log.Data[96:128]) // Extract tick (int24, but stored as int256) tick, err := sas.parseSignedInt24(log.Data[128:160]) if err != nil { sas.logger.Debug(fmt.Sprintf("Failed to parse tick: %v", err)) return nil } // CRITICAL FIX: Validate pool address is not zero before processing if log.Address == (common.Address{}) { return nil // Skip events with zero pool addresses } // Get pool tokens by querying the actual pool contract token0, token1, err := sas.getPoolTokens(log.Address) if err != nil { sas.logger.Error(fmt.Sprintf("Failed to get tokens for pool %s: %v", log.Address.Hex(), err)) return nil // Skip if we can't get pool tokens } sas.logger.Debug(fmt.Sprintf("Successfully got pool tokens: %s/%s for pool %s", token0.Hex(), token1.Hex(), log.Address.Hex())) // DEBUG: Log details about this swap event creation if log.Address == (common.Address{}) { sas.logger.Error(fmt.Sprintf("ZERO ADDRESS DEBUG [ARBITRAGE-2]: Creating SimpleSwapEvent with zero address - TxHash: %s, LogIndex: %d, BlockNumber: %d, LogTopics: %d, LogData: %d bytes", log.TxHash.Hex(), log.Index, log.BlockNumber, len(log.Topics), len(log.Data))) } return &SimpleSwapEvent{ TxHash: log.TxHash, PoolAddress: log.Address, Token0: token0, Token1: token1, Amount0: amount0, Amount1: amount1, SqrtPriceX96: sqrtPriceX96, Liquidity: liquidity, Tick: tick, BlockNumber: blockNumber, LogIndex: log.Index, Timestamp: time.Now(), } } // syncMarketData synchronizes market data between the two market managers func (sas *ArbitrageService) syncMarketData() { sas.logger.Debug("Syncing market data between managers") // Example of how to synchronize market data // In a real implementation, you would iterate through pools from the existing manager // and convert/add them to the new manager // This is a placeholder showing the pattern: // 1. Get pool data from existing manager // 2. Convert to marketmanager format // 3. Add to new manager // Example: // poolAddress := common.HexToAddress("0x...") // Some pool address // poolData, err := sas.marketManager.GetPool(sas.ctx, poolAddress) // if err == nil { // marketObj := sas.convertPoolDataToMarket(poolData, "UniswapV3") // if err := sas.marketDataManager.AddMarket(marketObj); err != nil { // sas.logger.Warn("Failed to add market to manager: ", err) // } // } sas.logger.Debug("Market data sync completed") } // SubmitBridgeOpportunity accepts arbitrage opportunities from the transaction analyzer bridge func (sas *ArbitrageService) SubmitBridgeOpportunity(ctx context.Context, bridgeOpportunity interface{}) error { if bridgeOpportunity == nil { return fmt.Errorf("bridge opportunity cannot be nil") } opp, ok := bridgeOpportunity.(*pkgtypes.ArbitrageOpportunity) if !ok { return fmt.Errorf("unsupported bridge opportunity type %T", bridgeOpportunity) } now := time.Now() if opp.DetectedAt.IsZero() { opp.DetectedAt = now } if opp.Timestamp == 0 { opp.Timestamp = now.Unix() } if opp.ExpiresAt.IsZero() { ttl := sas.config.OpportunityTTL if ttl == 0 { ttl = 30 * time.Second } opp.ExpiresAt = opp.DetectedAt.Add(ttl) } if opp.RequiredAmount == nil && opp.AmountIn != nil { opp.RequiredAmount = new(big.Int).Set(opp.AmountIn) } if opp.ID == "" { opp.ID = fmt.Sprintf("bridge-%s-%d", opp.TokenIn.Hex(), now.UnixNano()) } sas.logger.Info("📥 Received bridge arbitrage opportunity - analyzing with multi-hop scanner", "id", opp.ID, "path_length", len(opp.Path), "pools", len(opp.Pools), ) // ✅ CRITICAL FIX: Use multi-hop scanner to find REAL arbitrage paths // Instead of accepting the single-pool opportunity as-is, scan for multi-hop paths var tokensToScan []common.Address if opp.TokenIn != (common.Address{}) { tokensToScan = append(tokensToScan, opp.TokenIn) } if opp.TokenOut != (common.Address{}) && opp.TokenOut != opp.TokenIn { tokensToScan = append(tokensToScan, opp.TokenOut) } // If we have tokens, use multi-hop scanner to find profitable paths if len(tokensToScan) > 0 && sas.multiHopScanner != nil { scanAmount := opp.AmountIn if scanAmount == nil || scanAmount.Sign() == 0 { scanAmount = big.NewInt(100000000000000000) // Default 0.1 ETH scan amount } sas.logger.Info("🔍 Scanning for multi-hop arbitrage paths", "tokens", len(tokensToScan), "scanAmount", scanAmount.String(), ) for _, token := range tokensToScan { paths, err := sas.multiHopScanner.ScanForArbitrage(sas.ctx, token, scanAmount) if err != nil { sas.logger.Debug(fmt.Sprintf("Multi-hop scan failed for token %s: %v", token.Hex(), err)) continue } if len(paths) > 0 { sas.logger.Info(fmt.Sprintf("✅ Found %d multi-hop arbitrage paths!", len(paths))) // Create and execute opportunities for each profitable path for _, path := range paths { if path.NetProfit.Sign() <= 0 { continue // Skip unprofitable paths } pathTokens := make([]string, len(path.Tokens)) for i, tokenAddr := range path.Tokens { pathTokens[i] = tokenAddr.Hex() } poolAddresses := make([]string, len(path.Pools)) for i, poolInfo := range path.Pools { poolAddresses[i] = poolInfo.Address.Hex() } multiHopOpp := &pkgtypes.ArbitrageOpportunity{ ID: fmt.Sprintf("multihop-%s-%d", token.Hex(), now.UnixNano()), Path: pathTokens, Pools: poolAddresses, AmountIn: new(big.Int).Set(scanAmount), RequiredAmount: new(big.Int).Set(scanAmount), Profit: new(big.Int).Set(path.NetProfit), NetProfit: new(big.Int).Set(path.NetProfit), EstimatedProfit: new(big.Int).Set(path.NetProfit), DetectedAt: now, ExpiresAt: now.Add(sas.config.OpportunityTTL), Timestamp: now.Unix(), Urgency: sas.calculateUrgency(path), ROI: path.ROI, TokenIn: path.Tokens[0], TokenOut: path.Tokens[len(path.Tokens)-1], Confidence: 0.7, } // Store path sas.storeOpportunityPath(multiHopOpp.ID, path) // Save to database if sas.database != nil { if err := sas.database.SaveOpportunity(sas.ctx, multiHopOpp); err != nil { sas.logger.Warn("Failed to save multi-hop opportunity", "error", err) } } // Increment stats atomic.AddInt64(&sas.stats.TotalOpportunitiesDetected, 1) // Execute opportunity sas.logger.Info(fmt.Sprintf("🚀 Executing multi-hop opportunity: profit=%.6f ETH, ROI=%.2f%%", float64(path.NetProfit.Int64())/1e18, path.ROI*100)) go sas.executeOpportunity(multiHopOpp) } return nil // Successfully processed multi-hop paths } } } // Fallback: If multi-hop scan didn't find anything, process original opportunity sas.logger.Debug("No multi-hop paths found, processing original single-pool opportunity") if path := sas.fallbackPathFromOpportunity(opp); path != nil { sas.storeOpportunityPath(opp.ID, path) } saveCtx := ctx if saveCtx == nil { saveCtx = sas.ctx } if saveCtx == nil { saveCtx = context.Background() } if sas.database != nil { if err := sas.database.SaveOpportunity(saveCtx, opp); err != nil { sas.logger.Warn("Failed to persist bridge opportunity", "id", opp.ID, "error", err) } } atomic.AddInt64(&sas.stats.TotalOpportunitiesDetected, 1) // Only execute if it has positive profit if opp.NetProfit != nil && opp.NetProfit.Sign() > 0 { go sas.executeOpportunity(opp) } else { sas.logger.Debug("Skipping execution of zero-profit opportunity", "id", opp.ID) } return nil } // StartLiveMode starts the comprehensive live execution framework func (sas *ArbitrageService) StartLiveMode(ctx context.Context) error { sas.runMutex.Lock() defer sas.runMutex.Unlock() if !sas.liveMode { return fmt.Errorf("live framework not available - service running in legacy mode") } if sas.isRunning { return fmt.Errorf("service already running") } sas.logger.Info("🚀 Starting MEV bot in LIVE EXECUTION MODE...") sas.logger.Info("⚡ Comprehensive arbitrage detection and execution enabled") // Start the live execution framework if err := sas.liveFramework.Start(ctx); err != nil { return fmt.Errorf("failed to start live framework: %w", err) } // Start legacy monitoring components go sas.statsUpdater() go sas.blockchainMonitor() go sas.marketDataSyncer() sas.isRunning = true sas.logger.Info("✅ MEV bot started in live execution mode") return nil } // StartMonitoringMode starts the service in monitoring-only mode func (sas *ArbitrageService) StartMonitoringMode() error { sas.runMutex.Lock() defer sas.runMutex.Unlock() if sas.isRunning { return fmt.Errorf("service already running") } sas.logger.Info("👁️ Starting MEV bot in MONITORING-ONLY MODE...") sas.logger.Info("📊 Detection and analysis enabled, execution disabled") sas.monitoringOnly = true // Start monitoring components only go sas.statsUpdater() go sas.blockchainMonitor() go sas.marketDataSyncer() if sas.liveMode && sas.liveFramework != nil { // Start live framework in monitoring mode sas.liveFramework.SetMonitoringMode(true) if err := sas.liveFramework.Start(sas.ctx); err != nil { sas.logger.Warn(fmt.Sprintf("Failed to start live framework in monitoring mode: %v", err)) } } sas.isRunning = true sas.logger.Info("✅ MEV bot started in monitoring-only mode") return nil } // ScanTokenPairs scans for arbitrage opportunities between specific token pairs func (sas *ArbitrageService) ScanTokenPairs(ctx context.Context, pairs []TokenPair, amount *math.UniversalDecimal) ([]*pkgtypes.ArbitrageOpportunity, error) { if !sas.liveMode || sas.detectionEngine == nil { return nil, fmt.Errorf("comprehensive detection engine not available") } sas.logger.Info(fmt.Sprintf("🔍 Scanning %d token pairs for arbitrage opportunities...", len(pairs))) var allOpportunities []*pkgtypes.ArbitrageOpportunity for _, pair := range pairs { // Calculate optimal path between tokens opportunity, err := sas.arbitrageCalculator.FindOptimalPath(ctx, pair.TokenA, pair.TokenB, amount) if err != nil { sas.logger.Debug(fmt.Sprintf("No opportunity found for %s/%s: %v", pair.TokenA.Hex()[:8], pair.TokenB.Hex()[:8], err)) continue } if opportunity.NetProfit.Cmp(big.NewInt(sas.config.MinProfitWei)) > 0 { allOpportunities = append(allOpportunities, opportunity) } } sas.logger.Info(fmt.Sprintf("💎 Found %d profitable arbitrage opportunities", len(allOpportunities))) return allOpportunities, nil } // ExecuteOpportunityLive executes an opportunity using the live framework func (sas *ArbitrageService) ExecuteOpportunityLive(ctx context.Context, opportunity *pkgtypes.ArbitrageOpportunity) (*ExecutionResult, error) { if sas.monitoringOnly { return nil, fmt.Errorf("execution disabled - running in monitoring-only mode") } if !sas.liveMode || sas.liveFramework == nil { return nil, fmt.Errorf("live execution framework not available") } sas.logger.Info(fmt.Sprintf("⚡ Executing opportunity via live framework - expected profit: %s", opportunity.NetProfit.String())) // Create execution task task := &ExecutionTask{ Opportunity: opportunity, Priority: calculatePriority(opportunity), Deadline: time.Now().Add(30 * time.Second), } // Execute via live framework return sas.liveFramework.ExecuteOpportunity(ctx, task) } // GetLiveMetrics returns comprehensive metrics from all components func (sas *ArbitrageService) GetLiveMetrics() (*ComprehensiveMetrics, error) { metrics := &ComprehensiveMetrics{ ServiceStats: sas.GetStats(), LegacyMode: !sas.liveMode, LiveMode: sas.liveMode, MonitoringOnly: sas.monitoringOnly, } if sas.liveMode && sas.liveFramework != nil { liveMetrics := sas.liveFramework.GetMetrics() metrics.LiveMetrics = liveMetrics } if sas.exchangeRegistry != nil { metrics.SupportedExchanges = len(sas.exchangeRegistry.GetAllExchanges()) } return metrics, nil } // GetSupportedTokenPairs returns token pairs supported across all exchanges func (sas *ArbitrageService) GetSupportedTokenPairs() ([]TokenPair, error) { if sas.exchangeRegistry == nil { return nil, fmt.Errorf("exchange registry not available") } // Get common token pairs across exchanges exchanges := sas.exchangeRegistry.GetAllExchanges() var pairs []TokenPair // Add major pairs (would be enhanced with actual token registry) commonTokens := []common.Address{ common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), // WETH common.HexToAddress("0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9"), // USDT common.HexToAddress("0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8"), // USDC common.HexToAddress("0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f"), // WBTC common.HexToAddress("0x912CE59144191C1204E64559FE8253a0e49E6548"), // ARB } // Create pairs from common tokens for i, token0 := range commonTokens { for j, token1 := range commonTokens { if i != j { pairs = append(pairs, TokenPair{TokenA: token0, TokenB: token1}) } } } sas.logger.Info(fmt.Sprintf("📋 Found %d supported token pairs across %d exchanges", len(pairs), len(exchanges))) return pairs, nil } // ComprehensiveMetrics contains metrics from all service components type ComprehensiveMetrics struct { ServiceStats *ArbitrageStats LiveMetrics *LiveExecutionMetrics SupportedExchanges int LegacyMode bool LiveMode bool MonitoringOnly bool }