package dex import ( "context" "fmt" "math/big" "sync" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" ) // CrossDEXAnalyzer finds arbitrage opportunities across multiple DEXes type CrossDEXAnalyzer struct { registry *Registry client *ethclient.Client mu sync.RWMutex } // NewCrossDEXAnalyzer creates a new cross-DEX analyzer func NewCrossDEXAnalyzer(registry *Registry, client *ethclient.Client) *CrossDEXAnalyzer { return &CrossDEXAnalyzer{ registry: registry, client: client, } } // FindArbitrageOpportunities finds arbitrage opportunities for a token pair func (a *CrossDEXAnalyzer) FindArbitrageOpportunities( ctx context.Context, tokenA, tokenB common.Address, amountIn *big.Int, minProfitETH float64, ) ([]*ArbitragePath, error) { dexes := a.registry.GetAll() if len(dexes) < 2 { return nil, fmt.Errorf("need at least 2 active DEXes for arbitrage") } type quoteResult struct { dex DEXProtocol quote *PriceQuote err error } opportunities := make([]*ArbitragePath, 0) // Get quotes from all DEXes in parallel for A -> B buyQuotes := make(map[DEXProtocol]*PriceQuote) buyResults := make(chan quoteResult, len(dexes)) for _, dex := range dexes { go func(d *DEXInfo) { quote, err := d.Decoder.GetQuote(ctx, a.client, tokenA, tokenB, amountIn) buyResults <- quoteResult{dex: d.Protocol, quote: quote, err: err} }(dex) } // Collect buy quotes for i := 0; i < len(dexes); i++ { res := <-buyResults if res.err == nil && res.quote != nil { buyQuotes[res.dex] = res.quote } } // For each successful buy quote, get sell quotes on other DEXes for buyDEX, buyQuote := range buyQuotes { // Get amount out from buy intermediateAmount := buyQuote.ExpectedOut sellResults := make(chan quoteResult, len(dexes)-1) sellCount := 0 // Query all other DEXes for selling B -> A for _, dex := range dexes { if dex.Protocol == buyDEX { continue // Skip same DEX } sellCount++ go func(d *DEXInfo) { quote, err := d.Decoder.GetQuote(ctx, a.client, tokenB, tokenA, intermediateAmount) sellResults <- quoteResult{dex: d.Protocol, quote: quote, err: err} }(dex) } // Check each sell quote for profitability for i := 0; i < sellCount; i++ { res := <-sellResults if res.err != nil || res.quote == nil { continue } sellQuote := res.quote // Calculate profit finalAmount := sellQuote.ExpectedOut profit := new(big.Int).Sub(finalAmount, amountIn) // Estimate gas cost (rough estimate) gasUnits := buyQuote.GasEstimate + sellQuote.GasEstimate gasPrice := big.NewInt(100000000) // 0.1 gwei (rough estimate) gasCost := new(big.Int).Mul(big.NewInt(int64(gasUnits)), gasPrice) netProfit := new(big.Int).Sub(profit, gasCost) // Convert to ETH profitETH := new(big.Float).Quo( new(big.Float).SetInt(netProfit), new(big.Float).SetInt(big.NewInt(1e18)), ) profitFloat, _ := profitETH.Float64() // Only consider profitable opportunities if profitFloat > minProfitETH { roi := new(big.Float).Quo( new(big.Float).SetInt(netProfit), new(big.Float).SetInt(amountIn), ) roiFloat, _ := roi.Float64() path := &ArbitragePath{ Hops: []*PathHop{ { DEX: buyDEX, PoolAddress: buyQuote.PoolAddress, TokenIn: tokenA, TokenOut: tokenB, AmountIn: amountIn, AmountOut: buyQuote.ExpectedOut, Fee: buyQuote.Fee, }, { DEX: res.dex, PoolAddress: sellQuote.PoolAddress, TokenIn: tokenB, TokenOut: tokenA, AmountIn: intermediateAmount, AmountOut: sellQuote.ExpectedOut, Fee: sellQuote.Fee, }, }, TotalProfit: profit, ProfitETH: profitFloat, ROI: roiFloat, GasCost: gasCost, NetProfit: netProfit, Confidence: a.calculateConfidence(buyQuote, sellQuote), } opportunities = append(opportunities, path) } } } return opportunities, nil } // FindMultiHopOpportunities finds arbitrage opportunities with multiple hops func (a *CrossDEXAnalyzer) FindMultiHopOpportunities( ctx context.Context, startToken common.Address, intermediateTokens []common.Address, amountIn *big.Int, maxHops int, minProfitETH float64, ) ([]*ArbitragePath, error) { if maxHops < 2 || maxHops > 4 { return nil, fmt.Errorf("maxHops must be between 2 and 4") } opportunities := make([]*ArbitragePath, 0) // For 3-hop: Start -> Token1 -> Token2 -> Start if maxHops >= 3 { for _, token1 := range intermediateTokens { for _, token2 := range intermediateTokens { if token1 == token2 || token1 == startToken || token2 == startToken { continue } path, err := a.evaluate3HopPath(ctx, startToken, token1, token2, amountIn, minProfitETH) if err == nil && path != nil { opportunities = append(opportunities, path) } } } } // For 4-hop: Start -> Token1 -> Token2 -> Token3 -> Start if maxHops >= 4 { for _, token1 := range intermediateTokens { for _, token2 := range intermediateTokens { for _, token3 := range intermediateTokens { if token1 == token2 || token1 == token3 || token2 == token3 || token1 == startToken || token2 == startToken || token3 == startToken { continue } path, err := a.evaluate4HopPath(ctx, startToken, token1, token2, token3, amountIn, minProfitETH) if err == nil && path != nil { opportunities = append(opportunities, path) } } } } } return opportunities, nil } // evaluate3HopPath evaluates a 3-hop arbitrage path func (a *CrossDEXAnalyzer) evaluate3HopPath( ctx context.Context, token0, token1, token2 common.Address, amountIn *big.Int, minProfitETH float64, ) (*ArbitragePath, error) { // Hop 1: token0 -> token1 quote1, err := a.registry.GetBestQuote(ctx, token0, token1, amountIn) if err != nil { return nil, err } // Hop 2: token1 -> token2 quote2, err := a.registry.GetBestQuote(ctx, token1, token2, quote1.ExpectedOut) if err != nil { return nil, err } // Hop 3: token2 -> token0 (back to start) quote3, err := a.registry.GetBestQuote(ctx, token2, token0, quote2.ExpectedOut) if err != nil { return nil, err } // Calculate profit finalAmount := quote3.ExpectedOut profit := new(big.Int).Sub(finalAmount, amountIn) // Estimate gas cost gasUnits := quote1.GasEstimate + quote2.GasEstimate + quote3.GasEstimate gasPrice := big.NewInt(100000000) // 0.1 gwei gasCost := new(big.Int).Mul(big.NewInt(int64(gasUnits)), gasPrice) netProfit := new(big.Int).Sub(profit, gasCost) profitETH := new(big.Float).Quo( new(big.Float).SetInt(netProfit), new(big.Float).SetInt(big.NewInt(1e18)), ) profitFloat, _ := profitETH.Float64() if profitFloat < minProfitETH { return nil, fmt.Errorf("insufficient profit") } roi := new(big.Float).Quo( new(big.Float).SetInt(netProfit), new(big.Float).SetInt(amountIn), ) roiFloat, _ := roi.Float64() return &ArbitragePath{ Hops: []*PathHop{ { DEX: quote1.DEX, PoolAddress: quote1.PoolAddress, TokenIn: token0, TokenOut: token1, AmountIn: amountIn, AmountOut: quote1.ExpectedOut, Fee: quote1.Fee, }, { DEX: quote2.DEX, PoolAddress: quote2.PoolAddress, TokenIn: token1, TokenOut: token2, AmountIn: quote1.ExpectedOut, AmountOut: quote2.ExpectedOut, Fee: quote2.Fee, }, { DEX: quote3.DEX, PoolAddress: quote3.PoolAddress, TokenIn: token2, TokenOut: token0, AmountIn: quote2.ExpectedOut, AmountOut: quote3.ExpectedOut, Fee: quote3.Fee, }, }, TotalProfit: profit, ProfitETH: profitFloat, ROI: roiFloat, GasCost: gasCost, NetProfit: netProfit, Confidence: 0.6, // Lower confidence for 3-hop }, nil } // evaluate4HopPath evaluates a 4-hop arbitrage path func (a *CrossDEXAnalyzer) evaluate4HopPath( ctx context.Context, token0, token1, token2, token3 common.Address, amountIn *big.Int, minProfitETH float64, ) (*ArbitragePath, error) { // Similar to evaluate3HopPath but with 4 hops // Hop 1: token0 -> token1 quote1, err := a.registry.GetBestQuote(ctx, token0, token1, amountIn) if err != nil { return nil, err } // Hop 2: token1 -> token2 quote2, err := a.registry.GetBestQuote(ctx, token1, token2, quote1.ExpectedOut) if err != nil { return nil, err } // Hop 3: token2 -> token3 quote3, err := a.registry.GetBestQuote(ctx, token2, token3, quote2.ExpectedOut) if err != nil { return nil, err } // Hop 4: token3 -> token0 (back to start) quote4, err := a.registry.GetBestQuote(ctx, token3, token0, quote3.ExpectedOut) if err != nil { return nil, err } // Calculate profit finalAmount := quote4.ExpectedOut profit := new(big.Int).Sub(finalAmount, amountIn) // Estimate gas cost gasUnits := quote1.GasEstimate + quote2.GasEstimate + quote3.GasEstimate + quote4.GasEstimate gasPrice := big.NewInt(100000000) gasCost := new(big.Int).Mul(big.NewInt(int64(gasUnits)), gasPrice) netProfit := new(big.Int).Sub(profit, gasCost) profitETH := new(big.Float).Quo( new(big.Float).SetInt(netProfit), new(big.Float).SetInt(big.NewInt(1e18)), ) profitFloat, _ := profitETH.Float64() if profitFloat < minProfitETH { return nil, fmt.Errorf("insufficient profit") } roi := new(big.Float).Quo( new(big.Float).SetInt(netProfit), new(big.Float).SetInt(amountIn), ) roiFloat, _ := roi.Float64() return &ArbitragePath{ Hops: []*PathHop{ {DEX: quote1.DEX, PoolAddress: quote1.PoolAddress, TokenIn: token0, TokenOut: token1, AmountIn: amountIn, AmountOut: quote1.ExpectedOut, Fee: quote1.Fee}, {DEX: quote2.DEX, PoolAddress: quote2.PoolAddress, TokenIn: token1, TokenOut: token2, AmountIn: quote1.ExpectedOut, AmountOut: quote2.ExpectedOut, Fee: quote2.Fee}, {DEX: quote3.DEX, PoolAddress: quote3.PoolAddress, TokenIn: token2, TokenOut: token3, AmountIn: quote2.ExpectedOut, AmountOut: quote3.ExpectedOut, Fee: quote3.Fee}, {DEX: quote4.DEX, PoolAddress: quote4.PoolAddress, TokenIn: token3, TokenOut: token0, AmountIn: quote3.ExpectedOut, AmountOut: quote4.ExpectedOut, Fee: quote4.Fee}, }, TotalProfit: profit, ProfitETH: profitFloat, ROI: roiFloat, GasCost: gasCost, NetProfit: netProfit, Confidence: 0.4, // Lower confidence for 4-hop }, nil } // calculateConfidence calculates confidence score based on liquidity and price impact func (a *CrossDEXAnalyzer) calculateConfidence(quotes ...*PriceQuote) float64 { if len(quotes) == 0 { return 0 } totalImpact := 0.0 for _, quote := range quotes { totalImpact += quote.PriceImpact } avgImpact := totalImpact / float64(len(quotes)) // Confidence decreases with price impact // High impact (>5%) = low confidence // Low impact (<1%) = high confidence if avgImpact > 0.05 { return 0.3 } else if avgImpact > 0.03 { return 0.5 } else if avgImpact > 0.01 { return 0.7 } return 0.9 } // GetPriceComparison compares prices across all DEXes for a token pair func (a *CrossDEXAnalyzer) GetPriceComparison( ctx context.Context, tokenIn, tokenOut common.Address, amountIn *big.Int, ) (map[DEXProtocol]*PriceQuote, error) { dexes := a.registry.GetAll() quotes := make(map[DEXProtocol]*PriceQuote) type result struct { protocol DEXProtocol quote *PriceQuote err error } results := make(chan result, len(dexes)) // Query all DEXes in parallel for _, dex := range dexes { go func(d *DEXInfo) { quote, err := d.Decoder.GetQuote(ctx, a.client, tokenIn, tokenOut, amountIn) results <- result{protocol: d.Protocol, quote: quote, err: err} }(dex) } // Collect results for i := 0; i < len(dexes); i++ { res := <-results if res.err == nil && res.quote != nil { quotes[res.protocol] = res.quote } } if len(quotes) == 0 { return nil, fmt.Errorf("no valid quotes found") } return quotes, nil }