package events import ( "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/pkg/calldata" "github.com/fraktal/mev-beta/pkg/uniswap" ) // 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 } // NewEventParser creates a new event parser with official Arbitrum deployment addresses func NewEventParser() *EventParser { parser := &EventParser{ // 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 return parser } // ParseTransactionReceipt parses events from a transaction receipt func (ep *EventParser) ParseTransactionReceipt(receipt *types.Receipt, blockNumber uint64, timestamp uint64) ([]*Event, error) { events := make([]*Event, 0) // 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) case ep.swapEventV3Sig: event, err = ep.parseUniswapV3Swap(log, blockNumber, timestamp, receipt.TxHash) 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) (*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{}) { fmt.Printf("ZERO ADDRESS DEBUG [EVENTS-1]: Creating Event with zero address - BlockNumber: %d, LogIndex: %d, LogTopics: %d, LogData: %d bytes\n", blockNumber, log.Index, len(log.Topics), len(log.Data)) } event := &Event{ Type: Swap, Protocol: "UniswapV2", PoolAddress: log.Address, 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) (*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 amount0 := new(big.Int).SetBytes(log.Data[0:32]) amount1 := new(big.Int).SetBytes(log.Data[32:64]) 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]) // Convert to signed values if needed if amount0.Cmp(big.NewInt(0)) > 0x7fffffffffffffff { amount0 = amount0.Sub(amount0, new(big.Int).Lsh(big.NewInt(1), 256)) } if amount1.Cmp(big.NewInt(0)) > 0x7fffffffffffffff { amount1 = amount1.Sub(amount1, new(big.Int).Lsh(big.NewInt(1), 256)) } event := &Event{ Type: Swap, Protocol: "UniswapV3", PoolAddress: log.Address, 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 ) if swap != nil { 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 } } if poolAddress == (common.Address{}) { if token0 != (common.Address{}) && token1 != (common.Address{}) { poolAddress = ep.derivePoolAddress(token0, token1, protocol) } else if tx.To() != nil { poolAddress = *tx.To() } } 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 { swaps, err := calldata.DecodeSwapCallsFromMulticall(data, ctx) if err != nil || len(swaps) == 0 { return nil } for _, swap := range swaps { if swap == nil { continue } if !ep.isValidTokenAddress(swap.TokenIn) || !ep.isValidTokenAddress(swap.TokenOut) { continue } return swap } return nil } // 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 { // If either token is zero address, we cannot derive a valid pool address if token0 == (common.Address{}) || token1 == (common.Address{}) { return common.Address{} } // Check if either token is actually a router address (shouldn't happen but safety check) knownRouters := map[common.Address]bool{ ep.UniswapV2Router02: true, ep.UniswapV3Router: true, common.HexToAddress("0xA51afAFe0263b40EdaEf0Df8781eA9aa03E381a3"): true, // Universal Router common.HexToAddress("0x1111111254EEB25477B68fb85Ed929f73A960582"): true, // 1inch Router v5 common.HexToAddress("0xC36442b4a4522E871399CD717aBDD847Ab11FE88"): true, // Uniswap V3 Position Manager } if knownRouters[token0] || knownRouters[token1] { return common.Address{} } protocolLower := strings.ToLower(protocol) if strings.Contains(protocolLower, "uniswapv3") { fee := int64(3000) return uniswap.CalculatePoolAddress(ep.UniswapV3Factory, token0, token1, fee) } if strings.Contains(protocolLower, "sushi") { if addr := calculateUniswapV2Pair(ep.SushiSwapFactory, token0, token1); addr != (common.Address{}) { return addr } } if strings.Contains(protocolLower, "uniswapv2") || strings.Contains(protocolLower, "camelot") { if addr := calculateUniswapV2Pair(ep.UniswapV2Factory, token0, token1); addr != (common.Address{}) { return addr } } return common.Address{} } 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 }