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" "coppertone.tech/fraktal/mev-bot/pkg/cache" "coppertone.tech/fraktal/mev-bot/pkg/observability" pkgtypes "coppertone.tech/fraktal/mev-bot/pkg/types" ) // UniswapV2 event signatures var ( // Swap(address indexed sender, uint amount0In, uint amount1In, uint amount0Out, uint amount1Out, address indexed to) UniswapV2SwapSignature = common.HexToHash("0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822") // Mint(address indexed sender, uint amount0, uint amount1) UniswapV2MintSignature = common.HexToHash("0x4c209b5fc8ad50758f13e2e1088ba56a560dff690a1c6fef26394f4c03821c4f") // Burn(address indexed sender, uint amount0, uint amount1, address indexed to) UniswapV2BurnSignature = common.HexToHash("0xdccd412f0b1252fc5d8a58e0a26ce1e1b1e3c4f7e65b3d6e3e3e5e7e8e9e9e9e") ) // UniswapV2Parser implements the Parser interface for Uniswap V2 pools type UniswapV2Parser struct { cache cache.PoolCache logger observability.Logger // Pre-compiled ABI for efficient decoding swapABI abi.Event mintABI abi.Event burnABI abi.Event } // NewUniswapV2Parser creates a new UniswapV2 parser instance func NewUniswapV2Parser(cache cache.PoolCache, logger observability.Logger) (*UniswapV2Parser, error) { if cache == nil { return nil, fmt.Errorf("cache cannot be nil") } // Logger can be nil - we'll just skip logging if so // Define ABI for Swap event uint256Type, _ := abi.NewType("uint256", "", nil) addressType, _ := abi.NewType("address", "", nil) swapABI := abi.NewEvent( "Swap", "Swap", false, abi.Arguments{ {Name: "sender", Type: addressType, Indexed: true}, {Name: "amount0In", Type: uint256Type, Indexed: false}, {Name: "amount1In", Type: uint256Type, Indexed: false}, {Name: "amount0Out", Type: uint256Type, Indexed: false}, {Name: "amount1Out", Type: uint256Type, Indexed: false}, {Name: "to", Type: addressType, Indexed: true}, }, ) mintABI := abi.NewEvent( "Mint", "Mint", false, abi.Arguments{ {Name: "sender", Type: addressType, Indexed: true}, {Name: "amount0", Type: uint256Type, Indexed: false}, {Name: "amount1", Type: uint256Type, Indexed: false}, }, ) burnABI := abi.NewEvent( "Burn", "Burn", false, abi.Arguments{ {Name: "sender", Type: addressType, Indexed: true}, {Name: "amount0", Type: uint256Type, Indexed: false}, {Name: "amount1", Type: uint256Type, Indexed: false}, {Name: "to", Type: addressType, Indexed: true}, }, ) return &UniswapV2Parser{ cache: cache, logger: logger, swapABI: swapABI, mintABI: mintABI, burnABI: burnABI, }, nil } // Protocol returns the protocol type this parser handles func (p *UniswapV2Parser) Protocol() pkgtypes.ProtocolType { return pkgtypes.ProtocolUniswapV2 } // SupportsLog checks if this parser can handle the given log func (p *UniswapV2Parser) SupportsLog(log types.Log) bool { if len(log.Topics) == 0 { return false } topic := log.Topics[0] return topic == UniswapV2SwapSignature || topic == UniswapV2MintSignature || topic == UniswapV2BurnSignature } // ParseLog parses a single log entry into a SwapEvent func (p *UniswapV2Parser) ParseLog(ctx context.Context, log types.Log, tx *types.Transaction) (*pkgtypes.SwapEvent, error) { if len(log.Topics) == 0 { return nil, fmt.Errorf("log has no topics") } eventSignature := log.Topics[0] switch eventSignature { case UniswapV2SwapSignature: return p.parseSwap(ctx, log, tx) case UniswapV2MintSignature: return p.parseMint(ctx, log, tx) case UniswapV2BurnSignature: return p.parseBurn(ctx, log, tx) default: return nil, fmt.Errorf("unsupported event signature: %s", eventSignature.Hex()) } } // parseSwap parses a Uniswap V2 Swap event func (p *UniswapV2Parser) parseSwap(ctx context.Context, log types.Log, tx *types.Transaction) (*pkgtypes.SwapEvent, error) { // Decode event data var amount0In, amount1In, amount0Out, amount1Out big.Int // Manual decoding since it's more reliable than ABI unpacking for our use case if len(log.Data) < 128 { return nil, fmt.Errorf("invalid swap data length: %d", len(log.Data)) } amount0In.SetBytes(log.Data[0:32]) amount1In.SetBytes(log.Data[32:64]) amount0Out.SetBytes(log.Data[64:96]) amount1Out.SetBytes(log.Data[96:128]) // Extract indexed parameters from topics if len(log.Topics) < 3 { return nil, fmt.Errorf("insufficient topics for swap event") } sender := common.BytesToAddress(log.Topics[1].Bytes()) recipient := common.BytesToAddress(log.Topics[2].Bytes()) // Get pool info from cache to extract token addresses poolAddress := log.Address pool, err := p.cache.GetByAddress(ctx, poolAddress) if err != nil { p.logger.Warn("pool not found in cache, skipping", "pool", poolAddress.Hex()) return nil, fmt.Errorf("pool not in cache: %s", poolAddress.Hex()) } // Validate amounts - at least one direction must have non-zero amounts if (amount0In.Cmp(big.NewInt(0)) == 0 && amount1Out.Cmp(big.NewInt(0)) == 0) && (amount1In.Cmp(big.NewInt(0)) == 0 && amount0Out.Cmp(big.NewInt(0)) == 0) { return nil, fmt.Errorf("invalid swap amounts: all zero") } // Create SwapEvent event := &pkgtypes.SwapEvent{ Protocol: pkgtypes.ProtocolUniswapV2, PoolAddress: poolAddress, Token0: pool.Token0, Token1: pool.Token1, Token0Decimals: pool.Token0Decimals, Token1Decimals: pool.Token1Decimals, Amount0In: &amount0In, Amount1In: &amount1In, Amount0Out: &amount0Out, Amount1Out: &amount1Out, Sender: sender, Recipient: recipient, TxHash: tx.Hash(), BlockNumber: log.BlockNumber, LogIndex: uint(log.Index), } return event, nil } // parseMint parses a Uniswap V2 Mint event (liquidity addition) func (p *UniswapV2Parser) parseMint(ctx context.Context, log types.Log, tx *types.Transaction) (*pkgtypes.SwapEvent, error) { // For MVP, we're only interested in swaps, not liquidity events // Return nil without error (not an error, just not interesting) return nil, nil } // parseBurn parses a Uniswap V2 Burn event (liquidity removal) func (p *UniswapV2Parser) parseBurn(ctx context.Context, log types.Log, tx *types.Transaction) (*pkgtypes.SwapEvent, error) { // For MVP, we're only interested in swaps, not liquidity events // Return nil without error (not an error, just not interesting) return nil, nil } // ParseReceipt parses all logs in a transaction receipt func (p *UniswapV2Parser) ParseReceipt(ctx context.Context, receipt *types.Receipt, tx *types.Transaction) ([]*pkgtypes.SwapEvent, error) { var events []*pkgtypes.SwapEvent for _, log := range receipt.Logs { if !p.SupportsLog(*log) { continue } event, err := p.ParseLog(ctx, *log, tx) if err != nil { p.logger.Debug("failed to parse log", "error", err, "tx", tx.Hash().Hex()) continue } if event != nil { events = append(events, event) } } return events, nil }