Files
mev-beta/pkg/arbitrage/service_simple.go
2025-09-16 11:05:47 -05:00

832 lines
25 KiB
Go

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(),
}
}