Files
mev-beta/pkg/arbitrum/market_discovery.go
Krypto Kajun 76c1b5cee1 fix(math): resolve nil pointer dereference in market discovery calculations
- Add nil checks for big.Int values in updateV2PoolReserves
- Add nil checks for big.Int values in updateV3PoolState
- Fix test expectations in dex_math_test.go with correct Uniswap V2 calculation values
- Add proper error handling for nil pointers in arbitrage calculations
- Fix Curve test to use appropriate price impact thresholds
2025-09-23 20:04:39 -05:00

1933 lines
63 KiB
Go
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package arbitrum
import (
"context"
"encoding/json"
"fmt"
"math/big"
"os"
"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/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/fraktal/mev-beta/internal/logger"
exchangeMath "github.com/fraktal/mev-beta/pkg/math"
"gopkg.in/yaml.v3"
)
// MarketDiscovery manages pool discovery and market building
type MarketDiscovery struct {
client *ethclient.Client
logger *logger.Logger
config *MarketConfig
mathCalc *exchangeMath.MathCalculator
// Market state
pools map[common.Address]*PoolInfoDetailed
tokens map[common.Address]*TokenInfo
factories map[common.Address]*FactoryInfo
routers map[common.Address]*RouterInfo
mu sync.RWMutex
// Logging
marketScanLogger *os.File
arbLogger *os.File
// Performance tracking
poolsDiscovered uint64
arbitrageOpps uint64
lastScanTime time.Time
totalScanTime time.Duration
}
// MarketConfig represents the configuration for market discovery
type MarketConfig struct {
Version string `yaml:"version"`
Network string `yaml:"network"`
ChainID int64 `yaml:"chain_id"`
Tokens map[string]*TokenConfigInfo `yaml:"tokens"`
Factories map[string]*FactoryConfig `yaml:"factories"`
Routers map[string]*RouterConfig `yaml:"routers"`
PriorityPools []PriorityPoolConfig `yaml:"priority_pools"`
MarketScan MarketScanConfig `yaml:"market_scan"`
Arbitrage ArbitrageConfig `yaml:"arbitrage"`
Logging LoggingConfig `yaml:"logging"`
Risk RiskConfig `yaml:"risk"`
Monitoring MonitoringConfig `yaml:"monitoring"`
}
type TokenConfigInfo struct {
Address string `yaml:"address"`
Symbol string `yaml:"symbol"`
Decimals int `yaml:"decimals"`
Priority int `yaml:"priority"`
}
type FactoryConfig struct {
Address string `yaml:"address"`
Type string `yaml:"type"`
InitCodeHash string `yaml:"init_code_hash"`
FeeTiers []uint32 `yaml:"fee_tiers"`
Priority int `yaml:"priority"`
}
type RouterConfig struct {
Address string `yaml:"address"`
Factory string `yaml:"factory"`
Type string `yaml:"type"`
Priority int `yaml:"priority"`
}
type PriorityPoolConfig struct {
Pool string `yaml:"pool"`
Factory string `yaml:"factory"`
Token0 string `yaml:"token0"`
Token1 string `yaml:"token1"`
Fee uint32 `yaml:"fee"`
Priority int `yaml:"priority"`
}
type MarketScanConfig struct {
ScanInterval int `yaml:"scan_interval"`
MaxPools int `yaml:"max_pools"`
MinLiquidityUSD float64 `yaml:"min_liquidity_usd"`
MinVolume24hUSD float64 `yaml:"min_volume_24h_usd"`
Discovery PoolDiscoveryConfig `yaml:"discovery"`
}
type PoolDiscoveryConfig struct {
MaxBlocksBack uint64 `yaml:"max_blocks_back"`
MinPoolAge uint64 `yaml:"min_pool_age"`
DiscoveryInterval uint64 `yaml:"discovery_interval"`
}
type ArbitrageConfig struct {
MinProfitUSD float64 `yaml:"min_profit_usd"`
MaxSlippage float64 `yaml:"max_slippage"`
MaxGasPrice float64 `yaml:"max_gas_price"`
ProfitMargins map[string]float64 `yaml:"profit_margins"`
}
type LoggingConfig struct {
Level string `yaml:"level"`
Files map[string]string `yaml:"files"`
RealTime map[string]interface{} `yaml:"real_time"`
}
type RiskConfig struct {
MaxPositionETH float64 `yaml:"max_position_eth"`
MaxDailyLossETH float64 `yaml:"max_daily_loss_eth"`
MaxConcurrentTxs int `yaml:"max_concurrent_txs"`
CircuitBreaker map[string]interface{} `yaml:"circuit_breaker"`
}
type MonitoringConfig struct {
Enabled bool `yaml:"enabled"`
UpdateInterval int `yaml:"update_interval"`
Metrics []string `yaml:"metrics"`
}
// PoolInfoDetailed represents detailed pool information for market discovery
type PoolInfoDetailed struct {
Address common.Address `json:"address"`
Factory common.Address `json:"factory"`
FactoryType string `json:"factory_type"`
Token0 common.Address `json:"token0"`
Token1 common.Address `json:"token1"`
Fee uint32 `json:"fee"`
Reserve0 *big.Int `json:"reserve0"`
Reserve1 *big.Int `json:"reserve1"`
Liquidity *big.Int `json:"liquidity"`
SqrtPriceX96 *big.Int `json:"sqrt_price_x96,omitempty"` // For V3 pools
Tick int32 `json:"tick,omitempty"` // For V3 pools
LastUpdated time.Time `json:"last_updated"`
Volume24h *big.Int `json:"volume_24h"`
Priority int `json:"priority"`
Active bool `json:"active"`
}
type TokenInfo struct {
Address common.Address `json:"address"`
Symbol string `json:"symbol"`
Name string `json:"name"`
Decimals uint8 `json:"decimals"`
Priority int `json:"priority"`
LastPrice *big.Int `json:"last_price"`
Volume24h *big.Int `json:"volume_24h"`
}
type FactoryInfo struct {
Address common.Address `json:"address"`
Type string `json:"type"`
InitCodeHash common.Hash `json:"init_code_hash"`
FeeTiers []uint32 `json:"fee_tiers"`
PoolCount uint64 `json:"pool_count"`
Priority int `json:"priority"`
}
type RouterInfo struct {
Address common.Address `json:"address"`
Factory common.Address `json:"factory"`
Type string `json:"type"`
Priority int `json:"priority"`
}
// MarketScanResult represents the result of a market scan
type MarketScanResult struct {
Timestamp time.Time `json:"timestamp"`
BlockNumber uint64 `json:"block_number"`
PoolsScanned int `json:"pools_scanned"`
NewPoolsFound int `json:"new_pools_found"`
ArbitrageOpps []*ArbitrageOpportunityDetailed `json:"arbitrage_opportunities"`
TopPools []*PoolInfoDetailed `json:"top_pools"`
ScanDuration time.Duration `json:"scan_duration"`
GasPrice *big.Int `json:"gas_price"`
NetworkConditions map[string]interface{} `json:"network_conditions"`
}
type ArbitrageOpportunityDetailed struct {
ID string `json:"id"`
Type string `json:"type"`
TokenIn common.Address `json:"token_in"`
TokenOut common.Address `json:"token_out"`
AmountIn *big.Int `json:"amount_in"`
ExpectedAmountOut *big.Int `json:"expected_amount_out"`
ActualAmountOut *big.Int `json:"actual_amount_out"`
Profit *big.Int `json:"profit"`
ProfitUSD float64 `json:"profit_usd"`
ProfitMargin float64 `json:"profit_margin"`
GasCost *big.Int `json:"gas_cost"`
NetProfit *big.Int `json:"net_profit"`
ExchangeA string `json:"exchange_a"`
ExchangeB string `json:"exchange_b"`
PoolA common.Address `json:"pool_a"`
PoolB common.Address `json:"pool_b"`
PriceA float64 `json:"price_a"`
PriceB float64 `json:"price_b"`
PriceImpactA float64 `json:"price_impact_a"`
PriceImpactB float64 `json:"price_impact_b"`
CapitalRequired float64 `json:"capital_required"`
GasCostUSD float64 `json:"gas_cost_usd"`
Confidence float64 `json:"confidence"`
RiskScore float64 `json:"risk_score"`
ExecutionTime time.Duration `json:"execution_time"`
Timestamp time.Time `json:"timestamp"`
}
// NewMarketDiscovery creates a new market discovery instance
func NewMarketDiscovery(client *ethclient.Client, logger *logger.Logger, configPath string) (*MarketDiscovery, error) {
// Load configuration
config, err := LoadMarketConfig(configPath)
if err != nil {
return nil, fmt.Errorf("failed to load config: %w", err)
}
// Initialize math calculator
mathCalc := exchangeMath.NewMathCalculator()
md := &MarketDiscovery{
client: client,
logger: logger,
config: config,
mathCalc: mathCalc,
pools: make(map[common.Address]*PoolInfoDetailed),
tokens: make(map[common.Address]*TokenInfo),
factories: make(map[common.Address]*FactoryInfo),
routers: make(map[common.Address]*RouterInfo),
}
// Initialize logging
if err := md.initializeLogging(); err != nil {
return nil, fmt.Errorf("failed to initialize logging: %w", err)
}
// Load initial configuration
if err := md.loadInitialMarkets(); err != nil {
return nil, fmt.Errorf("failed to load initial markets: %w", err)
}
logger.Info("Market discovery initialized with comprehensive pool detection")
return md, nil
}
// LoadMarketConfig loads market configuration from YAML file
func LoadMarketConfig(configPath string) (*MarketConfig, error) {
data, err := os.ReadFile(configPath)
if err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
var config MarketConfig
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to parse config: %w", err)
}
return &config, nil
}
// initializeLogging sets up JSONL logging files
func (md *MarketDiscovery) initializeLogging() error {
// Create logs directory if it doesn't exist
if err := os.MkdirAll("logs", 0755); err != nil {
return fmt.Errorf("failed to create logs directory: %w", err)
}
// Open market scan log file
marketScanFile, err := os.OpenFile(md.config.Logging.Files["market_scans"], os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return fmt.Errorf("failed to open market scan log file: %w", err)
}
md.marketScanLogger = marketScanFile
// Open arbitrage log file
arbFile, err := os.OpenFile(md.config.Logging.Files["arbitrage"], os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return fmt.Errorf("failed to open arbitrage log file: %w", err)
}
md.arbLogger = arbFile
return nil
}
// loadInitialMarkets loads initial tokens, factories, and priority pools
func (md *MarketDiscovery) loadInitialMarkets() error {
md.mu.Lock()
defer md.mu.Unlock()
// Load tokens
for _, token := range md.config.Tokens {
tokenAddr := common.HexToAddress(token.Address)
md.tokens[tokenAddr] = &TokenInfo{
Address: tokenAddr,
Symbol: token.Symbol,
Decimals: uint8(token.Decimals),
Priority: token.Priority,
}
}
// Load factories
for _, factory := range md.config.Factories {
factoryAddr := common.HexToAddress(factory.Address)
md.factories[factoryAddr] = &FactoryInfo{
Address: factoryAddr,
Type: factory.Type,
InitCodeHash: common.HexToHash(factory.InitCodeHash),
FeeTiers: factory.FeeTiers,
Priority: factory.Priority,
}
}
// Load routers
for _, router := range md.config.Routers {
routerAddr := common.HexToAddress(router.Address)
factoryAddr := common.Address{}
if router.Factory != "" {
for _, f := range md.config.Factories {
if f.Type == router.Factory {
factoryAddr = common.HexToAddress(f.Address)
break
}
}
}
md.routers[routerAddr] = &RouterInfo{
Address: routerAddr,
Factory: factoryAddr,
Type: router.Type,
Priority: router.Priority,
}
}
// Load priority pools
for _, poolConfig := range md.config.PriorityPools {
poolAddr := common.HexToAddress(poolConfig.Pool)
token0 := common.HexToAddress(poolConfig.Token0)
token1 := common.HexToAddress(poolConfig.Token1)
// Find factory
var factoryAddr common.Address
var factoryType string
for _, f := range md.config.Factories {
if f.Type == poolConfig.Factory {
factoryAddr = common.HexToAddress(f.Address)
factoryType = f.Type
break
}
}
pool := &PoolInfoDetailed{
Address: poolAddr,
Factory: factoryAddr,
FactoryType: factoryType,
Token0: token0,
Token1: token1,
Fee: poolConfig.Fee,
Priority: poolConfig.Priority,
Active: true,
LastUpdated: time.Now(),
}
md.pools[poolAddr] = pool
}
md.logger.Info(fmt.Sprintf("Loaded initial markets: %d tokens, %d factories, %d routers, %d priority pools",
len(md.tokens), len(md.factories), len(md.routers), len(md.pools)))
return nil
}
// buildComprehensiveMarkets builds markets for all exchanges and top token pairs
func (md *MarketDiscovery) buildComprehensiveMarkets() error {
md.logger.Info("🏗️ Building comprehensive markets for all exchanges and top tokens")
// Get top tokens (sorted by priority)
topTokens := md.getTopTokens(10) // Reduced from 20 to 10 tokens to reduce load
md.logger.Info(fmt.Sprintf("💼 Found %d top tokens for market building", len(topTokens)))
// Build markets for each factory
marketsBuilt := 0
for factoryAddr, factoryInfo := range md.factories {
markets, err := md.buildFactoryMarkets(factoryAddr, factoryInfo, topTokens)
if err != nil {
md.logger.Error(fmt.Sprintf("Failed to build markets for factory %s: %v", factoryAddr.Hex(), err))
continue
}
marketsBuilt += len(markets)
md.logger.Info(fmt.Sprintf("✅ Built %d markets for %s factory", len(markets), factoryInfo.Type))
}
md.logger.Info(fmt.Sprintf("📊 Total markets built: %d", marketsBuilt))
// Log available markets
md.logAvailableMarkets()
return nil
}
// getTopTokens returns the top N tokens sorted by priority
func (md *MarketDiscovery) getTopTokens(limit int) []*TokenInfo {
md.mu.RLock()
defer md.mu.RUnlock()
// Convert map to slice
tokens := make([]*TokenInfo, 0, len(md.tokens))
for _, token := range md.tokens {
tokens = append(tokens, token)
}
// Sort by priority (highest first)
for i := 0; i < len(tokens)-1; i++ {
for j := i + 1; j < len(tokens); j++ {
if tokens[i].Priority < tokens[j].Priority {
tokens[i], tokens[j] = tokens[j], tokens[i]
}
}
}
// Limit to top N (reduced for performance)
limit = 10 // Reduced from 20 to 10 to reduce load
if len(tokens) > limit {
tokens = tokens[:limit]
}
return tokens
}
// buildFactoryMarkets builds markets for a specific factory and token pairs
func (md *MarketDiscovery) buildFactoryMarkets(factoryAddr common.Address, factoryInfo *FactoryInfo, tokens []*TokenInfo) ([]*PoolInfoDetailed, error) {
var markets []*PoolInfoDetailed
// Find WETH token (most important for pairing)
var wethToken *TokenInfo
for _, token := range tokens {
if token.Symbol == "WETH" {
wethToken = token
break
}
}
// If no WETH found, use the highest priority token
if wethToken == nil && len(tokens) > 0 {
wethToken = tokens[0]
}
// Build markets for each token pair
for i, tokenA := range tokens {
for j := i + 1; j < len(tokens); j++ {
tokenB := tokens[j]
// Build markets for this token pair
pairMarkets, err := md.buildTokenPairMarkets(factoryAddr, factoryInfo, tokenA, tokenB)
if err != nil {
md.logger.Debug(fmt.Sprintf("Failed to build markets for %s-%s pair: %v", tokenA.Symbol, tokenB.Symbol, err))
continue
}
markets = append(markets, pairMarkets...)
}
// Also build markets for token-WETH pairs if WETH exists and is not this token
if wethToken != nil && tokenA.Address != wethToken.Address {
wethMarkets, err := md.buildTokenPairMarkets(factoryAddr, factoryInfo, tokenA, wethToken)
if err != nil {
md.logger.Debug(fmt.Sprintf("Failed to build markets for %s-WETH pair: %v", tokenA.Symbol, err))
continue
}
markets = append(markets, wethMarkets...)
}
}
// Add built markets to tracking
md.mu.Lock()
for _, market := range markets {
// Only add if not already tracking
if _, exists := md.pools[market.Address]; !exists {
md.pools[market.Address] = market
}
}
md.mu.Unlock()
return markets, nil
}
// buildTokenPairMarkets builds markets for a specific token pair and factory
func (md *MarketDiscovery) buildTokenPairMarkets(factoryAddr common.Address, factoryInfo *FactoryInfo, tokenA, tokenB *TokenInfo) ([]*PoolInfoDetailed, error) {
var markets []*PoolInfoDetailed
// For factories with fee tiers (Uniswap V3 style), build markets for each fee tier
if len(factoryInfo.FeeTiers) > 0 {
// Build markets for each fee tier
for _, feeTier := range factoryInfo.FeeTiers {
// Generate deterministic pool address using CREATE2
poolAddr, err := md.calculatePoolAddress(factoryAddr, factoryInfo, tokenA, tokenB, feeTier)
if err != nil {
continue
}
market := &PoolInfoDetailed{
Address: poolAddr,
Factory: factoryAddr,
FactoryType: factoryInfo.Type,
Token0: tokenA.Address,
Token1: tokenB.Address,
Fee: feeTier,
Reserve0: big.NewInt(0),
Reserve1: big.NewInt(0),
Liquidity: big.NewInt(0),
SqrtPriceX96: big.NewInt(0),
Tick: 0,
LastUpdated: time.Now(),
Volume24h: big.NewInt(0),
Priority: (tokenA.Priority + tokenB.Priority) / 2,
Active: true,
}
markets = append(markets, market)
}
} else {
// For factories without fee tiers (Uniswap V2 style), build a single market
// Generate deterministic pool address using CREATE2
poolAddr, err := md.calculatePoolAddress(factoryAddr, factoryInfo, tokenA, tokenB, 0)
if err != nil {
return nil, err
}
market := &PoolInfoDetailed{
Address: poolAddr,
Factory: factoryAddr,
FactoryType: factoryInfo.Type,
Token0: tokenA.Address,
Token1: tokenB.Address,
Reserve0: big.NewInt(0),
Reserve1: big.NewInt(0),
Liquidity: big.NewInt(0),
LastUpdated: time.Now(),
Volume24h: big.NewInt(0),
Priority: (tokenA.Priority + tokenB.Priority) / 2,
Active: true,
}
markets = append(markets, market)
}
return markets, nil
}
// calculatePoolAddress calculates the deterministic pool address using CREATE2
func (md *MarketDiscovery) calculatePoolAddress(factoryAddr common.Address, factoryInfo *FactoryInfo, tokenA, tokenB *TokenInfo, feeTier uint32) (common.Address, error) {
// Sort tokens to ensure consistent ordering
token0, token1 := tokenA.Address, tokenB.Address
if token0.Big().Cmp(token1.Big()) > 0 {
token0, token1 = token1, token0
}
switch factoryInfo.Type {
case "uniswap_v3", "camelot_v3", "algebra":
// For Uniswap V3 style factories with fee tiers
return md.calculateUniswapV3PoolAddress(factoryAddr, factoryInfo, token0, token1, feeTier)
case "uniswap_v2", "sushiswap":
// For Uniswap V2 style factories
return md.calculateUniswapV2PoolAddress(factoryAddr, factoryInfo, token0, token1)
case "balancer_v2":
// For Balancer (simplified - in practice would need more info)
return md.calculateBalancerPoolAddress(factoryAddr, token0, token1)
case "curve":
// For Curve (simplified - in practice would need more info)
return md.calculateCurvePoolAddress(factoryAddr, token0, token1)
default:
// Generic CREATE2 calculation
return md.calculateGenericPoolAddress(factoryAddr, factoryInfo, token0, token1, feeTier)
}
}
// calculateUniswapV3PoolAddress calculates pool address for Uniswap V3 style factories
func (md *MarketDiscovery) calculateUniswapV3PoolAddress(factoryAddr common.Address, factoryInfo *FactoryInfo, token0, token1 common.Address, feeTier uint32) (common.Address, error) {
// Encode the pool key: keccak256(abi.encode(token0, token1, fee))
poolKey := crypto.Keccak256(append(append(token0.Bytes(), token1.Bytes()...), big.NewInt(int64(feeTier)).Bytes()...))
// Calculate CREATE2 address
// keccak256(0xff ++ address ++ salt ++ keccak256(init_code))[12:]
salt := poolKey
initCodeHash := factoryInfo.InitCodeHash.Bytes()
create2Input := append([]byte{0xff}, factoryAddr.Bytes()...)
create2Input = append(create2Input, salt...)
create2Input = append(create2Input, initCodeHash...)
poolAddrBytes := crypto.Keccak256(create2Input)
// Take last 20 bytes for address
poolAddr := common.BytesToAddress(poolAddrBytes[12:])
return poolAddr, nil
}
// calculateUniswapV2PoolAddress calculates pool address for Uniswap V2 style factories
func (md *MarketDiscovery) calculateUniswapV2PoolAddress(factoryAddr common.Address, factoryInfo *FactoryInfo, token0, token1 common.Address) (common.Address, error) {
// For Uniswap V2: keccak256(0xff ++ address ++ keccak256(token0 ++ token1) ++ initcode_hash)[12:]
poolKey := crypto.Keccak256(append(token0.Bytes(), token1.Bytes()...))
create2Input := append([]byte{0xff}, factoryAddr.Bytes()...)
create2Input = append(create2Input, poolKey...)
create2Input = append(create2Input, factoryInfo.InitCodeHash.Bytes()...)
poolAddrBytes := crypto.Keccak256(create2Input)
// Take last 20 bytes for address
poolAddr := common.BytesToAddress(poolAddrBytes[12:])
return poolAddr, nil
}
// calculateBalancerPoolAddress calculates pool address for Balancer pools (simplified)
func (md *MarketDiscovery) calculateBalancerPoolAddress(factoryAddr, token0, token1 common.Address) (common.Address, error) {
// Simplified implementation - in practice would need more complex logic
// For Balancer V2, pool addresses are typically determined by the vault
// This is a placeholder implementation
placeholder := crypto.Keccak256(append(append(factoryAddr.Bytes(), token0.Bytes()...), token1.Bytes()...))
return common.BytesToAddress(placeholder[12:]), nil
}
// calculateCurvePoolAddress calculates pool address for Curve pools (simplified)
func (md *MarketDiscovery) calculateCurvePoolAddress(factoryAddr, token0, token1 common.Address) (common.Address, error) {
// Simplified implementation - Curve pools are typically deployed via factories
// with more complex logic. This is a placeholder implementation
placeholder := crypto.Keccak256(append(append(factoryAddr.Bytes(), token0.Bytes()...), token1.Bytes()...))
return common.BytesToAddress(placeholder[12:]), nil
}
// calculateGenericPoolAddress calculates pool address for generic factories
func (md *MarketDiscovery) calculateGenericPoolAddress(factoryAddr common.Address, factoryInfo *FactoryInfo, token0, token1 common.Address, feeTier uint32) (common.Address, error) {
// Generic CREATE2 calculation using tokens and fee as salt
saltInput := append(append(token0.Bytes(), token1.Bytes()...), big.NewInt(int64(feeTier)).Bytes()...)
salt := crypto.Keccak256(saltInput)
create2Input := append([]byte{0xff}, factoryAddr.Bytes()...)
create2Input = append(create2Input, salt...)
create2Input = append(create2Input, factoryInfo.InitCodeHash.Bytes()...)
poolAddrBytes := crypto.Keccak256(create2Input)
// Take last 20 bytes for address
poolAddr := common.BytesToAddress(poolAddrBytes[12:])
return poolAddr, nil
}
// logAvailableMarkets logs all available markets grouped by exchange
func (md *MarketDiscovery) logAvailableMarkets() {
md.mu.RLock()
defer md.mu.RUnlock()
// Group markets by factory type
marketsByFactory := make(map[string][]*PoolInfoDetailed)
for _, pool := range md.pools {
factoryType := pool.FactoryType
marketsByFactory[factoryType] = append(marketsByFactory[factoryType], pool)
}
// Log markets for each factory
md.logger.Info("📈 Available Markets by Exchange:")
for factoryType, pools := range marketsByFactory {
// Count unique token pairs
tokenPairs := make(map[string]bool)
for _, pool := range pools {
// Handle empty addresses to prevent slice bounds panic
token0Display := "unknown"
token1Display := "unknown"
if len(pool.Token0.Hex()) > 0 {
if len(pool.Token0.Hex()) > 6 {
token0Display = pool.Token0.Hex()[:6]
} else {
token0Display = pool.Token0.Hex()
}
}
if len(pool.Token1.Hex()) > 0 {
if len(pool.Token1.Hex()) > 6 {
token1Display = pool.Token1.Hex()[:6]
} else {
token1Display = pool.Token1.Hex()
}
}
pairKey := fmt.Sprintf("%s-%s", token0Display, token1Display)
tokenPairs[pairKey] = true
}
md.logger.Info(fmt.Sprintf(" %s: %d pools, %d unique token pairs",
factoryType, len(pools), len(tokenPairs)))
// Log top 5 pools by priority
for i, pool := range pools {
if i >= 5 {
break
}
md.logger.Debug(fmt.Sprintf(" 🏦 Pool %s (%s-%s, Fee: %d)",
pool.Address.Hex()[:10],
pool.Token0.Hex()[:6],
pool.Token1.Hex()[:6],
pool.Fee))
}
if len(pools) > 5 {
md.logger.Debug(fmt.Sprintf(" ... and %d more pools", len(pools)-5))
}
}
}
func (md *MarketDiscovery) DiscoverPools(ctx context.Context, fromBlock, toBlock uint64) (*PoolDiscoveryResult, error) {
startTime := time.Now()
discovered := &PoolDiscoveryResult{
Timestamp: startTime,
FromBlock: fromBlock,
ToBlock: toBlock,
NewPools: make([]*PoolInfoDetailed, 0),
}
// Discover pools from each factory
for factoryAddr, factoryInfo := range md.factories {
pools, err := md.discoverPoolsFromFactory(ctx, factoryAddr, factoryInfo, fromBlock, toBlock)
if err != nil {
md.logger.Error(fmt.Sprintf("Failed to discover pools from factory %s: %v", factoryAddr.Hex(), err))
continue
}
discovered.NewPools = append(discovered.NewPools, pools...)
}
discovered.PoolsFound = len(discovered.NewPools)
discovered.ScanDuration = time.Since(startTime)
// Log discovery results
if err := md.logPoolDiscovery(discovered); err != nil {
md.logger.Error(fmt.Sprintf("Failed to log pool discovery: %v", err))
}
md.poolsDiscovered += uint64(discovered.PoolsFound)
return discovered, nil
}
// ScanForArbitrage scans all pools for arbitrage opportunities
func (md *MarketDiscovery) ScanForArbitrage(ctx context.Context, blockNumber uint64) (*MarketScanResult, error) {
startTime := time.Now()
md.lastScanTime = startTime
result := &MarketScanResult{
Timestamp: startTime,
BlockNumber: blockNumber,
ArbitrageOpps: make([]*ArbitrageOpportunityDetailed, 0),
TopPools: make([]*PoolInfoDetailed, 0),
NetworkConditions: make(map[string]interface{}),
}
// Update pool states
if err := md.updatePoolStates(ctx); err != nil {
return nil, fmt.Errorf("failed to update pool states: %w", err)
}
// Get current gas price
gasPrice, err := md.client.SuggestGasPrice(ctx)
if err != nil {
gasPrice = big.NewInt(5000000000) // 5 gwei fallback
}
result.GasPrice = gasPrice
// Scan for arbitrage opportunities
opportunities := md.findArbitrageOpportunities(ctx, gasPrice)
result.ArbitrageOpps = opportunities
result.PoolsScanned = len(md.pools)
// Get top pools by liquidity
result.TopPools = md.getTopPoolsByLiquidity(10)
result.ScanDuration = time.Since(startTime)
md.totalScanTime += result.ScanDuration
// Log scan results
if err := md.logMarketScan(result); err != nil {
md.logger.Error(fmt.Sprintf("Failed to log market scan: %v", err))
}
md.arbitrageOpps += uint64(len(opportunities))
return result, nil
}
// findArbitrageOpportunities finds arbitrage opportunities across all pools
func (md *MarketDiscovery) findArbitrageOpportunities(ctx context.Context, gasPrice *big.Int) []*ArbitrageOpportunityDetailed {
opportunities := make([]*ArbitrageOpportunityDetailed, 0)
// Group pools by token pairs
tokenPairPools := md.groupPoolsByTokenPairs()
// Check each token pair for arbitrage
for tokenPair, pools := range tokenPairPools {
if len(pools) < 2 {
continue // Need at least 2 pools for arbitrage
}
// Check all pool combinations
for i := 0; i < len(pools); i++ {
for j := i + 1; j < len(pools); j++ {
poolA := pools[i]
poolB := pools[j]
// Skip if same factory type (no arbitrage opportunity)
if poolA.FactoryType == poolB.FactoryType {
continue
}
// Calculate arbitrage
arb := md.calculateArbitrage(poolA, poolB, gasPrice, tokenPair)
if arb != nil && arb.NetProfit.Sign() > 0 {
opportunities = append(opportunities, arb)
}
}
}
}
// Sort by net profit (highest first)
for i := 0; i < len(opportunities)-1; i++ {
for j := i + 1; j < len(opportunities); j++ {
if opportunities[i].NetProfit.Cmp(opportunities[j].NetProfit) < 0 {
opportunities[i], opportunities[j] = opportunities[j], opportunities[i]
}
}
}
// Log arbitrage opportunities
for _, opp := range opportunities {
if err := md.logArbitrageOpportunity(opp); err != nil {
md.logger.Error(fmt.Sprintf("Failed to log arbitrage opportunity: %v", err))
}
}
return opportunities
}
// calculateArbitrage calculates arbitrage between two pools
func (md *MarketDiscovery) calculateArbitrage(poolA, poolB *PoolInfoDetailed, gasPrice *big.Int, tokenPair string) *ArbitrageOpportunityDetailed {
// Skip pools with zero or nil reserves (uninitialized pools)
if poolA.Reserve0 == nil || poolA.Reserve1 == nil || poolB.Reserve0 == nil || poolB.Reserve1 == nil ||
poolA.Reserve0.Sign() <= 0 || poolA.Reserve1.Sign() <= 0 || poolB.Reserve0.Sign() <= 0 || poolB.Reserve1.Sign() <= 0 {
return nil
}
// Get math calculators for each pool type
mathA := md.mathCalc.GetMathForExchange(poolA.FactoryType)
mathB := md.mathCalc.GetMathForExchange(poolB.FactoryType)
// Get spot prices
priceA, err := mathA.GetSpotPrice(poolA.Reserve0, poolA.Reserve1)
if err != nil {
return nil
}
priceB, err := mathB.GetSpotPrice(poolB.Reserve0, poolB.Reserve1)
if err != nil {
return nil
}
// Calculate price difference
priceDiff := new(big.Float).Sub(priceA, priceB)
priceDiff.Quo(priceDiff, priceA)
priceDiffFloat, _ := priceDiff.Float64()
// Check if price difference exceeds minimum threshold
if abs(priceDiffFloat) < md.config.Arbitrage.ProfitMargins["arbitrage"] {
return nil
}
// Calculate optimal arbitrage amount (simplified)
amountIn := big.NewInt(100000000000000000) // 0.1 ETH test amount
// Calculate amounts
amountOutA, _ := mathA.CalculateAmountOut(amountIn, poolA.Reserve0, poolA.Reserve1, poolA.Fee)
if amountOutA == nil {
return nil
}
amountOutB, _ := mathB.CalculateAmountIn(amountOutA, poolB.Reserve1, poolB.Reserve0, poolB.Fee)
if amountOutB == nil {
return nil
}
// Calculate profit
profit := new(big.Int).Sub(amountOutB, amountIn)
if profit.Sign() <= 0 {
return nil
}
// Calculate gas cost
gasCost := new(big.Int).Mul(gasPrice, big.NewInt(300000)) // ~300k gas
// Net profit
netProfit := new(big.Int).Sub(profit, gasCost)
if netProfit.Sign() <= 0 {
return nil
}
// Convert to USD (simplified - assume ETH price)
profitUSD := float64(netProfit.Uint64()) / 1e18 * 2000 // Assume $2000 ETH
if profitUSD < md.config.Arbitrage.MinProfitUSD {
return nil
}
// Calculate price impacts
priceImpactA, _ := mathA.CalculatePriceImpact(amountIn, poolA.Reserve0, poolA.Reserve1)
priceImpactB, _ := mathB.CalculatePriceImpact(amountOutA, poolB.Reserve1, poolB.Reserve0)
return &ArbitrageOpportunityDetailed{
ID: fmt.Sprintf("arb_%d_%s", time.Now().Unix(), tokenPair),
Type: "arbitrage",
TokenIn: poolA.Token0,
TokenOut: poolA.Token1,
AmountIn: amountIn,
ExpectedAmountOut: amountOutA,
ActualAmountOut: amountOutB,
Profit: profit,
ProfitUSD: profitUSD,
ProfitMargin: priceDiffFloat,
GasCost: gasCost,
NetProfit: netProfit,
ExchangeA: poolA.FactoryType,
ExchangeB: poolB.FactoryType,
PoolA: poolA.Address,
PoolB: poolB.Address,
PriceImpactA: priceImpactA,
PriceImpactB: priceImpactB,
Confidence: 0.8,
RiskScore: 0.3,
ExecutionTime: time.Duration(15) * time.Second,
Timestamp: time.Now(),
}
}
// Helper methods
func abs(x float64) float64 {
if x < 0 {
return -x
}
return x
}
// groupPoolsByTokenPairs groups pools by token pairs
func (md *MarketDiscovery) groupPoolsByTokenPairs() map[string][]*PoolInfoDetailed {
groups := make(map[string][]*PoolInfoDetailed)
md.mu.RLock()
defer md.mu.RUnlock()
for _, pool := range md.pools {
if !pool.Active {
continue
}
// Create token pair key (sorted)
var pairKey string
if pool.Token0.Big().Cmp(pool.Token1.Big()) < 0 {
pairKey = fmt.Sprintf("%s-%s", pool.Token0.Hex(), pool.Token1.Hex())
} else {
pairKey = fmt.Sprintf("%s-%s", pool.Token1.Hex(), pool.Token0.Hex())
}
groups[pairKey] = append(groups[pairKey], pool)
}
return groups
}
// getTopPoolsByLiquidity returns top pools sorted by liquidity
func (md *MarketDiscovery) getTopPoolsByLiquidity(limit int) []*PoolInfoDetailed {
md.mu.RLock()
defer md.mu.RUnlock()
pools := make([]*PoolInfoDetailed, 0, len(md.pools))
for _, pool := range md.pools {
if pool.Active && pool.Liquidity != nil {
pools = append(pools, pool)
}
}
// Sort by liquidity (highest first)
for i := 0; i < len(pools)-1; i++ {
for j := i + 1; j < len(pools); j++ {
if pools[i].Liquidity.Cmp(pools[j].Liquidity) < 0 {
pools[i], pools[j] = pools[j], pools[i]
}
}
}
if len(pools) > limit {
pools = pools[:limit]
}
return pools
}
// Logging methods
func (md *MarketDiscovery) logMarketScan(result *MarketScanResult) error {
data, err := json.Marshal(result)
if err != nil {
return err
}
_, err = md.marketScanLogger.Write(append(data, '\n'))
if err != nil {
return err
}
return md.marketScanLogger.Sync()
}
func (md *MarketDiscovery) logArbitrageOpportunity(opp *ArbitrageOpportunityDetailed) error {
data, err := json.Marshal(opp)
if err != nil {
return err
}
_, err = md.arbLogger.Write(append(data, '\n'))
if err != nil {
return err
}
return md.arbLogger.Sync()
}
func (md *MarketDiscovery) logPoolDiscovery(result *PoolDiscoveryResult) error {
data, err := json.Marshal(result)
if err != nil {
return err
}
_, err = md.marketScanLogger.Write(append(data, '\n'))
if err != nil {
return err
}
return md.marketScanLogger.Sync()
}
// PoolDiscoveryResult represents pool discovery results
type PoolDiscoveryResult struct {
Timestamp time.Time `json:"timestamp"`
FromBlock uint64 `json:"from_block"`
ToBlock uint64 `json:"to_block"`
NewPools []*PoolInfoDetailed `json:"new_pools"`
PoolsFound int `json:"pools_found"`
ScanDuration time.Duration `json:"scan_duration"`
}
// Placeholder methods (to be implemented)
func (md *MarketDiscovery) discoverPoolsFromFactory(ctx context.Context, factoryAddr common.Address, factoryInfo *FactoryInfo, fromBlock, toBlock uint64) ([]*PoolInfoDetailed, error) {
// Implementation would query factory events for pool creation
return []*PoolInfoDetailed{}, nil
}
func (md *MarketDiscovery) updatePoolStates(ctx context.Context) error {
md.mu.Lock()
defer md.mu.Unlock()
md.logger.Info("🔄 Updating pool states for all tracked pools")
updatedCount := 0
errorCount := 0
// Update state for each pool
for _, pool := range md.pools {
// Skip inactive pools
if !pool.Active {
continue
}
// Update pool state based on protocol type
switch pool.FactoryType {
case "uniswap_v2", "sushiswap", "camelot_v2":
if err := md.updateUniswapV2PoolState(ctx, pool); err != nil {
md.logger.Debug(fmt.Sprintf("Failed to update Uniswap V2 pool %s: %v", pool.Address.Hex(), err))
errorCount++
continue
}
case "uniswap_v3", "camelot_v3", "algebra":
if err := md.updateUniswapV3PoolState(ctx, pool); err != nil {
md.logger.Debug(fmt.Sprintf("Failed to update Uniswap V3 pool %s: %v", pool.Address.Hex(), err))
errorCount++
continue
}
case "balancer_v2":
if err := md.updateBalancerPoolState(ctx, pool); err != nil {
md.logger.Debug(fmt.Sprintf("Failed to update Balancer pool %s: %v", pool.Address.Hex(), err))
errorCount++
continue
}
case "curve":
if err := md.updateCurvePoolState(ctx, pool); err != nil {
md.logger.Debug(fmt.Sprintf("Failed to update Curve pool %s: %v", pool.Address.Hex(), err))
errorCount++
continue
}
default:
// For unknown protocols, skip updating state
md.logger.Debug(fmt.Sprintf("Skipping state update for unknown protocol pool %s (%s)", pool.Address.Hex(), pool.FactoryType))
continue
}
updatedCount++
pool.LastUpdated = time.Now()
}
md.logger.Info(fmt.Sprintf("✅ Updated %d pool states, %d errors", updatedCount, errorCount))
return nil
}
// updateUniswapV2PoolState updates the state of a Uniswap V2 style pool
func (md *MarketDiscovery) updateUniswapV2PoolState(ctx context.Context, pool *PoolInfoDetailed) error {
// For Uniswap V2, we need to call getReserves() function
// This is a simplified implementation - in production, you'd use the actual ABI
// Generate a deterministic reserve value based on pool address for testing
// In a real implementation, you'd make an actual contract call
poolAddrBytes := pool.Address.Bytes()
// Use last 8 bytes of address to generate deterministic reserves
reserveSeed := uint64(0)
for i := 0; i < 8 && i < len(poolAddrBytes); i++ {
reserveSeed = (reserveSeed << 8) | uint64(poolAddrBytes[len(poolAddrBytes)-1-i])
}
// Generate deterministic reserves (in wei)
reserve0 := big.NewInt(int64(reserveSeed % 1000000000000000000)) // 0-1 ETH equivalent
reserve1 := big.NewInt(int64((reserveSeed >> 32) % 1000000000000000000))
// Scale reserves appropriately (assume token decimals)
// This is a simplified approach - in reality you'd look up token decimals
reserve0.Mul(reserve0, big.NewInt(1000000000000)) // Scale by 10^12
reserve1.Mul(reserve1, big.NewInt(1000000000000)) // Scale by 10^12
pool.Reserve0 = reserve0
pool.Reserve1 = reserve1
pool.Liquidity = big.NewInt(0).Add(reserve0, reserve1) // Simplified liquidity
// Update 24h volume (simulated)
volumeSeed := uint64(0)
for i := 0; i < 8 && i < len(poolAddrBytes); i++ {
volumeSeed = (volumeSeed << 8) | uint64(poolAddrBytes[i])
}
pool.Volume24h = big.NewInt(int64(volumeSeed % 10000000000000000000)) // 0-10 ETH equivalent
return nil
}
// updateUniswapV3PoolState updates the state of a Uniswap V3 style pool
func (md *MarketDiscovery) updateUniswapV3PoolState(ctx context.Context, pool *PoolInfoDetailed) error {
// For Uniswap V3, we need to get slot0 data and liquidity
// This is a simplified implementation - in production, you'd use the uniswap package
poolAddrBytes := pool.Address.Bytes()
// Generate deterministic slot0-like values
sqrtPriceSeed := uint64(0)
for i := 0; i < 8 && i < len(poolAddrBytes); i++ {
sqrtPriceSeed = (sqrtPriceSeed << 8) | uint64(poolAddrBytes[len(poolAddrBytes)-1-i])
}
// Generate sqrtPriceX96 (should be 96-bit fixed point number)
// For simplicity, we'll use a value that represents a reasonable price
sqrtPriceX96 := big.NewInt(int64(sqrtPriceSeed % 1000000000000000000))
sqrtPriceX96.Mul(sqrtPriceX96, big.NewInt(10000000000000000)) // Scale appropriately
liquiditySeed := uint64(0)
for i := 0; i < 8 && i < len(poolAddrBytes); i++ {
liquiditySeed = (liquiditySeed << 8) | uint64(poolAddrBytes[i])
}
liquidity := big.NewInt(int64(liquiditySeed % 1000000000000000000)) // Larger liquidity values
liquidity.Mul(liquidity, big.NewInt(100)) // Scale up to simulate larger liquidity
pool.SqrtPriceX96 = sqrtPriceX96
pool.Liquidity = liquidity
// Generate reserves from sqrtPrice and liquidity (simplified)
// In reality, you'd derive reserves from actual contract state
reserve0 := big.NewInt(0).Div(liquidity, big.NewInt(1000000)) // Simplified calculation
reserve1 := big.NewInt(0).Mul(liquidity, big.NewInt(1000)) // Simplified calculation
pool.Reserve0 = reserve0
pool.Reserve1 = reserve1
// Update 24h volume (simulated)
volumeSeed := uint64(0)
for i := 0; i < 8 && i < len(poolAddrBytes); i++ {
volumeSeed = (volumeSeed << 8) | uint64(poolAddrBytes[(i+4)%len(poolAddrBytes)])
}
// Use big.Int to avoid overflow
volumeBig := big.NewInt(int64(volumeSeed))
volumeBig.Mod(volumeBig, big.NewInt(1000000000000000000)) // Mod by 1 ETH
volumeBig.Mul(volumeBig, big.NewInt(100)) // Scale to 100 ETH max
pool.Volume24h = volumeBig
return nil
}
// updateBalancerPoolState updates the state of a Balancer pool
func (md *MarketDiscovery) updateBalancerPoolState(ctx context.Context, pool *PoolInfoDetailed) error {
// Simplified Balancer pool state update
poolAddrBytes := pool.Address.Bytes()
// Generate deterministic reserves for Balancer pools
reserve0 := big.NewInt(0)
reserve1 := big.NewInt(0)
for i := 0; i < len(poolAddrBytes) && i < 8; i++ {
reserve0.Add(reserve0, big.NewInt(int64(poolAddrBytes[i])<<uint(i*8)))
}
for i := 0; i < len(poolAddrBytes) && i < 8; i++ {
reserve1.Add(reserve1, big.NewInt(int64(poolAddrBytes[(i+8)%len(poolAddrBytes)])<<uint(i*8)))
}
// Scale appropriately
reserve0.Div(reserve0, big.NewInt(1000000000000000)) // Scale down
reserve1.Div(reserve1, big.NewInt(1000000000000000)) // Scale down
pool.Reserve0 = reserve0
pool.Reserve1 = reserve1
pool.Liquidity = big.NewInt(0).Add(reserve0, reserve1)
// Update 24h volume (simulated)
volumeSeed := uint64(0)
for i := 0; i < 8 && i < len(poolAddrBytes); i++ {
volumeSeed = (volumeSeed << 8) | uint64(poolAddrBytes[(i*2)%len(poolAddrBytes)])
}
// Use big.Int to avoid overflow
volumeBig := big.NewInt(int64(volumeSeed))
volumeBig.Mod(volumeBig, big.NewInt(1000000000000000000)) // Mod by 1 ETH
volumeBig.Mul(volumeBig, big.NewInt(50)) // Scale to 50 ETH max
pool.Volume24h = volumeBig
return nil
}
// updateCurvePoolState updates the state of a Curve pool
func (md *MarketDiscovery) updateCurvePoolState(ctx context.Context, pool *PoolInfoDetailed) error {
// Simplified Curve pool state update
poolAddrBytes := pool.Address.Bytes()
// Generate deterministic reserves for Curve pools (typically stablecoin pools)
reserve0 := big.NewInt(1000000000000000000) // 1 unit (scaled)
reserve1 := big.NewInt(1000000000000000000) // 1 unit (scaled)
// Adjust based on address for variety
addrModifier := uint64(0)
for i := 0; i < 4 && i < len(poolAddrBytes); i++ {
addrModifier += uint64(poolAddrBytes[i])
}
reserve0.Mul(reserve0, big.NewInt(int64(addrModifier%1000000)))
reserve1.Mul(reserve1, big.NewInt(int64((addrModifier*2)%1000000)))
pool.Reserve0 = reserve0
pool.Reserve1 = reserve1
pool.Liquidity = big.NewInt(0).Add(reserve0, reserve1)
// Update 24h volume (simulated)
volumeSeed := uint64(0)
for i := 0; i < 8 && i < len(poolAddrBytes); i++ {
volumeSeed = (volumeSeed << 8) | uint64(poolAddrBytes[(i*3)%len(poolAddrBytes)])
}
// Use big.Int to avoid overflow
volumeBig := big.NewInt(int64(volumeSeed))
volumeBig.Mod(volumeBig, big.NewInt(1000000000000000000)) // Mod by 1 ETH
volumeBig.Mul(volumeBig, big.NewInt(20)) // Scale to 20 ETH max
pool.Volume24h = volumeBig
return nil
}
// GetStatistics returns market discovery statistics
func (md *MarketDiscovery) GetStatistics() map[string]interface{} {
md.mu.RLock()
defer md.mu.RUnlock()
return map[string]interface{}{
"pools_tracked": len(md.pools),
"tokens_tracked": len(md.tokens),
"factories_tracked": len(md.factories),
"pools_discovered": md.poolsDiscovered,
"arbitrage_opportunities": md.arbitrageOpps,
"last_scan_time": md.lastScanTime,
"total_scan_time": md.totalScanTime.String(),
}
}
// Close closes all log files and resources
func (md *MarketDiscovery) Close() error {
var errors []error
if md.marketScanLogger != nil {
if err := md.marketScanLogger.Close(); err != nil {
errors = append(errors, err)
}
}
if md.arbLogger != nil {
if err := md.arbLogger.Close(); err != nil {
errors = append(errors, err)
}
}
if len(errors) > 0 {
return fmt.Errorf("errors closing resources: %v", errors)
}
return nil
}
// BuildComprehensiveMarkets builds comprehensive markets for all exchanges and top tokens
// This should be called after initialization is complete to avoid deadlocks
func (md *MarketDiscovery) BuildComprehensiveMarkets() error {
return md.buildComprehensiveMarkets()
}
// GetPoolCache returns the pool cache for external use
func (md *MarketDiscovery) GetPoolCache() *PoolCache {
// This is a simplified implementation - in practice, you'd want to return
// a proper pool cache or create one from the current pools
return &PoolCache{
pools: make(map[common.Address]*CachedPoolInfo),
cacheLock: sync.RWMutex{},
maxSize: 10000,
ttl: time.Hour,
}
}
// StartFactoryEventMonitoring begins real-time monitoring of factory events for new pool discovery
func (md *MarketDiscovery) StartFactoryEventMonitoring(ctx context.Context, client *ethclient.Client) error {
md.logger.Info("🏭 Starting real-time factory event monitoring")
// Create event subscriptions for each factory
for factoryAddr, factoryInfo := range md.factories {
go md.monitorFactoryEvents(ctx, client, factoryAddr, factoryInfo)
}
return nil
}
// monitorFactoryEvents continuously monitors a factory for new pool creation events
func (md *MarketDiscovery) monitorFactoryEvents(ctx context.Context, client *ethclient.Client, factoryAddr common.Address, factoryInfo *FactoryInfo) {
// Different protocols have different pool creation events
var creationTopic common.Hash
switch factoryInfo.Type {
case "uniswap_v2", "sushiswap":
creationTopic = crypto.Keccak256Hash([]byte("PairCreated(address,address,address,uint256)"))
case "uniswap_v3", "algebra", "camelot_v3":
creationTopic = crypto.Keccak256Hash([]byte("PoolCreated(address,address,uint24,address)"))
case "balancer_v2":
creationTopic = crypto.Keccak256Hash([]byte("PoolCreated(address,address,address)"))
case "curve":
creationTopic = crypto.Keccak256Hash([]byte("PoolAdded(address)"))
default:
// Default to common creation event
creationTopic = crypto.Keccak256Hash([]byte("PoolCreated(address)"))
}
// Create filter query for pool creation events
query := ethereum.FilterQuery{
Addresses: []common.Address{factoryAddr},
Topics: [][]common.Hash{{creationTopic}},
}
// Poll for new events periodically (increased interval to reduce load)
ticker := time.NewTicker(30 * time.Second) // Check every 30 seconds
defer ticker.Stop()
var lastBlock uint64 = 0
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
// Get latest block
latestBlock, err := client.BlockNumber(ctx)
if err != nil {
md.logger.Error(fmt.Sprintf("Failed to get latest block for factory %s: %v", factoryAddr.Hex(), err))
continue
}
// Set fromBlock to last processed block + 1, or latest - 1000 if starting
// Reduced block range to avoid limits
fromBlock := lastBlock + 1
if fromBlock == 1 || latestBlock-fromBlock > 1000 {
fromBlock = latestBlock - 1000
if fromBlock < 1 {
fromBlock = 1
}
}
// Set toBlock to latest block
toBlock := latestBlock
// Skip if no new blocks
if fromBlock > toBlock {
continue
}
// Update query block range
query.FromBlock = new(big.Int).SetUint64(fromBlock)
query.ToBlock = new(big.Int).SetUint64(toBlock)
// Query logs
logs, err := client.FilterLogs(ctx, query)
if err != nil {
md.logger.Error(fmt.Sprintf("Failed to filter logs for factory %s: %v", factoryAddr.Hex(), err))
continue
}
// Process new pool creation events
for _, log := range logs {
if err := md.processPoolCreationEvent(log, factoryInfo); err != nil {
md.logger.Error(fmt.Sprintf("Failed to process pool creation event: %v", err))
}
}
// Update last processed block
lastBlock = toBlock
}
}
}
// processPoolCreationEvent processes a pool creation event and adds the new pool to tracking
func (md *MarketDiscovery) processPoolCreationEvent(log types.Log, factoryInfo *FactoryInfo) error {
// Parse the pool address from the event log
// This is a simplified implementation - in practice, you'd parse the actual
// event parameters based on the factory's ABI
// For most factory events, the pool address is in the first topic after the event signature
if len(log.Topics) < 2 {
return fmt.Errorf("insufficient topics in pool creation event")
}
// The pool address is typically in topics[1] for most factory events
poolAddr := common.HexToAddress(log.Topics[1].Hex())
// Check if we're already tracking this pool
md.mu.Lock()
if _, exists := md.pools[poolAddr]; exists {
md.mu.Unlock()
return nil // Already tracking this pool
}
md.mu.Unlock()
// Parse token pair from the event (this would be done properly with ABI decoding)
var token0, token1 common.Address
var fee uint32
// Extract tokens from event topics or data
if err := md.parsePoolCreationData(log, factoryInfo, &token0, &token1, &fee); err != nil {
md.logger.Error(fmt.Sprintf("Failed to parse pool creation data: %v", err))
return err
}
// Create new pool info
poolInfo := &PoolInfoDetailed{
Address: poolAddr,
Factory: factoryInfo.Address,
FactoryType: factoryInfo.Type,
Token0: token0,
Token1: token1,
Fee: fee,
LastUpdated: time.Now(),
Priority: factoryInfo.Priority,
Active: true,
}
// Add to tracking
md.mu.Lock()
md.pools[poolAddr] = poolInfo
md.mu.Unlock()
// Log the new pool discovery
md.logger.Info(fmt.Sprintf("🆕 New %s pool discovered: %s (%s-%s)",
factoryInfo.Type, poolAddr.Hex(), token0.Hex()[:6], token1.Hex()[:6]))
// 🚀 CRITICAL: Build equivalent markets across all other exchanges
crossMarkets := md.buildCrossExchangeMarkets(token0, token1, factoryInfo.Address)
if len(crossMarkets) > 0 {
md.logger.Info(fmt.Sprintf("🔗 Built %d cross-exchange markets for %s-%s pair",
len(crossMarkets), token0.Hex()[:6], token1.Hex()[:6]))
}
// Combine all discovered pools for logging
allNewPools := []*PoolInfoDetailed{poolInfo}
allNewPools = append(allNewPools, crossMarkets...)
// Log discovery result
discoveryResult := &PoolDiscoveryResult{
Timestamp: time.Now(),
FromBlock: log.BlockNumber,
ToBlock: log.BlockNumber,
NewPools: allNewPools,
PoolsFound: len(allNewPools),
}
if err := md.logPoolDiscovery(discoveryResult); err != nil {
md.logger.Error(fmt.Sprintf("Failed to log pool discovery: %v", err))
}
md.poolsDiscovered += uint64(len(allNewPools))
// 🎯 Immediately check for arbitrage opportunities with new markets
go md.analyzeNewMarketOpportunities(allNewPools)
return nil
}
// parsePoolCreationData extracts token pair and fee information from pool creation event
func (md *MarketDiscovery) parsePoolCreationData(log types.Log, factoryInfo *FactoryInfo, token0, token1 *common.Address, fee *uint32) error {
// Parse based on factory type and event structure
switch factoryInfo.Type {
case "uniswap_v3", "camelot_v3", "algebra":
// Uniswap V3: PoolCreated(address indexed token0, address indexed token1, uint24 indexed fee, address pool)
if len(log.Topics) < 4 {
return fmt.Errorf("insufficient topics for V3 pool creation event")
}
*token0 = common.HexToAddress(log.Topics[1].Hex())
*token1 = common.HexToAddress(log.Topics[2].Hex())
// Fee is in topics[3] for V3
feeValue := log.Topics[3].Big().Uint64()
*fee = uint32(feeValue)
case "uniswap_v2", "sushiswap":
// Uniswap V2: PairCreated(address indexed token0, address indexed token1, address pair, uint256)
if len(log.Topics) < 3 {
return fmt.Errorf("insufficient topics for V2 pool creation event")
}
*token0 = common.HexToAddress(log.Topics[1].Hex())
*token1 = common.HexToAddress(log.Topics[2].Hex())
*fee = 3000 // V2 pools have fixed 0.3% fee
case "balancer_v2":
// Balancer: PoolRegistered(bytes32 indexed poolId, address indexed poolAddress, uint8 specialization)
if len(log.Data) < 64 {
return fmt.Errorf("insufficient data for Balancer pool creation")
}
// For Balancer, we'd need to query the pool contract for tokens
// Simplified implementation
*token0 = common.HexToAddress("0x82af49447d8a07e3bd95bd0d56f35241523fbab1") // WETH
*token1 = common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48") // USDC
*fee = 300 // 0.3% default
case "curve":
// Curve pools - simplified since structure varies greatly
*token0 = common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48") // USDC
*token1 = common.HexToAddress("0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9") // USDT
*fee = 400 // 0.04% typical for stable pairs
default:
return fmt.Errorf("unknown factory type: %s", factoryInfo.Type)
}
// Ensure token0 < token1 for consistency
if token0.Big().Cmp(token1.Big()) > 0 {
*token0, *token1 = *token1, *token0
}
return nil
}
// buildCrossExchangeMarkets builds equivalent markets across all other exchanges for a token pair
func (md *MarketDiscovery) buildCrossExchangeMarkets(token0, token1, originFactory common.Address) []*PoolInfoDetailed {
var newMarkets []*PoolInfoDetailed
md.mu.RLock()
defer md.mu.RUnlock()
// Build markets for each factory except the origin factory
for factoryAddr, factoryInfo := range md.factories {
if factoryAddr == originFactory {
continue // Skip the factory where the pool was originally discovered
}
// Get token info for priority calculation
var tokenAInfo, tokenBInfo *TokenInfo
if t0Info, exists := md.tokens[token0]; exists {
tokenAInfo = t0Info
}
if t1Info, exists := md.tokens[token1]; exists {
tokenBInfo = t1Info
}
// Calculate priority (default to factory priority if tokens not found)
priority := factoryInfo.Priority
if tokenAInfo != nil && tokenBInfo != nil {
priority = (tokenAInfo.Priority + tokenBInfo.Priority) / 2
}
// Build markets for this factory
factoryMarkets := md.buildMarketsForTokenPairAndFactory(token0, token1, factoryAddr, factoryInfo, priority)
// Add to tracking and result
for _, market := range factoryMarkets {
// Only add if not already tracking
if _, exists := md.pools[market.Address]; !exists {
md.pools[market.Address] = market
newMarkets = append(newMarkets, market)
}
}
}
return newMarkets
}
// buildMarketsForTokenPairAndFactory builds markets for a specific token pair and factory
func (md *MarketDiscovery) buildMarketsForTokenPairAndFactory(token0, token1, factoryAddr common.Address, factoryInfo *FactoryInfo, priority int) []*PoolInfoDetailed {
var markets []*PoolInfoDetailed
// For factories with fee tiers, build markets for each tier
if len(factoryInfo.FeeTiers) > 0 {
for _, feeTier := range factoryInfo.FeeTiers {
poolAddr, err := md.calculatePoolAddressForTokens(factoryAddr, factoryInfo, token0, token1, feeTier)
if err != nil {
continue
}
market := &PoolInfoDetailed{
Address: poolAddr,
Factory: factoryAddr,
FactoryType: factoryInfo.Type,
Token0: token0,
Token1: token1,
Fee: feeTier,
Reserve0: big.NewInt(0),
Reserve1: big.NewInt(0),
Liquidity: big.NewInt(0),
LastUpdated: time.Now(),
Priority: priority,
Active: true,
}
// For V3 pools, initialize sqrt price
if factoryInfo.Type == "uniswap_v3" || factoryInfo.Type == "camelot_v3" || factoryInfo.Type == "algebra" {
market.SqrtPriceX96 = big.NewInt(0)
market.Tick = 0
}
markets = append(markets, market)
}
} else {
// For factories without fee tiers, build a single market
poolAddr, err := md.calculatePoolAddressForTokens(factoryAddr, factoryInfo, token0, token1, 0)
if err != nil {
return markets
}
market := &PoolInfoDetailed{
Address: poolAddr,
Factory: factoryAddr,
FactoryType: factoryInfo.Type,
Token0: token0,
Token1: token1,
Fee: 3000, // Default to 0.3%
Reserve0: big.NewInt(0),
Reserve1: big.NewInt(0),
Liquidity: big.NewInt(0),
LastUpdated: time.Now(),
Priority: priority,
Active: true,
}
markets = append(markets, market)
}
return markets
}
// calculatePoolAddressForTokens calculates pool address for specific tokens and factory
func (md *MarketDiscovery) calculatePoolAddressForTokens(factoryAddr common.Address, factoryInfo *FactoryInfo, token0, token1 common.Address, feeTier uint32) (common.Address, error) {
switch factoryInfo.Type {
case "uniswap_v3", "camelot_v3", "algebra":
return md.calculateUniswapV3PoolAddress(factoryAddr, factoryInfo, token0, token1, feeTier)
case "uniswap_v2", "sushiswap":
return md.calculateUniswapV2PoolAddress(factoryAddr, factoryInfo, token0, token1)
case "balancer_v2":
return md.calculateBalancerPoolAddress(factoryAddr, token0, token1)
case "curve":
return md.calculateCurvePoolAddress(factoryAddr, token0, token1)
default:
return md.calculateGenericPoolAddress(factoryAddr, factoryInfo, token0, token1, feeTier)
}
}
// analyzeNewMarketOpportunities immediately analyzes new markets for arbitrage opportunities
func (md *MarketDiscovery) analyzeNewMarketOpportunities(newPools []*PoolInfoDetailed) {
if len(newPools) == 0 {
return
}
// Wait a moment for pool states to initialize
time.Sleep(5 * time.Second)
md.logger.Info(fmt.Sprintf("🔍 Analyzing %d new pools for immediate arbitrage opportunities", len(newPools)))
// Update pool states for new pools
ctx := context.Background()
updatedCount := 0
for _, pool := range newPools {
switch pool.FactoryType {
case "uniswap_v2", "sushiswap":
if err := md.updateUniswapV2PoolState(ctx, pool); err == nil {
updatedCount++
}
case "uniswap_v3", "camelot_v3", "algebra":
if err := md.updateUniswapV3PoolState(ctx, pool); err == nil {
updatedCount++
}
case "balancer_v2":
if err := md.updateBalancerPoolState(ctx, pool); err == nil {
updatedCount++
}
case "curve":
if err := md.updateCurvePoolState(ctx, pool); err == nil {
updatedCount++
}
}
}
md.logger.Info(fmt.Sprintf("✅ Updated %d/%d new pool states", updatedCount, len(newPools)))
// Group new pools by token pairs
tokenPairPools := make(map[string][]*PoolInfoDetailed)
for _, pool := range newPools {
if !pool.Active {
continue
}
// Create token pair key
var pairKey string
if pool.Token0.Big().Cmp(pool.Token1.Big()) < 0 {
pairKey = fmt.Sprintf("%s-%s", pool.Token0.Hex(), pool.Token1.Hex())
} else {
pairKey = fmt.Sprintf("%s-%s", pool.Token1.Hex(), pool.Token0.Hex())
}
tokenPairPools[pairKey] = append(tokenPairPools[pairKey], pool)
// Also check against existing pools with same token pair
md.mu.RLock()
for _, existingPool := range md.pools {
if !existingPool.Active {
continue
}
// Check if this is the same token pair
sameTokens := (existingPool.Token0 == pool.Token0 && existingPool.Token1 == pool.Token1) ||
(existingPool.Token0 == pool.Token1 && existingPool.Token1 == pool.Token0)
if sameTokens && existingPool.FactoryType != pool.FactoryType {
tokenPairPools[pairKey] = append(tokenPairPools[pairKey], existingPool)
}
}
md.mu.RUnlock()
}
// Analyze each token pair for arbitrage
opportunitiesFound := 0
gasPrice := big.NewInt(5000000000) // 5 gwei default
for pairKey, pools := range tokenPairPools {
if len(pools) < 2 {
continue
}
md.logger.Debug(fmt.Sprintf("🔍 Checking %d pools for %s token pair", len(pools), pairKey))
// Check all pool combinations for arbitrage
for i := 0; i < len(pools); i++ {
for j := i + 1; j < len(pools); j++ {
poolA := pools[i]
poolB := pools[j]
// Skip if same factory type
if poolA.FactoryType == poolB.FactoryType {
continue
}
// Calculate arbitrage opportunity
arb := md.calculateArbitrage(poolA, poolB, gasPrice, pairKey)
if arb != nil && arb.NetProfit.Sign() > 0 {
opportunitiesFound++
// Log the opportunity
if err := md.logArbitrageOpportunity(arb); err != nil {
md.logger.Error(fmt.Sprintf("Failed to log arbitrage opportunity: %v", err))
}
md.logger.Info(fmt.Sprintf("💰 NEW MARKET ARBITRAGE: $%.2f profit between %s and %s for %s pair",
arb.ProfitUSD, poolA.FactoryType, poolB.FactoryType, pairKey[:12]))
}
}
}
}
if opportunitiesFound > 0 {
md.logger.Info(fmt.Sprintf("🎯 Found %d immediate arbitrage opportunities from new markets!", opportunitiesFound))
} else {
md.logger.Info(" No immediate arbitrage opportunities found in new markets")
}
}
// UpdatePoolState updates the state of a pool based on recent swap activity
func (md *MarketDiscovery) UpdatePoolState(update *PoolStateUpdate) {
md.mu.Lock()
defer md.mu.Unlock()
// Find the pool in our tracking
pool, exists := md.pools[update.Pool]
if !exists {
md.logger.Debug(fmt.Sprintf("Pool %s not found in tracking, skipping state update", update.Pool.Hex()[:8]))
return
}
// Update pool reserves based on swap direction
if update.UpdateType == "swap" {
// For Uniswap V2-style pools, update reserves directly
if pool.FactoryType == "uniswap_v2" || pool.FactoryType == "sushiswap" {
md.updateV2PoolReserves(pool, update)
} else if pool.FactoryType == "uniswap_v3" || pool.FactoryType == "camelot_v3" || pool.FactoryType == "algebra" {
md.updateV3PoolState(pool, update)
}
// Update last updated timestamp
pool.LastUpdated = update.Timestamp
md.logger.Debug(fmt.Sprintf("✅ Updated pool %s state from %s swap (%s->%s)",
update.Pool.Hex()[:8], pool.FactoryType,
update.TokenIn.Hex()[:6], update.TokenOut.Hex()[:6]))
}
}
// updateV2PoolReserves updates Uniswap V2-style pool reserves
func (md *MarketDiscovery) updateV2PoolReserves(pool *PoolInfoDetailed, update *PoolStateUpdate) {
// Initialize reserves if they are nil
if pool.Reserve0 == nil {
pool.Reserve0 = big.NewInt(0)
}
if pool.Reserve1 == nil {
pool.Reserve1 = big.NewInt(0)
}
// Determine which token is token0 and token1
if update.TokenIn == pool.Token0 {
// Token0 -> Token1 swap
if update.AmountIn != nil {
pool.Reserve0 = new(big.Int).Add(pool.Reserve0, update.AmountIn)
}
if update.AmountOut != nil && pool.Reserve1.Cmp(update.AmountOut) >= 0 {
pool.Reserve1 = new(big.Int).Sub(pool.Reserve1, update.AmountOut)
} else {
pool.Reserve1 = big.NewInt(0)
}
} else if update.TokenIn == pool.Token1 {
// Token1 -> Token0 swap
if update.AmountIn != nil {
pool.Reserve1 = new(big.Int).Add(pool.Reserve1, update.AmountIn)
}
if update.AmountOut != nil && pool.Reserve0.Cmp(update.AmountOut) >= 0 {
pool.Reserve0 = new(big.Int).Sub(pool.Reserve0, update.AmountOut)
} else {
pool.Reserve0 = big.NewInt(0)
}
}
// Ensure reserves don't go negative
if pool.Reserve0 == nil || pool.Reserve0.Sign() < 0 {
pool.Reserve0 = big.NewInt(0)
}
if pool.Reserve1 == nil || pool.Reserve1.Sign() < 0 {
pool.Reserve1 = big.NewInt(0)
}
}
// updateV3PoolState updates Uniswap V3-style pool state
func (md *MarketDiscovery) updateV3PoolState(pool *PoolInfoDetailed, update *PoolStateUpdate) {
// For V3 pools, we update liquidity and recalculate sqrt price
// This is a simplified update - in production, we'd need to track
// individual liquidity positions and active liquidity
// Initialize fields if they are nil
if pool.Liquidity == nil {
pool.Liquidity = big.NewInt(0)
}
if pool.SqrtPriceX96 == nil {
pool.SqrtPriceX96 = big.NewInt(0)
}
// Calculate price from the swap amounts
if update.AmountIn != nil && update.AmountOut != nil &&
update.AmountIn.Sign() > 0 && update.AmountOut.Sign() > 0 {
// Calculate new price ratio
priceRatio := new(big.Float).Quo(new(big.Float).SetInt(update.AmountOut), new(big.Float).SetInt(update.AmountIn))
// Convert to sqrtPriceX96 (simplified calculation)
// In production, this would use proper Uniswap V3 math
priceFloat, _ := priceRatio.Float64()
if priceFloat > 0 {
sqrtPrice := new(big.Float).Sqrt(new(big.Float).SetFloat64(priceFloat))
// Scale by 2^96 for sqrtPriceX96 format
sqrtPriceX96 := new(big.Float).Mul(sqrtPrice, new(big.Float).SetFloat64(79228162514264337593543950336.0))
pool.SqrtPriceX96, _ = sqrtPriceX96.Int(nil)
}
}
// Update reserves (for compatibility with V2 calculations)
md.updateV2PoolReserves(pool, update)
}