package arbitrage import ( "context" "fmt" "math/big" "sync" "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/fraktal/mev-beta/internal/config" "github.com/fraktal/mev-beta/internal/logger" "github.com/fraktal/mev-beta/pkg/security" ) // TokenPair represents the two tokens in a pool type TokenPair struct { Token0 common.Address Token1 common.Address } // ArbitrageOpportunity represents a detected arbitrage opportunity type ArbitrageOpportunity struct { ID string Path *ArbitragePath TriggerEvent *SimpleSwapEvent DetectedAt time.Time EstimatedProfit *big.Int RequiredAmount *big.Int Urgency int // 1-10 priority level ExpiresAt time.Time } // ArbitrageStats contains service statistics type ArbitrageStats struct { TotalOpportunitiesDetected int64 TotalOpportunitiesExecuted int64 TotalSuccessfulExecutions int64 TotalProfitRealized *big.Int TotalGasSpent *big.Int AverageExecutionTime time.Duration LastExecutionTime time.Time } // ArbitrageDatabase interface for persistence type ArbitrageDatabase interface { SaveOpportunity(ctx context.Context, opportunity *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) } // SimpleArbitrageService is a simplified arbitrage service without circular dependencies type SimpleArbitrageService struct { client *ethclient.Client logger *logger.Logger config *config.ArbitrageConfig // Core components multiHopScanner *MultiHopScanner executor *ArbitrageExecutor // Token cache for pool addresses tokenCache map[common.Address]TokenPair tokenCacheMutex sync.RWMutex // State management isRunning bool 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 } // NewSimpleArbitrageService creates a new simplified arbitrage service func NewSimpleArbitrageService( client *ethclient.Client, logger *logger.Logger, config *config.ArbitrageConfig, keyManager *security.KeyManager, database ArbitrageDatabase, ) (*SimpleArbitrageService, error) { ctx, cancel := context.WithCancel(context.Background()) // Create multi-hop scanner with simple market manager multiHopScanner := NewMultiHopScanner(logger, nil) // Create arbitrage executor executor, err := NewArbitrageExecutor( client, logger, keyManager, common.HexToAddress(config.ArbitrageContractAddress), common.HexToAddress(config.FlashSwapContractAddress), ) if err != nil { cancel() return nil, fmt.Errorf("failed to create arbitrage executor: %w", err) } // Initialize stats stats := &ArbitrageStats{ TotalProfitRealized: big.NewInt(0), TotalGasSpent: big.NewInt(0), } service := &SimpleArbitrageService{ client: client, logger: logger, config: config, multiHopScanner: multiHopScanner, executor: executor, ctx: ctx, cancel: cancel, stats: stats, database: database, tokenCache: make(map[common.Address]TokenPair), } return service, nil } // Start begins the simplified arbitrage service func (sas *SimpleArbitrageService) 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() sas.isRunning = true sas.logger.Info("Simplified arbitrage service started successfully") return nil } // Stop stops the arbitrage service func (sas *SimpleArbitrageService) 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 *SimpleArbitrageService) 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 *SimpleArbitrageService) 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 *SimpleArbitrageService) detectArbitrageOpportunities(event *SimpleSwapEvent) error { start := time.Now() // Determine the tokens involved in potential arbitrage tokens := []common.Address{event.Token0, event.Token1} var allOpportunities []*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) { opportunity := &ArbitrageOpportunity{ ID: sas.generateOpportunityID(path, event), Path: path, DetectedAt: time.Now(), EstimatedProfit: path.NetProfit, RequiredAmount: scanAmount, Urgency: sas.calculateUrgency(path), ExpiresAt: time.Now().Add(sas.config.OpportunityTTL), } 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 sas.statsMutex.Lock() sas.stats.TotalOpportunitiesDetected++ sas.statsMutex.Unlock() // 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 *SimpleArbitrageService) executeOpportunity(opportunity *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 sas.statsMutex.Lock() sas.stats.TotalOpportunitiesExecuted++ sas.statsMutex.Unlock() // Prepare execution parameters params := &ArbitrageParams{ Path: opportunity.Path, InputAmount: opportunity.RequiredAmount, MinOutputAmount: sas.calculateMinOutput(opportunity), Deadline: big.NewInt(time.Now().Add(5 * time.Minute).Unix()), FlashSwapData: []byte{}, // Additional data if needed } sas.logger.Info(fmt.Sprintf("Executing arbitrage opportunity %s with estimated profit %s ETH", opportunity.ID, formatEther(opportunity.EstimatedProfit))) // 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 *SimpleArbitrageService) 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 *SimpleArbitrageService) 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 *SimpleArbitrageService) 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 *SimpleArbitrageService) rankOpportunities(opportunities []*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 *SimpleArbitrageService) calculateMinOutput(opportunity *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 *SimpleArbitrageService) processExecutionResult(result *ExecutionResult) { sas.statsMutex.Lock() if result.Success { sas.stats.TotalSuccessfulExecutions++ sas.stats.TotalProfitRealized.Add(sas.stats.TotalProfitRealized, result.ProfitRealized) } gasCost := new(big.Int).Mul(result.GasPrice, big.NewInt(int64(result.GasUsed))) sas.stats.TotalGasSpent.Add(sas.stats.TotalGasSpent, gasCost) sas.stats.LastExecutionTime = time.Now() 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 { sas.logger.Info(fmt.Sprintf("Arbitrage execution successful: TX %s, Profit: %s ETH, Gas: %d", result.TransactionHash.Hex(), formatEther(result.ProfitRealized), result.GasUsed)) } else { sas.logger.Error(fmt.Sprintf("Arbitrage execution failed: TX %s, Error: %v", result.TransactionHash.Hex(), result.Error)) } } func (sas *SimpleArbitrageService) 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 *SimpleArbitrageService) logStats() { sas.statsMutex.RLock() stats := *sas.stats sas.statsMutex.RUnlock() successRate := 0.0 if stats.TotalOpportunitiesExecuted > 0 { successRate = float64(stats.TotalSuccessfulExecutions) / float64(stats.TotalOpportunitiesExecuted) * 100 } sas.logger.Info(fmt.Sprintf("Arbitrage Service Stats - Detected: %d, Executed: %d, Success Rate: %.2f%%, "+ "Total Profit: %s ETH, Total Gas: %s ETH", stats.TotalOpportunitiesDetected, stats.TotalOpportunitiesExecuted, successRate, formatEther(stats.TotalProfitRealized), formatEther(stats.TotalGasSpent))) } func (sas *SimpleArbitrageService) 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 *SimpleArbitrageService) GetStats() *ArbitrageStats { sas.statsMutex.RLock() defer sas.statsMutex.RUnlock() statsCopy := *sas.stats return &statsCopy } func (sas *SimpleArbitrageService) IsRunning() bool { sas.runMutex.RLock() defer sas.runMutex.RUnlock() return sas.isRunning } // blockchainMonitor monitors the Arbitrum sequencer using the proper ArbitrumMonitor func (sas *SimpleArbitrageService) blockchainMonitor() { defer sas.logger.Info("Arbitrum sequencer monitor stopped") sas.logger.Info("Starting Arbitrum sequencer monitor for MEV opportunities...") sas.logger.Info("Initializing Arbitrum L2 parser for transaction analysis...") // Create the proper Arbitrum monitor with sequencer reader monitor, err := sas.createArbitrumMonitor() if err != nil { sas.logger.Error(fmt.Sprintf("Failed to create Arbitrum monitor: %v", err)) // 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 transactions...") // Start the monitor if err := monitor.Start(sas.ctx); err != nil { sas.logger.Error(fmt.Sprintf("Failed to start Arbitrum monitor: %v", err)) sas.fallbackBlockPolling() return } sas.logger.Info("Arbitrum sequencer monitoring started - processing live transactions") // Keep the monitor running <-sas.ctx.Done() sas.logger.Info("Stopping Arbitrum sequencer monitor...") } // fallbackBlockPolling provides fallback block monitoring through polling func (sas *SimpleArbitrageService) fallbackBlockPolling() { sas.logger.Info("Using fallback block polling...") ticker := time.NewTicker(3 * time.Second) // Poll every 3 seconds defer ticker.Stop() var lastBlock uint64 for { select { case <-sas.ctx.Done(): return case <-ticker.C: header, err := sas.client.HeaderByNumber(sas.ctx, nil) if err != nil { sas.logger.Debug(fmt.Sprintf("Failed to get latest block: %v", err)) continue } if header.Number.Uint64() > lastBlock { lastBlock = header.Number.Uint64() sas.processNewBlock(header) } } } } // processNewBlock processes a new block looking for swap events func (sas *SimpleArbitrageService) processNewBlock(header *types.Header) { blockNumber := header.Number.Uint64() // Skip processing if block has no transactions if header.TxHash == (common.Hash{}) { return } sas.logger.Info(fmt.Sprintf("Processing block %d for Uniswap V3 swap events", blockNumber)) // 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", len(swapEvents), blockNumber)) // Process each swap event for _, event := range swapEvents { go func(e *SimpleSwapEvent) { if err := sas.ProcessSwapEvent(e); err != nil { sas.logger.Debug(fmt.Sprintf("Failed to process swap event: %v", err)) } }(event) } } } // processTransaction analyzes a transaction for swap events func (sas *SimpleArbitrageService) 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 *SimpleArbitrageService) 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) < 192 { // 6 * 32 bytes 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 amount0 := new(big.Int).SetBytes(log.Data[0:32]) amount1 := new(big.Int).SetBytes(log.Data[32:64]) 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) tickBytes := log.Data[128:160] tick := new(big.Int).SetBytes(tickBytes) if tick.Bit(255) == 1 { // Check if negative (two's complement) tick.Sub(tick, new(big.Int).Lsh(big.NewInt(1), 256)) } // 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 } return &SimpleSwapEvent{ TxHash: tx.Hash(), PoolAddress: log.Address, Token0: token0, Token1: token1, Amount0: amount0, Amount1: amount1, SqrtPriceX96: sqrtPriceX96, Liquidity: liquidity, Tick: int32(tick.Int64()), BlockNumber: blockNumber, LogIndex: log.Index, Timestamp: time.Now(), } } // getPoolTokens retrieves token addresses for a Uniswap V3 pool with caching func (sas *SimpleArbitrageService) 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.Token0, cached.Token1, 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{Token0: token0, Token1: token1} sas.tokenCacheMutex.Unlock() return token0, token1, nil } // getSwapEventsFromBlock retrieves Uniswap V3 swap events from a specific block using log filtering func (sas *SimpleArbitrageService) getSwapEventsFromBlock(blockNumber uint64) []*SimpleSwapEvent { // Uniswap V3 Pool Swap event signature swapEventSig := common.HexToHash("0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67") // Create filter query for this specific block query := ethereum.FilterQuery{ FromBlock: big.NewInt(int64(blockNumber)), ToBlock: big.NewInt(int64(blockNumber)), 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 func (sas *SimpleArbitrageService) parseSwapEvent(log types.Log, blockNumber uint64) *SimpleSwapEvent { // Validate log structure if len(log.Topics) < 3 || len(log.Data) < 192 { // 6 * 32 bytes 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 amount0 := new(big.Int).SetBytes(log.Data[0:32]) amount1 := new(big.Int).SetBytes(log.Data[32:64]) 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) tickBytes := log.Data[128:160] tick := new(big.Int).SetBytes(tickBytes) if tick.Bit(255) == 1 { // Check if negative (two's complement) tick.Sub(tick, new(big.Int).Lsh(big.NewInt(1), 256)) } // 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())) return &SimpleSwapEvent{ TxHash: log.TxHash, PoolAddress: log.Address, Token0: token0, Token1: token1, Amount0: amount0, Amount1: amount1, SqrtPriceX96: sqrtPriceX96, Liquidity: liquidity, Tick: int32(tick.Int64()), BlockNumber: blockNumber, LogIndex: log.Index, Timestamp: time.Now(), } }