package arbitrum import ( "context" "fmt" "math/big" "time" "github.com/ethereum/go-ethereum/common" "github.com/fraktal/mev-beta/internal/logger" "github.com/fraktal/mev-beta/pkg/oracle" ) // EventEnrichmentService provides comprehensive event data enrichment type EventEnrichmentService struct { priceOracle *oracle.PriceOracle tokenMetadata *TokenMetadataService logger *logger.Logger // USD conversion constants usdcAddr common.Address wethAddr common.Address } // NewEventEnrichmentService creates a new event enrichment service func NewEventEnrichmentService( priceOracle *oracle.PriceOracle, tokenMetadata *TokenMetadataService, logger *logger.Logger, ) *EventEnrichmentService { return &EventEnrichmentService{ priceOracle: priceOracle, tokenMetadata: tokenMetadata, logger: logger, usdcAddr: common.HexToAddress("0xaf88d065e77c8cC2239327C5EDb3A432268e5831"), // USDC on Arbitrum wethAddr: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), // WETH on Arbitrum } } // EnrichEvent adds comprehensive metadata and USD values to a DEX event func (s *EventEnrichmentService) EnrichEvent(ctx context.Context, event *EnhancedDEXEvent) error { // Add token metadata if err := s.addTokenMetadata(ctx, event); err != nil { s.logger.Debug(fmt.Sprintf("Failed to add token metadata: %v", err)) } // Calculate USD values if err := s.calculateUSDValues(ctx, event); err != nil { s.logger.Debug(fmt.Sprintf("Failed to calculate USD values: %v", err)) } // Add factory and router information s.addContractMetadata(event) // Calculate price impact and slippage if err := s.calculatePriceMetrics(ctx, event); err != nil { s.logger.Debug(fmt.Sprintf("Failed to calculate price metrics: %v", err)) } // Assess MEV potential s.assessMEVPotential(event) return nil } // addTokenMetadata enriches the event with token metadata func (s *EventEnrichmentService) addTokenMetadata(ctx context.Context, event *EnhancedDEXEvent) error { // Get metadata for token in if event.TokenIn != (common.Address{}) { if metadata, err := s.tokenMetadata.GetTokenMetadata(ctx, event.TokenIn); err == nil { event.TokenInSymbol = metadata.Symbol event.TokenInName = metadata.Name event.TokenInDecimals = metadata.Decimals event.TokenInRiskScore = metadata.RiskScore } } // Get metadata for token out if event.TokenOut != (common.Address{}) { if metadata, err := s.tokenMetadata.GetTokenMetadata(ctx, event.TokenOut); err == nil { event.TokenOutSymbol = metadata.Symbol event.TokenOutName = metadata.Name event.TokenOutDecimals = metadata.Decimals event.TokenOutRiskScore = metadata.RiskScore } } // Get metadata for token0 and token1 if available if event.Token0 != (common.Address{}) { if metadata, err := s.tokenMetadata.GetTokenMetadata(ctx, event.Token0); err == nil { event.Token0Symbol = metadata.Symbol event.Token0Decimals = metadata.Decimals } } if event.Token1 != (common.Address{}) { if metadata, err := s.tokenMetadata.GetTokenMetadata(ctx, event.Token1); err == nil { event.Token1Symbol = metadata.Symbol event.Token1Decimals = metadata.Decimals } } return nil } // calculateUSDValues calculates USD values for all amounts in the event func (s *EventEnrichmentService) calculateUSDValues(ctx context.Context, event *EnhancedDEXEvent) error { // Calculate AmountInUSD if event.AmountIn != nil && event.TokenIn != (common.Address{}) { if usdValue, err := s.getTokenValueInUSD(ctx, event.TokenIn, event.AmountIn); err == nil { event.AmountInUSD = usdValue } } // Calculate AmountOutUSD if event.AmountOut != nil && event.TokenOut != (common.Address{}) { if usdValue, err := s.getTokenValueInUSD(ctx, event.TokenOut, event.AmountOut); err == nil { event.AmountOutUSD = usdValue } } // Calculate Amount0USD and Amount1USD for V3 events if event.Amount0 != nil && event.Token0 != (common.Address{}) { if usdValue, err := s.getTokenValueInUSD(ctx, event.Token0, new(big.Int).Abs(event.Amount0)); err == nil { event.Amount0USD = usdValue } } if event.Amount1 != nil && event.Token1 != (common.Address{}) { if usdValue, err := s.getTokenValueInUSD(ctx, event.Token1, new(big.Int).Abs(event.Amount1)); err == nil { event.Amount1USD = usdValue } } // Calculate fee in USD if event.AmountInUSD > 0 && event.FeeBps > 0 { event.FeeUSD = event.AmountInUSD * float64(event.FeeBps) / 10000.0 } return nil } // getTokenValueInUSD converts a token amount to USD value func (s *EventEnrichmentService) getTokenValueInUSD(ctx context.Context, tokenAddr common.Address, amount *big.Int) (float64, error) { if amount == nil || amount.Sign() == 0 { return 0, nil } // Direct USDC conversion if tokenAddr == s.usdcAddr { // USDC has 6 decimals amountFloat := new(big.Float).SetInt(amount) amountFloat.Quo(amountFloat, big.NewFloat(1e6)) result, _ := amountFloat.Float64() return result, nil } // Get price from oracle priceReq := &oracle.PriceRequest{ TokenIn: tokenAddr, TokenOut: s.usdcAddr, // Convert to USDC first AmountIn: amount, Timestamp: time.Now(), } priceResp, err := s.priceOracle.GetPrice(ctx, priceReq) if err != nil { // Fallback: try converting through WETH if direct conversion fails if tokenAddr != s.wethAddr { return s.getUSDValueThroughWETH(ctx, tokenAddr, amount) } return 0, fmt.Errorf("failed to get price: %w", err) } if !priceResp.Valid || priceResp.AmountOut == nil { return 0, fmt.Errorf("invalid price response") } // Convert USDC amount to USD (USDC has 6 decimals) usdcAmount := new(big.Float).SetInt(priceResp.AmountOut) usdcAmount.Quo(usdcAmount, big.NewFloat(1e6)) result, _ := usdcAmount.Float64() return result, nil } // getUSDValueThroughWETH converts token value to USD through WETH func (s *EventEnrichmentService) getUSDValueThroughWETH(ctx context.Context, tokenAddr common.Address, amount *big.Int) (float64, error) { // First convert token to WETH wethReq := &oracle.PriceRequest{ TokenIn: tokenAddr, TokenOut: s.wethAddr, AmountIn: amount, Timestamp: time.Now(), } wethResp, err := s.priceOracle.GetPrice(ctx, wethReq) if err != nil || !wethResp.Valid { return 0, fmt.Errorf("failed to convert to WETH: %w", err) } // Then convert WETH to USD return s.getTokenValueInUSD(ctx, s.wethAddr, wethResp.AmountOut) } // addContractMetadata adds factory and router contract information func (s *EventEnrichmentService) addContractMetadata(event *EnhancedDEXEvent) { // Set factory addresses based on protocol switch event.Protocol { case ProtocolUniswapV2: event.Factory = common.HexToAddress("0xf1D7CC64Fb4452F05c498126312eBE29f30Fbcf9") event.Router = common.HexToAddress("0x4752ba5dbc23f44d87826276bf6fd6b1c372ad24") case ProtocolUniswapV3: event.Factory = common.HexToAddress("0x1F98431c8aD98523631AE4a59f267346ea31F984") event.Router = common.HexToAddress("0xE592427A0AEce92De3Edee1F18E0157C05861564") case ProtocolSushiSwapV2: event.Factory = common.HexToAddress("0xc35DADB65012eC5796536bD9864eD8773aBc74C4") event.Router = common.HexToAddress("0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506") case ProtocolSushiSwapV3: event.Factory = common.HexToAddress("0x7770978eED668a3ba661d51a773d3a992Fc9DDCB") event.Router = common.HexToAddress("0x34af5256F1FC2e9F5b5c0f3d8ED82D5a15B69C88") case ProtocolCamelotV2: event.Factory = common.HexToAddress("0x6EcCab422D763aC031210895C81787E87B91B678") event.Router = common.HexToAddress("0xc873fEcbd354f5A56E00E710B90EF4201db2448d") case ProtocolCamelotV3: event.Factory = common.HexToAddress("0x1a3c9B1d2F0529D97f2afC5136Cc23e58f1FD35B") event.Router = common.HexToAddress("0x1F721E2E82F6676FCE4eA07A5958cF098D339e18") } } // calculatePriceMetrics calculates price impact and slippage func (s *EventEnrichmentService) calculatePriceMetrics(ctx context.Context, event *EnhancedDEXEvent) error { // Skip if we don't have enough data if event.AmountIn == nil || event.AmountOut == nil || event.TokenIn == (common.Address{}) || event.TokenOut == (common.Address{}) { return nil } // Get current market price marketReq := &oracle.PriceRequest{ TokenIn: event.TokenIn, TokenOut: event.TokenOut, AmountIn: big.NewInt(1e18), // 1 token for reference price Timestamp: time.Now(), } marketResp, err := s.priceOracle.GetPrice(ctx, marketReq) if err != nil || !marketResp.Valid { return fmt.Errorf("failed to get market price: %w", err) } // Calculate effective price from the trade effectivePrice := new(big.Float).Quo( new(big.Float).SetInt(event.AmountOut), new(big.Float).SetInt(event.AmountIn), ) // Calculate market price marketPrice := new(big.Float).Quo( new(big.Float).SetInt(marketResp.AmountOut), new(big.Float).SetInt(marketReq.AmountIn), ) // Calculate price impact: (marketPrice - effectivePrice) / marketPrice priceDiff := new(big.Float).Sub(marketPrice, effectivePrice) priceImpact := new(big.Float).Quo(priceDiff, marketPrice) impact, _ := priceImpact.Float64() event.PriceImpact = impact // Convert to basis points for slippage event.SlippageBps = uint64(impact * 10000) return nil } // assessMEVPotential determines if the event has MEV potential func (s *EventEnrichmentService) assessMEVPotential(event *EnhancedDEXEvent) { // Initialize MEV assessment event.IsMEV = false event.MEVType = "" event.ProfitUSD = 0.0 // High-value transactions are more likely to be MEV if event.AmountInUSD > 50000 { // $50k threshold event.IsMEV = true event.MEVType = "high_value" event.ProfitUSD = event.AmountInUSD * 0.001 // Estimate 0.1% profit } // High price impact suggests potential sandwich opportunity if event.PriceImpact > 0.02 { // 2% price impact event.IsMEV = true event.MEVType = "sandwich_opportunity" event.ProfitUSD = event.AmountInUSD * event.PriceImpact * 0.5 // Estimate half the impact as profit } // High slippage tolerance indicates MEV potential if event.SlippageBps > 500 { // 5% slippage tolerance event.IsMEV = true if event.MEVType == "" { event.MEVType = "arbitrage" } event.ProfitUSD = event.AmountInUSD * 0.002 // Estimate 0.2% profit } // Transactions involving risky tokens if event.TokenInRiskScore > 0.7 || event.TokenOutRiskScore > 0.7 { event.IsMEV = true if event.MEVType == "" { event.MEVType = "risky_arbitrage" } event.ProfitUSD = event.AmountInUSD * 0.005 // Higher profit for risky trades } // Flash loan indicators (large amounts with no sender balance check) if event.AmountInUSD > 100000 && event.MEVType == "" { event.IsMEV = true event.MEVType = "flash_loan_arbitrage" event.ProfitUSD = event.AmountInUSD * 0.003 // Estimate 0.3% profit } } // CalculatePoolTVL calculates the total value locked in a pool func (s *EventEnrichmentService) CalculatePoolTVL(ctx context.Context, poolAddr common.Address, token0, token1 common.Address, reserve0, reserve1 *big.Int) (float64, error) { if reserve0 == nil || reserve1 == nil { return 0, fmt.Errorf("invalid reserves") } // Get USD value of both reserves value0, err := s.getTokenValueInUSD(ctx, token0, reserve0) if err != nil { value0 = 0 // Continue with just one side if the other fails } value1, err := s.getTokenValueInUSD(ctx, token1, reserve1) if err != nil { value1 = 0 } // TVL is the sum of both reserves in USD tvl := value0 + value1 return tvl, nil } // EnhancedDEXEvent extensions for enriched data type EnhancedDEXEventExtended struct { *EnhancedDEXEvent // Additional enriched fields TokenInRiskScore float64 `json:"tokenInRiskScore"` TokenOutRiskScore float64 `json:"tokenOutRiskScore"` // Pool information PoolTVL float64 `json:"poolTVL"` PoolUtilization float64 `json:"poolUtilization"` // How much of the pool was used // MEV analysis SandwichRisk float64 `json:"sandwichRisk"` // 0.0 to 1.0 ArbitrageProfit float64 `json:"arbitrageProfit"` // Estimated profit in USD // Market context VolumeRank24h int `json:"volumeRank24h"` // Rank by 24h volume PriceChange24h float64 `json:"priceChange24h"` // Price change in last 24h }