package arbitrum import ( "context" "encoding/hex" "fmt" "math/big" "strings" "time" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/rpc" "github.com/fraktal/mev-beta/internal/logger" "github.com/fraktal/mev-beta/pkg/oracle" "github.com/fraktal/mev-beta/pkg/pools" "github.com/fraktal/mev-beta/pkg/security" ) // RawL2Transaction represents a raw Arbitrum L2 transaction type RawL2Transaction struct { Hash string `json:"hash"` From string `json:"from"` To string `json:"to"` Value string `json:"value"` Gas string `json:"gas"` GasPrice string `json:"gasPrice"` Input string `json:"input"` Nonce string `json:"nonce"` TransactionIndex string `json:"transactionIndex"` Type string `json:"type"` ChainID string `json:"chainId,omitempty"` V string `json:"v,omitempty"` R string `json:"r,omitempty"` S string `json:"s,omitempty"` BlockNumber string `json:"blockNumber,omitempty"` } // RawL2Block represents a raw Arbitrum L2 block type RawL2Block struct { Hash string `json:"hash"` Number string `json:"number"` Timestamp string `json:"timestamp"` Transactions []RawL2Transaction `json:"transactions"` } // RawL2BlockWithLogs represents a raw Arbitrum L2 block with logs type RawL2BlockWithLogs struct { Hash string `json:"hash"` Number string `json:"number"` Timestamp string `json:"timestamp"` Transactions []RawL2TransactionWithLogs `json:"transactions"` } // RawL2TransactionWithLogs includes transaction logs for pool discovery type RawL2TransactionWithLogs struct { Hash string `json:"hash"` From string `json:"from"` To string `json:"to"` Value string `json:"value"` Gas string `json:"gas"` GasPrice string `json:"gasPrice"` Input string `json:"input"` Logs []interface{} `json:"logs"` TransactionIndex string `json:"transactionIndex"` Type string `json:"type"` } // DEXFunctionSignature represents a DEX function signature type DEXFunctionSignature struct { Signature string Name string Protocol string Description string } // ArbitrumL2Parser handles parsing of Arbitrum L2 transactions type ArbitrumL2Parser struct { client *rpc.Client logger *logger.Logger oracle *oracle.PriceOracle // DEX contract addresses on Arbitrum dexContracts map[common.Address]string // DEX function signatures dexFunctions map[string]DEXFunctionSignature // Pool discovery system poolDiscovery *pools.PoolDiscovery // ABI decoders for sophisticated parameter parsing uniswapV2ABI abi.ABI uniswapV3ABI abi.ABI sushiSwapABI abi.ABI } // NewArbitrumL2Parser creates a new Arbitrum L2 transaction parser func NewArbitrumL2Parser(rpcEndpoint string, logger *logger.Logger, priceOracle *oracle.PriceOracle) (*ArbitrumL2Parser, error) { client, err := rpc.Dial(rpcEndpoint) if err != nil { return nil, fmt.Errorf("failed to connect to Arbitrum RPC: %v", err) } parser := &ArbitrumL2Parser{ client: client, logger: logger, oracle: priceOracle, dexContracts: make(map[common.Address]string), dexFunctions: make(map[string]DEXFunctionSignature), } // Initialize DEX contracts and functions parser.initializeDEXData() // Initialize ABI decoders for sophisticated parsing if err := parser.initializeABIs(); err != nil { logger.Warn(fmt.Sprintf("Failed to initialize ABI decoders: %v", err)) } // Initialize pool discovery system parser.poolDiscovery = pools.NewPoolDiscovery(client, logger) logger.Info(fmt.Sprintf("Pool discovery system initialized - %d pools, %d exchanges loaded", parser.poolDiscovery.GetPoolCount(), parser.poolDiscovery.GetExchangeCount())) return parser, nil } // initializeDEXData initializes known DEX contracts and function signatures func (p *ArbitrumL2Parser) initializeDEXData() { // Official Arbitrum DEX contracts p.dexContracts[common.HexToAddress("0xf1D7CC64Fb4452F05c498126312eBE29f30Fbcf9")] = "UniswapV2Factory" p.dexContracts[common.HexToAddress("0x1F98431c8aD98523631AE4a59f267346ea31F984")] = "UniswapV3Factory" p.dexContracts[common.HexToAddress("0xc35DADB65012eC5796536bD9864eD8773aBc74C4")] = "SushiSwapFactory" p.dexContracts[common.HexToAddress("0x4752ba5dbc23f44d87826276bf6fd6b1c372ad24")] = "UniswapV2Router02" p.dexContracts[common.HexToAddress("0xE592427A0AEce92De3Edee1F18E0157C05861564")] = "UniswapV3Router" p.dexContracts[common.HexToAddress("0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45")] = "UniswapV3Router02" p.dexContracts[common.HexToAddress("0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506")] = "SushiSwapRouter" p.dexContracts[common.HexToAddress("0xC36442b4a4522E871399CD717aBDD847Ab11FE88")] = "UniswapV3PositionManager" // MISSING HIGH-ACTIVITY DEX CONTRACTS (based on log analysis) p.dexContracts[common.HexToAddress("0xaa78afc926d0df40458ad7b1f7eed37251bd2b5f")] = "SushiSwapRouter_Arbitrum" // 44 transactions p.dexContracts[common.HexToAddress("0x87d66368cd08a7ca42252f5ab44b2fb6d1fb8d15")] = "TraderJoeRouter" // 50 transactions p.dexContracts[common.HexToAddress("0x16e71b13fe6079b4312063f7e81f76d165ad32ad")] = "SushiSwapRouter_V2" // Frequent p.dexContracts[common.HexToAddress("0xaa277cb7914b7e5514946da92cb9de332ce610ef")] = "RamsesExchange" // Multi calls p.dexContracts[common.HexToAddress("0xc873fEcbd354f5A56E00E710B90EF4201db2448d")] = "CamelotRouter" // Camelot DEX p.dexContracts[common.HexToAddress("0x5ffe7FB82894076ECB99A30D6A32e969e6e35E98")] = "CurveAddressProvider" // Curve p.dexContracts[common.HexToAddress("0xba12222222228d8ba445958a75a0704d566bf2c8")] = "BalancerVault" // Balancer V2 // HIGH-ACTIVITY UNISWAP V3 POOLS (detected from logs) p.dexContracts[common.HexToAddress("0xC6962004f452bE9203591991D15f6b388e09E8D0")] = "UniswapV3Pool_WETH_USDC" // 381 occurrences p.dexContracts[common.HexToAddress("0x641C00A822e8b671738d32a431a4Fb6074E5c79d")] = "UniswapV3Pool_WETH_USDT" // 168 occurrences p.dexContracts[common.HexToAddress("0x2f5e87C9312fa29aed5c179E456625D79015299c")] = "UniswapV3Pool_ARB_ETH" // 169 occurrences // 1INCH AGGREGATOR (major MEV source) p.dexContracts[common.HexToAddress("0x1111111254eeb25477b68fb85ed929f73a960582")] = "1InchAggregatorV5" p.dexContracts[common.HexToAddress("0x1111111254fb6c44bac0bed2854e76f90643097d")] = "1InchAggregatorV4" // CORRECT DEX function signatures verified for Arbitrum (first 4 bytes of keccak256(function_signature)) // Uniswap V2 swap functions p.dexFunctions["0x38ed1739"] = DEXFunctionSignature{ Signature: "0x38ed1739", Name: "swapExactTokensForTokens", Protocol: "UniswapV2", Description: "Swap exact tokens for tokens", } p.dexFunctions["0x8803dbee"] = DEXFunctionSignature{ Signature: "0x8803dbee", Name: "swapTokensForExactTokens", Protocol: "UniswapV2", Description: "Swap tokens for exact tokens", } p.dexFunctions["0x7ff36ab5"] = DEXFunctionSignature{ Signature: "0x7ff36ab5", Name: "swapExactETHForTokens", Protocol: "UniswapV2", Description: "Swap exact ETH for tokens", } p.dexFunctions["0x4a25d94a"] = DEXFunctionSignature{ Signature: "0x4a25d94a", Name: "swapTokensForExactETH", Protocol: "UniswapV2", Description: "Swap tokens for exact ETH", } p.dexFunctions["0x18cbafe5"] = DEXFunctionSignature{ Signature: "0x18cbafe5", Name: "swapExactTokensForETH", Protocol: "UniswapV2", Description: "Swap exact tokens for ETH", } p.dexFunctions["0x791ac947"] = DEXFunctionSignature{ Signature: "0x791ac947", Name: "swapExactTokensForETHSupportingFeeOnTransferTokens", Protocol: "UniswapV2", Description: "Swap exact tokens for ETH supporting fee-on-transfer tokens", } p.dexFunctions["0xb6f9de95"] = DEXFunctionSignature{ Signature: "0xb6f9de95", Name: "swapExactETHForTokensSupportingFeeOnTransferTokens", Protocol: "UniswapV2", Description: "Swap exact ETH for tokens supporting fee-on-transfer tokens", } p.dexFunctions["0x5c11d795"] = DEXFunctionSignature{ Signature: "0x5c11d795", Name: "swapExactTokensForTokensSupportingFeeOnTransferTokens", Protocol: "UniswapV2", Description: "Swap exact tokens for tokens supporting fee-on-transfer tokens", } // Uniswap V2 liquidity functions p.dexFunctions["0xe8e33700"] = DEXFunctionSignature{ Signature: "0xe8e33700", Name: "addLiquidity", Protocol: "UniswapV2", Description: "Add liquidity to pool", } p.dexFunctions["0xf305d719"] = DEXFunctionSignature{ Signature: "0xf305d719", Name: "addLiquidityETH", Protocol: "UniswapV2", Description: "Add liquidity with ETH", } p.dexFunctions["0xbaa2abde"] = DEXFunctionSignature{ Signature: "0xbaa2abde", Name: "removeLiquidity", Protocol: "UniswapV2", Description: "Remove liquidity from pool", } p.dexFunctions["0x02751cec"] = DEXFunctionSignature{ Signature: "0x02751cec", Name: "removeLiquidityETH", Protocol: "UniswapV2", Description: "Remove liquidity with ETH", } // Uniswap V3 swap functions p.dexFunctions["0x414bf389"] = DEXFunctionSignature{ Signature: "0x414bf389", Name: "exactInputSingle", Protocol: "UniswapV3", Description: "Exact input single swap", } p.dexFunctions["0xc04b8d59"] = DEXFunctionSignature{ Signature: "0xc04b8d59", Name: "exactInput", Protocol: "UniswapV3", Description: "Exact input multi-hop swap", } p.dexFunctions["0xdb3e2198"] = DEXFunctionSignature{ Signature: "0xdb3e2198", Name: "exactOutputSingle", Protocol: "UniswapV3", Description: "Exact output single swap", } p.dexFunctions["0xf28c0498"] = DEXFunctionSignature{ Signature: "0xf28c0498", Name: "exactOutput", Protocol: "UniswapV3", Description: "Exact output multi-hop swap", } p.dexFunctions["0xac9650d8"] = DEXFunctionSignature{ Signature: "0xac9650d8", Name: "multicall", Protocol: "UniswapV3", Description: "Batch multiple function calls", } // Uniswap V3 position management functions p.dexFunctions["0x88316456"] = DEXFunctionSignature{ Signature: "0x88316456", Name: "mint", Protocol: "UniswapV3", Description: "Mint new liquidity position", } p.dexFunctions["0xfc6f7865"] = DEXFunctionSignature{ Signature: "0xfc6f7865", Name: "collect", Protocol: "UniswapV3", Description: "Collect fees from position", } p.dexFunctions["0x219f5d17"] = DEXFunctionSignature{ Signature: "0x219f5d17", Name: "increaseLiquidity", Protocol: "UniswapV3", Description: "Increase liquidity in position", } p.dexFunctions["0x0c49ccbe"] = DEXFunctionSignature{ Signature: "0x0c49ccbe", Name: "decreaseLiquidity", Protocol: "UniswapV3", Description: "Decrease liquidity in position", } // MISSING CRITICAL FUNCTION SIGNATURES (major MEV sources) // Multicall functions (used heavily in V3 and aggregators) p.dexFunctions["0xac9650d8"] = DEXFunctionSignature{ Signature: "0xac9650d8", Name: "multicall", Protocol: "Multicall", Description: "Execute multiple function calls in single transaction", } p.dexFunctions["0x5ae401dc"] = DEXFunctionSignature{ Signature: "0x5ae401dc", Name: "multicall", Protocol: "MultiV2", Description: "Multicall with deadline", } p.dexFunctions["0x1f0464d1"] = DEXFunctionSignature{ Signature: "0x1f0464d1", Name: "multicall", Protocol: "MultiV3", Description: "Multicall with previous blockhash guard", } // 1INCH Aggregator functions (major arbitrage source) p.dexFunctions["0x7c025200"] = DEXFunctionSignature{ Signature: "0x7c025200", Name: "swap", Protocol: "1Inch", Description: "1inch aggregator swap", } p.dexFunctions["0xe449022e"] = DEXFunctionSignature{ Signature: "0xe449022e", Name: "uniswapV3Swap", Protocol: "1Inch", Description: "1inch uniswap v3 swap", } p.dexFunctions["0x12aa3caf"] = DEXFunctionSignature{ Signature: "0x12aa3caf", Name: "ethUnoswap", Protocol: "1Inch", Description: "1inch ETH unoswap", } p.dexFunctions["0x0502b1c5"] = DEXFunctionSignature{ Signature: "0x0502b1c5", Name: "swapMulti", Protocol: "1Inch", Description: "1inch multi-hop swap", } p.dexFunctions["0x2e95b6c8"] = DEXFunctionSignature{ Signature: "0x2e95b6c8", Name: "unoswapTo", Protocol: "1Inch", Description: "1inch unoswap to recipient", } p.dexFunctions["0xbabe3335"] = DEXFunctionSignature{ Signature: "0xbabe3335", Name: "clipperSwap", Protocol: "1Inch", Description: "1inch clipper swap", } // Balancer V2 functions p.dexFunctions["0x52bbbe29"] = DEXFunctionSignature{ Signature: "0x52bbbe29", Name: "swap", Protocol: "BalancerV2", Description: "Balancer V2 single swap", } p.dexFunctions["0x945bcec9"] = DEXFunctionSignature{ Signature: "0x945bcec9", Name: "batchSwap", Protocol: "BalancerV2", Description: "Balancer V2 batch swap", } // Curve functions p.dexFunctions["0x3df02124"] = DEXFunctionSignature{ Signature: "0x3df02124", Name: "exchange", Protocol: "Curve", Description: "Curve token exchange", } p.dexFunctions["0xa6417ed6"] = DEXFunctionSignature{ Signature: "0xa6417ed6", Name: "exchange_underlying", Protocol: "Curve", Description: "Curve exchange underlying tokens", } // SushiSwap specific functions p.dexFunctions["0x02751cec"] = DEXFunctionSignature{ Signature: "0x02751cec", Name: "removeLiquidityETH", Protocol: "SushiSwap", Description: "Remove liquidity with ETH", } // TraderJoe functions p.dexFunctions["0x18cbafe5"] = DEXFunctionSignature{ Signature: "0x18cbafe5", Name: "swapExactTokensForETH", Protocol: "TraderJoe", Description: "TraderJoe exact tokens for ETH", } // Universal Router functions (Uniswap's new router) p.dexFunctions["0x3593564c"] = DEXFunctionSignature{ Signature: "0x3593564c", Name: "execute", Protocol: "UniversalRouter", Description: "Universal router execute", } // Generic DEX functions that appear frequently p.dexFunctions["0x022c0d9f"] = DEXFunctionSignature{ Signature: "0x022c0d9f", Name: "swap", Protocol: "Generic", Description: "Generic swap function", } p.dexFunctions["0x128acb08"] = DEXFunctionSignature{ Signature: "0x128acb08", Name: "swapTokensForTokens", Protocol: "Generic", Description: "Generic token to token swap", } } // GetBlockByNumber fetches a block with full transaction details using raw RPC func (p *ArbitrumL2Parser) GetBlockByNumber(ctx context.Context, blockNumber uint64) (*RawL2Block, error) { var block RawL2Block blockNumHex := fmt.Sprintf("0x%x", blockNumber) err := p.client.CallContext(ctx, &block, "eth_getBlockByNumber", blockNumHex, true) if err != nil { return nil, fmt.Errorf("failed to get block %d: %v", blockNumber, err) } p.logger.Debug(fmt.Sprintf("Retrieved L2 block %d with %d transactions", blockNumber, len(block.Transactions))) return &block, nil } // ParseDEXTransactions analyzes transactions in a block for DEX interactions func (p *ArbitrumL2Parser) ParseDEXTransactions(ctx context.Context, block *RawL2Block) []DEXTransaction { var dexTransactions []DEXTransaction for _, tx := range block.Transactions { if dexTx := p.parseDEXTransaction(tx); dexTx != nil { if tx.BlockNumber != "" { dexTx.BlockNumber = tx.BlockNumber } else if block.Number != "" { dexTx.BlockNumber = block.Number } dexTransactions = append(dexTransactions, *dexTx) } } if len(dexTransactions) > 0 { p.logger.Info(fmt.Sprintf("Block %s: Found %d DEX transactions", block.Number, len(dexTransactions))) } return dexTransactions } // ParseDEXTransaction analyzes a single raw transaction for DEX interaction details. func (p *ArbitrumL2Parser) ParseDEXTransaction(tx RawL2Transaction) (*DEXTransaction, error) { dexTx := p.parseDEXTransaction(tx) if dexTx == nil { return nil, fmt.Errorf("transaction %s is not a recognized DEX interaction", tx.Hash) } if tx.BlockNumber != "" { dexTx.BlockNumber = tx.BlockNumber } return dexTx, nil } // SwapDetails contains detailed information about a DEX swap type SwapDetails struct { AmountIn *big.Int AmountOut *big.Int AmountMin *big.Int TokenIn string TokenOut string TokenInAddress common.Address TokenOutAddress common.Address Fee uint32 Deadline uint64 Recipient string IsValid bool } // DEXTransaction represents a parsed DEX transaction type DEXTransaction struct { Hash string From string To string Value *big.Int FunctionSig string FunctionName string Protocol string InputData []byte ContractName string BlockNumber string SwapDetails *SwapDetails // Detailed swap information } // parseDEXTransaction checks if a transaction is a DEX interaction func (p *ArbitrumL2Parser) parseDEXTransaction(tx RawL2Transaction) *DEXTransaction { // Skip transactions without recipient (contract creation) if tx.To == "" || tx.To == "0x" { return nil } // Skip transactions without input data if tx.Input == "" || tx.Input == "0x" || len(tx.Input) < 10 { return nil } toAddr := common.HexToAddress(tx.To) // Check if transaction is to a known DEX contract contractName, isDEXContract := p.dexContracts[toAddr] // Extract function signature (first 4 bytes of input data) functionSig := tx.Input[:10] // "0x" + 8 hex chars = 10 chars // Check if function signature matches known DEX functions if funcInfo, isDEXFunction := p.dexFunctions[functionSig]; isDEXFunction { // Parse value value := big.NewInt(0) if tx.Value != "" && tx.Value != "0x" && tx.Value != "0x0" { value.SetString(strings.TrimPrefix(tx.Value, "0x"), 16) } // Parse input data inputData, err := hex.DecodeString(strings.TrimPrefix(tx.Input, "0x")) if err != nil { p.logger.Debug(fmt.Sprintf("Failed to decode input data for transaction %s: %v", tx.Hash, err)) inputData = []byte{} } // Decode function parameters based on function type swapDetails := p.decodeFunctionDataStructured(funcInfo, inputData) // Log basic transaction detection (opportunities logged later with actual amounts from events) if swapDetails != nil && swapDetails.IsValid { p.logger.Info(fmt.Sprintf("DEX Transaction detected: %s -> %s (%s) calling %s (%s) - TokenIn: %s, TokenOut: %s", tx.From, tx.To, contractName, funcInfo.Name, funcInfo.Protocol, swapDetails.TokenIn, swapDetails.TokenOut)) } else { // Fallback to simple logging swapDetailsStr := p.decodeFunctionData(funcInfo, inputData) p.logger.Info(fmt.Sprintf("DEX Transaction detected: %s -> %s (%s) calling %s (%s), Value: %s ETH%s", tx.From, tx.To, contractName, funcInfo.Name, funcInfo.Protocol, new(big.Float).Quo(new(big.Float).SetInt(value), big.NewFloat(1e18)).String(), swapDetailsStr)) } // CRITICAL FIX: Only include SwapDetails if valid, otherwise set to nil // This prevents zero address corruption from invalid swap details var validSwapDetails *SwapDetails if swapDetails != nil && swapDetails.IsValid { validSwapDetails = swapDetails } return &DEXTransaction{ Hash: tx.Hash, From: tx.From, To: tx.To, Value: value, FunctionSig: functionSig, FunctionName: funcInfo.Name, Protocol: funcInfo.Protocol, InputData: inputData, ContractName: contractName, BlockNumber: "", // Will be set by caller SwapDetails: validSwapDetails, } } // Check if it's to a known DEX contract but unknown function if isDEXContract { p.logger.Debug(fmt.Sprintf("Unknown DEX function call: %s -> %s (%s), Function: %s", tx.From, tx.To, contractName, functionSig)) } return nil } // decodeFunctionData extracts parameters from transaction input data func (p *ArbitrumL2Parser) decodeFunctionData(funcInfo DEXFunctionSignature, inputData []byte) string { if len(inputData) < 4 { return "" } // Skip the 4-byte function selector params := inputData[4:] switch funcInfo.Name { case "swapExactTokensForTokens": return p.decodeSwapExactTokensForTokens(params) case "swapTokensForExactTokens": return p.decodeSwapTokensForExactTokens(params) case "swapExactETHForTokens": return p.decodeSwapExactETHForTokens(params) case "swapExactTokensForETH": return p.decodeSwapExactTokensForETH(params) case "exactInputSingle": return p.decodeExactInputSingle(params) case "exactInput": return p.decodeExactInput(params) case "exactOutputSingle": return p.decodeExactOutputSingle(params) case "multicall": return p.decodeMulticall(params) default: return fmt.Sprintf(", Raw input: %d bytes", len(inputData)) } } // decodeSwapExactTokensForTokens decodes UniswapV2 swapExactTokensForTokens parameters // function swapExactTokensForTokens(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) func (p *ArbitrumL2Parser) decodeSwapExactTokensForTokens(params []byte) string { if len(params) < 160 { // 5 parameters * 32 bytes each return ", Invalid parameters" } // Use sophisticated ABI decoding instead of basic byte slicing fullInputData := append([]byte{0x38, 0xed, 0x17, 0x39}, params...) // Add function selector decoded, err := p.decodeWithABI("UniswapV2", "swapExactTokensForTokens", fullInputData) if err != nil { // Fallback to basic decoding if len(params) >= 64 { amountIn := new(big.Int).SetBytes(params[0:32]) amountOutMin := new(big.Int).SetBytes(params[32:64]) amountInEth := new(big.Float).Quo(new(big.Float).SetInt(amountIn), big.NewFloat(1e18)) amountOutMinEth := new(big.Float).Quo(new(big.Float).SetInt(amountOutMin), big.NewFloat(1e18)) return fmt.Sprintf(", AmountIn: %s tokens, MinOut: %s tokens (fallback)", amountInEth.Text('f', 6), amountOutMinEth.Text('f', 6)) } return ", Invalid parameters" } // Extract values from ABI decoded parameters amountIn, ok1 := decoded["amountIn"].(*big.Int) amountOutMin, ok2 := decoded["amountOutMin"].(*big.Int) path, ok3 := decoded["path"].([]common.Address) deadline, ok4 := decoded["deadline"].(*big.Int) if !ok1 || !ok2 || !ok3 || !ok4 { return ", Failed to decode parameters" } // Convert to readable format with enhanced details amountInEth := new(big.Float).Quo(new(big.Float).SetInt(amountIn), big.NewFloat(1e18)) amountOutMinEth := new(big.Float).Quo(new(big.Float).SetInt(amountOutMin), big.NewFloat(1e18)) // Extract token addresses from path tokenIn := "0x0000000000000000000000000000000000000000" tokenOut := "0x0000000000000000000000000000000000000000" if len(path) >= 2 { tokenIn = path[0].Hex() tokenOut = path[len(path)-1].Hex() } return fmt.Sprintf(", AmountIn: %s (%s), MinOut: %s (%s), Hops: %d, Deadline: %s", amountInEth.Text('f', 6), tokenIn, amountOutMinEth.Text('f', 6), tokenOut, len(path)-1, time.Unix(deadline.Int64(), 0).Format("15:04:05")) } // decodeSwapTokensForExactTokens decodes UniswapV2 swapTokensForExactTokens parameters func (p *ArbitrumL2Parser) decodeSwapTokensForExactTokens(params []byte) string { if len(params) < 160 { return ", Invalid parameters" } amountOut := new(big.Int).SetBytes(params[0:32]) amountInMax := new(big.Int).SetBytes(params[32:64]) amountOutEth := new(big.Float).Quo(new(big.Float).SetInt(amountOut), big.NewFloat(1e18)) amountInMaxEth := new(big.Float).Quo(new(big.Float).SetInt(amountInMax), big.NewFloat(1e18)) return fmt.Sprintf(", AmountOut: %s tokens, MaxIn: %s tokens", amountOutEth.Text('f', 6), amountInMaxEth.Text('f', 6)) } // decodeSwapExactETHForTokens decodes UniswapV2 swapExactETHForTokens parameters func (p *ArbitrumL2Parser) decodeSwapExactETHForTokens(params []byte) string { if len(params) < 32 { return ", Invalid parameters" } amountOutMin := new(big.Int).SetBytes(params[0:32]) amountOutMinEth := new(big.Float).Quo(new(big.Float).SetInt(amountOutMin), big.NewFloat(1e18)) return fmt.Sprintf(", MinOut: %s tokens", amountOutMinEth.Text('f', 6)) } // decodeSwapExactTokensForETH decodes UniswapV2 swapExactTokensForETH parameters func (p *ArbitrumL2Parser) decodeSwapExactTokensForETH(params []byte) string { if len(params) < 64 { return ", Invalid parameters" } amountIn := new(big.Int).SetBytes(params[0:32]) amountOutMin := new(big.Int).SetBytes(params[32:64]) amountInEth := new(big.Float).Quo(new(big.Float).SetInt(amountIn), big.NewFloat(1e18)) amountOutMinEth := new(big.Float).Quo(new(big.Float).SetInt(amountOutMin), big.NewFloat(1e18)) return fmt.Sprintf(", AmountIn: %s tokens, MinETH: %s", amountInEth.Text('f', 6), amountOutMinEth.Text('f', 6)) } // decodeExactInputSingle decodes UniswapV3 exactInputSingle parameters func (p *ArbitrumL2Parser) decodeExactInputSingle(params []byte) string { if len(params) < 160 { // ExactInputSingleParams struct return ", Invalid parameters" } // Simplified decoding - real implementation would parse the struct properly amountIn := new(big.Int).SetBytes(params[128:160]) // approximation amountInEth := new(big.Float).Quo(new(big.Float).SetInt(amountIn), big.NewFloat(1e18)) return fmt.Sprintf(", AmountIn: %s tokens", amountInEth.Text('f', 6)) } // decodeExactInput decodes UniswapV3 exactInput parameters func (p *ArbitrumL2Parser) decodeExactInput(params []byte) string { if len(params) < 128 { return ", Invalid parameters" } amountIn := new(big.Int).SetBytes(params[64:96]) // approximation amountInEth := new(big.Float).Quo(new(big.Float).SetInt(amountIn), big.NewFloat(1e18)) return fmt.Sprintf(", AmountIn: %s tokens (multi-hop)", amountInEth.Text('f', 6)) } // decodeExactOutputSingle decodes UniswapV3 exactOutputSingle parameters func (p *ArbitrumL2Parser) decodeExactOutputSingle(params []byte) string { if len(params) < 160 { return ", Invalid parameters" } amountOut := new(big.Int).SetBytes(params[160:192]) // approximation amountOutEth := new(big.Float).Quo(new(big.Float).SetInt(amountOut), big.NewFloat(1e18)) return fmt.Sprintf(", AmountOut: %s tokens", amountOutEth.Text('f', 6)) } // decodeMulticall decodes UniswapV3 multicall parameters func (p *ArbitrumL2Parser) decodeMulticall(params []byte) string { if len(params) < 32 { return ", Invalid parameters" } // Multicall contains an array of encoded function calls // This is complex to decode without full ABI parsing return fmt.Sprintf(", Multicall with %d bytes of data", len(params)) } // decodeFunctionDataStructured extracts structured parameters from transaction input data func (p *ArbitrumL2Parser) decodeFunctionDataStructured(funcInfo DEXFunctionSignature, inputData []byte) *SwapDetails { if len(inputData) < 4 { return &SwapDetails{IsValid: false} } // Skip the 4-byte function selector params := inputData[4:] switch funcInfo.Name { case "swapExactTokensForTokens": return p.decodeSwapExactTokensForTokensStructured(params) case "swapTokensForExactTokens": return p.decodeSwapTokensForExactTokensStructured(params) case "swapExactETHForTokens": return p.decodeSwapExactETHForTokensStructured(params) case "swapExactTokensForETH": return p.decodeSwapExactTokensForETHStructured(params) case "exactInputSingle": return p.decodeExactInputSingleStructured(params) case "exactInput": return p.decodeExactInputStructured(params) case "exactOutputSingle": return p.decodeExactOutputSingleStructured(params) case "multicall": return p.decodeMulticallStructured(params) default: return &SwapDetails{IsValid: false} } } // decodeSwapExactTokensForTokensStructured decodes UniswapV2 swapExactTokensForTokens parameters func (p *ArbitrumL2Parser) decodeSwapExactTokensForTokensStructured(params []byte) *SwapDetails { if len(params) < 160 { // 5 parameters * 32 bytes each return &SwapDetails{IsValid: false} } // Extract amounts directly amountIn := new(big.Int).SetBytes(params[0:32]) amountMin := new(big.Int).SetBytes(params[32:64]) // CRITICAL FIX: Use the working extraction method instead of broken inline extraction // Build full calldata with function signature fullCalldata := make([]byte, len(params)+4) // swapExactTokensForTokens signature: 0x38ed1739 fullCalldata[0] = 0x38 fullCalldata[1] = 0xed fullCalldata[2] = 0x17 fullCalldata[3] = 0x39 copy(fullCalldata[4:], params) tokenInAddr, tokenOutAddr, err := p.ExtractTokensFromCalldata(fullCalldata) var ( tokenIn string tokenOut string ) if err == nil && tokenInAddr != (common.Address{}) && tokenOutAddr != (common.Address{}) { tokenIn = p.resolveTokenSymbol(tokenInAddr.Hex()) tokenOut = p.resolveTokenSymbol(tokenOutAddr.Hex()) } else { // Fallback to zero addresses if extraction fails tokenIn = "0x0000000000000000000000000000000000000000" tokenOut = "0x0000000000000000000000000000000000000000" } return &SwapDetails{ AmountIn: amountIn, AmountOut: amountMin, // For UniswapV2, this is actually AmountMin but we display it as expected output AmountMin: amountMin, TokenIn: tokenIn, TokenOut: tokenOut, TokenInAddress: tokenInAddr, TokenOutAddress: tokenOutAddr, Deadline: new(big.Int).SetBytes(params[128:160]).Uint64(), Recipient: fmt.Sprintf("0x%x", params[96:128]), // address is last 20 bytes IsValid: true, } } // decodeSwapExactTokensForETHStructured decodes UniswapV2 swapExactTokensForETH parameters func (p *ArbitrumL2Parser) decodeSwapExactTokensForETHStructured(params []byte) *SwapDetails { if len(params) < 64 { return &SwapDetails{IsValid: false} } return &SwapDetails{ AmountIn: new(big.Int).SetBytes(params[0:32]), AmountOut: new(big.Int).SetBytes(params[32:64]), // For UniswapV2, this is actually AmountMin but we display it as expected output AmountMin: new(big.Int).SetBytes(params[32:64]), TokenIn: "0x0000000000000000000000000000000000000000", TokenOut: "ETH", TokenInAddress: common.Address{}, TokenOutAddress: common.Address{}, IsValid: true, } } // decodeExactInputSingleStructured decodes UniswapV3 exactInputSingle parameters func (p *ArbitrumL2Parser) decodeExactInputSingleStructured(params []byte) *SwapDetails { if len(params) < 160 { // ExactInputSingleParams struct return &SwapDetails{IsValid: false} } // ExactInputSingleParams structure: // struct ExactInputSingleParams { // address tokenIn; // offset 0, 32 bytes // address tokenOut; // offset 32, 32 bytes // uint24 fee; // offset 64, 32 bytes (padded) // address recipient; // offset 96, 32 bytes // uint256 deadline; // offset 128, 32 bytes // uint256 amountIn; // offset 160, 32 bytes // uint256 amountOutMinimum; // offset 192, 32 bytes // uint160 sqrtPriceLimitX96; // offset 224, 32 bytes // } // Properly extract token addresses (last 20 bytes of each 32-byte slot) tokenInAddr := common.BytesToAddress(params[12:32]) // Skip first 12 bytes, take last 20 tokenOutAddr := common.BytesToAddress(params[44:64]) // Skip first 12 bytes, take last 20 recipient := common.BytesToAddress(params[108:128]) // Extract amounts and other values rawFee := new(big.Int).SetBytes(params[64:96]).Uint64() fee, err := security.SafeUint64ToUint32(rawFee) if err != nil { p.logger.Error("Fee value exceeds uint32 maximum", "rawFee", rawFee, "error", err) return &SwapDetails{IsValid: false} // Return invalid if fee is invalid } deadline := new(big.Int).SetBytes(params[128:160]).Uint64() amountIn := new(big.Int).SetBytes(params[160:192]) amountOutMin := new(big.Int).SetBytes(params[192:224]) return &SwapDetails{ AmountIn: amountIn, AmountOut: amountOutMin, // For exactInputSingle, we display amountOutMinimum as expected output AmountMin: amountOutMin, TokenIn: p.resolveTokenSymbol(tokenInAddr.Hex()), TokenOut: p.resolveTokenSymbol(tokenOutAddr.Hex()), TokenInAddress: tokenInAddr, TokenOutAddress: tokenOutAddr, Fee: fee, Deadline: deadline, Recipient: recipient.Hex(), IsValid: true, } } // decodeSwapTokensForExactTokensStructured decodes UniswapV2 swapTokensForExactTokens parameters func (p *ArbitrumL2Parser) decodeSwapTokensForExactTokensStructured(params []byte) *SwapDetails { if len(params) < 160 { return &SwapDetails{IsValid: false} } // CRITICAL FIX: Use the working extraction method fullCalldata := make([]byte, len(params)+4) // swapTokensForExactTokens signature: 0x8803dbee fullCalldata[0] = 0x88 fullCalldata[1] = 0x03 fullCalldata[2] = 0xdb fullCalldata[3] = 0xee copy(fullCalldata[4:], params) tokenInAddr, tokenOutAddr, err := p.ExtractTokensFromCalldata(fullCalldata) var ( tokenIn string tokenOut string ) if err == nil && tokenInAddr != (common.Address{}) && tokenOutAddr != (common.Address{}) { tokenIn = p.resolveTokenSymbol(tokenInAddr.Hex()) tokenOut = p.resolveTokenSymbol(tokenOutAddr.Hex()) } else { tokenIn = "0x0000000000000000000000000000000000000000" tokenOut = "0x0000000000000000000000000000000000000000" } return &SwapDetails{ AmountOut: new(big.Int).SetBytes(params[0:32]), AmountIn: new(big.Int).SetBytes(params[32:64]), // Max amount in TokenIn: tokenIn, TokenOut: tokenOut, TokenInAddress: tokenInAddr, TokenOutAddress: tokenOutAddr, IsValid: true, } } // decodeSwapExactETHForTokensStructured decodes UniswapV2 swapExactETHForTokens parameters func (p *ArbitrumL2Parser) decodeSwapExactETHForTokensStructured(params []byte) *SwapDetails { if len(params) < 32 { return &SwapDetails{IsValid: false} } // CRITICAL FIX: Use the working extraction method fullCalldata := make([]byte, len(params)+4) // swapExactETHForTokens signature: 0x7ff36ab5 fullCalldata[0] = 0x7f fullCalldata[1] = 0xf3 fullCalldata[2] = 0x6a fullCalldata[3] = 0xb5 copy(fullCalldata[4:], params) tokenInAddr, tokenOutAddr, err := p.ExtractTokensFromCalldata(fullCalldata) var ( tokenIn string tokenOut string ) if err == nil && tokenOutAddr != (common.Address{}) { tokenIn = "ETH" tokenOut = p.resolveTokenSymbol(tokenOutAddr.Hex()) } else { tokenIn = "ETH" tokenOut = "0x0000000000000000000000000000000000000000" } return &SwapDetails{ AmountMin: new(big.Int).SetBytes(params[0:32]), TokenIn: tokenIn, TokenOut: tokenOut, TokenInAddress: tokenInAddr, // Will be WETH TokenOutAddress: tokenOutAddr, IsValid: true, } } // decodeExactInputStructured decodes UniswapV3 exactInput parameters func (p *ArbitrumL2Parser) decodeExactInputStructured(params []byte) *SwapDetails { if len(params) < 128 { return &SwapDetails{IsValid: false} } // ExactInputParams struct: // struct ExactInputParams { // bytes path; // offset 0: pointer to path data // address recipient; // offset 32: 32 bytes // uint256 deadline; // offset 64: 32 bytes // uint256 amountIn; // offset 96: 32 bytes // uint256 amountOutMinimum; // offset 128: 32 bytes // } recipient := common.BytesToAddress(params[44:64]) // Skip padding, take last 20 bytes deadline := new(big.Int).SetBytes(params[64:96]).Uint64() amountIn := new(big.Int).SetBytes(params[96:128]) var amountOutMin *big.Int if len(params) >= 160 { amountOutMin = new(big.Int).SetBytes(params[128:160]) } else { amountOutMin = big.NewInt(0) } return &SwapDetails{ AmountIn: amountIn, AmountOut: amountOutMin, // For exactInput, we display amountOutMinimum as expected output AmountMin: amountOutMin, TokenIn: "0x0000000000000000000000000000000000000000", // Would need to decode path data at offset specified in params[0:32] TokenOut: "0x0000000000000000000000000000000000000000", // Would need to decode path data Deadline: deadline, Recipient: recipient.Hex(), IsValid: true, } } // decodeExactOutputSingleStructured decodes UniswapV3 exactOutputSingle parameters func (p *ArbitrumL2Parser) decodeExactOutputSingleStructured(params []byte) *SwapDetails { if len(params) < 160 { return &SwapDetails{IsValid: false} } tokenInAddr := common.BytesToAddress(params[12:32]) tokenOutAddr := common.BytesToAddress(params[44:64]) return &SwapDetails{ AmountOut: new(big.Int).SetBytes(params[160:192]), TokenIn: p.resolveTokenSymbol(tokenInAddr.Hex()), TokenOut: p.resolveTokenSymbol(tokenOutAddr.Hex()), TokenInAddress: tokenInAddr, TokenOutAddress: tokenOutAddr, IsValid: true, } } // decodeMulticallStructured decodes UniswapV3 multicall parameters func (p *ArbitrumL2Parser) decodeMulticallStructured(params []byte) *SwapDetails { if len(params) < 64 { return &SwapDetails{IsValid: false} } // Multicall contains an array of encoded function calls // First 32 bytes is the offset to the array // Next 32 bytes is the array length if len(params) < 64 { return &SwapDetails{IsValid: false} } // Get array length (skip offset, get length) if len(params) < 64+32 { return &SwapDetails{IsValid: false} } arrayLength := new(big.Int).SetBytes(params[32:64]) // Validate array length if arrayLength.Sign() <= 0 || arrayLength.Cmp(big.NewInt(100)) > 0 { return &SwapDetails{IsValid: false} } // Enhanced multicall parsing to handle various router patterns if arrayLength.Cmp(big.NewInt(0)) > 0 && len(params) > 96 { // Try to extract tokens from any function call in the multicall token0, token1 := p.extractTokensFromMulticallData(params) if token0 != "" && token1 != "" { token0Addr := common.HexToAddress(token0) token1Addr := common.HexToAddress(token1) return &SwapDetails{ TokenIn: p.resolveTokenSymbol(token0), TokenOut: p.resolveTokenSymbol(token1), TokenInAddress: token0Addr, TokenOutAddress: token1Addr, IsValid: true, } } } // If we can't decode specific parameters, mark as invalid rather than returning zeros // This will trigger fallback processing return &SwapDetails{ TokenIn: "0x0000000000000000000000000000000000000000", TokenOut: "0x0000000000000000000000000000000000000000", TokenInAddress: common.Address{}, TokenOutAddress: common.Address{}, IsValid: false, // Mark as invalid so fallback processing can handle it } } // calculateProfitWithOracle calculates profit estimation using the price oracle func (p *ArbitrumL2Parser) calculateProfitWithOracle(swapDetails *SwapDetails) (float64, error) { if p.oracle == nil { return 0.0, fmt.Errorf("price oracle not available") } // Skip calculation for invalid swaps if !swapDetails.IsValid || swapDetails.AmountIn == nil || swapDetails.AmountIn.Sign() == 0 { return 0.0, nil } // Convert token addresses from string to common.Address tokenIn := swapDetails.TokenInAddress tokenOut := swapDetails.TokenOutAddress // Fall back to decoding from string if address fields are empty if tokenIn == (common.Address{}) { if !common.IsHexAddress(swapDetails.TokenIn) { return 0.0, fmt.Errorf("invalid tokenIn address: %s", swapDetails.TokenIn) } tokenIn = common.HexToAddress(swapDetails.TokenIn) } if tokenOut == (common.Address{}) { if !common.IsHexAddress(swapDetails.TokenOut) { return 0.0, fmt.Errorf("invalid tokenOut address: %s", swapDetails.TokenOut) } tokenOut = common.HexToAddress(swapDetails.TokenOut) } // Create price request priceReq := &oracle.PriceRequest{ TokenIn: tokenIn, TokenOut: tokenOut, AmountIn: swapDetails.AmountIn, Timestamp: time.Now(), } // Get price from oracle with timeout ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() priceResp, err := p.oracle.GetPrice(ctx, priceReq) if err != nil { return 0.0, fmt.Errorf("oracle price lookup failed: %w", err) } if !priceResp.Valid { return 0.0, fmt.Errorf("oracle returned invalid price") } // Convert amounts to float for USD calculation (assuming 18 decimals) amountInFloat := new(big.Float).Quo(new(big.Float).SetInt(swapDetails.AmountIn), big.NewFloat(1e18)) amountOutFloat := new(big.Float).Quo(new(big.Float).SetInt(priceResp.AmountOut), big.NewFloat(1e18)) amountInVal, _ := amountInFloat.Float64() amountOutVal, _ := amountOutFloat.Float64() // Estimate profit based on price difference // This is a simplified calculation - in reality you'd need: // 1. USD prices for both tokens // 2. Gas cost estimation // 3. Market impact analysis // 4. Arbitrage opportunity assessment // For now, calculate potential arbitrage profit as percentage of swap value profitPercentage := 0.0 slippageBps := int64(0) // Initialize slippage variable if amountInVal > 0 { // Simple profit estimation based on price impact slippageBps = priceResp.SlippageBps.Int64() if slippageBps > 0 { // Higher slippage = higher potential arbitrage profit profitPercentage = float64(slippageBps) / 10000.0 * 0.1 // 10% of slippage as profit estimate } } // Convert to USD estimate (simplified - assumes token has $1 base value) estimatedProfitUSD := amountInVal * profitPercentage p.logger.Debug(fmt.Sprintf("Calculated profit for swap %s->%s: amountIn=%.6f, amountOut=%.6f, slippage=%d bps, profit=$%.2f", tokenIn.Hex()[:8], tokenOut.Hex()[:8], amountInVal, amountOutVal, slippageBps, estimatedProfitUSD)) return estimatedProfitUSD, nil } // initializeABIs initializes the ABI decoders for sophisticated parameter parsing func (p *ArbitrumL2Parser) initializeABIs() error { // Uniswap V2 Router ABI (essential functions) uniswapV2JSON := `[ { "name": "swapExactTokensForTokens", "type": "function", "inputs": [ {"name": "amountIn", "type": "uint256"}, {"name": "amountOutMin", "type": "uint256"}, {"name": "path", "type": "address[]"}, {"name": "to", "type": "address"}, {"name": "deadline", "type": "uint256"} ] }, { "name": "swapTokensForExactTokens", "type": "function", "inputs": [ {"name": "amountOut", "type": "uint256"}, {"name": "amountInMax", "type": "uint256"}, {"name": "path", "type": "address[]"}, {"name": "to", "type": "address"}, {"name": "deadline", "type": "uint256"} ] }, { "name": "swapExactETHForTokens", "type": "function", "inputs": [ {"name": "amountOutMin", "type": "uint256"}, {"name": "path", "type": "address[]"}, {"name": "to", "type": "address"}, {"name": "deadline", "type": "uint256"} ] }, { "name": "swapExactTokensForETH", "type": "function", "inputs": [ {"name": "amountIn", "type": "uint256"}, {"name": "amountOutMin", "type": "uint256"}, {"name": "path", "type": "address[]"}, {"name": "to", "type": "address"}, {"name": "deadline", "type": "uint256"} ] } ]` // Uniswap V3 Router ABI (essential functions) uniswapV3JSON := `[ { "name": "exactInputSingle", "type": "function", "inputs": [ { "name": "params", "type": "tuple", "components": [ {"name": "tokenIn", "type": "address"}, {"name": "tokenOut", "type": "address"}, {"name": "fee", "type": "uint24"}, {"name": "recipient", "type": "address"}, {"name": "deadline", "type": "uint256"}, {"name": "amountIn", "type": "uint256"}, {"name": "amountOutMinimum", "type": "uint256"}, {"name": "sqrtPriceLimitX96", "type": "uint160"} ] } ] }, { "name": "exactInput", "type": "function", "inputs": [ { "name": "params", "type": "tuple", "components": [ {"name": "path", "type": "bytes"}, {"name": "recipient", "type": "address"}, {"name": "deadline", "type": "uint256"}, {"name": "amountIn", "type": "uint256"}, {"name": "amountOutMinimum", "type": "uint256"} ] } ] }, { "name": "exactOutputSingle", "type": "function", "inputs": [ { "name": "params", "type": "tuple", "components": [ {"name": "tokenIn", "type": "address"}, {"name": "tokenOut", "type": "address"}, {"name": "fee", "type": "uint24"}, {"name": "recipient", "type": "address"}, {"name": "deadline", "type": "uint256"}, {"name": "amountOut", "type": "uint256"}, {"name": "amountInMaximum", "type": "uint256"}, {"name": "sqrtPriceLimitX96", "type": "uint160"} ] } ] }, { "name": "multicall", "type": "function", "inputs": [ {"name": "data", "type": "bytes[]"} ] } ]` var err error // Parse Uniswap V2 ABI p.uniswapV2ABI, err = abi.JSON(strings.NewReader(uniswapV2JSON)) if err != nil { return fmt.Errorf("failed to parse Uniswap V2 ABI: %w", err) } // Parse Uniswap V3 ABI p.uniswapV3ABI, err = abi.JSON(strings.NewReader(uniswapV3JSON)) if err != nil { return fmt.Errorf("failed to parse Uniswap V3 ABI: %w", err) } // Use same ABI for SushiSwap (same interface as Uniswap V2) p.sushiSwapABI = p.uniswapV2ABI p.logger.Info("ABI decoders initialized successfully for sophisticated transaction parsing") return nil } // decodeWithABI uses proper ABI decoding instead of basic byte slicing func (p *ArbitrumL2Parser) decodeWithABI(protocol, functionName string, inputData []byte) (map[string]interface{}, error) { if len(inputData) < 4 { return nil, fmt.Errorf("input data too short") } // Remove function selector (first 4 bytes) params := inputData[4:] var targetABI abi.ABI switch protocol { case "UniswapV2": targetABI = p.uniswapV2ABI case "UniswapV3": targetABI = p.uniswapV3ABI case "SushiSwap": targetABI = p.sushiSwapABI default: return nil, fmt.Errorf("unsupported protocol: %s", protocol) } // Get the method from ABI method, exists := targetABI.Methods[functionName] if !exists { return nil, fmt.Errorf("method %s not found in %s ABI", functionName, protocol) } // Decode the parameters values, err := method.Inputs.Unpack(params) if err != nil { return nil, fmt.Errorf("failed to unpack parameters: %w", err) } // Convert to map for easier access result := make(map[string]interface{}) for i, input := range method.Inputs { if i < len(values) { result[input.Name] = values[i] } } p.logger.Debug(fmt.Sprintf("Successfully decoded %s.%s with %d parameters", protocol, functionName, len(values))) return result, nil } // GetDetailedSwapInfo extracts detailed swap information from DEXTransaction func (p *ArbitrumL2Parser) GetDetailedSwapInfo(dexTx *DEXTransaction) *DetailedSwapInfo { if dexTx == nil || dexTx.SwapDetails == nil || !dexTx.SwapDetails.IsValid { return &DetailedSwapInfo{IsValid: false} } return &DetailedSwapInfo{ TxHash: dexTx.Hash, From: dexTx.From, To: dexTx.To, MethodName: dexTx.FunctionName, Protocol: dexTx.Protocol, AmountIn: dexTx.SwapDetails.AmountIn, AmountOut: dexTx.SwapDetails.AmountOut, AmountMin: dexTx.SwapDetails.AmountMin, TokenIn: dexTx.SwapDetails.TokenIn, TokenOut: dexTx.SwapDetails.TokenOut, TokenInAddress: dexTx.SwapDetails.TokenInAddress, TokenOutAddress: dexTx.SwapDetails.TokenOutAddress, Fee: dexTx.SwapDetails.Fee, Recipient: dexTx.SwapDetails.Recipient, IsValid: true, } } // DetailedSwapInfo represents enhanced swap information for external processing type DetailedSwapInfo struct { TxHash string From string To string MethodName string Protocol string AmountIn *big.Int AmountOut *big.Int AmountMin *big.Int TokenIn string TokenOut string TokenInAddress common.Address TokenOutAddress common.Address Fee uint32 Recipient string IsValid bool } // Close closes the RPC connection // resolveTokenSymbol converts token address to human-readable symbol func (p *ArbitrumL2Parser) resolveTokenSymbol(tokenAddress string) string { // Convert to lowercase for consistent lookup addr := strings.ToLower(tokenAddress) // Known Arbitrum token mappings tokenMap := map[string]string{ "0x82af49447d8a07e3bd95bd0d56f35241523fbab1": "WETH", "0xaf88d065e77c8cc2239327c5edb3a432268e5831": "USDC", "0xff970a61a04b1ca14834a43f5de4533ebddb5cc8": "USDC.e", "0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9": "USDT", "0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f": "WBTC", "0x912ce59144191c1204e64559fe8253a0e49e6548": "ARB", "0xfc5a1a6eb076a2c7ad06ed22c90d7e710e35ad0a": "GMX", "0xf97f4df75117a78c1a5a0dbb814af92458539fb4": "LINK", "0xfa7f8980b0f1e64a2062791cc3b0871572f1f7f0": "UNI", "0xba5ddd1f9d7f570dc94a51479a000e3bce967196": "AAVE", "0x0de59c86c306b9fead9fb67e65551e2b6897c3f6": "KUMA", "0x6efa9b8883dfb78fd75cd89d8474c44c3cbda469": "DIA", "0x440017a1b021006d556d7fc06a54c32e42eb745b": "G@ARB", "0x11cdb42b0eb46d95f990bedd4695a6e3fa034978": "CRV", "0x040d1edc9569d4bab2d15287dc5a4f10f56a56b8": "BAL", "0x354a6da3fcde098f8389cad84b0182725c6c91de": "COMP", "0x2e9a6df78e42c50b0cefcf9000d0c3a4d34e1dd5": "MKR", "0x539bde0d7dbd336b79148aa742883198bbf60342": "MAGIC", "0x3d9907f9a368ad0a51be60f7da3b97cf940982d8": "GRAIL", "0x6c2c06790b3e3e3c38e12ee22f8183b37a13ee55": "DPX", "0x3082cc23568ea640225c2467653db90e9250aaa0": "RDNT", "0xaaa6c1e32c55a7bfa8066a6fae9b42650f262418": "RAM", "0x0c880f6761f1af8d9aa9c466984b80dab9a8c9e8": "PENDLE", } if symbol, exists := tokenMap[addr]; exists { return symbol } // Return shortened address if not found if len(tokenAddress) > 10 { return tokenAddress[:6] + "..." + tokenAddress[len(tokenAddress)-4:] } return tokenAddress } // extractTokensFromMulticallData extracts token addresses from multicall transaction data // CRITICAL FIX: Decode multicall structure and route to working extraction methods // instead of calling broken multicall.go heuristics func (p *ArbitrumL2Parser) extractTokensFromMulticallData(params []byte) (token0, token1 string) { if len(params) < 32 { return "", "" } // Multicall format: offset (32 bytes) + length (32 bytes) + data array offset := new(big.Int).SetBytes(params[0:32]).Uint64() if offset >= uint64(len(params)) { return "", "" } // Read array length arrayLength := new(big.Int).SetBytes(params[offset : offset+32]).Uint64() if arrayLength == 0 { return "", "" } // Process each call in the multicall currentOffset := offset + 32 for i := uint64(0); i < arrayLength && i < 10; i++ { // Limit to first 10 calls if currentOffset+32 > uint64(len(params)) { break } // Read call data offset (this is a relative offset from the array start) callOffsetRaw := new(big.Int).SetBytes(params[currentOffset : currentOffset+32]).Uint64() currentOffset += 32 // Calculate absolute offset (relative to params start + array offset) callOffset := offset + callOffsetRaw // Bounds check for callOffset if callOffset+32 > uint64(len(params)) { continue } // Read call data length callLength := new(big.Int).SetBytes(params[callOffset : callOffset+32]).Uint64() callStart := callOffset + 32 callEnd := callStart + callLength // Bounds check for call data if callEnd > uint64(len(params)) || callEnd < callStart { continue } // Extract the actual call data callData := params[callStart:callEnd] if len(callData) < 4 { continue } // Try to extract tokens using our WORKING signature-based methods t0, t1, err := p.ExtractTokensFromCalldata(callData) if err == nil && t0 != (common.Address{}) && t1 != (common.Address{}) { return t0.Hex(), t1.Hex() } } return "", "" } // isValidTokenAddress checks if an address looks like a valid token address func (p *ArbitrumL2Parser) isValidTokenAddress(hexAddr string) bool { // Basic validation - more sophisticated validation could be added if len(hexAddr) != 40 { return false } // Check if it's all hex for _, r := range hexAddr { if !((r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F')) { return false } } // Filter out common non-token addresses (routers, common addresses, etc.) commonNonTokens := []string{ "e592427a0aece92de3edee1f18e0157c05861564", // Uniswap V3 Router "a51afafe0263b40edaef0df8781ea9aa03e381a3", // Universal Router "4752ba5dbc23f44d87826276bf6fd6b1c372ad24", // Uniswap V2 Router "0000000000000000000000000000000000000000", // Zero address "ffffffffffffffffffffffffffffffffffffffff", // Max address "0000000000000000000000000000000000000001", // Common system addresses } for _, nonToken := range commonNonTokens { if strings.EqualFold(hexAddr, nonToken) { return false } } return true } func (p *ArbitrumL2Parser) Close() { if p.client != nil { p.client.Close() } } // CRITICAL FIX: Public wrapper for token extraction - exposed for events parser integration func (p *ArbitrumL2Parser) ExtractTokensFromMulticallData(params []byte) (token0, token1 string) { return p.extractTokensFromMulticallData(params) } // ExtractTokensFromCalldata implements interfaces.TokenExtractor for direct calldata parsing func (p *ArbitrumL2Parser) ExtractTokensFromCalldata(calldata []byte) (token0, token1 common.Address, err error) { if len(calldata) < 4 { return common.Address{}, common.Address{}, fmt.Errorf("calldata too short") } // Try to parse using known function signatures functionSignature := hex.EncodeToString(calldata[:4]) switch functionSignature { case "3593564c": // execute (UniversalRouter) return p.extractTokensFromUniversalRouter(calldata[4:]) case "38ed1739": // swapExactTokensForTokens return p.extractTokensFromSwapExactTokensForTokens(calldata[4:]) case "8803dbee": // swapTokensForExactTokens return p.extractTokensFromSwapTokensForExactTokens(calldata[4:]) case "7ff36ab5": // swapExactETHForTokens return p.extractTokensFromSwapExactETHForTokens(calldata[4:]) case "18cbafe5": // swapExactTokensForETH return p.extractTokensFromSwapExactTokensForETH(calldata[4:]) case "414bf389": // exactInputSingle (Uniswap V3) return p.extractTokensFromExactInputSingle(calldata[4:]) case "ac9650d8": // multicall // For multicall, extract tokens from first successful call stringToken0, stringToken1 := p.extractTokensFromMulticallData(calldata[4:]) if stringToken0 != "" && stringToken1 != "" { return common.HexToAddress(stringToken0), common.HexToAddress(stringToken1), nil } return common.Address{}, common.Address{}, fmt.Errorf("no tokens found in multicall") default: return common.Address{}, common.Address{}, fmt.Errorf("unknown function signature: %s", functionSignature) } } // Helper methods for specific function signature parsing func (p *ArbitrumL2Parser) extractTokensFromSwapExactTokensForTokens(params []byte) (token0, token1 common.Address, err error) { if len(params) < 160 { return common.Address{}, common.Address{}, fmt.Errorf("invalid parameters length") } // Extract path offset (3rd parameter) pathOffset := new(big.Int).SetBytes(params[64:96]).Uint64() if pathOffset >= uint64(len(params)) { return common.Address{}, common.Address{}, fmt.Errorf("invalid path offset") } // Extract path length pathLengthBytes := params[pathOffset:pathOffset+32] pathLength := new(big.Int).SetBytes(pathLengthBytes).Uint64() if pathLength < 2 || pathOffset+32+pathLength*32 > uint64(len(params)) { return common.Address{}, common.Address{}, fmt.Errorf("invalid path length") } // Extract first and last addresses from path token0 = common.BytesToAddress(params[pathOffset+32:pathOffset+64]) token1 = common.BytesToAddress(params[pathOffset+32+(pathLength-1)*32:pathOffset+32+pathLength*32]) return token0, token1, nil } func (p *ArbitrumL2Parser) extractTokensFromSwapTokensForExactTokens(params []byte) (token0, token1 common.Address, err error) { // Similar to swapExactTokensForTokens but with different parameter order return p.extractTokensFromSwapExactTokensForTokens(params) } func (p *ArbitrumL2Parser) extractTokensFromSwapExactETHForTokens(params []byte) (token0, token1 common.Address, err error) { if len(params) < 96 { return common.Address{}, common.Address{}, fmt.Errorf("invalid parameters length") } // ETH is typically represented as WETH token0 = common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1") // WETH on Arbitrum // Extract path offset (2nd parameter) pathOffset := new(big.Int).SetBytes(params[32:64]).Uint64() if pathOffset >= uint64(len(params)) { return common.Address{}, common.Address{}, fmt.Errorf("invalid path offset") } // Extract path length and last token pathLengthBytes := params[pathOffset:pathOffset+32] pathLength := new(big.Int).SetBytes(pathLengthBytes).Uint64() if pathLength < 2 || pathOffset+32+pathLength*32 > uint64(len(params)) { return common.Address{}, common.Address{}, fmt.Errorf("invalid path length") } token1 = common.BytesToAddress(params[pathOffset+32+(pathLength-1)*32:pathOffset+32+pathLength*32]) return token0, token1, nil } func (p *ArbitrumL2Parser) extractTokensFromSwapExactTokensForETH(params []byte) (token0, token1 common.Address, err error) { token0, token1, err = p.extractTokensFromSwapExactETHForTokens(params) // Swap the order since this is tokens -> ETH return token1, token0, err } func (p *ArbitrumL2Parser) extractTokensFromExactInputSingle(params []byte) (token0, token1 common.Address, err error) { if len(params) < 64 { return common.Address{}, common.Address{}, fmt.Errorf("invalid parameters length") } // Extract tokenIn and tokenOut from exactInputSingle struct token0 = common.BytesToAddress(params[0:32]) token1 = common.BytesToAddress(params[32:64]) return token0, token1, nil } // extractTokensFromUniversalRouter decodes UniversalRouter execute() commands func (p *ArbitrumL2Parser) extractTokensFromUniversalRouter(params []byte) (token0, token1 common.Address, err error) { // UniversalRouter execute format: // bytes commands, bytes[] inputs, uint256 deadline if len(params) < 96 { return common.Address{}, common.Address{}, fmt.Errorf("params too short for universal router") } // Parse commands offset (first 32 bytes) commandsOffset := new(big.Int).SetBytes(params[0:32]).Uint64() // Parse inputs offset (second 32 bytes) inputsOffset := new(big.Int).SetBytes(params[32:64]).Uint64() if commandsOffset >= uint64(len(params)) || inputsOffset >= uint64(len(params)) { return common.Address{}, common.Address{}, fmt.Errorf("invalid offsets") } // Read commands length commandsLength := new(big.Int).SetBytes(params[commandsOffset : commandsOffset+32]).Uint64() commandsStart := commandsOffset + 32 // Read first command (V3_SWAP_EXACT_IN = 0x00, V2_SWAP_EXACT_IN = 0x08) if commandsStart >= uint64(len(params)) || commandsLength == 0 { return common.Address{}, common.Address{}, fmt.Errorf("no commands") } firstCommand := params[commandsStart] // Read inputs array inputsLength := new(big.Int).SetBytes(params[inputsOffset : inputsOffset+32]).Uint64() if inputsLength == 0 { return common.Address{}, common.Address{}, fmt.Errorf("no inputs") } // Read first input offset and data firstInputOffset := inputsOffset + 32 inputDataOffset := new(big.Int).SetBytes(params[firstInputOffset : firstInputOffset+32]).Uint64() if inputDataOffset >= uint64(len(params)) { return common.Address{}, common.Address{}, fmt.Errorf("invalid input offset") } inputDataLength := new(big.Int).SetBytes(params[inputDataOffset : inputDataOffset+32]).Uint64() inputDataStart := inputDataOffset + 32 inputDataEnd := inputDataStart + inputDataLength if inputDataEnd > uint64(len(params)) { return common.Address{}, common.Address{}, fmt.Errorf("input data out of bounds") } inputData := params[inputDataStart:inputDataEnd] // Decode based on command type switch firstCommand { case 0x00: // V3_SWAP_EXACT_IN // Format: recipient(addr), amountIn(uint256), amountOutMin(uint256), path(bytes), payerIsUser(bool) if len(inputData) >= 160 { // Path starts at offset 128 (4th parameter) pathOffset := new(big.Int).SetBytes(inputData[96:128]).Uint64() if pathOffset < uint64(len(inputData)) { pathLength := new(big.Int).SetBytes(inputData[pathOffset : pathOffset+32]).Uint64() pathStart := pathOffset + 32 // V3 path format: token0(20 bytes) + fee(3 bytes) + token1(20 bytes) if pathLength >= 43 && pathStart+43 <= uint64(len(inputData)) { token0 = common.BytesToAddress(inputData[pathStart : pathStart+20]) token1 = common.BytesToAddress(inputData[pathStart+23 : pathStart+43]) return token0, token1, nil } } } case 0x08: // V2_SWAP_EXACT_IN // Format: recipient(addr), amountIn(uint256), amountOutMin(uint256), path(addr[]), payerIsUser(bool) if len(inputData) >= 128 { // Path array offset is at position 96 (4th parameter) pathOffset := new(big.Int).SetBytes(inputData[96:128]).Uint64() if pathOffset < uint64(len(inputData)) { pathArrayLength := new(big.Int).SetBytes(inputData[pathOffset : pathOffset+32]).Uint64() if pathArrayLength >= 2 { // First token token0 = common.BytesToAddress(inputData[pathOffset+32 : pathOffset+64]) // Last token lastTokenOffset := pathOffset + 32 + (pathArrayLength-1)*32 if lastTokenOffset+32 <= uint64(len(inputData)) { token1 = common.BytesToAddress(inputData[lastTokenOffset : lastTokenOffset+32]) return token0, token1, nil } } } } } return common.Address{}, common.Address{}, fmt.Errorf("unsupported universal router command: 0x%02x", firstCommand) }