package events import ( "fmt" "math/big" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/holiman/uint256" ) // 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 "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) } 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) } default: // For unknown functions, create a basic event event := &Event{ Type: Swap, Protocol: protocol, PoolAddress: *tx.To(), 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]) } } } } event := &Event{ Type: Swap, Protocol: protocol, PoolAddress: *tx.To(), 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]) event := &Event{ Type: Swap, Protocol: protocol, PoolAddress: *tx.To(), // This is the router, not the pool 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 // Simple approximation: skip to last token position if pathLength >= 43 { // tokenA(20) + fee(3) + tokenB(20) token1 = common.BytesToAddress(data[pathOffset+55 : pathOffset+75]) } } } } event := &Event{ Type: Swap, Protocol: protocol, PoolAddress: *tx.To(), 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 } // 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 }