feat(core): implement core MEV bot functionality with market scanning and Uniswap V3 pricing
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
@@ -6,21 +6,21 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
)
|
||||
|
||||
// AuthConfig holds authentication configuration
|
||||
type AuthConfig struct {
|
||||
APIKey string
|
||||
BasicUsername string
|
||||
BasicPassword string
|
||||
AllowedIPs []string
|
||||
RequireHTTPS bool
|
||||
RateLimitRPS int
|
||||
Logger *logger.Logger
|
||||
APIKey string
|
||||
BasicUsername string
|
||||
BasicPassword string
|
||||
AllowedIPs []string
|
||||
RequireHTTPS bool
|
||||
RateLimitRPS int
|
||||
Logger *logger.Logger
|
||||
}
|
||||
|
||||
// Middleware provides authentication middleware for HTTP endpoints
|
||||
@@ -246,17 +246,17 @@ func (m *Middleware) CleanupRateLimiters() {
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
|
||||
for ip, limiter := range m.rateLimiter {
|
||||
// Remove limiters that haven't been used recently
|
||||
if len(limiter.requests) == 0 {
|
||||
delete(m.rateLimiter, ip)
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
lastRequest := limiter.requests[len(limiter.requests)-1]
|
||||
if lastRequest.Before(cutoff) {
|
||||
delete(m.rateLimiter, ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
@@ -140,10 +138,10 @@ func Load(filename string) (*Config, error) {
|
||||
func expandEnvVars(s string) string {
|
||||
// Pattern to match ${VAR} and $VAR
|
||||
envVarPattern := regexp.MustCompile(`\$\{([^}]+)\}|\$([A-Za-z_][A-Za-z0-9_]*)`)
|
||||
|
||||
|
||||
return envVarPattern.ReplaceAllStringFunc(s, func(match string) string {
|
||||
var varName string
|
||||
|
||||
|
||||
// Handle ${VAR} format
|
||||
if strings.HasPrefix(match, "${") && strings.HasSuffix(match, "}") {
|
||||
varName = match[2 : len(match)-1]
|
||||
@@ -151,12 +149,12 @@ func expandEnvVars(s string) string {
|
||||
// Handle $VAR format
|
||||
varName = match[1:]
|
||||
}
|
||||
|
||||
|
||||
// Get environment variable value
|
||||
if value := os.Getenv(varName); value != "" {
|
||||
return value
|
||||
}
|
||||
|
||||
|
||||
// Return empty string if environment variable is not set
|
||||
// This prevents invalid YAML when variables are missing
|
||||
return ""
|
||||
@@ -200,4 +198,4 @@ func (c *Config) OverrideWithEnv() {
|
||||
c.Bot.ChannelBufferSize = val
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,4 +126,4 @@ bot:
|
||||
assert.Equal(t, "https://override.arbitrum.io/rpc", cfg.Arbitrum.RPCEndpoint)
|
||||
assert.Equal(t, 20, cfg.Arbitrum.RateLimit.RequestsPerSecond)
|
||||
assert.Equal(t, 20, cfg.Bot.MaxWorkers)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ func New(level string, format string, file string) *Logger {
|
||||
logger := log.New(output, "", 0) // No flags, we'll format ourselves
|
||||
|
||||
logLevel := parseLogLevel(level)
|
||||
|
||||
|
||||
return &Logger{
|
||||
logger: logger,
|
||||
level: logLevel,
|
||||
@@ -123,7 +123,7 @@ func (l *Logger) Error(v ...interface{}) {
|
||||
// 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
|
||||
@@ -135,7 +135,7 @@ func (l *Logger) Opportunity(txHash, from, to, method, protocol string, amountIn
|
||||
└── Additional Data: %v`,
|
||||
timestamp, txHash, from, to, method, protocol,
|
||||
amountIn, amountOut, minOut, profitUSD, additionalData)
|
||||
|
||||
|
||||
l.logger.Println(message)
|
||||
}
|
||||
|
||||
@@ -144,4 +144,4 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,4 +242,4 @@ func TestErrorWithAllLevels(t *testing.T) {
|
||||
assert.Contains(t, output, "ERROR:")
|
||||
assert.Contains(t, output, "test error message")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,36 +24,36 @@ type AdaptiveRateLimiter struct {
|
||||
|
||||
// AdaptiveEndpoint represents an endpoint with adaptive rate limiting
|
||||
type AdaptiveEndpoint struct {
|
||||
URL string
|
||||
limiter *rate.Limiter
|
||||
config config.RateLimitConfig
|
||||
circuitBreaker *CircuitBreaker
|
||||
metrics *EndpointMetrics
|
||||
healthChecker *HealthChecker
|
||||
lastAdjustment time.Time
|
||||
consecutiveErrors int64
|
||||
URL string
|
||||
limiter *rate.Limiter
|
||||
config config.RateLimitConfig
|
||||
circuitBreaker *CircuitBreaker
|
||||
metrics *EndpointMetrics
|
||||
healthChecker *HealthChecker
|
||||
lastAdjustment time.Time
|
||||
consecutiveErrors int64
|
||||
consecutiveSuccess int64
|
||||
}
|
||||
|
||||
// EndpointMetrics tracks performance metrics for an endpoint
|
||||
type EndpointMetrics struct {
|
||||
TotalRequests int64
|
||||
TotalRequests int64
|
||||
SuccessfulRequests int64
|
||||
FailedRequests int64
|
||||
TotalLatency int64 // nanoseconds
|
||||
LastRequestTime int64 // unix timestamp
|
||||
SuccessRate float64
|
||||
AverageLatency float64 // milliseconds
|
||||
FailedRequests int64
|
||||
TotalLatency int64 // nanoseconds
|
||||
LastRequestTime int64 // unix timestamp
|
||||
SuccessRate float64
|
||||
AverageLatency float64 // milliseconds
|
||||
}
|
||||
|
||||
// CircuitBreaker implements circuit breaker pattern for failed endpoints
|
||||
type CircuitBreaker struct {
|
||||
state int32 // 0: Closed, 1: Open, 2: HalfOpen
|
||||
failureCount int64
|
||||
lastFailTime int64
|
||||
threshold int64
|
||||
timeout time.Duration // How long to wait before trying again
|
||||
testRequests int64 // Number of test requests in half-open state
|
||||
state int32 // 0: Closed, 1: Open, 2: HalfOpen
|
||||
failureCount int64
|
||||
lastFailTime int64
|
||||
threshold int64
|
||||
timeout time.Duration // How long to wait before trying again
|
||||
testRequests int64 // Number of test requests in half-open state
|
||||
}
|
||||
|
||||
// Circuit breaker states
|
||||
@@ -65,12 +65,12 @@ const (
|
||||
|
||||
// HealthChecker monitors endpoint health
|
||||
type HealthChecker struct {
|
||||
endpoint string
|
||||
interval time.Duration
|
||||
timeout time.Duration
|
||||
isHealthy int64 // atomic bool
|
||||
lastCheck int64 // unix timestamp
|
||||
stopChan chan struct{}
|
||||
endpoint string
|
||||
interval time.Duration
|
||||
timeout time.Duration
|
||||
isHealthy int64 // atomic bool
|
||||
lastCheck int64 // unix timestamp
|
||||
stopChan chan struct{}
|
||||
}
|
||||
|
||||
// NewAdaptiveRateLimiter creates a new adaptive rate limiter
|
||||
@@ -239,7 +239,7 @@ func (arl *AdaptiveRateLimiter) calculateEndpointScore(endpoint *AdaptiveEndpoin
|
||||
loadWeight := 0.1
|
||||
|
||||
successScore := endpoint.metrics.SuccessRate
|
||||
|
||||
|
||||
// Invert latency score (lower latency = higher score)
|
||||
latencyScore := 1.0
|
||||
if endpoint.metrics.AverageLatency > 0 {
|
||||
@@ -317,7 +317,7 @@ func (arl *AdaptiveRateLimiter) adjustEndpointRateLimit(url string, endpoint *Ad
|
||||
if abs(newLimit-currentLimit)/currentLimit > 0.05 { // 5% change threshold
|
||||
endpoint.limiter.SetLimit(rate.Limit(newLimit))
|
||||
endpoint.lastAdjustment = time.Now()
|
||||
|
||||
|
||||
arl.logger.Info(fmt.Sprintf("Adjusted rate limit for %s: %.2f -> %.2f (success: %.2f%%, latency: %.2fms)",
|
||||
url, currentLimit, newLimit, successRate*100, avgLatency))
|
||||
}
|
||||
@@ -376,7 +376,7 @@ func (cb *CircuitBreaker) recordSuccess() {
|
||||
func (cb *CircuitBreaker) recordFailure() {
|
||||
failures := atomic.AddInt64(&cb.failureCount, 1)
|
||||
atomic.StoreInt64(&cb.lastFailTime, time.Now().Unix())
|
||||
|
||||
|
||||
if failures >= cb.threshold {
|
||||
atomic.StoreInt32(&cb.state, CircuitOpen)
|
||||
}
|
||||
@@ -405,13 +405,13 @@ func (hc *HealthChecker) checkHealth() {
|
||||
// Simple health check - try to connect
|
||||
// In production, this might make a simple RPC call
|
||||
healthy := hc.performHealthCheck(ctx)
|
||||
|
||||
|
||||
if healthy {
|
||||
atomic.StoreInt64(&hc.isHealthy, 1)
|
||||
} else {
|
||||
atomic.StoreInt64(&hc.isHealthy, 0)
|
||||
}
|
||||
|
||||
|
||||
atomic.StoreInt64(&hc.lastCheck, time.Now().Unix())
|
||||
}
|
||||
|
||||
@@ -425,7 +425,7 @@ func (hc *HealthChecker) performHealthCheck(ctx context.Context) bool {
|
||||
// Stop stops the adaptive rate limiter
|
||||
func (arl *AdaptiveRateLimiter) Stop() {
|
||||
close(arl.stopChan)
|
||||
|
||||
|
||||
// Stop all health checkers
|
||||
arl.mu.RLock()
|
||||
for _, endpoint := range arl.endpoints {
|
||||
@@ -445,6 +445,6 @@ func (arl *AdaptiveRateLimiter) GetMetrics() map[string]*EndpointMetrics {
|
||||
arl.updateCalculatedMetrics(endpoint)
|
||||
metrics[url] = endpoint.metrics
|
||||
}
|
||||
|
||||
|
||||
return metrics
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,4 +125,4 @@ func (lm *LimiterManager) GetEndpoints() []string {
|
||||
}
|
||||
|
||||
return endpoints
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,4 +230,4 @@ func TestRateLimiting(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
duration = time.Since(start)
|
||||
assert.True(t, duration >= time.Second, "Second request should be delayed by rate limiter")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ func (sc *SecureConfig) Set(key, value string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt value for key %s: %w", key, err)
|
||||
}
|
||||
|
||||
|
||||
sc.values[key] = encrypted
|
||||
return nil
|
||||
}
|
||||
@@ -151,11 +151,11 @@ func (sc *SecureConfig) GetRequired(key string) (string, error) {
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("required configuration value missing: %s", key)
|
||||
}
|
||||
|
||||
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return "", fmt.Errorf("required configuration value empty: %s", key)
|
||||
}
|
||||
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
@@ -176,13 +176,13 @@ func (sc *SecureConfig) LoadFromEnvironment(keys []string) error {
|
||||
sc.manager.logger.Warn(fmt.Sprintf("Could not load secure config for %s: %v", key, err))
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// Store encrypted in memory
|
||||
if err := sc.Set(key, value); err != nil {
|
||||
return fmt.Errorf("failed to store secure config for %s: %w", key, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -199,17 +199,17 @@ func (sc *SecureConfig) Clear() {
|
||||
// Validate checks that all required configuration is present
|
||||
func (sc *SecureConfig) Validate(requiredKeys []string) error {
|
||||
var missingKeys []string
|
||||
|
||||
|
||||
for _, key := range requiredKeys {
|
||||
if _, err := sc.GetRequired(key); err != nil {
|
||||
missingKeys = append(missingKeys, key)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if len(missingKeys) > 0 {
|
||||
return fmt.Errorf("missing required configuration keys: %s", strings.Join(missingKeys, ", "))
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@ func GenerateConfigKey() (string, error) {
|
||||
if _, err := rand.Read(key); err != nil {
|
||||
return "", fmt.Errorf("failed to generate random key: %w", err)
|
||||
}
|
||||
|
||||
|
||||
return base64.StdEncoding.EncodeToString(key), nil
|
||||
}
|
||||
|
||||
@@ -240,11 +240,11 @@ func (cv *ConfigValidator) ValidateURL(url string) error {
|
||||
if url == "" {
|
||||
return errors.New("URL cannot be empty")
|
||||
}
|
||||
|
||||
|
||||
if !strings.HasPrefix(url, "https://") && !strings.HasPrefix(url, "wss://") {
|
||||
return errors.New("URL must use HTTPS or WSS protocol")
|
||||
}
|
||||
|
||||
|
||||
// Additional validation could go here (DNS lookup, connection test, etc.)
|
||||
return nil
|
||||
}
|
||||
@@ -254,16 +254,16 @@ func (cv *ConfigValidator) ValidateAPIKey(key string) error {
|
||||
if key == "" {
|
||||
return errors.New("API key cannot be empty")
|
||||
}
|
||||
|
||||
|
||||
if len(key) < 32 {
|
||||
return errors.New("API key must be at least 32 characters")
|
||||
}
|
||||
|
||||
|
||||
// Check for basic entropy (not all same character, contains mixed case, etc.)
|
||||
if strings.Count(key, string(key[0])) == len(key) {
|
||||
return errors.New("API key lacks sufficient entropy")
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -272,21 +272,21 @@ func (cv *ConfigValidator) ValidateAddress(address string) error {
|
||||
if address == "" {
|
||||
return errors.New("address cannot be empty")
|
||||
}
|
||||
|
||||
|
||||
if !strings.HasPrefix(address, "0x") {
|
||||
return errors.New("address must start with 0x")
|
||||
}
|
||||
|
||||
|
||||
if len(address) != 42 { // 0x + 40 hex chars
|
||||
return errors.New("address must be 42 characters long")
|
||||
}
|
||||
|
||||
|
||||
// Validate hex format
|
||||
for i, char := range address[2:] {
|
||||
if !((char >= '0' && char <= '9') || (char >= 'a' && char <= 'f') || (char >= 'A' && char <= 'F')) {
|
||||
return fmt.Errorf("invalid hex character at position %d: %c", i+2, char)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,4 +31,4 @@ func Max(a, b int) int {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user