package parsers import ( "context" "fmt" "math/big" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/your-org/mev-bot/pkg/cache" mevtypes "github.com/your-org/mev-bot/pkg/types" ) // UniswapV3 Swap event signature: // event Swap(address indexed sender, address indexed recipient, int256 amount0, int256 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick) var ( // SwapV3EventSignature is the event signature for UniswapV3 Swap events SwapV3EventSignature = crypto.Keccak256Hash([]byte("Swap(address,address,int256,int256,uint160,uint128,int24)")) ) // UniswapV3Parser implements the Parser interface for UniswapV3 pools type UniswapV3Parser struct { cache cache.PoolCache logger mevtypes.Logger } // NewUniswapV3Parser creates a new UniswapV3 parser func NewUniswapV3Parser(cache cache.PoolCache, logger mevtypes.Logger) *UniswapV3Parser { return &UniswapV3Parser{ cache: cache, logger: logger, } } // Protocol returns the protocol type this parser handles func (p *UniswapV3Parser) Protocol() mevtypes.ProtocolType { return mevtypes.ProtocolUniswapV3 } // SupportsLog checks if this parser can handle the given log func (p *UniswapV3Parser) SupportsLog(log types.Log) bool { // Check if log has the Swap event signature if len(log.Topics) == 0 { return false } return log.Topics[0] == SwapV3EventSignature } // ParseLog parses a UniswapV3 Swap event from a log func (p *UniswapV3Parser) ParseLog(ctx context.Context, log types.Log, tx *types.Transaction) (*mevtypes.SwapEvent, error) { // Verify this is a Swap event if !p.SupportsLog(log) { return nil, fmt.Errorf("unsupported log") } // Get pool info from cache to extract token addresses and decimals poolInfo, err := p.cache.GetByAddress(ctx, log.Address) if err != nil { return nil, fmt.Errorf("pool not found in cache: %w", err) } // Parse event data // Data contains: amount0, amount1, sqrtPriceX96, liquidity, tick (non-indexed) // Topics contain: [signature, sender, recipient] (indexed) if len(log.Topics) != 3 { return nil, fmt.Errorf("invalid number of topics: expected 3, got %d", len(log.Topics)) } // Define ABI for data decoding int256Type, err := abi.NewType("int256", "", nil) if err != nil { return nil, fmt.Errorf("failed to create int256 type: %w", err) } uint160Type, err := abi.NewType("uint160", "", nil) if err != nil { return nil, fmt.Errorf("failed to create uint160 type: %w", err) } uint128Type, err := abi.NewType("uint128", "", nil) if err != nil { return nil, fmt.Errorf("failed to create uint128 type: %w", err) } int24Type, err := abi.NewType("int24", "", nil) if err != nil { return nil, fmt.Errorf("failed to create int24 type: %w", err) } arguments := abi.Arguments{ {Type: int256Type, Name: "amount0"}, {Type: int256Type, Name: "amount1"}, {Type: uint160Type, Name: "sqrtPriceX96"}, {Type: uint128Type, Name: "liquidity"}, {Type: int24Type, Name: "tick"}, } // Decode data values, err := arguments.Unpack(log.Data) if err != nil { return nil, fmt.Errorf("failed to decode event data: %w", err) } if len(values) != 5 { return nil, fmt.Errorf("invalid number of values: expected 5, got %d", len(values)) } // Extract indexed parameters from topics sender := common.BytesToAddress(log.Topics[1].Bytes()) recipient := common.BytesToAddress(log.Topics[2].Bytes()) // Extract amounts from decoded data (signed integers) amount0Signed := values[0].(*big.Int) amount1Signed := values[1].(*big.Int) sqrtPriceX96 := values[2].(*big.Int) liquidity := values[3].(*big.Int) tick := values[4].(*big.Int) // int24 is returned as *big.Int // Convert signed amounts to in/out amounts // Positive amount = token added to pool (user receives this token = out) // Negative amount = token removed from pool (user sends this token = in) var amount0In, amount0Out, amount1In, amount1Out *big.Int if amount0Signed.Sign() < 0 { // Negative = input (user sends token0) amount0In = new(big.Int).Abs(amount0Signed) amount0Out = big.NewInt(0) } else { // Positive = output (user receives token0) amount0In = big.NewInt(0) amount0Out = new(big.Int).Set(amount0Signed) } if amount1Signed.Sign() < 0 { // Negative = input (user sends token1) amount1In = new(big.Int).Abs(amount1Signed) amount1Out = big.NewInt(0) } else { // Positive = output (user receives token1) amount1In = big.NewInt(0) amount1Out = new(big.Int).Set(amount1Signed) } // Scale amounts to 18 decimals for internal representation amount0InScaled := mevtypes.ScaleToDecimals(amount0In, poolInfo.Token0Decimals, 18) amount1InScaled := mevtypes.ScaleToDecimals(amount1In, poolInfo.Token1Decimals, 18) amount0OutScaled := mevtypes.ScaleToDecimals(amount0Out, poolInfo.Token0Decimals, 18) amount1OutScaled := mevtypes.ScaleToDecimals(amount1Out, poolInfo.Token1Decimals, 18) // Convert tick from *big.Int to *int32 tickInt64 := tick.Int64() tickInt32 := int32(tickInt64) // Create swap event event := &mevtypes.SwapEvent{ TxHash: tx.Hash(), BlockNumber: log.BlockNumber, LogIndex: uint(log.Index), PoolAddress: log.Address, Protocol: mevtypes.ProtocolUniswapV3, Token0: poolInfo.Token0, Token1: poolInfo.Token1, Token0Decimals: poolInfo.Token0Decimals, Token1Decimals: poolInfo.Token1Decimals, Amount0In: amount0InScaled, Amount1In: amount1InScaled, Amount0Out: amount0OutScaled, Amount1Out: amount1OutScaled, Sender: sender, Recipient: recipient, Fee: big.NewInt(int64(poolInfo.Fee)), SqrtPriceX96: sqrtPriceX96, Liquidity: liquidity, Tick: &tickInt32, } // Validate the parsed event if err := event.Validate(); err != nil { return nil, fmt.Errorf("validation failed: %w", err) } p.logger.Debug("parsed UniswapV3 swap event", "txHash", event.TxHash.Hex(), "pool", event.PoolAddress.Hex(), "token0", event.Token0.Hex(), "token1", event.Token1.Hex(), "tick", tickInt32, "sqrtPriceX96", sqrtPriceX96.String(), ) return event, nil } // ParseReceipt parses all UniswapV3 Swap events from a transaction receipt func (p *UniswapV3Parser) ParseReceipt(ctx context.Context, receipt *types.Receipt, tx *types.Transaction) ([]*mevtypes.SwapEvent, error) { var events []*mevtypes.SwapEvent for _, log := range receipt.Logs { if p.SupportsLog(*log) { event, err := p.ParseLog(ctx, *log, tx) if err != nil { // Log error but continue processing other logs p.logger.Warn("failed to parse log", "txHash", tx.Hash().Hex(), "logIndex", log.Index, "error", err, ) continue } events = append(events, event) } } return events, nil } // CalculatePriceFromSqrtPriceX96 converts sqrtPriceX96 to a human-readable price // Price = (sqrtPriceX96 / 2^96)^2 func CalculatePriceFromSqrtPriceX96(sqrtPriceX96 *big.Int, token0Decimals, token1Decimals uint8) *big.Float { if sqrtPriceX96 == nil || sqrtPriceX96.Sign() == 0 { return big.NewFloat(0) } // sqrtPriceX96 is Q64.96 format (fixed-point with 96 fractional bits) // Price = (sqrtPriceX96 / 2^96)^2 // Convert to float sqrtPriceFloat := new(big.Float).SetInt(sqrtPriceX96) // Divide by 2^96 divisor := new(big.Float).SetInt(new(big.Int).Lsh(big.NewInt(1), 96)) sqrtPrice := new(big.Float).Quo(sqrtPriceFloat, divisor) // Square to get price price := new(big.Float).Mul(sqrtPrice, sqrtPrice) // Adjust for decimal differences if token0Decimals != token1Decimals { decimalAdjustment := new(big.Float).SetInt( new(big.Int).Exp( big.NewInt(10), big.NewInt(int64(token0Decimals)-int64(token1Decimals)), nil, ), ) price = new(big.Float).Mul(price, decimalAdjustment) } return price }