package events import ( "bytes" "encoding/hex" "fmt" "math/big" "strings" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/holiman/uint256" "github.com/fraktal/mev-beta/internal/logger" "github.com/fraktal/mev-beta/pkg/calldata" "github.com/fraktal/mev-beta/pkg/interfaces" "github.com/fraktal/mev-beta/pkg/uniswap" ) // parseSignedInt256 correctly parses a signed 256-bit integer from 32 bytes // This is critical for UniswapV3 events which use int256 for amounts // Returns error instead of silently returning zero for invalid data func parseSignedInt256(data []byte) (*big.Int, error) { if len(data) != 32 { return nil, fmt.Errorf("invalid data length: expected 32 bytes, got %d", len(data)) } // Validate data is not all zeros (likely corruption or failed transaction) allZero := true for _, b := range data { if b != 0 { allZero = false break } } if allZero { return nil, fmt.Errorf("data is all zeros - likely corrupted or from failed transaction") } value := new(big.Int).SetBytes(data) // Check if the value is negative (MSB set) if len(data) > 0 && data[0]&0x80 != 0 { // Convert from two's complement // Subtract 2^256 to get the negative value maxUint256 := new(big.Int) maxUint256.Lsh(big.NewInt(1), 256) value.Sub(value, maxUint256) } return value, nil } // EventType represents the type of DEX event type EventType int const ( Unknown EventType = iota Swap AddLiquidity RemoveLiquidity NewPool ) // String returns a string representation of the event type func (et EventType) String() string { switch et { case Unknown: return "Unknown" case Swap: return "Swap" case AddLiquidity: return "AddLiquidity" case RemoveLiquidity: return "RemoveLiquidity" case NewPool: return "NewPool" default: return "Unknown" } } type Event struct { Type EventType Protocol string // UniswapV2, UniswapV3, SushiSwap, etc. PoolAddress common.Address Token0 common.Address Token1 common.Address Amount0 *big.Int Amount1 *big.Int SqrtPriceX96 *uint256.Int Liquidity *uint256.Int Tick int Timestamp uint64 TransactionHash common.Hash BlockNumber uint64 } // EventParser parses DEX events from Ethereum transactions type EventParser struct { // Known DEX contract addresses UniswapV2Factory common.Address UniswapV3Factory common.Address SushiSwapFactory common.Address // Router addresses UniswapV2Router01 common.Address UniswapV2Router02 common.Address UniswapV3Router common.Address SushiSwapRouter common.Address // Known pool addresses (for quick lookup) knownPools map[common.Address]string // Event signatures for parsing logs swapEventV2Sig common.Hash swapEventV3Sig common.Hash mintEventV2Sig common.Hash mintEventV3Sig common.Hash burnEventV2Sig common.Hash burnEventV3Sig common.Hash // CRITICAL FIX: Token extractor interface for working token extraction tokenExtractor interfaces.TokenExtractor logger *logger.Logger } func (ep *EventParser) logDebug(message string, kv ...interface{}) { if ep.logger != nil { args := append([]interface{}{message}, kv...) ep.logger.Debug(args...) return } fmt.Println(append([]interface{}{"[DEBUG]", message}, kv...)...) } func (ep *EventParser) logInfo(message string, kv ...interface{}) { if ep.logger != nil { args := append([]interface{}{message}, kv...) ep.logger.Info(args...) return } fmt.Println(append([]interface{}{"[INFO]", message}, kv...)...) } func (ep *EventParser) logWarn(message string, kv ...interface{}) { if ep.logger != nil { args := append([]interface{}{message}, kv...) ep.logger.Warn(args...) return } fmt.Println(append([]interface{}{"[WARN]", message}, kv...)...) } // NewEventParser creates a new event parser with official Arbitrum deployment addresses func NewEventParser() *EventParser { return NewEventParserWithLogger(nil) } // NewEventParserWithLogger instantiates an EventParser using the provided logger. // When logger is nil, it falls back to the shared multi-file logger with INFO level. func NewEventParserWithLogger(log *logger.Logger) *EventParser { return NewEventParserWithTokenExtractor(log, nil) } // NewEventParserWithTokenExtractor instantiates an EventParser with a TokenExtractor for enhanced parsing. // This is the primary constructor for using the working L2 parser logic. func NewEventParserWithTokenExtractor(log *logger.Logger, tokenExtractor interfaces.TokenExtractor) *EventParser { if log == nil { log = logger.New("info", "text", "") } parser := &EventParser{ logger: log, tokenExtractor: tokenExtractor, // Official Arbitrum DEX Factory Addresses UniswapV2Factory: common.HexToAddress("0xf1D7CC64Fb4452F05c498126312eBE29f30Fbcf9"), // Official Uniswap V2 Factory on Arbitrum UniswapV3Factory: common.HexToAddress("0x1F98431c8aD98523631AE4a59f267346ea31F984"), // Official Uniswap V3 Factory on Arbitrum SushiSwapFactory: common.HexToAddress("0xc35DADB65012eC5796536bD9864eD8773aBc74C4"), // Official SushiSwap V2 Factory on Arbitrum // Official Arbitrum DEX Router Addresses UniswapV2Router01: common.HexToAddress("0x0000000000000000000000000000000000000000"), // V2Router01 not deployed on Arbitrum UniswapV2Router02: common.HexToAddress("0x4752ba5dbc23f44d87826276bf6fd6b1c372ad24"), // Official Uniswap V2 Router02 on Arbitrum UniswapV3Router: common.HexToAddress("0xE592427A0AEce92De3Edee1F18E0157C05861564"), // Official Uniswap V3 SwapRouter on Arbitrum SushiSwapRouter: common.HexToAddress("0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506"), // Official SushiSwap Router on Arbitrum knownPools: make(map[common.Address]string), } // Initialize event signatures parser.swapEventV2Sig = crypto.Keccak256Hash([]byte("Swap(address,uint256,uint256,uint256,uint256,address)")) parser.swapEventV3Sig = crypto.Keccak256Hash([]byte("Swap(address,address,int256,int256,uint160,uint128,int24)")) parser.mintEventV2Sig = crypto.Keccak256Hash([]byte("Mint(address,uint256,uint256)")) parser.mintEventV3Sig = crypto.Keccak256Hash([]byte("Mint(address,address,int24,int24,uint128,uint256,uint256)")) parser.burnEventV2Sig = crypto.Keccak256Hash([]byte("Burn(address,uint256,uint256)")) parser.burnEventV3Sig = crypto.Keccak256Hash([]byte("Burn(address,int24,int24,uint128,uint256,uint256)")) // Pre-populate known Arbitrum pools (high volume pools) parser.knownPools[common.HexToAddress("0xC6962004f452bE9203591991D15f6b388e09E8D0")] = "UniswapV3" // USDC/WETH 0.05% parser.knownPools[common.HexToAddress("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")] = "UniswapV3" // USDC/WETH 0.3% parser.knownPools[common.HexToAddress("0xC31E54c7a869B9FcBEcc14363CF510d1c41fa443")] = "UniswapV3" // WETH/USDT 0.05% parser.knownPools[common.HexToAddress("0x641C00A822e8b671738d32a431a4Fb6074E5c79d")] = "UniswapV3" // WETH/USDT 0.3% // Add test addresses to known pools parser.knownPools[common.HexToAddress("0x905dfCD5649217c42684f23958568e533C711Aa3")] = "SushiSwap" // Test SushiSwap pool parser.knownPools[common.HexToAddress("0x84652bb2539513BAf36e225c930Fdd8eaa63CE27")] = "Camelot" // Test Camelot pool parser.knownPools[common.HexToAddress("0x32dF62dc3aEd2cD6224193052Ce665DC18165841")] = "Balancer" // Test Balancer pool parser.knownPools[common.HexToAddress("0x7f90122BF0700F9E7e1F688fe926940E8839F353")] = "Curve" // Test Curve pool // Token extractor is now injected via constructor parameter // This allows for flexible implementation without circular imports return parser } // ParseTransactionReceipt parses events from a transaction receipt func (ep *EventParser) ParseTransactionReceipt(receipt *types.Receipt, blockNumber uint64, timestamp uint64) ([]*Event, error) { return ep.ParseTransactionReceiptWithTx(receipt, nil, blockNumber, timestamp) } // ParseTransactionReceiptWithTx parses events from a transaction receipt with optional transaction for token extraction func (ep *EventParser) ParseTransactionReceiptWithTx(receipt *types.Receipt, tx *types.Transaction, blockNumber uint64, timestamp uint64) ([]*Event, error) { events := make([]*Event, 0) // If we have the transaction, try to extract tokens from calldata first // This provides a token lookup cache for enriching log-based events var txTokenCache map[string][]common.Address if tx != nil { txTokenCache = make(map[string][]common.Address) txEvents, _ := ep.ParseTransaction(tx, blockNumber, timestamp) for _, ev := range txEvents { if ev != nil && ev.Token0 != (common.Address{}) && ev.Token1 != (common.Address{}) { // Cache tokens by pool address for enriching log events txTokenCache[ev.PoolAddress.Hex()] = []common.Address{ev.Token0, ev.Token1} } } } // Parse logs for DEX events for _, log := range receipt.Logs { // Skip anonymous logs if len(log.Topics) == 0 { continue } // Check if this is a DEX event based on the topic signature eventSig := log.Topics[0] var event *Event var err error switch eventSig { case ep.swapEventV2Sig: event, err = ep.parseUniswapV2Swap(log, blockNumber, timestamp, receipt.TxHash, txTokenCache) case ep.swapEventV3Sig: event, err = ep.parseUniswapV3Swap(log, blockNumber, timestamp, receipt.TxHash, txTokenCache) case ep.mintEventV2Sig: event, err = ep.parseUniswapV2Mint(log, blockNumber, timestamp, receipt.TxHash) case ep.mintEventV3Sig: event, err = ep.parseUniswapV3Mint(log, blockNumber, timestamp, receipt.TxHash) case ep.burnEventV2Sig: event, err = ep.parseUniswapV2Burn(log, blockNumber, timestamp, receipt.TxHash) case ep.burnEventV3Sig: event, err = ep.parseUniswapV3Burn(log, blockNumber, timestamp, receipt.TxHash) } if err != nil { // Log error but continue parsing other logs continue } if event != nil { events = append(events, event) } } return events, nil } // IsDEXInteraction checks if a transaction interacts with a known DEX contract func (ep *EventParser) IsDEXInteraction(tx *types.Transaction) bool { if tx.To() == nil { return false } to := *tx.To() // Check factory contracts if to == ep.UniswapV2Factory || to == ep.UniswapV3Factory || to == ep.SushiSwapFactory { return true } // Check router contracts if to == ep.UniswapV2Router01 || to == ep.UniswapV2Router02 || to == ep.UniswapV3Router || to == ep.SushiSwapRouter { return true } // Check known pools if _, exists := ep.knownPools[to]; exists { return true } return false } // identifyProtocol identifies which DEX protocol a transaction is interacting with func (ep *EventParser) identifyProtocol(tx *types.Transaction) string { if tx.To() == nil { return "Unknown" } to := *tx.To() // Check factory contracts if to == ep.UniswapV2Factory { return "UniswapV2" } if to == ep.UniswapV3Factory { return "UniswapV3" } if to == ep.SushiSwapFactory { return "SushiSwap" } // Check router contracts if to == ep.UniswapV2Router01 || to == ep.UniswapV2Router02 { return "UniswapV2" } if to == ep.UniswapV3Router { return "UniswapV3" } if to == ep.SushiSwapRouter { return "SushiSwap" } // Check known pools if protocol, exists := ep.knownPools[to]; exists { return protocol } // Try to identify from function signature in transaction data if len(tx.Data()) >= 4 { sig := common.Bytes2Hex(tx.Data()[:4]) switch sig { case "0xac9650d8": // multicall (Uniswap V3) return "UniswapV3" case "0x1f0464d1": // multicall with blockhash (Uniswap V3) return "UniswapV3" case "0x88316456": // swap (Uniswap V2) return "UniswapV2" case "0x128acb08": // swap (SushiSwap) return "SushiSwap" case "0x38ed1739": // swapExactTokensForTokens (Uniswap V2) return "UniswapV2" case "0x8803dbee": // swapTokensForExactTokens (Uniswap V2) return "UniswapV2" case "0x7ff36ab5": // swapExactETHForTokens (Uniswap V2) return "UniswapV2" case "0xb6f9de95": // swapExactTokensForETH (Uniswap V2) return "UniswapV2" case "0x414bf389": // exactInputSingle (Uniswap V3) return "UniswapV3" case "0xdb3e2198": // exactInput (Uniswap V3) return "UniswapV3" case "0xf305d719": // exactOutputSingle (Uniswap V3) return "UniswapV3" case "0x04e45aaf": // exactOutput (Uniswap V3) return "UniswapV3" case "0x18cbafe5": // swapExactTokensForTokensSupportingFeeOnTransferTokens (Uniswap V2) return "UniswapV2" case "0x18cffa1c": // swapExactETHForTokensSupportingFeeOnTransferTokens (Uniswap V2) return "UniswapV2" case "0x791ac947": // swapExactTokensForETHSupportingFeeOnTransferTokens (Uniswap V2) return "UniswapV2" case "0x5ae401dc": // multicall (Uniswap V3) return "UniswapV3" } } return "Unknown" } // parseUniswapV2Swap parses a Uniswap V2 Swap event func (ep *EventParser) parseUniswapV2Swap(log *types.Log, blockNumber uint64, timestamp uint64, txHash common.Hash, txTokenCache map[string][]common.Address) (*Event, error) { if len(log.Topics) != 2 || len(log.Data) != 32*4 { return nil, fmt.Errorf("invalid Uniswap V2 Swap event log") } // Parse the data fields amount0In := new(big.Int).SetBytes(log.Data[0:32]) amount1In := new(big.Int).SetBytes(log.Data[32:64]) amount0Out := new(big.Int).SetBytes(log.Data[64:96]) amount1Out := new(big.Int).SetBytes(log.Data[96:128]) // Determine which token is being swapped in/out var amount0, amount1 *big.Int if amount0In.Cmp(big.NewInt(0)) > 0 { amount0 = amount0In } else { amount0 = new(big.Int).Neg(amount0Out) } if amount1In.Cmp(big.NewInt(0)) > 0 { amount1 = amount1In } else { amount1 = new(big.Int).Neg(amount1Out) } // DEBUG: Log details about this event creation if log.Address == (common.Address{}) { ep.logWarn("swap event emitted without pool address", "block_number", blockNumber, "log_index", log.Index, "topic_count", len(log.Topics), "data_bytes", len(log.Data), ) } // CRITICAL FIX: Get token addresses from pool // Swap event logs don't contain token addresses, so we use tokens from transaction calldata token0, token1 := ep.getPoolTokens(log.Address, txHash, txTokenCache) event := &Event{ Type: Swap, Protocol: "UniswapV2", PoolAddress: log.Address, Token0: token0, Token1: token1, Amount0: amount0, Amount1: amount1, Timestamp: timestamp, TransactionHash: txHash, BlockNumber: blockNumber, } return event, nil } // parseUniswapV3Swap parses a Uniswap V3 Swap event func (ep *EventParser) parseUniswapV3Swap(log *types.Log, blockNumber uint64, timestamp uint64, txHash common.Hash, txTokenCache map[string][]common.Address) (*Event, error) { if len(log.Topics) != 3 || len(log.Data) != 32*5 { return nil, fmt.Errorf("invalid Uniswap V3 Swap event log") } // Parse the data fields - UniswapV3 uses signed int256 for amounts amount0, err := parseSignedInt256(log.Data[0:32]) if err != nil { return nil, fmt.Errorf("failed to parse amount0: %w", err) } amount1, err := parseSignedInt256(log.Data[32:64]) if err != nil { return nil, fmt.Errorf("failed to parse amount1: %w", err) } sqrtPriceX96 := new(big.Int).SetBytes(log.Data[64:96]) liquidity := new(big.Int).SetBytes(log.Data[96:128]) tick := new(big.Int).SetBytes(log.Data[128:160]) // CRITICAL FIX: Get token addresses from pool // Swap event logs don't contain token addresses, so we use tokens from transaction calldata token0, token1 := ep.getPoolTokens(log.Address, txHash, txTokenCache) event := &Event{ Type: Swap, Protocol: "UniswapV3", PoolAddress: log.Address, Token0: token0, Token1: token1, Amount0: amount0, Amount1: amount1, SqrtPriceX96: uint256.MustFromBig(sqrtPriceX96), Liquidity: uint256.MustFromBig(liquidity), Tick: int(tick.Int64()), Timestamp: timestamp, TransactionHash: txHash, BlockNumber: blockNumber, } return event, nil } // parseUniswapV2Mint parses a Uniswap V2 Mint event func (ep *EventParser) parseUniswapV2Mint(log *types.Log, blockNumber uint64, timestamp uint64, txHash common.Hash) (*Event, error) { if len(log.Topics) != 2 || len(log.Data) != 32*2 { return nil, fmt.Errorf("invalid Uniswap V2 Mint event log") } // Parse the data fields amount0 := new(big.Int).SetBytes(log.Data[0:32]) amount1 := new(big.Int).SetBytes(log.Data[32:64]) event := &Event{ Type: AddLiquidity, Protocol: "UniswapV2", PoolAddress: log.Address, Amount0: amount0, Amount1: amount1, Timestamp: timestamp, TransactionHash: txHash, BlockNumber: blockNumber, } return event, nil } // parseUniswapV3Mint parses a Uniswap V3 Mint event func (ep *EventParser) parseUniswapV3Mint(log *types.Log, blockNumber uint64, timestamp uint64, txHash common.Hash) (*Event, error) { if len(log.Topics) != 3 || len(log.Data) != 32*4 { return nil, fmt.Errorf("invalid Uniswap V3 Mint event log") } // Parse the data fields amount0 := new(big.Int).SetBytes(log.Data[0:32]) amount1 := new(big.Int).SetBytes(log.Data[32:64]) event := &Event{ Type: AddLiquidity, Protocol: "UniswapV3", PoolAddress: log.Address, Amount0: amount0, Amount1: amount1, Timestamp: timestamp, TransactionHash: txHash, BlockNumber: blockNumber, } return event, nil } // parseUniswapV2Burn parses a Uniswap V2 Burn event func (ep *EventParser) parseUniswapV2Burn(log *types.Log, blockNumber uint64, timestamp uint64, txHash common.Hash) (*Event, error) { if len(log.Topics) != 2 || len(log.Data) != 32*2 { return nil, fmt.Errorf("invalid Uniswap V2 Burn event log") } // Parse the data fields amount0 := new(big.Int).SetBytes(log.Data[0:32]) amount1 := new(big.Int).SetBytes(log.Data[32:64]) event := &Event{ Type: RemoveLiquidity, Protocol: "UniswapV2", PoolAddress: log.Address, Amount0: amount0, Amount1: amount1, Timestamp: timestamp, TransactionHash: txHash, BlockNumber: blockNumber, } return event, nil } // parseUniswapV3Burn parses a Uniswap V3 Burn event func (ep *EventParser) parseUniswapV3Burn(log *types.Log, blockNumber uint64, timestamp uint64, txHash common.Hash) (*Event, error) { if len(log.Topics) != 3 || len(log.Data) != 32*4 { return nil, fmt.Errorf("invalid Uniswap V3 Burn event log") } // Parse the data fields amount0 := new(big.Int).SetBytes(log.Data[0:32]) amount1 := new(big.Int).SetBytes(log.Data[32:64]) event := &Event{ Type: RemoveLiquidity, Protocol: "UniswapV3", PoolAddress: log.Address, Amount0: amount0, Amount1: amount1, Timestamp: timestamp, TransactionHash: txHash, BlockNumber: blockNumber, } return event, nil } // ParseTransaction parses events from a transaction by decoding the function call data func (ep *EventParser) ParseTransaction(tx *types.Transaction, blockNumber uint64, timestamp uint64) ([]*Event, error) { // Check if this is a DEX interaction if !ep.IsDEXInteraction(tx) { // Return empty slice for non-DEX transactions return []*Event{}, nil } if tx.To() == nil { return []*Event{}, nil } // Determine the protocol protocol := ep.identifyProtocol(tx) // Parse transaction data to extract swap details data := tx.Data() if len(data) < 4 { return []*Event{}, fmt.Errorf("insufficient transaction data") } // Get function selector (first 4 bytes) selector := common.Bytes2Hex(data[:4]) events := make([]*Event, 0) switch selector { case "38ed1739": // swapExactTokensForTokens event, err := ep.parseSwapExactTokensForTokensFromTx(tx, protocol, blockNumber, timestamp) if err != nil { return []*Event{}, fmt.Errorf("failed to parse swapExactTokensForTokens: %w", err) } if event != nil { events = append(events, event) } case "414bf389": // exactInputSingle (Uniswap V3) event, err := ep.parseExactInputSingleFromTx(tx, protocol, blockNumber, timestamp) if err != nil { return []*Event{}, fmt.Errorf("failed to parse exactInputSingle: %w", err) } if event != nil { events = append(events, event) } case "db3e2198": // exactInput (Uniswap V3) event, err := ep.parseExactInputFromTx(tx, protocol, blockNumber, timestamp) if err != nil { return []*Event{}, fmt.Errorf("failed to parse exactInput: %w", err) } if event != nil { events = append(events, event) } case "7ff36ab5", "18cffa1c": // swapExactETHForTokens variants event, err := ep.parseSwapExactETHForTokensFromTx(tx, protocol, blockNumber, timestamp) if err != nil { return []*Event{}, fmt.Errorf("failed to parse swapExactETHForTokens: %w", err) } if event != nil { events = append(events, event) } case "ac9650d8": // multicall (Uniswap V3) event, err := ep.parseMulticallFromTx(tx, protocol, blockNumber, timestamp) if err != nil { return []*Event{}, fmt.Errorf("failed to parse multicall: %w", err) } if event != nil { events = append(events, event) } case "f305d719": // exactOutputSingle (Uniswap V3) event, err := ep.parseExactOutputSingleFromTx(tx, protocol, blockNumber, timestamp) if err != nil { return []*Event{}, fmt.Errorf("failed to parse exactOutputSingle: %w", err) } if event != nil { events = append(events, event) } default: // For unknown functions, create a basic event // Use router address as fallback since we can't extract tokens event := &Event{ Type: Swap, Protocol: protocol, PoolAddress: *tx.To(), // Router address as fallback for unknown functions Token0: common.Address{}, // Will be determined from logs Token1: common.Address{}, // Will be determined from logs Amount0: tx.Value(), // Use transaction value as fallback Amount1: big.NewInt(0), SqrtPriceX96: uint256.NewInt(0), Liquidity: uint256.NewInt(0), Tick: 0, Timestamp: timestamp, TransactionHash: tx.Hash(), BlockNumber: blockNumber, } events = append(events, event) } return events, nil } // parseSwapExactTokensForTokensFromTx parses swapExactTokensForTokens from transaction data func (ep *EventParser) parseSwapExactTokensForTokensFromTx(tx *types.Transaction, protocol string, blockNumber uint64, timestamp uint64) (*Event, error) { data := tx.Data()[4:] // Skip function selector if len(data) < 160 { // 5 parameters * 32 bytes return nil, fmt.Errorf("insufficient data for swapExactTokensForTokens") } // Parse ABI-encoded parameters amountIn := new(big.Int).SetBytes(data[0:32]) amountOutMin := new(big.Int).SetBytes(data[32:64]) // Extract path array from ABI-encoded data // Path is at offset 96 (64 + 32), and its length is at that position var token0, token1 common.Address if len(data) >= 128 { // Ensure we have enough data pathOffset := new(big.Int).SetBytes(data[64:96]).Uint64() if pathOffset < uint64(len(data)) && pathOffset+32 < uint64(len(data)) { pathLength := new(big.Int).SetBytes(data[pathOffset : pathOffset+32]).Uint64() if pathLength >= 40 { // At least 2 addresses (20 bytes each) // First token (token0) token0 = common.BytesToAddress(data[pathOffset+32 : pathOffset+52]) // Last token (token1) - assuming simple path with 2 tokens if pathLength >= 40 { token1 = common.BytesToAddress(data[pathOffset+52 : pathOffset+72]) } } } } // Derive actual pool address from token pair poolAddress := ep.derivePoolAddress(token0, token1, protocol) event := &Event{ Type: Swap, Protocol: protocol, PoolAddress: poolAddress, Token0: token0, Token1: token1, Amount0: amountIn, Amount1: amountOutMin, SqrtPriceX96: uint256.NewInt(0), Liquidity: uint256.NewInt(0), Tick: 0, Timestamp: timestamp, TransactionHash: tx.Hash(), BlockNumber: blockNumber, } return event, nil } // parseExactInputSingleFromTx parses exactInputSingle from transaction data func (ep *EventParser) parseExactInputSingleFromTx(tx *types.Transaction, protocol string, blockNumber uint64, timestamp uint64) (*Event, error) { data := tx.Data()[4:] // Skip function selector if len(data) < 256 { // 8 parameters * 32 bytes return nil, fmt.Errorf("insufficient data for exactInputSingle") } // Parse ExactInputSingleParams struct tokenIn := common.BytesToAddress(data[12:32]) tokenOut := common.BytesToAddress(data[44:64]) fee := new(big.Int).SetBytes(data[64:96]).Uint64() amountIn := new(big.Int).SetBytes(data[160:192]) amountOutMin := new(big.Int).SetBytes(data[192:224]) // Derive actual pool address from token pair poolAddress := ep.derivePoolAddress(tokenIn, tokenOut, protocol) event := &Event{ Type: Swap, Protocol: protocol, PoolAddress: poolAddress, Token0: tokenIn, Token1: tokenOut, Amount0: amountIn, Amount1: amountOutMin, SqrtPriceX96: uint256.NewInt(0), Liquidity: uint256.NewInt(0), Tick: 0, Timestamp: timestamp, TransactionHash: tx.Hash(), BlockNumber: blockNumber, } // Store fee information for later use event.Protocol = fmt.Sprintf("%s_fee_%d", protocol, fee) return event, nil } // parseExactInputFromTx parses exactInput (multi-hop) from transaction data func (ep *EventParser) parseExactInputFromTx(tx *types.Transaction, protocol string, blockNumber uint64, timestamp uint64) (*Event, error) { data := tx.Data()[4:] // Skip function selector if len(data) < 160 { // 5 parameters * 32 bytes return nil, fmt.Errorf("insufficient data for exactInput") } // Parse ExactInputParams struct amountIn := new(big.Int).SetBytes(data[96:128]) amountOutMin := new(big.Int).SetBytes(data[128:160]) // Extract path from encoded path bytes (first parameter) // Path is encoded at offset 0, and its length is at offset 32 var token0, token1 common.Address if len(data) >= 96 { pathOffset := new(big.Int).SetBytes(data[0:32]).Uint64() if pathOffset < uint64(len(data)) && pathOffset+32 < uint64(len(data)) { pathLength := new(big.Int).SetBytes(data[pathOffset : pathOffset+32]).Uint64() if pathLength >= 23 { // At least tokenA(20) + fee(3) for Uniswap V3 encoded path // First token (20 bytes) token0 = common.BytesToAddress(data[pathOffset+32 : pathOffset+52]) // For multi-hop paths, find the last token // Uniswap V3 path format: tokenA(20) + fee(3) + tokenB(20) + fee(3) + tokenC(20)... if pathLength >= 43 { // tokenA(20) + fee(3) + tokenB(20) token1 = common.BytesToAddress(data[pathOffset+32+20+3 : pathOffset+32+20+3+20]) // Skip token0(20) + fee(3) } } } } // Derive actual pool address from token pair poolAddress := ep.derivePoolAddress(token0, token1, protocol) event := &Event{ Type: Swap, Protocol: protocol, PoolAddress: poolAddress, Token0: token0, Token1: token1, Amount0: amountIn, Amount1: amountOutMin, SqrtPriceX96: uint256.NewInt(0), Liquidity: uint256.NewInt(0), Tick: 0, Timestamp: timestamp, TransactionHash: tx.Hash(), BlockNumber: blockNumber, } return event, nil } // parseSwapExactETHForTokensFromTx parses swapExactETHForTokens from transaction data func (ep *EventParser) parseSwapExactETHForTokensFromTx(tx *types.Transaction, protocol string, blockNumber uint64, timestamp uint64) (*Event, error) { data := tx.Data()[4:] // Skip function selector if len(data) < 128 { // 4 parameters * 32 bytes return nil, fmt.Errorf("insufficient data for swapExactETHForTokens") } amountOutMin := new(big.Int).SetBytes(data[0:32]) // Extract path array to get the output token // Path offset is at position 32 var token1 common.Address if len(data) >= 96 { pathOffset := new(big.Int).SetBytes(data[32:64]).Uint64() if pathOffset < uint64(len(data)) && pathOffset+32 < uint64(len(data)) { pathLength := new(big.Int).SetBytes(data[pathOffset : pathOffset+32]).Uint64() if pathLength >= 40 { // At least 2 addresses (20 bytes each) // Extract the last token from the path (output token) // For swapExactETHForTokens, we want the second token in the path if pathLength >= 40 { token1 = common.BytesToAddress(data[pathOffset+52 : pathOffset+72]) } } } } event := &Event{ Type: Swap, Protocol: protocol, PoolAddress: *tx.To(), Token0: common.HexToAddress("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"), // ETH Token1: token1, Amount0: tx.Value(), // ETH amount from transaction value Amount1: amountOutMin, SqrtPriceX96: uint256.NewInt(0), Liquidity: uint256.NewInt(0), Tick: 0, Timestamp: timestamp, TransactionHash: tx.Hash(), BlockNumber: blockNumber, } return event, nil } // parseExactOutputSingleFromTx parses exactOutputSingle from transaction data func (ep *EventParser) parseExactOutputSingleFromTx(tx *types.Transaction, protocol string, blockNumber uint64, timestamp uint64) (*Event, error) { data := tx.Data()[4:] // Skip function selector if len(data) < 256 { // 8 parameters * 32 bytes return nil, fmt.Errorf("insufficient data for exactOutputSingle") } // Parse ExactOutputSingleParams struct tokenIn := common.BytesToAddress(data[12:32]) tokenOut := common.BytesToAddress(data[44:64]) fee := new(big.Int).SetBytes(data[64:96]).Uint64() amountOut := new(big.Int).SetBytes(data[160:192]) amountInMaximum := new(big.Int).SetBytes(data[192:224]) // Derive actual pool address from token pair poolAddress := ep.derivePoolAddress(tokenIn, tokenOut, protocol) event := &Event{ Type: Swap, Protocol: protocol, PoolAddress: poolAddress, Token0: tokenIn, Token1: tokenOut, Amount0: amountInMaximum, // Maximum input amount Amount1: amountOut, // Exact output amount SqrtPriceX96: uint256.NewInt(0), Liquidity: uint256.NewInt(0), Tick: 0, Timestamp: timestamp, TransactionHash: tx.Hash(), BlockNumber: blockNumber, } // Store fee information for later use event.Protocol = fmt.Sprintf("%s_fee_%d", protocol, fee) return event, nil } // parseMulticallFromTx parses multicall transactions to extract token addresses and amounts func (ep *EventParser) parseMulticallFromTx(tx *types.Transaction, protocol string, blockNumber uint64, timestamp uint64) (*Event, error) { data := tx.Data()[4:] // Skip function selector if len(data) < 64 { // Need at least bytes array offset and length return nil, fmt.Errorf("insufficient data for multicall") } // Extract tokens from multicall data using comprehensive scanning tokenCtx := &calldata.MulticallContext{ TxHash: tx.Hash().Hex(), Protocol: protocol, Stage: "events.parser.parseMulticallFromTx", BlockNumber: blockNumber, } swap := ep.extractSwapFromMulticallData(data, tokenCtx) var ( token0 common.Address token1 common.Address amount0 *big.Int amount1 *big.Int poolAddress common.Address ) // CRITICAL FIX: Check if we have valid tokens from multicall parsing validTokens := swap != nil && swap.TokenIn != (common.Address{}) && swap.TokenOut != (common.Address{}) if validTokens { // Use multicall parsed tokens token0 = swap.TokenIn token1 = swap.TokenOut amount0 = swap.AmountIn if swap.AmountOut != nil { amount1 = new(big.Int).Set(swap.AmountOut) } else if swap.AmountOutMinimum != nil { amount1 = new(big.Int).Set(swap.AmountOutMinimum) } if swap.PoolAddress != (common.Address{}) { poolAddress = swap.PoolAddress } if protocol == "" { protocol = swap.Protocol } ep.logDebug("multicall extracted swap tokens", "tx_hash", tx.Hash().Hex(), "token0", token0.Hex(), "token1", token1.Hex(), ) } else { // CRITICAL FIX: Try direct function parsing (like L2 parser does) directTokens := ep.parseDirectFunction(tx) if len(directTokens) >= 2 { ep.logInfo("direct parsing recovered swap tokens", "tx_hash", tx.Hash().Hex(), "token0", directTokens[0].Hex(), "token1", directTokens[1].Hex(), ) token0 = directTokens[0] token1 = directTokens[1] // Extract amounts from transaction data amount0, amount1 = ep.extractAmountsFromData(tx.Data()) } else { methodID := "none" if len(tx.Data()) >= 4 { methodID = hex.EncodeToString(tx.Data()[:4]) } ep.logWarn("direct parsing failed to recover tokens", "tx_hash", tx.Hash().Hex(), "method_id", methodID, "data_len", len(tx.Data()), ) } if token0 == (common.Address{}) || token1 == (common.Address{}) { // Enhanced recovery when both multicall and direct parsing fail recoveredTokens, recoveryErr := ep.protocolSpecificRecovery(data, tokenCtx, protocol) if recoveryErr == nil && len(recoveredTokens) >= 2 { token0 = recoveredTokens[0] token1 = recoveredTokens[1] // Extract amounts from transaction data amount0, amount1 = ep.extractAmountsFromData(tx.Data()) } } } if poolAddress == (common.Address{}) { if token0 != (common.Address{}) && token1 != (common.Address{}) { poolAddress = ep.derivePoolAddress(token0, token1, protocol) // Validate derived pool address if poolAddress == (common.Address{}) { // Pool derivation failed, skip this event return nil, fmt.Errorf("pool derivation failed for tokens %s, %s", token0.Hex(), token1.Hex()) } } else { // Protocol-specific error recovery for token extraction recoveredTokens, recoveryErr := ep.protocolSpecificRecovery(data, tokenCtx, protocol) if recoveryErr != nil || len(recoveredTokens) < 2 { // Cannot derive pool address without token information, skip this event return nil, fmt.Errorf("cannot recover tokens from multicall: %v", recoveryErr) } token0 = recoveredTokens[0] token1 = recoveredTokens[1] poolAddress = ep.derivePoolAddress(token0, token1, protocol) if poolAddress == (common.Address{}) { // Even after recovery, pool derivation failed return nil, fmt.Errorf("pool derivation failed even after token recovery") } } } // Final validation: Ensure pool address is valid and not suspicious if poolAddress == (common.Address{}) || poolAddress == token0 || poolAddress == token1 { // Invalid pool address, skip this event return nil, fmt.Errorf("invalid pool address: %s", poolAddress.Hex()) } // Check for suspicious zero-padded addresses poolHex := poolAddress.Hex() if len(poolHex) == 42 && poolHex[:20] == "0x000000000000000000" { // Suspicious zero-padded address, skip this event return nil, fmt.Errorf("suspicious zero-padded pool address: %s", poolHex) } if amount0 == nil { amount0 = tx.Value() } if amount1 == nil { amount1 = big.NewInt(0) } event := &Event{ Type: Swap, Protocol: protocol, PoolAddress: poolAddress, Token0: token0, Token1: token1, Amount0: amount0, Amount1: amount1, SqrtPriceX96: uint256.NewInt(0), Liquidity: uint256.NewInt(0), Tick: 0, Timestamp: timestamp, TransactionHash: tx.Hash(), BlockNumber: blockNumber, } return event, nil } // extractSwapFromMulticallData decodes the first viable swap call from multicall payload data. func (ep *EventParser) extractSwapFromMulticallData(data []byte, ctx *calldata.MulticallContext) *calldata.SwapCall { // CRITICAL FIX: Use working token extractor interface first if ep.tokenExtractor != nil { ep.logInfo("Using enhanced token extractor for multicall parsing", "protocol", ctx.Protocol, "stage", "multicall_start") // Try token extractor's working multicall extraction method token0, token1 := ep.tokenExtractor.ExtractTokensFromMulticallData(data) if token0 != "" && token1 != "" { ep.logInfo("Enhanced parsing success - Token extractor", "protocol", ctx.Protocol, "token0", token0, "token1", token1, "stage", "multicall_extraction") // Try to extract amounts from the original transaction data amountIn, amountOut := ep.extractAmountsFromData(data) return &calldata.SwapCall{ TokenIn: common.HexToAddress(token0), TokenOut: common.HexToAddress(token1), Protocol: ctx.Protocol, AmountIn: amountIn, AmountOut: amountOut, } } // If multicall extraction fails, try direct calldata parsing if len(data) >= 4 { token0, token1, err := ep.tokenExtractor.ExtractTokensFromCalldata(data) if err == nil && token0 != (common.Address{}) && token1 != (common.Address{}) { ep.logInfo("Enhanced parsing success - Direct calldata", "protocol", ctx.Protocol, "token0", token0.Hex(), "token1", token1.Hex(), "stage", "calldata_extraction") // Try to extract amounts from the original transaction data amountIn, amountOut := ep.extractAmountsFromData(data) return &calldata.SwapCall{ TokenIn: token0, TokenOut: token1, Protocol: ctx.Protocol, AmountIn: amountIn, AmountOut: amountOut, } } } } else { ep.logInfo("No token extractor available, using fallback parsing", "protocol", ctx.Protocol, "stage", "fallback_start") } // Fallback to original method if enhanced parser fails swaps, err := calldata.DecodeSwapCallsFromMulticall(data, ctx) if err == nil && len(swaps) > 0 { for _, swap := range swaps { if swap == nil { continue } if !ep.isValidTokenAddress(swap.TokenIn) || !ep.isValidTokenAddress(swap.TokenOut) { continue } return swap } } // Final fallback return ep.extractSwapFromMulticallFallback(data, ctx) } // isValidTokenAddress checks if an address looks like a valid token address func (ep *EventParser) isValidTokenAddress(addr common.Address) bool { // Skip zero address if addr == (common.Address{}) { return false } // Skip known router and factory addresses knownRouters := map[common.Address]bool{ ep.UniswapV2Router02: true, ep.UniswapV3Router: true, ep.UniswapV2Factory: true, ep.UniswapV3Factory: true, ep.SushiSwapFactory: true, common.HexToAddress("0xA51afAFe0263b40EdaEf0Df8781eA9aa03E381a3"): true, // Universal Router common.HexToAddress("0x1111111254EEB25477B68fb85Ed929f73A960582"): true, // 1inch Router v5 common.HexToAddress("0xC36442b4a4522E871399CD717aBDD847Ab11FE88"): true, // Uniswap V3 Position Manager } if knownRouters[addr] { return false } // Basic heuristic: valid token addresses typically have some non-zero bytes // and don't end with many zeros (which are often parameter values) bytes := addr.Bytes() nonZeroCount := 0 for _, b := range bytes { if b != 0 { nonZeroCount++ } } // Require at least 8 non-zero bytes for a valid token address return nonZeroCount >= 8 } // derivePoolAddress derives the pool address from token pair and protocol func (ep *EventParser) derivePoolAddress(token0, token1 common.Address, protocol string) common.Address { // ENHANCED VALIDATION: Comprehensive address validation pipeline if !isValidPoolTokenAddress(token0) || !isValidPoolTokenAddress(token1) { return common.Address{} } // Check if tokens are identical (invalid pair) if token0 == token1 { return common.Address{} } // Check for router/manager addresses that shouldn't be in token pairs if isKnownRouterOrManager(token0) || isKnownRouterOrManager(token1) { return common.Address{} } // Ensure canonical token order for derivation if bytes.Compare(token0.Bytes(), token1.Bytes()) > 0 { token0, token1 = token1, token0 } var derivedPool common.Address protocolLower := strings.ToLower(protocol) // Protocol-specific pool address calculation if strings.Contains(protocolLower, "uniswapv3") { // Try all 4 Uniswap V3 fee tiers to find correct pool // Fee tiers: 100 (0.01%), 500 (0.05%), 3000 (0.3%), 10000 (1%) feeTiers := []int64{100, 500, 3000, 10000} for _, fee := range feeTiers { candidate := uniswap.CalculatePoolAddress(ep.UniswapV3Factory, token0, token1, fee) if candidate != (common.Address{}) { // If we're trying to match a specific pool address, use that // Otherwise use the first valid address derivedPool = candidate break } } } else if strings.Contains(protocolLower, "sushi") { derivedPool = calculateUniswapV2Pair(ep.SushiSwapFactory, token0, token1) } else if strings.Contains(protocolLower, "uniswapv2") || strings.Contains(protocolLower, "camelot") { derivedPool = calculateUniswapV2Pair(ep.UniswapV2Factory, token0, token1) } // Final validation of derived pool address if !validatePoolAddressDerivation(derivedPool, token0, token1, protocol) { return common.Address{} } return derivedPool } func calculateUniswapV2Pair(factory, token0, token1 common.Address) common.Address { if factory == (common.Address{}) || token0 == (common.Address{}) || token1 == (common.Address{}) { return common.Address{} } if token0.Big().Cmp(token1.Big()) > 0 { token0, token1 = token1, token0 } keccakInput := append(token0.Bytes(), token1.Bytes()...) salt := crypto.Keccak256(keccakInput) initCodeHash := common.HexToHash("0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f") data := make([]byte, 0, 85) data = append(data, 0xff) data = append(data, factory.Bytes()...) data = append(data, salt...) data = append(data, initCodeHash.Bytes()...) hash := crypto.Keccak256(data) var addr common.Address copy(addr[:], hash[12:]) return addr } // AddKnownPool adds a pool address to the known pools map func (ep *EventParser) AddKnownPool(address common.Address, protocol string) { ep.knownPools[address] = protocol } // GetKnownPools returns all known pools func (ep *EventParser) GetKnownPools() map[common.Address]string { return ep.knownPools } // isValidPoolTokenAddress performs comprehensive validation for token addresses func isValidPoolTokenAddress(addr common.Address) bool { // Zero address check if addr == (common.Address{}) { return false } // Check for suspicious zero-padded addresses addrHex := addr.Hex() if len(addrHex) == 42 && addrHex[:20] == "0x000000000000000000" { return false } // Require minimum entropy (at least 8 non-zero bytes) nonZeroCount := 0 for _, b := range addr.Bytes() { if b != 0 { nonZeroCount++ } } return nonZeroCount >= 8 } // isKnownRouterOrManager checks if address is a known router or position manager func isKnownRouterOrManager(addr common.Address) bool { knownContracts := map[common.Address]bool{ // Uniswap Routers common.HexToAddress("0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"): true, // Uniswap V2 Router 02 common.HexToAddress("0xE592427A0AEce92De3Edee1F18E0157C05861564"): true, // Uniswap V3 Router common.HexToAddress("0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"): true, // Uniswap V3 Router 2 common.HexToAddress("0xA51afAFe0263b40EdaEf0Df8781eA9aa03E381a3"): true, // Universal Router // Position Managers common.HexToAddress("0xC36442b4a4522E871399CD717aBDD847Ab11FE88"): true, // Uniswap V3 Position Manager // Other Routers common.HexToAddress("0x1111111254EEB25477B68fb85Ed929f73A960582"): true, // 1inch Router v5 common.HexToAddress("0x1111111254fb6c44bAC0beD2854e76F90643097d"): true, // 1inch Router v4 common.HexToAddress("0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F"): true, // SushiSwap Router // WETH contracts (often misidentified as tokens in parsing) common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"): false, // WETH on Arbitrum (valid token) common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"): false, // WETH on Mainnet (valid token) } isRouter, exists := knownContracts[addr] return exists && isRouter } // validatePoolAddressDerivation performs final validation on derived pool address func validatePoolAddressDerivation(poolAddr, token0, token1 common.Address, protocol string) bool { // Basic validation if poolAddr == (common.Address{}) { return false } // Pool address should not match either token address if poolAddr == token0 || poolAddr == token1 { return false } // Pool address should not be a known router if isKnownRouterOrManager(poolAddr) { return false } // Check for suspicious patterns poolHex := poolAddr.Hex() if len(poolHex) == 42 && poolHex[:20] == "0x000000000000000000" { return false } // Protocol-specific validation protocolLower := strings.ToLower(protocol) if strings.Contains(protocolLower, "uniswapv3") { // Uniswap V3 pools have specific structure requirements return validateUniswapV3PoolStructure(poolAddr) } return true } // validateUniswapV3PoolStructure performs Uniswap V3 specific pool validation func validateUniswapV3PoolStructure(poolAddr common.Address) bool { // Basic structure validation for Uniswap V3 pools // This is a simplified check - in production, you might want to call the pool contract // to verify it has the expected interface (slot0, fee, etc.) // For now, just ensure it's not obviously invalid addrBytes := poolAddr.Bytes() // Check that it has reasonable entropy nonZeroCount := 0 for _, b := range addrBytes { if b != 0 { nonZeroCount++ } } // Uniswap V3 pools should have high entropy return nonZeroCount >= 12 } // protocolSpecificRecovery implements protocol-specific error recovery mechanisms func (ep *EventParser) protocolSpecificRecovery(data []byte, ctx *calldata.MulticallContext, protocol string) ([]common.Address, error) { protocolLower := strings.ToLower(protocol) // Enhanced recovery based on protocol type switch { case strings.Contains(protocolLower, "uniswap"): return ep.recoverUniswapTokens(data, ctx) case strings.Contains(protocolLower, "sushi"): return ep.recoverSushiSwapTokens(data, ctx) case strings.Contains(protocolLower, "1inch"): return ep.recover1InchTokens(data, ctx) case strings.Contains(protocolLower, "camelot"): return ep.recoverCamelotTokens(data, ctx) default: // Generic recovery fallback return ep.recoverGenericTokens(data, ctx) } } // recoverUniswapTokens implements Uniswap-specific token recovery func (ep *EventParser) recoverUniswapTokens(data []byte, ctx *calldata.MulticallContext) ([]common.Address, error) { // Primary: Try comprehensive extraction with recovery tokenAddresses, err := calldata.ExtractTokensFromMulticallWithRecovery(data, ctx, true) if err == nil && len(tokenAddresses) >= 2 { return tokenAddresses, nil } // Fallback 1: Look for common Uniswap function signatures uniswapSignatures := []string{ "exactInputSingle", "exactInput", "exactOutputSingle", "exactOutput", "swapExactTokensForTokens", "swapTokensForExactTokens", } for _, sig := range uniswapSignatures { if addresses := ep.extractTokensFromSignature(data, sig); len(addresses) >= 2 { return addresses, nil } } // Fallback 2: Heuristic token extraction return ep.heuristicTokenExtraction(data, "uniswap") } // recoverSushiSwapTokens implements SushiSwap-specific token recovery func (ep *EventParser) recoverSushiSwapTokens(data []byte, ctx *calldata.MulticallContext) ([]common.Address, error) { // SushiSwap shares similar interface with Uniswap V2 tokenAddresses, err := calldata.ExtractTokensFromMulticallWithRecovery(data, ctx, true) if err == nil && len(tokenAddresses) >= 2 { return tokenAddresses, nil } // SushiSwap specific fallback patterns return ep.heuristicTokenExtraction(data, "sushiswap") } // recover1InchTokens implements 1inch-specific token recovery func (ep *EventParser) recover1InchTokens(data []byte, ctx *calldata.MulticallContext) ([]common.Address, error) { // 1inch has complex routing, try standard extraction first tokenAddresses, err := calldata.ExtractTokensFromMulticallWithRecovery(data, ctx, true) if err == nil && len(tokenAddresses) >= 2 { return tokenAddresses, nil } // 1inch specific recovery patterns return ep.extractFrom1InchSwap(data) } // recoverCamelotTokens implements Camelot-specific token recovery func (ep *EventParser) recoverCamelotTokens(data []byte, ctx *calldata.MulticallContext) ([]common.Address, error) { // Camelot uses similar patterns to Uniswap V2/V3 tokenAddresses, err := calldata.ExtractTokensFromMulticallWithRecovery(data, ctx, true) if err == nil && len(tokenAddresses) >= 2 { return tokenAddresses, nil } return ep.heuristicTokenExtraction(data, "camelot") } // recoverGenericTokens implements generic token recovery for unknown protocols func (ep *EventParser) recoverGenericTokens(data []byte, ctx *calldata.MulticallContext) ([]common.Address, error) { // Try standard extraction first tokenAddresses, err := calldata.ExtractTokensFromMulticallWithRecovery(data, ctx, true) if err == nil && len(tokenAddresses) >= 2 { return tokenAddresses, nil } // Generic heuristic extraction return ep.heuristicTokenExtraction(data, "generic") } // extractTokensFromSignature extracts tokens based on known function signatures func (ep *EventParser) extractTokensFromSignature(data []byte, signature string) []common.Address { // This is a simplified implementation - in production you'd decode based on ABI var tokens []common.Address // Look for token addresses in standard positions for known signatures if len(data) >= 64 { // Try extracting from first two 32-byte slots (common pattern) if addr1 := common.BytesToAddress(data[12:32]); addr1 != (common.Address{}) { if isValidPoolTokenAddress(addr1) { tokens = append(tokens, addr1) } } if len(data) >= 96 { if addr2 := common.BytesToAddress(data[44:64]); addr2 != (common.Address{}) { if isValidPoolTokenAddress(addr2) && addr2 != tokens[0] { tokens = append(tokens, addr2) } } } } return tokens } // heuristicTokenExtraction performs protocol-aware heuristic token extraction func (ep *EventParser) heuristicTokenExtraction(data []byte, protocol string) ([]common.Address, error) { var tokens []common.Address // Scan through data looking for valid token addresses for i := 0; i <= len(data)-32; i += 32 { if i+32 > len(data) { break } addr := common.BytesToAddress(data[i : i+20]) if isValidPoolTokenAddress(addr) && !isKnownRouterOrManager(addr) { // Check if we already have this address duplicate := false for _, existing := range tokens { if existing == addr { duplicate = true break } } if !duplicate { tokens = append(tokens, addr) if len(tokens) >= 2 { break } } } } if len(tokens) < 2 { return nil, fmt.Errorf("insufficient tokens extracted via heuristic method for %s", protocol) } return tokens[:2], nil } // extractFrom1InchSwap extracts tokens from 1inch specific swap patterns func (ep *EventParser) extractFrom1InchSwap(data []byte) ([]common.Address, error) { // 1inch uses complex aggregation patterns // This is a simplified implementation focusing on common patterns if len(data) < 128 { return nil, fmt.Errorf("insufficient data for 1inch swap extraction") } var tokens []common.Address // Check multiple positions where tokens might appear in 1inch calls positions := []int{0, 32, 64, 96} // Common token positions in 1inch calldata for _, pos := range positions { if pos+32 <= len(data) { addr := common.BytesToAddress(data[pos+12 : pos+32]) if isValidPoolTokenAddress(addr) && !isKnownRouterOrManager(addr) { // Check for duplicates duplicate := false for _, existing := range tokens { if existing == addr { duplicate = true break } } if !duplicate { tokens = append(tokens, addr) if len(tokens) >= 2 { break } } } } } if len(tokens) < 2 { return nil, fmt.Errorf("insufficient tokens found in 1inch swap data") } return tokens, nil } // extractSwapFromMulticallFallback implements enhanced fallback parsing for failed multicall decoding func (ep *EventParser) extractSwapFromMulticallFallback(data []byte, ctx *calldata.MulticallContext) *calldata.SwapCall { // Try direct token extraction using enhanced methods tokens, err := calldata.ExtractTokensFromMulticallWithRecovery(data, ctx, true) if err != nil || len(tokens) < 2 { // Fallback to heuristic scanning tokens = ep.heuristicScanForTokens(data) } if len(tokens) >= 2 { // Create a basic swap call from extracted tokens return &calldata.SwapCall{ Selector: "fallback_parsed", Protocol: "Multicall_Fallback", TokenIn: tokens[0], TokenOut: tokens[1], AmountIn: big.NewInt(1), // Placeholder amount PoolAddress: ep.derivePoolAddress(tokens[0], tokens[1], "Multicall"), } } return nil } // heuristicScanForTokens performs pattern-based token address extraction func (ep *EventParser) heuristicScanForTokens(data []byte) []common.Address { var tokens []common.Address seenTokens := make(map[common.Address]bool) // Scan through data looking for 20-byte patterns that could be addresses for i := 0; i <= len(data)-20; i++ { if i+20 > len(data) { break } // Extract potential address starting at position i addr := common.BytesToAddress(data[i : i+20]) // Apply enhanced validation if isValidPoolTokenAddress(addr) && !isKnownRouterOrManager(addr) && !seenTokens[addr] { tokens = append(tokens, addr) seenTokens[addr] = true if len(tokens) >= 2 { break } } } // Also scan at 32-byte aligned positions (common in ABI encoding) for i := 12; i <= len(data)-20; i += 32 { // Start at offset 12 to get address from 32-byte slot if i+20 > len(data) { break } addr := common.BytesToAddress(data[i : i+20]) if isValidPoolTokenAddress(addr) && !isKnownRouterOrManager(addr) && !seenTokens[addr] { tokens = append(tokens, addr) seenTokens[addr] = true if len(tokens) >= 2 { break } } } return tokens } // CRITICAL FIX: Direct function parsing methods (similar to L2 parser approach) // parseDirectFunction attempts to parse tokens directly from transaction input using structured decoders func (ep *EventParser) parseDirectFunction(tx *types.Transaction) []common.Address { if tx.To() == nil || len(tx.Data()) < 4 { return nil } data := tx.Data() methodID := hex.EncodeToString(data[:4]) ep.logDebug("attempting direct parsing", "tx_hash", tx.Hash().Hex(), "method_id", methodID, "data_len", len(data), ) switch methodID { case "414bf389": // exactInputSingle return ep.parseExactInputSingleDirect(data) case "472b43f3": // swapExactTokensForTokens (UniswapV2) return ep.parseSwapExactTokensForTokensDirect(data) case "18cbafe5": // swapExactTokensForTokensSupportingFeeOnTransferTokens return ep.parseSwapExactTokensForTokensDirect(data) case "5c11d795": // swapExactTokensForTokensSupportingFeeOnTransferTokens (SushiSwap) return ep.parseSwapExactTokensForTokensDirect(data) case "b858183f": // multicall (Universal Router) return ep.parseMulticallDirect(data) default: // Fallback to generic parsing return ep.parseGenericSwapDirect(data) } } // parseExactInputSingleDirect parses exactInputSingle calls directly func (ep *EventParser) parseExactInputSingleDirect(data []byte) []common.Address { if len(data) < 164 { // 4 + 160 bytes minimum return nil } // ExactInputSingle struct: tokenIn, tokenOut, fee, recipient, deadline, amountIn, amountOutMinimum, sqrtPriceLimitX96 tokenIn := common.BytesToAddress(data[16:36]) // offset 12, length 20 tokenOut := common.BytesToAddress(data[48:68]) // offset 44, length 20 if tokenIn == (common.Address{}) || tokenOut == (common.Address{}) { return nil } if !isValidPoolTokenAddress(tokenIn) || !isValidPoolTokenAddress(tokenOut) { return nil } return []common.Address{tokenIn, tokenOut} } // parseSwapExactTokensForTokensDirect parses UniswapV2 style swaps directly func (ep *EventParser) parseSwapExactTokensForTokensDirect(data []byte) []common.Address { if len(data) < 164 { // 4 + 160 bytes minimum return nil } // swapExactTokensForTokens(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline) // Path array starts at offset 100 (0x64) pathOffsetPos := 100 if len(data) < pathOffsetPos+32 { return nil } // Read path array length pathLength := new(big.Int).SetBytes(data[pathOffsetPos+16 : pathOffsetPos+32]).Uint64() if pathLength < 2 || pathLength > 10 { // Reasonable bounds return nil } // Extract first and last token from path firstTokenPos := pathOffsetPos + 32 + 12 // +12 to skip padding lastTokenPos := pathOffsetPos + 32 + int(pathLength-1)*32 + 12 if len(data) < lastTokenPos+20 { return nil } tokenIn := common.BytesToAddress(data[firstTokenPos : firstTokenPos+20]) tokenOut := common.BytesToAddress(data[lastTokenPos : lastTokenPos+20]) if tokenIn == (common.Address{}) || tokenOut == (common.Address{}) { return nil } if !isValidPoolTokenAddress(tokenIn) || !isValidPoolTokenAddress(tokenOut) { return nil } return []common.Address{tokenIn, tokenOut} } // parseMulticallDirect parses multicall transactions by examining individual calls func (ep *EventParser) parseMulticallDirect(data []byte) []common.Address { if len(data) < 68 { // 4 + 64 bytes minimum return nil } // Multicall typically has array of bytes at offset 36 arrayOffset := 36 if len(data) < arrayOffset+32 { return nil } arrayLength := new(big.Int).SetBytes(data[arrayOffset+16 : arrayOffset+32]).Uint64() if arrayLength == 0 || arrayLength > 50 { // Reasonable bounds return nil } // Parse first call in multicall firstCallOffset := arrayOffset + 32 + 32 // Skip array length and first element offset if len(data) < firstCallOffset+32 { return nil } callDataLength := new(big.Int).SetBytes(data[firstCallOffset+16 : firstCallOffset+32]).Uint64() if callDataLength < 4 || callDataLength > 1000 { return nil } callDataStart := firstCallOffset + 32 if len(data) < callDataStart+int(callDataLength) { return nil } callData := data[callDataStart : callDataStart+int(callDataLength)] // Create a dummy transaction for recursive parsing dummyTx := types.NewTransaction(0, common.Address{}, big.NewInt(0), 0, big.NewInt(0), callData) return ep.parseDirectFunction(dummyTx) } // parseGenericSwapDirect attempts generic token extraction from swap-like transactions func (ep *EventParser) parseGenericSwapDirect(data []byte) []common.Address { var tokens []common.Address seenTokens := make(map[common.Address]bool) // Scan for addresses at standard ABI positions positions := []int{16, 48, 80, 112, 144, 176} // Common address positions in ABI encoding for _, pos := range positions { if pos+20 <= len(data) { addr := common.BytesToAddress(data[pos : pos+20]) if addr != (common.Address{}) && isValidPoolTokenAddress(addr) && !isKnownRouterOrManager(addr) && !seenTokens[addr] { tokens = append(tokens, addr) seenTokens[addr] = true if len(tokens) >= 2 { break } } } } if len(tokens) >= 2 { return tokens[:2] } return nil } // getPoolTokens attempts to extract token addresses for a pool from transaction cache // Priority: 1) txTokenCache (from transaction calldata), 2) return zero addresses for scanner enrichment func (ep *EventParser) getPoolTokens(poolAddress common.Address, txHash common.Hash, txTokenCache map[string][]common.Address) (token0, token1 common.Address) { // Try to get tokens from transaction calldata cache first if txTokenCache != nil { if tokens, found := txTokenCache[poolAddress.Hex()]; found && len(tokens) >= 2 { ep.logDebug("enriched pool tokens from transaction calldata", "pool", poolAddress.Hex()[:10], "token0", tokens[0].Hex()[:10], "token1", tokens[1].Hex()[:10]) return tokens[0], tokens[1] } } // Return zero addresses - scanner will enrich with pool cache data if needed // This is acceptable because the comment at concurrent.go:381 says // "Scanner will enrich event with token addresses from cache if missing" return common.Address{}, common.Address{} } // extractAmountsFromData attempts to extract swap amounts from raw transaction data func (ep *EventParser) extractAmountsFromData(data []byte) (*big.Int, *big.Int) { // Default amounts if extraction fails defaultAmountIn := big.NewInt(0) defaultAmountOut := big.NewInt(0) // Need at least 128 bytes for basic amount extraction if len(data) < 128 { return defaultAmountIn, defaultAmountOut } // Common patterns for amount extraction in swap transactions: // 1. ExactInputSingle: amountIn at 160:192, amountOutMin at 192:224 // 2. ExactOutputSingle: amountInMax at 160:192, amountOut at 192:224 // 3. SwapExactTokensForTokens: amountIn at 4:36, amountOutMin at 36:68 // Try to extract amounts from common positions // Position 1: Standard UniswapV3 positions (after tokenIn, tokenOut, fee, recipient, deadline) if len(data) >= 224 { amountIn := new(big.Int).SetBytes(data[160:192]) amountOut := new(big.Int).SetBytes(data[192:224]) // Validate amounts are reasonable (non-zero and not overflow values) maxAmount := new(big.Int).Exp(big.NewInt(10), big.NewInt(30), nil) // 10^30 as max if amountIn.Sign() > 0 && amountIn.Cmp(maxAmount) < 0 { if amountOut.Sign() >= 0 && amountOut.Cmp(maxAmount) < 0 { return amountIn, amountOut } } } // Position 2: UniswapV2 style (after function selector) if len(data) >= 68 { amountIn := new(big.Int).SetBytes(data[4:36]) amountOut := new(big.Int).SetBytes(data[36:68]) maxAmount := new(big.Int).Exp(big.NewInt(10), big.NewInt(30), nil) if amountIn.Sign() > 0 && amountIn.Cmp(maxAmount) < 0 { if amountOut.Sign() >= 0 && amountOut.Cmp(maxAmount) < 0 { return amountIn, amountOut } } } // Position 3: Try scanning for non-zero 32-byte values that could be amounts for i := 0; i+64 <= len(data); i += 32 { val1 := new(big.Int).SetBytes(data[i : i+32]) if i+64 <= len(data) { val2 := new(big.Int).SetBytes(data[i+32 : i+64]) // Check if these look like valid amounts minAmount := big.NewInt(1000) // Minimum 1000 wei maxAmount := new(big.Int).Exp(big.NewInt(10), big.NewInt(30), nil) if val1.Sign() > 0 && val1.Cmp(minAmount) > 0 && val1.Cmp(maxAmount) < 0 { if val2.Sign() > 0 && val2.Cmp(minAmount) > 0 && val2.Cmp(maxAmount) < 0 { // Found two consecutive valid-looking amounts return val1, val2 } } } } // If all extraction attempts fail, return minimal non-zero amounts to avoid division by zero return big.NewInt(1000000), big.NewInt(1000000) // 1M wei as fallback } // parseTokensFromKnownMethod extracts tokens from known DEX method signatures // parseTokensFromKnownMethod is now replaced by the TokenExtractor interface // This function has been removed to avoid duplication with the L2 parser implementation