package profitcalc import ( "context" "fmt" "math/big" "strings" "sync" "time" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" "github.com/fraktal/mev-beta/internal/logger" ) // RealPriceFeed fetches actual on-chain prices from DEX pools type RealPriceFeed struct { logger *logger.Logger client *ethclient.Client priceCache map[string]*PriceData priceMutex *sync.RWMutex updateTicker *time.Ticker stopChan chan struct{} // ABIs for contract calls uniswapV3PoolABI abi.ABI uniswapV2PairABI abi.ABI factoryABI abi.ABI // DEX factory addresses uniswapV3Factory common.Address sushiswapFactory common.Address camelotFactory common.Address } // NewRealPriceFeed creates a new real price feed with on-chain data func NewRealPriceFeed(logger *logger.Logger, client *ethclient.Client) (*RealPriceFeed, error) { // Parse Uniswap V3 Pool ABI uniswapV3PoolABI, err := abi.JSON(strings.NewReader(uniswapV3PoolABIJSON)) if err != nil { return nil, fmt.Errorf("failed to parse Uniswap V3 Pool ABI: %w", err) } // Parse Uniswap V2 Pair ABI uniswapV2PairABI, err := abi.JSON(strings.NewReader(uniswapV2PairABIJSON)) if err != nil { return nil, fmt.Errorf("failed to parse Uniswap V2 Pair ABI: %w", err) } // Parse Factory ABI factoryABI, err := abi.JSON(strings.NewReader(factoryABIJSON)) if err != nil { return nil, fmt.Errorf("failed to parse Factory ABI: %w", err) } rpf := &RealPriceFeed{ logger: logger, client: client, priceCache: make(map[string]*PriceData), priceMutex: &sync.RWMutex{}, stopChan: make(chan struct{}), uniswapV3PoolABI: uniswapV3PoolABI, uniswapV2PairABI: uniswapV2PairABI, factoryABI: factoryABI, // Arbitrum mainnet factory addresses uniswapV3Factory: common.HexToAddress("0x1F98431c8aD98523631AE4a59f267346ea31F984"), sushiswapFactory: common.HexToAddress("0xc35DADB65012eC5796536bD9864eD8773aBc74C4"), camelotFactory: common.HexToAddress("0x6EcCab422D763aC031210895C81787E87B82A80f"), } return rpf, nil } // Start begins real-time price updates func (rpf *RealPriceFeed) Start() { rpf.updateTicker = time.NewTicker(5 * time.Second) // Update every 5 seconds for production go rpf.priceUpdateLoop() rpf.logger.Info("✅ Real price feed started with 5-second update interval") } // Stop stops the price feed func (rpf *RealPriceFeed) Stop() { close(rpf.stopChan) if rpf.updateTicker != nil { rpf.updateTicker.Stop() } rpf.logger.Info("✅ Real price feed stopped") } // priceUpdateLoop continuously updates prices func (rpf *RealPriceFeed) priceUpdateLoop() { for { select { case <-rpf.updateTicker.C: rpf.updateAllPrices() case <-rpf.stopChan: return } } } // updateAllPrices updates all tracked token pairs func (rpf *RealPriceFeed) updateAllPrices() { ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second) defer cancel() // Common trading pairs on Arbitrum pairs := []struct { TokenA common.Address TokenB common.Address Name string }{ { TokenA: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), // WETH TokenB: common.HexToAddress("0xaf88d065e77c8cC2239327C5EDb3A432268e5831"), // USDC Name: "WETH/USDC", }, { TokenA: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), // WETH TokenB: common.HexToAddress("0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9"), // USDT Name: "WETH/USDT", }, { TokenA: common.HexToAddress("0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f"), // WBTC TokenB: common.HexToAddress("0xaf88d065e77c8cC2239327C5EDb3A432268e5831"), // USDC Name: "WBTC/USDC", }, } for _, pair := range pairs { // Query Uniswap V3 go rpf.updatePriceFromUniswapV3(ctx, pair.TokenA, pair.TokenB) // Query SushiSwap go rpf.updatePriceFromV2DEX(ctx, pair.TokenA, pair.TokenB, "SushiSwap", rpf.sushiswapFactory) // Query Camelot go rpf.updatePriceFromV2DEX(ctx, pair.TokenA, pair.TokenB, "Camelot", rpf.camelotFactory) } } // updatePriceFromUniswapV3 fetches real price from Uniswap V3 pool func (rpf *RealPriceFeed) updatePriceFromUniswapV3(ctx context.Context, tokenA, tokenB common.Address) { // Get pool address from factory poolAddress, err := rpf.getUniswapV3Pool(ctx, tokenA, tokenB, 3000) // 0.3% fee tier if err != nil { rpf.logger.Debug(fmt.Sprintf("Failed to get Uniswap V3 pool for %s/%s: %v", tokenA.Hex()[:8], tokenB.Hex()[:8], err)) return } // Create bound contract poolContract := bind.NewBoundContract(poolAddress, rpf.uniswapV3PoolABI, rpf.client, rpf.client, rpf.client) // Call slot0 to get current price var result []interface{} err = poolContract.Call(&bind.CallOpts{Context: ctx}, &result, "slot0") if err != nil { rpf.logger.Debug(fmt.Sprintf("Failed to call slot0 for Uniswap V3 pool: %v", err)) return } if len(result) == 0 { rpf.logger.Debug("Empty result from slot0 call") return } // Extract sqrtPriceX96 from result sqrtPriceX96, ok := result[0].(*big.Int) if !ok { rpf.logger.Debug("Failed to parse sqrtPriceX96 from slot0") return } // Calculate price from sqrtPriceX96 // price = (sqrtPriceX96 / 2^96)^2 q96 := new(big.Int).Lsh(big.NewInt(1), 96) // 2^96 sqrtPrice := new(big.Float).SetInt(sqrtPriceX96) q96Float := new(big.Float).SetInt(q96) sqrtPriceScaled := new(big.Float).Quo(sqrtPrice, q96Float) price := new(big.Float).Mul(sqrtPriceScaled, sqrtPriceScaled) // Get liquidity var liquidityResult []interface{} err = poolContract.Call(&bind.CallOpts{Context: ctx}, &liquidityResult, "liquidity") var liquidity *big.Float if err == nil && len(liquidityResult) > 0 { if liquidityInt, ok := liquidityResult[0].(*big.Int); ok { liquidity = new(big.Float).SetInt(liquidityInt) } } if liquidity == nil { liquidity = big.NewFloat(0) } // Store price data rpf.priceMutex.Lock() defer rpf.priceMutex.Unlock() key := fmt.Sprintf("%s_%s_UniswapV3", tokenA.Hex(), tokenB.Hex()) rpf.priceCache[key] = &PriceData{ TokenA: tokenA, TokenB: tokenB, Price: price, InversePrice: new(big.Float).Quo(big.NewFloat(1), price), Liquidity: liquidity, DEX: "UniswapV3", PoolAddress: poolAddress, LastUpdated: time.Now(), IsValid: true, } rpf.logger.Debug(fmt.Sprintf("✅ Updated UniswapV3 price for %s/%s: %s", tokenA.Hex()[:8], tokenB.Hex()[:8], price.Text('f', 6))) } // updatePriceFromV2DEX fetches real price from V2-style DEX (SushiSwap, Camelot) func (rpf *RealPriceFeed) updatePriceFromV2DEX(ctx context.Context, tokenA, tokenB common.Address, dexName string, factory common.Address) { // Get pair address from factory pairAddress, err := rpf.getV2Pair(ctx, factory, tokenA, tokenB) if err != nil { rpf.logger.Debug(fmt.Sprintf("Failed to get %s pair for %s/%s: %v", dexName, tokenA.Hex()[:8], tokenB.Hex()[:8], err)) return } // Create bound contract pairContract := bind.NewBoundContract(pairAddress, rpf.uniswapV2PairABI, rpf.client, rpf.client, rpf.client) // Call getReserves var result []interface{} err = pairContract.Call(&bind.CallOpts{Context: ctx}, &result, "getReserves") if err != nil { rpf.logger.Debug(fmt.Sprintf("Failed to call getReserves for %s pair: %v", dexName, err)) return } if len(result) < 2 { rpf.logger.Debug(fmt.Sprintf("Invalid result from getReserves for %s", dexName)) return } // Parse reserves reserve0, ok0 := result[0].(*big.Int) reserve1, ok1 := result[1].(*big.Int) if !ok0 || !ok1 { rpf.logger.Debug(fmt.Sprintf("Failed to parse reserves for %s", dexName)) return } // Calculate price (reserve1 / reserve0) reserve0Float := new(big.Float).SetInt(reserve0) reserve1Float := new(big.Float).SetInt(reserve1) price := new(big.Float).Quo(reserve1Float, reserve0Float) // Calculate total liquidity (sum of reserves in tokenB equivalent) liquidity := new(big.Float).Add(reserve1Float, new(big.Float).Mul(reserve0Float, price)) // Store price data rpf.priceMutex.Lock() defer rpf.priceMutex.Unlock() key := fmt.Sprintf("%s_%s_%s", tokenA.Hex(), tokenB.Hex(), dexName) rpf.priceCache[key] = &PriceData{ TokenA: tokenA, TokenB: tokenB, Price: price, InversePrice: new(big.Float).Quo(big.NewFloat(1), price), Liquidity: liquidity, DEX: dexName, PoolAddress: pairAddress, LastUpdated: time.Now(), IsValid: true, } rpf.logger.Debug(fmt.Sprintf("✅ Updated %s price for %s/%s: %s", dexName, tokenA.Hex()[:8], tokenB.Hex()[:8], price.Text('f', 6))) } // getUniswapV3Pool gets pool address from Uniswap V3 factory func (rpf *RealPriceFeed) getUniswapV3Pool(ctx context.Context, tokenA, tokenB common.Address, fee uint32) (common.Address, error) { factoryContract := bind.NewBoundContract(rpf.uniswapV3Factory, rpf.factoryABI, rpf.client, rpf.client, rpf.client) var result []interface{} err := factoryContract.Call(&bind.CallOpts{Context: ctx}, &result, "getPool", tokenA, tokenB, big.NewInt(int64(fee))) if err != nil { return common.Address{}, fmt.Errorf("failed to get pool: %w", err) } if len(result) == 0 { return common.Address{}, fmt.Errorf("no pool found") } poolAddress, ok := result[0].(common.Address) if !ok { return common.Address{}, fmt.Errorf("failed to parse pool address") } if poolAddress == (common.Address{}) { return common.Address{}, fmt.Errorf("pool does not exist") } return poolAddress, nil } // getV2Pair gets pair address from V2-style factory func (rpf *RealPriceFeed) getV2Pair(ctx context.Context, factory, tokenA, tokenB common.Address) (common.Address, error) { factoryContract := bind.NewBoundContract(factory, rpf.factoryABI, rpf.client, rpf.client, rpf.client) var result []interface{} err := factoryContract.Call(&bind.CallOpts{Context: ctx}, &result, "getPair", tokenA, tokenB) if err != nil { return common.Address{}, fmt.Errorf("failed to get pair: %w", err) } if len(result) == 0 { return common.Address{}, fmt.Errorf("no pair found") } pairAddress, ok := result[0].(common.Address) if !ok { return common.Address{}, fmt.Errorf("failed to parse pair address") } if pairAddress == (common.Address{}) { return common.Address{}, fmt.Errorf("pair does not exist") } return pairAddress, nil } // GetPrice retrieves cached price data func (rpf *RealPriceFeed) GetPrice(tokenA, tokenB common.Address, dex string) (*PriceData, error) { rpf.priceMutex.RLock() defer rpf.priceMutex.RUnlock() key := fmt.Sprintf("%s_%s_%s", tokenA.Hex(), tokenB.Hex(), dex) priceData, ok := rpf.priceCache[key] if !ok { return nil, fmt.Errorf("price not found for %s on %s", tokenA.Hex()[:8], dex) } // Check if price is stale (older than 30 seconds for production) if time.Since(priceData.LastUpdated) > 30*time.Second { return nil, fmt.Errorf("price data is stale (last updated: %v)", priceData.LastUpdated) } return priceData, nil } // ABI JSON strings (simplified for key functions) const uniswapV3PoolABIJSON = `[ { "inputs": [], "name": "slot0", "outputs": [ {"internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"}, {"internalType": "int24", "name": "tick", "type": "int24"}, {"internalType": "uint16", "name": "observationIndex", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinality", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinalityNext", "type": "uint16"}, {"internalType": "uint8", "name": "feeProtocol", "type": "uint8"}, {"internalType": "bool", "name": "unlocked", "type": "bool"} ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "liquidity", "outputs": [{"internalType": "uint128", "name": "", "type": "uint128"}], "stateMutability": "view", "type": "function" } ]` const uniswapV2PairABIJSON = `[ { "inputs": [], "name": "getReserves", "outputs": [ {"internalType": "uint112", "name": "reserve0", "type": "uint112"}, {"internalType": "uint112", "name": "reserve1", "type": "uint112"}, {"internalType": "uint32", "name": "blockTimestampLast", "type": "uint32"} ], "stateMutability": "view", "type": "function" } ]` const factoryABIJSON = `[ { "inputs": [ {"internalType": "address", "name": "tokenA", "type": "address"}, {"internalType": "address", "name": "tokenB", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"} ], "name": "getPool", "outputs": [{"internalType": "address", "name": "pool", "type": "address"}], "stateMutability": "view", "type": "function" }, { "inputs": [ {"internalType": "address", "name": "tokenA", "type": "address"}, {"internalType": "address", "name": "tokenB", "type": "address"} ], "name": "getPair", "outputs": [{"internalType": "address", "name": "pair", "type": "address"}], "stateMutability": "view", "type": "function" } ]`