Files
mev-beta/pkg/scanner/swap/analyzer.go
2025-10-04 09:31:02 -05:00

586 lines
22 KiB
Go

package swap
import (
"context"
"fmt"
"math/big"
"strings"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/fraktal/mev-beta/internal/logger"
"github.com/fraktal/mev-beta/pkg/events"
"github.com/fraktal/mev-beta/pkg/marketdata"
"github.com/fraktal/mev-beta/pkg/profitcalc"
"github.com/fraktal/mev-beta/pkg/scanner/market"
stypes "github.com/fraktal/mev-beta/pkg/types"
"github.com/fraktal/mev-beta/pkg/uniswap"
"github.com/holiman/uint256"
)
// SwapAnalyzer handles analysis of swap events for price movement opportunities
type SwapAnalyzer struct {
logger *logger.Logger
marketDataLogger *marketdata.MarketDataLogger
profitCalculator *profitcalc.ProfitCalculator
opportunityRanker *profitcalc.OpportunityRanker
}
// NewSwapAnalyzer creates a new swap analyzer
func NewSwapAnalyzer(
logger *logger.Logger,
marketDataLogger *marketdata.MarketDataLogger,
profitCalculator *profitcalc.ProfitCalculator,
opportunityRanker *profitcalc.OpportunityRanker,
) *SwapAnalyzer {
return &SwapAnalyzer{
logger: logger,
marketDataLogger: marketDataLogger,
profitCalculator: profitCalculator,
opportunityRanker: opportunityRanker,
}
}
// AnalyzeSwapEvent analyzes a swap event for arbitrage opportunities
func (s *SwapAnalyzer) AnalyzeSwapEvent(event events.Event, marketScanner *market.MarketScanner) {
s.logger.Debug(fmt.Sprintf("Analyzing swap event in pool %s", event.PoolAddress))
// Get comprehensive pool data to determine factory and fee
poolInfo, poolExists := s.marketDataLogger.GetPoolInfo(event.PoolAddress)
factory := common.Address{}
fee := uint32(3000) // Default 0.3%
if poolExists {
factory = poolInfo.Factory
fee = poolInfo.Fee
} else {
// Determine factory from known DEX protocols
factory = marketScanner.GetFactoryForProtocol(event.Protocol)
}
// Create comprehensive swap event data for market data logger
swapData := &marketdata.SwapEventData{
TxHash: event.TransactionHash,
BlockNumber: event.BlockNumber,
LogIndex: uint(0), // Default log index (would need to be extracted from receipt)
Timestamp: time.Now(),
PoolAddress: event.PoolAddress,
Factory: factory,
Protocol: event.Protocol,
Token0: event.Token0,
Token1: event.Token1,
Sender: common.Address{}, // Default sender (would need to be extracted from transaction)
Recipient: common.Address{}, // Default recipient (would need to be extracted from transaction)
SqrtPriceX96: event.SqrtPriceX96,
Liquidity: event.Liquidity,
Tick: int32(event.Tick),
}
// Extract swap amounts from event (handle signed amounts correctly)
if event.Amount0 != nil && event.Amount1 != nil {
amount0Float := new(big.Float).SetInt(event.Amount0)
amount1Float := new(big.Float).SetInt(event.Amount1)
// Determine input/output based on sign (negative means token was removed from pool = output)
if amount0Float.Sign() < 0 {
// Token0 out, Token1 in
swapData.Amount0Out = new(big.Int).Abs(event.Amount0)
swapData.Amount1In = event.Amount1
swapData.Amount0In = big.NewInt(0)
swapData.Amount1Out = big.NewInt(0)
} else if amount1Float.Sign() < 0 {
// Token0 in, Token1 out
swapData.Amount0In = event.Amount0
swapData.Amount1Out = new(big.Int).Abs(event.Amount1)
swapData.Amount0Out = big.NewInt(0)
swapData.Amount1In = big.NewInt(0)
} else {
// Both positive (shouldn't happen in normal swaps, but handle gracefully)
swapData.Amount0In = event.Amount0
swapData.Amount1In = event.Amount1
swapData.Amount0Out = big.NewInt(0)
swapData.Amount1Out = big.NewInt(0)
}
}
// Calculate USD values using profit calculator's price oracle
swapData.AmountInUSD, swapData.AmountOutUSD, swapData.FeeUSD = s.calculateSwapUSDValues(swapData, fee)
// Calculate price impact based on pool liquidity and swap amounts
swapData.PriceImpact = s.calculateSwapPriceImpact(event, swapData)
// Log comprehensive swap event to market data logger
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := s.marketDataLogger.LogSwapEvent(ctx, event, swapData); err != nil {
s.logger.Debug(fmt.Sprintf("Failed to log swap event to market data logger: %v", err))
}
// Log the swap event to database (legacy)
marketScanner.LogSwapEvent(event)
// Get pool data with caching
poolData, err := marketScanner.GetPoolData(event.PoolAddress.Hex())
if err != nil {
s.logger.Error(fmt.Sprintf("Error getting pool data for %s: %v", event.PoolAddress, err))
return
}
// Calculate price impact
priceMovement, err := s.calculatePriceMovement(event, poolData)
if err != nil {
s.logger.Error(fmt.Sprintf("Error calculating price movement for pool %s: %v", event.PoolAddress, err))
return
}
// Log opportunity with actual swap amounts from event (legacy)
s.logSwapOpportunity(event, poolData, priceMovement, marketScanner)
// Check if the movement is significant
if marketScanner.IsSignificantMovement(priceMovement, marketScanner.Config().MinProfitThreshold) {
s.logger.Info(fmt.Sprintf("Significant price movement detected in pool %s: %+v", event.PoolAddress, priceMovement))
// Look for arbitrage opportunities
opportunities := s.findArbitrageOpportunities(event, priceMovement, marketScanner)
if len(opportunities) > 0 {
s.logger.Info(fmt.Sprintf("Found %d arbitrage opportunities for pool %s", len(opportunities), event.PoolAddress))
for _, opp := range opportunities {
s.logger.Info(fmt.Sprintf("Arbitrage opportunity: %+v", opp))
// Execute the arbitrage opportunity
marketScanner.ExecuteArbitrageOpportunity(opp)
}
} else {
s.logger.Debug(fmt.Sprintf("Price movement in pool %s is not significant: %f", event.PoolAddress, priceMovement.PriceImpact))
}
}
}
// logSwapOpportunity logs swap opportunities using actual amounts from events
func (s *SwapAnalyzer) logSwapOpportunity(event events.Event, poolData *market.CachedData, priceMovement *market.PriceMovement, marketScanner *market.MarketScanner) {
// Convert amounts from big.Int to big.Float for profit calculation
amountInFloat := big.NewFloat(0)
amountOutFloat := big.NewFloat(0)
amountInDisplay := float64(0)
amountOutDisplay := float64(0)
// For swap events, Amount0 and Amount1 represent the actual swap amounts
// The sign indicates direction (positive = token added to pool, negative = token removed from pool)
if event.Amount0 != nil {
amount0Float := new(big.Float).SetInt(event.Amount0)
if event.Amount1 != nil {
amount1Float := new(big.Float).SetInt(event.Amount1)
// Determine input/output based on sign (negative means token was removed from pool = output)
if amount0Float.Sign() < 0 {
// Token0 out, Token1 in
amountOutFloat = new(big.Float).Abs(amount0Float)
amountInFloat = amount1Float
amountOutDisplay, _ = new(big.Float).Quo(amountOutFloat, big.NewFloat(1e18)).Float64()
amountInDisplay, _ = new(big.Float).Quo(amountInFloat, big.NewFloat(1e18)).Float64()
} else {
// Token0 in, Token1 out
amountInFloat = amount0Float
amountOutFloat = new(big.Float).Abs(amount1Float)
amountInDisplay, _ = new(big.Float).Quo(amountInFloat, big.NewFloat(1e18)).Float64()
amountOutDisplay, _ = new(big.Float).Quo(amountOutFloat, big.NewFloat(1e18)).Float64()
}
}
}
// Analyze arbitrage opportunity using the profit calculator
var estimatedProfitUSD float64 = 0.0
var profitData map[string]interface{}
if amountInFloat.Sign() > 0 && amountOutFloat.Sign() > 0 {
opportunity := s.profitCalculator.AnalyzeSwapOpportunity(
context.Background(),
event.Token0,
event.Token1,
new(big.Float).Quo(amountInFloat, big.NewFloat(1e18)), // Convert to ETH units
new(big.Float).Quo(amountOutFloat, big.NewFloat(1e18)), // Convert to ETH units
event.Protocol,
)
if opportunity != nil {
// Add opportunity to ranking system
rankedOpp := s.opportunityRanker.AddOpportunity(opportunity)
// Use the calculated profit for logging
if opportunity.NetProfit != nil {
estimatedProfitFloat, _ := opportunity.NetProfit.Float64()
estimatedProfitUSD = estimatedProfitFloat * 2000 // Assume 1 ETH = $2000 for USD conversion
}
// Add detailed profit analysis to additional data
profitData = map[string]interface{}{
"arbitrageId": opportunity.ID,
"isExecutable": opportunity.IsExecutable,
"rejectReason": opportunity.RejectReason,
"confidence": opportunity.Confidence,
"profitMargin": opportunity.ProfitMargin,
"netProfitETH": s.profitCalculator.FormatEther(opportunity.NetProfit),
"gasCostETH": s.profitCalculator.FormatEther(opportunity.GasCost),
"estimatedProfitETH": s.profitCalculator.FormatEther(opportunity.EstimatedProfit),
}
// Add ranking data if available
if rankedOpp != nil {
profitData["opportunityScore"] = rankedOpp.Score
profitData["opportunityRank"] = rankedOpp.Rank
profitData["competitionRisk"] = rankedOpp.CompetitionRisk
profitData["updateCount"] = rankedOpp.UpdateCount
}
}
} else if priceMovement != nil {
// Fallback to simple price impact calculation
estimatedProfitUSD = priceMovement.PriceImpact * 100
}
// Resolve token symbols
tokenIn := s.resolveTokenSymbol(event.Token0.Hex())
tokenOut := s.resolveTokenSymbol(event.Token1.Hex())
// Create additional data with profit analysis
additionalData := map[string]interface{}{
"poolAddress": event.PoolAddress.Hex(),
"protocol": event.Protocol,
"token0": event.Token0.Hex(),
"token1": event.Token1.Hex(),
"tokenIn": tokenIn,
"tokenOut": tokenOut,
"blockNumber": event.BlockNumber,
}
// Add price impact if available
if priceMovement != nil {
additionalData["priceImpact"] = priceMovement.PriceImpact
}
// Merge profit analysis data
if profitData != nil {
for k, v := range profitData {
additionalData[k] = v
}
}
// Log the opportunity using actual swap amounts and profit analysis
s.logger.Opportunity(event.TransactionHash.Hex(), "", event.PoolAddress.Hex(), "Swap", event.Protocol,
amountInDisplay, amountOutDisplay, 0.0, estimatedProfitUSD, additionalData)
}
// resolveTokenSymbol converts token address to human-readable symbol
func (s *SwapAnalyzer) resolveTokenSymbol(tokenAddress string) string {
// Convert to lowercase for consistent lookup
addr := strings.ToLower(tokenAddress)
// Known Arbitrum token mappings (same as in L2 parser)
tokenMap := map[string]string{
"0x82af49447d8a07e3bd95bd0d56f35241523fbab1": "WETH",
"0xaf88d065e77c8cc2239327c5edb3a432268e5831": "USDC",
"0xff970a61a04b1ca14834a43f5de4533ebddb5cc8": "USDC.e",
"0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9": "USDT",
"0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f": "WBTC",
"0x912ce59144191c1204e64559fe8253a0e49e6548": "ARB",
"0xfc5a1a6eb076a2c7ad06ed22c90d7e710e35ad0a": "GMX",
"0xf97f4df75117a78c1a5a0dbb814af92458539fb4": "LINK",
"0xfa7f8980b0f1e64a2062791cc3b0871572f1f7f0": "UNI",
"0xba5ddd1f9d7f570dc94a51479a000e3bce967196": "AAVE",
"0x0de59c86c306b9fead9fb67e65551e2b6897c3f6": "KUMA",
"0x6efa9b8883dfb78fd75cd89d8474c44c3cbda469": "DIA",
"0x440017a1b021006d556d7fc06a54c32e42eb745b": "G@ARB",
"0x11cdb42b0eb46d95f990bedd4695a6e3fa034978": "CRV",
"0x040d1edc9569d4bab2d15287dc5a4f10f56a56b8": "BAL",
"0x354a6da3fcde098f8389cad84b0182725c6c91de": "COMP",
"0x2e9a6df78e42c50b0cefcf9000d0c3a4d34e1dd5": "MKR",
}
if symbol, exists := tokenMap[addr]; exists {
return symbol
}
// Return truncated address if not in mapping
if len(tokenAddress) > 10 {
return tokenAddress[:6] + "..." + tokenAddress[len(tokenAddress)-4:]
}
return tokenAddress
}
// calculatePriceMovement calculates the price movement from a swap event using cached mathematical functions
func (s *SwapAnalyzer) calculatePriceMovement(event events.Event, poolData *market.CachedData) (*market.PriceMovement, error) {
s.logger.Debug(fmt.Sprintf("Calculating price movement for pool %s", event.PoolAddress))
// Get current price from pool data using cached function
currentPrice := uniswap.SqrtPriceX96ToPriceCached(poolData.SqrtPriceX96.ToBig())
if currentPrice == nil {
return nil, fmt.Errorf("failed to calculate current price from sqrtPriceX96")
}
// Calculate price impact based on swap amounts
var priceImpact float64
if event.Amount0.Sign() > 0 && event.Amount1.Sign() > 0 {
// Both amounts are positive, calculate the impact
amount0Float := new(big.Float).SetInt(event.Amount0)
amount1Float := new(big.Float).SetInt(event.Amount1)
// Price impact = |amount1 / amount0 - current_price| / current_price
swapPrice := new(big.Float).Quo(amount1Float, amount0Float)
priceDiff := new(big.Float).Sub(swapPrice, currentPrice)
priceDiff.Abs(priceDiff)
priceImpactFloat := new(big.Float).Quo(priceDiff, currentPrice)
priceImpact, _ = priceImpactFloat.Float64()
}
movement := &market.PriceMovement{
Token0: event.Token0.Hex(),
Token1: event.Token1.Hex(),
Pool: event.PoolAddress.Hex(),
Protocol: event.Protocol,
AmountIn: event.Amount0,
AmountOut: event.Amount1,
PriceBefore: currentPrice,
PriceAfter: currentPrice, // For now, assume same price (could be calculated based on swap)
PriceImpact: priceImpact,
TickBefore: poolData.Tick,
TickAfter: poolData.Tick, // For now, assume same tick
Timestamp: time.Now(),
}
s.logger.Debug(fmt.Sprintf("Price movement calculated: impact=%.6f%%, amount_in=%s", priceImpact*100, event.Amount0.String()))
return movement, nil
}
// findArbitrageOpportunities looks for arbitrage opportunities based on price movements
func (s *SwapAnalyzer) findArbitrageOpportunities(event events.Event, movement *market.PriceMovement, marketScanner *market.MarketScanner) []stypes.ArbitrageOpportunity {
s.logger.Debug(fmt.Sprintf("Searching for arbitrage opportunities for pool %s", event.PoolAddress))
opportunities := make([]stypes.ArbitrageOpportunity, 0)
// Get related pools for the same token pair
relatedPools := marketScanner.FindRelatedPools(event.Token0, event.Token1)
// If we have related pools, compare prices
if len(relatedPools) > 0 {
// Get the current price in this pool
currentPrice := movement.PriceBefore
// Compare with prices in related pools
for _, pool := range relatedPools {
// Skip the same pool
if pool.Address == event.PoolAddress {
continue
}
// Get pool data
poolData, err := marketScanner.GetPoolData(pool.Address.Hex())
if err != nil {
s.logger.Error(fmt.Sprintf("Error getting pool data for related pool %s: %v", pool.Address.Hex(), err))
continue
}
// Check if poolData.SqrtPriceX96 is nil to prevent panic
if poolData.SqrtPriceX96 == nil {
s.logger.Error(fmt.Sprintf("Pool data for %s has nil SqrtPriceX96", pool.Address.Hex()))
continue
}
// Calculate price in the related pool using cached function
relatedPrice := uniswap.SqrtPriceX96ToPriceCached(poolData.SqrtPriceX96.ToBig())
// Check if currentPrice or relatedPrice is nil to prevent panic
if currentPrice == nil || relatedPrice == nil {
s.logger.Error(fmt.Sprintf("Nil price detected for pool comparison"))
continue
}
// Calculate price difference
priceDiff := new(big.Float).Sub(currentPrice, relatedPrice)
priceDiffRatio := new(big.Float).Quo(priceDiff, relatedPrice)
// If there's a significant price difference, we might have an arbitrage opportunity
priceDiffFloat, _ := priceDiffRatio.Float64()
// Lower threshold for Arbitrum where spreads are smaller
arbitrageThreshold := 0.001 // 0.1% threshold instead of 0.5%
if priceDiffFloat > arbitrageThreshold {
// Estimate potential profit
estimatedProfit := marketScanner.EstimateProfit(event, pool, priceDiffFloat)
if estimatedProfit != nil && estimatedProfit.Sign() > 0 {
opp := stypes.ArbitrageOpportunity{
Path: []string{event.Token0.Hex(), event.Token1.Hex()},
Pools: []string{event.PoolAddress.Hex(), pool.Address.Hex()},
Profit: estimatedProfit,
GasEstimate: big.NewInt(300000), // Estimated gas cost
ROI: priceDiffFloat * 100, // Convert to percentage
Protocol: fmt.Sprintf("%s->%s", event.Protocol, pool.Protocol),
}
opportunities = append(opportunities, opp)
s.logger.Info(fmt.Sprintf("Found arbitrage opportunity: %+v", opp))
}
}
}
}
// Also look for triangular arbitrage opportunities
triangularOpps := marketScanner.FindTriangularArbitrageOpportunities(event)
opportunities = append(opportunities, triangularOpps...)
return opportunities
}
// calculateSwapUSDValues calculates USD values for swap amounts using the profit calculator's price oracle
func (s *SwapAnalyzer) calculateSwapUSDValues(swapData *marketdata.SwapEventData, fee uint32) (amountInUSD, amountOutUSD, feeUSD float64) {
if s.profitCalculator == nil {
return 0, 0, 0
}
// Get token prices in USD
token0Price := s.getTokenPriceUSD(swapData.Token0)
token1Price := s.getTokenPriceUSD(swapData.Token1)
// Calculate decimals for proper conversion
token0Decimals := s.getTokenDecimals(swapData.Token0)
token1Decimals := s.getTokenDecimals(swapData.Token1)
// Calculate amount in USD
if swapData.Amount0In != nil && swapData.Amount0In.Sign() > 0 {
amount0InFloat := s.bigIntToFloat(swapData.Amount0In, token0Decimals)
amountInUSD = amount0InFloat * token0Price
} else if swapData.Amount1In != nil && swapData.Amount1In.Sign() > 0 {
amount1InFloat := s.bigIntToFloat(swapData.Amount1In, token1Decimals)
amountInUSD = amount1InFloat * token1Price
}
// Calculate amount out USD
if swapData.Amount0Out != nil && swapData.Amount0Out.Sign() > 0 {
amount0OutFloat := s.bigIntToFloat(swapData.Amount0Out, token0Decimals)
amountOutUSD = amount0OutFloat * token0Price
} else if swapData.Amount1Out != nil && swapData.Amount1Out.Sign() > 0 {
amount1OutFloat := s.bigIntToFloat(swapData.Amount1Out, token1Decimals)
amountOutUSD = amount1OutFloat * token1Price
}
// Calculate fee USD (fee tier as percentage of input amount)
feePercent := float64(fee) / 1000000.0 // Convert from basis points
feeUSD = amountInUSD * feePercent
return amountInUSD, amountOutUSD, feeUSD
}
// calculateSwapPriceImpact calculates the price impact of a swap based on pool liquidity and amounts
func (s *SwapAnalyzer) calculateSwapPriceImpact(event events.Event, swapData *marketdata.SwapEventData) float64 {
if event.SqrtPriceX96 == nil || event.Liquidity == nil {
return 0.0
}
// Get pre-swap price from sqrtPriceX96
prePrice := s.sqrtPriceX96ToPrice(event.SqrtPriceX96)
if prePrice == 0 {
return 0.0
}
// Calculate effective swap size in token0 terms
var swapSize *big.Int
if swapData.Amount0In != nil && swapData.Amount0In.Sign() > 0 {
swapSize = swapData.Amount0In
} else if swapData.Amount0Out != nil && swapData.Amount0Out.Sign() > 0 {
swapSize = swapData.Amount0Out
} else {
return 0.0
}
// Calculate price impact as percentage of pool liquidity
liquidity := event.Liquidity.ToBig()
if liquidity.Sign() == 0 {
return 0.0
}
// Proper price impact calculation for AMMs: impact = swapSize / (liquidity + swapSize)
// This is more accurate than the quadratic approximation for real AMMs
swapSizeFloat := new(big.Float).SetInt(swapSize)
liquidityFloat := new(big.Float).SetInt(liquidity)
// Calculate the price impact ratio
priceImpactRatio := new(big.Float).Quo(swapSizeFloat, new(big.Float).Add(liquidityFloat, swapSizeFloat))
// Convert to percentage
priceImpactPercent, _ := priceImpactRatio.Float64()
return priceImpactPercent * 100.0
}
// getTokenPriceUSD gets the USD price of a token using various price sources
func (s *SwapAnalyzer) getTokenPriceUSD(tokenAddr common.Address) float64 {
// Known token prices (in a production system, this would query price oracles)
knownPrices := map[common.Address]float64{
common.HexToAddress("0x82af49447d8a07e3bd95bd0d56f35241523fbab1"): 2000.0, // WETH
common.HexToAddress("0xaf88d065e77c8cc2239327c5edb3a432268e5831"): 1.0, // USDC
common.HexToAddress("0xff970a61a04b1ca14834a43f5de4533ebddb5cc8"): 1.0, // USDC.e
common.HexToAddress("0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9"): 1.0, // USDT
common.HexToAddress("0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f"): 43000.0, // WBTC
common.HexToAddress("0x912ce59144191c1204e64559fe8253a0e49e6548"): 0.75, // ARB
common.HexToAddress("0xfc5a1a6eb076a2c7ad06ed22c90d7e710e35ad0a"): 45.0, // GMX
common.HexToAddress("0xf97f4df75117a78c1a5a0dbb814af92458539fb4"): 12.0, // LINK
common.HexToAddress("0xfa7f8980b0f1e64a2062791cc3b0871572f1f7f0"): 8.0, // UNI
common.HexToAddress("0xba5ddd1f9d7f570dc94a51479a000e3bce967196"): 85.0, // AAVE
}
if price, exists := knownPrices[tokenAddr]; exists {
return price
}
// For unknown tokens, return 0 (in production, would query price oracle or DEX)
return 0.0
}
// getTokenDecimals returns the decimal places for a token
func (s *SwapAnalyzer) getTokenDecimals(tokenAddr common.Address) uint8 {
// Known token decimals
knownDecimals := map[common.Address]uint8{
common.HexToAddress("0x82af49447d8a07e3bd95bd0d56f35241523fbab1"): 18, // WETH
common.HexToAddress("0xaf88d065e77c8cc2239327c5edb3a432268e5831"): 6, // USDC
common.HexToAddress("0xff970a61a04b1ca14834a43f5de4533ebddb5cc8"): 6, // USDC.e
common.HexToAddress("0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9"): 6, // USDT
common.HexToAddress("0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f"): 8, // WBTC
common.HexToAddress("0x912ce59144191c1204e64559fe8253a0e49e6548"): 18, // ARB
common.HexToAddress("0xfc5a1a6eb076a2c7ad06ed22c90d7e710e35ad0a"): 18, // GMX
common.HexToAddress("0xf97f4df75117a78c1a5a0dbb814af92458539fb4"): 18, // LINK
common.HexToAddress("0xfa7f8980b0f1e64a2062791cc3b0871572f1f7f0"): 18, // UNI
common.HexToAddress("0xba5ddd1f9d7f570dc94a51479a000e3bce967196"): 18, // AAVE
}
if decimals, exists := knownDecimals[tokenAddr]; exists {
return decimals
}
// Default to 18 for unknown tokens
return 18
}
// bigIntToFloat converts a big.Int amount to float64 accounting for token decimals
func (s *SwapAnalyzer) bigIntToFloat(amount *big.Int, decimals uint8) float64 {
if amount == nil {
return 0.0
}
divisor := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(decimals)), nil)
amountFloat := new(big.Float).SetInt(amount)
divisorFloat := new(big.Float).SetInt(divisor)
result := new(big.Float).Quo(amountFloat, divisorFloat)
resultFloat, _ := result.Float64()
return resultFloat
}
// sqrtPriceX96ToPrice converts sqrtPriceX96 to a regular price using cached mathematical functions
func (s *SwapAnalyzer) sqrtPriceX96ToPrice(sqrtPriceX96 *uint256.Int) float64 {
if sqrtPriceX96 == nil {
return 0.0
}
// Use cached function for optimized calculation
price := uniswap.SqrtPriceX96ToPriceCached(sqrtPriceX96.ToBig())
priceFloat, _ := price.Float64()
return priceFloat
}