package oracle import ( "context" "fmt" "math/big" "sync" "time" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" "github.com/fraktal/mev-beta/internal/logger" "github.com/fraktal/mev-beta/pkg/uniswap" ) // PriceOracle provides real-time price data for tokens type PriceOracle struct { client *ethclient.Client logger *logger.Logger priceCache map[string]*PriceData cacheMutex sync.RWMutex cacheExpiry time.Duration updateTicker *time.Ticker stopChan chan struct{} chainlinkFeeds map[common.Address]common.Address // token -> chainlink feed uniswapPools map[string]common.Address // token pair -> pool address } // PriceData represents cached price information type PriceData struct { Price *big.Int // Price in wei (18 decimals) Timestamp time.Time Source string // "chainlink", "uniswap", "coingecko" Confidence float64 // 0.0 to 1.0 } // PriceRequest represents a price query type PriceRequest struct { TokenIn common.Address TokenOut common.Address AmountIn *big.Int Timestamp time.Time } // PriceResponse contains the price calculation result type PriceResponse struct { Price *big.Int AmountOut *big.Int SlippageBps *big.Int // basis points (1% = 100 bps) Source string Timestamp time.Time Valid bool } // NewPriceOracle creates a new price oracle instance func NewPriceOracle(client *ethclient.Client, logger *logger.Logger) *PriceOracle { oracle := &PriceOracle{ client: client, logger: logger, priceCache: make(map[string]*PriceData), cacheExpiry: 30 * time.Second, // 30-second cache stopChan: make(chan struct{}), chainlinkFeeds: getChainlinkFeeds(), uniswapPools: getUniswapPools(), } // Start background price updates oracle.updateTicker = time.NewTicker(15 * time.Second) go oracle.backgroundUpdater() return oracle } // GetPrice returns the current price for a token pair func (p *PriceOracle) GetPrice(ctx context.Context, req *PriceRequest) (*PriceResponse, error) { if req.TokenIn == req.TokenOut { return &PriceResponse{ Price: big.NewInt(1e18), // 1:1 ratio AmountOut: new(big.Int).Set(req.AmountIn), Source: "identity", Timestamp: time.Now(), Valid: true, }, nil } // Try multiple price sources in order of preference sources := []func(context.Context, *PriceRequest) (*PriceResponse, error){ p.getChainlinkPrice, p.getUniswapV3Price, p.getUniswapV2Price, } var lastErr error for _, getPrice := range sources { if response, err := getPrice(ctx, req); err == nil && response.Valid { p.cachePrice(req, response) return response, nil } else { lastErr = err } } // Fallback to cached price if available if cached := p.getCachedPrice(req); cached != nil { p.logger.Warn(fmt.Sprintf("Using cached price for %s/%s due to oracle failures", req.TokenIn.Hex(), req.TokenOut.Hex())) return cached, nil } return nil, fmt.Errorf("all price sources failed, last error: %w", lastErr) } // getChainlinkPrice gets price from Chainlink price feeds func (p *PriceOracle) getChainlinkPrice(ctx context.Context, req *PriceRequest) (*PriceResponse, error) { feedAddr, exists := p.chainlinkFeeds[req.TokenIn] if !exists { return nil, fmt.Errorf("no chainlink feed for token %s", req.TokenIn.Hex()) } // Chainlink ABI for latestRoundData() chainlinkABI := `[{"inputs":[],"name":"latestRoundData","outputs":[{"internalType":"uint80","name":"roundId","type":"uint80"},{"internalType":"int256","name":"answer","type":"int256"},{"internalType":"uint256","name":"startedAt","type":"uint256"},{"internalType":"uint256","name":"updatedAt","type":"uint256"},{"internalType":"uint80","name":"answeredInRound","type":"uint80"}],"stateMutability":"view","type":"function"}]` contractABI, err := uniswap.ParseABI(chainlinkABI) if err != nil { return nil, fmt.Errorf("failed to parse chainlink ABI: %w", err) } // Call latestRoundData callData, err := contractABI.Pack("latestRoundData") if err != nil { return nil, fmt.Errorf("failed to pack chainlink call: %w", err) } result, err := p.client.CallContract(ctx, ethereum.CallMsg{ To: &feedAddr, Data: callData, }, nil) if err != nil { return nil, fmt.Errorf("chainlink call failed: %w", err) } // Unpack result unpacked, err := contractABI.Unpack("latestRoundData", result) if err != nil { return nil, fmt.Errorf("failed to unpack chainlink result: %w", err) } if len(unpacked) < 5 { return nil, fmt.Errorf("invalid chainlink response length") } answer, ok := unpacked[1].(*big.Int) if !ok || answer.Sign() <= 0 { return nil, fmt.Errorf("invalid chainlink price: %v", unpacked[1]) } updatedAt, ok := unpacked[3].(*big.Int) if !ok { return nil, fmt.Errorf("invalid chainlink timestamp: %v", unpacked[3]) } // Check if price is stale (older than 1 hour) if time.Since(time.Unix(updatedAt.Int64(), 0)) > time.Hour { return nil, fmt.Errorf("chainlink price is stale") } // Convert amount using chainlink price // Chainlink prices are typically 8 decimals, convert to 18 priceWei := new(big.Int).Mul(answer, big.NewInt(1e10)) amountOut := new(big.Int).Mul(req.AmountIn, priceWei) amountOut.Div(amountOut, big.NewInt(1e18)) return &PriceResponse{ Price: priceWei, AmountOut: amountOut, SlippageBps: big.NewInt(0), // Chainlink has no slippage Source: "chainlink", Timestamp: time.Unix(updatedAt.Int64(), 0), Valid: true, }, nil } // getUniswapV3Price gets price from Uniswap V3 pools func (p *PriceOracle) getUniswapV3Price(ctx context.Context, req *PriceRequest) (*PriceResponse, error) { poolKey := fmt.Sprintf("%s-%s", req.TokenIn.Hex(), req.TokenOut.Hex()) poolAddr, exists := p.uniswapPools[poolKey] if !exists { // Try reverse pair poolKey = fmt.Sprintf("%s-%s", req.TokenOut.Hex(), req.TokenIn.Hex()) poolAddr, exists = p.uniswapPools[poolKey] if !exists { return nil, fmt.Errorf("no uniswap v3 pool for pair %s/%s", req.TokenIn.Hex(), req.TokenOut.Hex()) } } // Get pool state poolState, err := p.getPoolState(ctx, poolAddr) if err != nil { return nil, fmt.Errorf("failed to get pool state: %w", err) } // Calculate price impact and slippage pricing := uniswap.NewUniswapV3Pricing() // Get current price from pool currentPrice, err := pricing.SqrtPriceX96ToPrice(poolState.SqrtPriceX96, poolState.Token0, poolState.Token1) if err != nil { return nil, fmt.Errorf("failed to convert sqrt price: %w", err) } // Calculate output amount with slippage amountOut, err := pricing.CalculateAmountOut(req.AmountIn, poolState.SqrtPriceX96, poolState.Liquidity) if err != nil { return nil, fmt.Errorf("failed to calculate amount out: %w", err) } // Calculate slippage in basis points slippageBps, err := p.calculateSlippage(req.AmountIn, amountOut, currentPrice) if err != nil { return nil, fmt.Errorf("failed to calculate slippage: %w", err) } return &PriceResponse{ Price: currentPrice, AmountOut: amountOut, SlippageBps: slippageBps, Source: "uniswap_v3", Timestamp: time.Now(), Valid: true, }, nil } // getUniswapV2Price gets price from Uniswap V2 style pools func (p *PriceOracle) getUniswapV2Price(ctx context.Context, req *PriceRequest) (*PriceResponse, error) { // Implementation for Uniswap V2 pricing (simplified for now) return nil, fmt.Errorf("uniswap v2 pricing not implemented") } // PoolState represents the current state of a Uniswap V3 pool type PoolState struct { SqrtPriceX96 *big.Int Tick int32 Liquidity *big.Int Token0 common.Address Token1 common.Address } // getPoolState retrieves the current state of a Uniswap V3 pool func (p *PriceOracle) getPoolState(ctx context.Context, poolAddr common.Address) (*PoolState, error) { // Uniswap V3 Pool ABI (slot0 function) poolABI := `[{"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"},{"inputs":[],"name":"token0","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"token1","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"}]` contractABI, err := uniswap.ParseABI(poolABI) if err != nil { return nil, fmt.Errorf("failed to parse pool ABI: %w", err) } // Get slot0 data slot0Data, err := contractABI.Pack("slot0") if err != nil { return nil, fmt.Errorf("failed to pack slot0 call: %w", err) } slot0Result, err := p.client.CallContract(ctx, ethereum.CallMsg{ To: &poolAddr, Data: slot0Data, }, nil) if err != nil { return nil, fmt.Errorf("slot0 call failed: %w", err) } slot0Unpacked, err := contractABI.Unpack("slot0", slot0Result) if err != nil { return nil, fmt.Errorf("failed to unpack slot0: %w", err) } // Get liquidity liquidityData, err := contractABI.Pack("liquidity") if err != nil { return nil, fmt.Errorf("failed to pack liquidity call: %w", err) } liquidityResult, err := p.client.CallContract(ctx, ethereum.CallMsg{ To: &poolAddr, Data: liquidityData, }, nil) if err != nil { return nil, fmt.Errorf("liquidity call failed: %w", err) } liquidityUnpacked, err := contractABI.Unpack("liquidity", liquidityResult) if err != nil { return nil, fmt.Errorf("failed to unpack liquidity: %w", err) } // Get token addresses token0Data, err := contractABI.Pack("token0") if err != nil { return nil, fmt.Errorf("failed to pack token0 call: %w", err) } token0Result, err := p.client.CallContract(ctx, ethereum.CallMsg{ To: &poolAddr, Data: token0Data, }, nil) if err != nil { return nil, fmt.Errorf("token0 call failed: %w", err) } token0Unpacked, err := contractABI.Unpack("token0", token0Result) if err != nil { return nil, fmt.Errorf("failed to unpack token0: %w", err) } token1Data, err := contractABI.Pack("token1") if err != nil { return nil, fmt.Errorf("failed to pack token1 call: %w", err) } token1Result, err := p.client.CallContract(ctx, ethereum.CallMsg{ To: &poolAddr, Data: token1Data, }, nil) if err != nil { return nil, fmt.Errorf("token1 call failed: %w", err) } token1Unpacked, err := contractABI.Unpack("token1", token1Result) if err != nil { return nil, fmt.Errorf("failed to unpack token1: %w", err) } // Extract values with proper type conversion sqrtPriceX96, ok := slot0Unpacked[0].(*big.Int) if !ok { return nil, fmt.Errorf("invalid sqrtPriceX96 type: %T", slot0Unpacked[0]) } // Convert tick from interface to int32 var tick int32 switch v := slot0Unpacked[1].(type) { case *big.Int: tick = int32(v.Int64()) case int32: tick = v default: return nil, fmt.Errorf("invalid tick type: %T", slot0Unpacked[1]) } liquidity, ok := liquidityUnpacked[0].(*big.Int) if !ok { return nil, fmt.Errorf("invalid liquidity type: %T", liquidityUnpacked[0]) } token0, ok := token0Unpacked[0].(common.Address) if !ok { return nil, fmt.Errorf("invalid token0 type: %T", token0Unpacked[0]) } token1, ok := token1Unpacked[0].(common.Address) if !ok { return nil, fmt.Errorf("invalid token1 type: %T", token1Unpacked[0]) } return &PoolState{ SqrtPriceX96: sqrtPriceX96, Tick: tick, Liquidity: liquidity, Token0: token0, Token1: token1, }, nil } // calculateSlippage calculates slippage in basis points func (p *PriceOracle) calculateSlippage(amountIn, amountOut, currentPrice *big.Int) (*big.Int, error) { if amountIn.Sign() == 0 || currentPrice.Sign() == 0 { return big.NewInt(0), nil } // Expected amount out at current price expectedOut := new(big.Int).Mul(amountIn, currentPrice) expectedOut.Div(expectedOut, big.NewInt(1e18)) // Calculate slippage percentage if expectedOut.Sign() == 0 { return big.NewInt(0), nil } // Slippage = (expectedOut - actualOut) / expectedOut * 10000 (basis points) diff := new(big.Int).Sub(expectedOut, amountOut) slippage := new(big.Int).Mul(diff, big.NewInt(10000)) slippage.Div(slippage, expectedOut) // Ensure non-negative slippage if slippage.Sign() < 0 { slippage.Neg(slippage) } return slippage, nil } // cachePrice stores a price in the cache func (p *PriceOracle) cachePrice(req *PriceRequest, response *PriceResponse) { key := fmt.Sprintf("%s-%s", req.TokenIn.Hex(), req.TokenOut.Hex()) p.cacheMutex.Lock() defer p.cacheMutex.Unlock() p.priceCache[key] = &PriceData{ Price: response.Price, Timestamp: response.Timestamp, Source: response.Source, Confidence: 1.0, // Full confidence for fresh data } } // getCachedPrice retrieves a cached price if available and not expired func (p *PriceOracle) getCachedPrice(req *PriceRequest) *PriceResponse { key := fmt.Sprintf("%s-%s", req.TokenIn.Hex(), req.TokenOut.Hex()) p.cacheMutex.RLock() defer p.cacheMutex.RUnlock() cached, exists := p.priceCache[key] if !exists { return nil } // Check if cache is expired if time.Since(cached.Timestamp) > p.cacheExpiry { return nil } // Calculate amount out using cached price amountOut := new(big.Int).Mul(req.AmountIn, cached.Price) amountOut.Div(amountOut, big.NewInt(1e18)) return &PriceResponse{ Price: cached.Price, AmountOut: amountOut, Source: cached.Source + "_cached", Timestamp: cached.Timestamp, Valid: true, } } // backgroundUpdater runs periodic price updates func (p *PriceOracle) backgroundUpdater() { for { select { case <-p.updateTicker.C: p.updatePriceCache() case <-p.stopChan: return } } } // updatePriceCache updates cached prices for major pairs func (p *PriceOracle) updatePriceCache() { // Update prices for major trading pairs majorPairs := []struct { tokenIn common.Address tokenOut common.Address }{ // Add major pairs here based on your configuration } for _, pair := range majorPairs { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) req := &PriceRequest{ TokenIn: pair.tokenIn, TokenOut: pair.tokenOut, AmountIn: big.NewInt(1e18), // 1 token Timestamp: time.Now(), } if response, err := p.GetPrice(ctx, req); err == nil { p.cachePrice(req, response) } cancel() } } // Stop stops the price oracle and cleanup resources func (p *PriceOracle) Stop() { if p.updateTicker != nil { p.updateTicker.Stop() } close(p.stopChan) } // getChainlinkFeeds returns the mapping of tokens to Chainlink price feeds func getChainlinkFeeds() map[common.Address]common.Address { return map[common.Address]common.Address{ // Add Chainlink feed addresses for major tokens // Example: WETH -> ETH/USD feed common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"): common.HexToAddress("0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612"), // WETH/USD common.HexToAddress("0xA0b862F60edEf4452F25B4160F177db44DeB6Cf1"): common.HexToAddress("0x50834F3163758fcC1Df9973b6e91f0F0F0434aD3"), // GNO/USD // Add more feeds as needed } } // getUniswapPools returns the mapping of token pairs to Uniswap pool addresses func getUniswapPools() map[string]common.Address { return map[string]common.Address{ // Add Uniswap V3 pool addresses for major pairs // Format: "token0-token1" -> pool address // Use lowercase hex addresses for consistency // Example: WETH-USDC pool "0x82af49447d8a07e3bd95bd0d56f35241523fbab1-0xa0b862f60edef4452f25b4160f177db44deb6cf1": common.HexToAddress("0xC31E54c7a869B9FcBEcc14363CF510d1c41fa443"), // WETH-GNO // Add more pools as needed } }