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