832 lines
25 KiB
Go
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(),
|
|
}
|
|
}
|