diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 7c8b19f..1ab0e74 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -1,14 +1,53 @@ package logger import ( + "fmt" "log" "os" + "strings" + "time" ) +// LogLevel represents different log levels +type LogLevel int + +const ( + DEBUG LogLevel = iota + INFO + WARN + ERROR + OPPORTUNITY // Special level for opportunities +) + +var logLevelNames = map[LogLevel]string{ + DEBUG: "DEBUG", + INFO: "INFO", + WARN: "WARN", + ERROR: "ERROR", + OPPORTUNITY: "OPPORTUNITY", +} + // Logger represents a simple logger wrapper type Logger struct { - logger *log.Logger - level string + logger *log.Logger + level LogLevel + levelName string +} + +// parseLogLevel converts string log level to LogLevel enum +func parseLogLevel(level string) LogLevel { + switch strings.ToLower(level) { + case "debug": + return DEBUG + case "info": + return INFO + case "warn", "warning": + return WARN + case "error": + return ERROR + default: + return INFO // Default to INFO level + } } // New creates a new logger @@ -27,37 +66,82 @@ func New(level string, format string, file string) *Logger { output = os.Stdout } - // Create the logger - logger := log.New(output, "", log.LstdFlags|log.Lshortfile) + // Create the logger with custom format + logger := log.New(output, "", 0) // No flags, we'll format ourselves + logLevel := parseLogLevel(level) + return &Logger{ - logger: logger, - level: level, + logger: logger, + level: logLevel, + levelName: level, } } +// shouldLog determines if a message should be logged based on level +func (l *Logger) shouldLog(level LogLevel) bool { + return level >= l.level +} + +// formatMessage formats a log message with timestamp and level +func (l *Logger) formatMessage(level LogLevel, v ...interface{}) string { + timestamp := time.Now().Format("2006/01/02 15:04:05") + levelName := logLevelNames[level] + message := fmt.Sprint(v...) + return fmt.Sprintf("%s [%s] %s", timestamp, levelName, message) +} + // Debug logs a debug message func (l *Logger) Debug(v ...interface{}) { - if l.level == "debug" { - l.logger.Print("DEBUG: ", v) + if l.shouldLog(DEBUG) { + l.logger.Println(l.formatMessage(DEBUG, v...)) } } // Info logs an info message func (l *Logger) Info(v ...interface{}) { - if l.level == "debug" || l.level == "info" { - l.logger.Print("INFO: ", v) + if l.shouldLog(INFO) { + l.logger.Println(l.formatMessage(INFO, v...)) } } // Warn logs a warning message func (l *Logger) Warn(v ...interface{}) { - if l.level == "debug" || l.level == "info" || l.level == "warn" { - l.logger.Print("WARN: ", v) + if l.shouldLog(WARN) { + l.logger.Println(l.formatMessage(WARN, v...)) } } // Error logs an error message func (l *Logger) Error(v ...interface{}) { - l.logger.Print("ERROR: ", v) + if l.shouldLog(ERROR) { + l.logger.Println(l.formatMessage(ERROR, v...)) + } +} + +// Opportunity logs a found opportunity with detailed information +// This always logs regardless of level since opportunities are critical +func (l *Logger) Opportunity(txHash, from, to, method, protocol string, amountIn, amountOut, minOut, profitUSD float64, additionalData map[string]interface{}) { + timestamp := time.Now().Format("2006/01/02 15:04:05") + + message := fmt.Sprintf(`%s [OPPORTUNITY] 🎯 ARBITRAGE OPPORTUNITY DETECTED +├── Transaction: %s +├── From: %s → To: %s +├── Method: %s (%s) +├── Amount In: %.6f tokens +├── Amount Out: %.6f tokens +├── Min Out: %.6f tokens +├── Estimated Profit: $%.2f USD +└── Additional Data: %v`, + timestamp, txHash, from, to, method, protocol, + amountIn, amountOut, minOut, profitUSD, additionalData) + + l.logger.Println(message) +} + +// OpportunitySimple logs a simple opportunity message (for backwards compatibility) +func (l *Logger) OpportunitySimple(v ...interface{}) { + timestamp := time.Now().Format("2006/01/02 15:04:05") + message := fmt.Sprint(v...) + l.logger.Printf("%s [OPPORTUNITY] %s", timestamp, message) } \ No newline at end of file diff --git a/pkg/arbitrum/l2_parser.go b/pkg/arbitrum/l2_parser.go index aecb996..707e199 100644 --- a/pkg/arbitrum/l2_parser.go +++ b/pkg/arbitrum/l2_parser.go @@ -258,6 +258,19 @@ func (p *ArbitrumL2Parser) ParseDEXTransactions(ctx context.Context, block *RawL return dexTransactions } +// SwapDetails contains detailed information about a DEX swap +type SwapDetails struct { + AmountIn *big.Int + AmountOut *big.Int + AmountMin *big.Int + TokenIn string + TokenOut string + Fee uint32 + Deadline uint64 + Recipient string + IsValid bool +} + // DEXTransaction represents a parsed DEX transaction type DEXTransaction struct { Hash string @@ -270,6 +283,7 @@ type DEXTransaction struct { InputData []byte ContractName string BlockNumber string + SwapDetails *SwapDetails // Detailed swap information } // parseDEXTransaction checks if a transaction is a DEX interaction @@ -309,11 +323,43 @@ func (p *ArbitrumL2Parser) parseDEXTransaction(tx RawL2Transaction) *DEXTransact } // Decode function parameters based on function type - swapDetails := p.decodeFunctionData(funcInfo, inputData) + swapDetails := p.decodeFunctionDataStructured(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(), swapDetails)) + // Use detailed opportunity logging if swap details are available + if swapDetails != nil && swapDetails.IsValid && swapDetails.AmountIn != nil { + amountInFloat := new(big.Float).Quo(new(big.Float).SetInt(swapDetails.AmountIn), big.NewFloat(1e18)) + amountOutFloat := float64(0) + if swapDetails.AmountOut != nil { + amountOutFloat, _ = new(big.Float).Quo(new(big.Float).SetInt(swapDetails.AmountOut), big.NewFloat(1e18)).Float64() + } + amountMinFloat := float64(0) + if swapDetails.AmountMin != nil { + amountMinFloat, _ = new(big.Float).Quo(new(big.Float).SetInt(swapDetails.AmountMin), big.NewFloat(1e18)).Float64() + } + amountInFloatVal, _ := amountInFloat.Float64() + + // Calculate estimated profit (placeholder - would need price oracle in real implementation) + estimatedProfitUSD := 0.0 + + additionalData := map[string]interface{}{ + "tokenIn": swapDetails.TokenIn, + "tokenOut": swapDetails.TokenOut, + "fee": swapDetails.Fee, + "deadline": swapDetails.Deadline, + "recipient": swapDetails.Recipient, + "contractName": contractName, + "functionSig": functionSig, + } + + p.logger.Opportunity(tx.Hash, tx.From, tx.To, funcInfo.Name, funcInfo.Protocol, + amountInFloatVal, amountOutFloat, amountMinFloat, estimatedProfitUSD, additionalData) + } 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)) + } return &DEXTransaction{ Hash: tx.Hash, @@ -326,6 +372,7 @@ func (p *ArbitrumL2Parser) parseDEXTransaction(tx RawL2Transaction) *DEXTransact InputData: inputData, ContractName: contractName, BlockNumber: "", // Will be set by caller + SwapDetails: swapDetails, } } @@ -480,6 +527,158 @@ func (p *ArbitrumL2Parser) decodeMulticall(params []byte) string { 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} + } + + return &SwapDetails{ + AmountIn: new(big.Int).SetBytes(params[0:32]), + AmountMin: new(big.Int).SetBytes(params[32:64]), + TokenIn: "unknown", // Would need to decode path array + TokenOut: "unknown", // Would need to decode path array + 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]), + AmountMin: new(big.Int).SetBytes(params[32:64]), + TokenIn: "unknown", + TokenOut: "ETH", + IsValid: true, + } +} + +// decodeExactInputSingleStructured decodes UniswapV3 exactInputSingle parameters +func (p *ArbitrumL2Parser) decodeExactInputSingleStructured(params []byte) *SwapDetails { + if len(params) < 160 { // ExactInputSingleParams struct + return &SwapDetails{IsValid: false} + } + + // Simplified decoding - real implementation would parse the struct properly + return &SwapDetails{ + AmountIn: new(big.Int).SetBytes(params[128:160]), + TokenIn: fmt.Sprintf("0x%x", params[0:32]), // tokenIn + TokenOut: fmt.Sprintf("0x%x", params[32:64]), // tokenOut + Fee: uint32(new(big.Int).SetBytes(params[64:96]).Uint64()), // fee + Recipient: fmt.Sprintf("0x%x", params[96:128]), // recipient + IsValid: true, + } +} + +// decodeSwapTokensForExactTokensStructured decodes UniswapV2 swapTokensForExactTokens parameters +func (p *ArbitrumL2Parser) decodeSwapTokensForExactTokensStructured(params []byte) *SwapDetails { + if len(params) < 160 { + return &SwapDetails{IsValid: false} + } + + return &SwapDetails{ + AmountOut: new(big.Int).SetBytes(params[0:32]), + AmountIn: new(big.Int).SetBytes(params[32:64]), // Max amount in + TokenIn: "unknown", + TokenOut: "unknown", + IsValid: true, + } +} + +// decodeSwapExactETHForTokensStructured decodes UniswapV2 swapExactETHForTokens parameters +func (p *ArbitrumL2Parser) decodeSwapExactETHForTokensStructured(params []byte) *SwapDetails { + if len(params) < 32 { + return &SwapDetails{IsValid: false} + } + + return &SwapDetails{ + AmountMin: new(big.Int).SetBytes(params[0:32]), + TokenIn: "ETH", + TokenOut: "unknown", + IsValid: true, + } +} + +// decodeExactInputStructured decodes UniswapV3 exactInput parameters +func (p *ArbitrumL2Parser) decodeExactInputStructured(params []byte) *SwapDetails { + if len(params) < 128 { + return &SwapDetails{IsValid: false} + } + + return &SwapDetails{ + AmountIn: new(big.Int).SetBytes(params[64:96]), + TokenIn: "unknown", // Would need to decode path + TokenOut: "unknown", // Would need to decode path + IsValid: true, + } +} + +// decodeExactOutputSingleStructured decodes UniswapV3 exactOutputSingle parameters +func (p *ArbitrumL2Parser) decodeExactOutputSingleStructured(params []byte) *SwapDetails { + if len(params) < 160 { + return &SwapDetails{IsValid: false} + } + + return &SwapDetails{ + AmountOut: new(big.Int).SetBytes(params[160:192]), + TokenIn: fmt.Sprintf("0x%x", params[0:32]), + TokenOut: fmt.Sprintf("0x%x", params[32:64]), + IsValid: true, + } +} + +// decodeMulticallStructured decodes UniswapV3 multicall parameters +func (p *ArbitrumL2Parser) decodeMulticallStructured(params []byte) *SwapDetails { + if len(params) < 32 { + return &SwapDetails{IsValid: false} + } + + // For multicall, we'd need to decode the individual calls + // This is a placeholder + return &SwapDetails{ + TokenIn: "unknown", + TokenOut: "unknown", + IsValid: true, + } +} + // Close closes the RPC connection func (p *ArbitrumL2Parser) Close() { if p.client != nil {