feat(foundation): create V2 foundation with core interfaces and types
Some checks failed
V2 CI/CD Pipeline / Pre-Flight Checks (push) Has been cancelled
V2 CI/CD Pipeline / Build & Dependencies (push) Has been cancelled
V2 CI/CD Pipeline / Code Quality & Linting (push) Has been cancelled
V2 CI/CD Pipeline / Unit Tests (100% Coverage Required) (push) Has been cancelled
V2 CI/CD Pipeline / Integration Tests (push) Has been cancelled
V2 CI/CD Pipeline / Performance Benchmarks (push) Has been cancelled
V2 CI/CD Pipeline / Decimal Precision Validation (push) Has been cancelled
V2 CI/CD Pipeline / Modularity Validation (push) Has been cancelled
V2 CI/CD Pipeline / Final Validation Summary (push) Has been cancelled

Created complete V2 foundation infrastructure for modular MEV bot:

Core Types (pkg/types/):
- swap.go: SwapEvent with protocol support for 13+ DEXs
- pool.go: PoolInfo with multi-index cache support
- errors.go: Comprehensive error definitions
- Full validation methods and helper functions
- Proper decimal scaling (18 decimals internal representation)

Parser Interface (pkg/parsers/):
- Parser interface for protocol-specific implementations
- Factory interface for routing and registration
- Support for single log and full transaction parsing
- SupportsLog() for dynamic protocol identification

Cache Interface (pkg/cache/):
- PoolCache interface with multi-index support
- O(1) lookups by: address, token pair, protocol, liquidity
- Add, Update, Remove, Count, Clear operations
- Context-aware for cancellation support

Validation Interface (pkg/validation/):
- Validator interface for swap events and pools
- ValidationRules with configurable thresholds
- FilterValid() for batch validation
- Zero address, zero amount, decimal precision checks

Observability (pkg/observability/):
- Logger interface with structured logging (slog)
- Metrics interface with Prometheus integration
- Performance tracking (parse latency, execution)
- Business metrics (arbitrage opportunities, profit)

Project Files:
- go.mod: Module definition with ethereum and prometheus deps
- README.md: Complete project overview and documentation

Architecture Principles:
- Interface-first design for testability
- Single responsibility per interface
- Dependency injection ready
- Observable by default
- 18 decimal internal representation

Ready for Phase 1 implementation:
- All interfaces defined
- Error types established
- Core types validated
- Logging and metrics infrastructure ready

Next: Implement protocol-specific parsers (P2-001 through P2-055)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Administrator
2025-11-10 14:43:36 +01:00
parent 24b4d90e98
commit 5d9a1d72a0
10 changed files with 956 additions and 0 deletions

41
pkg/cache/interface.go vendored Normal file
View File

@@ -0,0 +1,41 @@
// Package cache defines the pool cache interface for multi-index lookups
package cache
import (
"context"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/your-org/mev-bot/pkg/types"
)
// PoolCache defines the interface for multi-index pool cache
type PoolCache interface {
// GetByAddress retrieves a pool by its contract address
GetByAddress(ctx context.Context, address common.Address) (*types.PoolInfo, error)
// GetByTokenPair retrieves all pools for a given token pair
GetByTokenPair(ctx context.Context, token0, token1 common.Address) ([]*types.PoolInfo, error)
// GetByProtocol retrieves all pools for a given protocol
GetByProtocol(ctx context.Context, protocol types.ProtocolType) ([]*types.PoolInfo, error)
// GetByLiquidity retrieves pools sorted by liquidity (descending)
GetByLiquidity(ctx context.Context, minLiquidity *big.Int, limit int) ([]*types.PoolInfo, error)
// Add adds or updates a pool in the cache
Add(ctx context.Context, pool *types.PoolInfo) error
// Update updates pool information
Update(ctx context.Context, address common.Address, updateFn func(*types.PoolInfo) error) error
// Remove removes a pool from the cache
Remove(ctx context.Context, address common.Address) error
// Count returns the total number of pools in the cache
Count(ctx context.Context) (int, error)
// Clear removes all pools from the cache
Clear(ctx context.Context) error
}

View File

@@ -0,0 +1,76 @@
// Package observability provides logging and metrics infrastructure
package observability
import (
"context"
"log/slog"
"os"
)
// Logger defines the logging interface
type Logger interface {
// Debug logs a debug message
Debug(msg string, args ...any)
// Info logs an info message
Info(msg string, args ...any)
// Warn logs a warning message
Warn(msg string, args ...any)
// Error logs an error message
Error(msg string, args ...any)
// With returns a logger with additional context fields
With(args ...any) Logger
// WithContext returns a logger with context
WithContext(ctx context.Context) Logger
}
// slogLogger wraps slog.Logger to implement our Logger interface
type slogLogger struct {
logger *slog.Logger
}
// NewLogger creates a new structured logger
func NewLogger(level slog.Level) Logger {
opts := &slog.HandlerOptions{
Level: level,
AddSource: true,
}
handler := slog.NewJSONHandler(os.Stdout, opts)
logger := slog.New(handler)
return &slogLogger{
logger: logger,
}
}
func (l *slogLogger) Debug(msg string, args ...any) {
l.logger.Debug(msg, args...)
}
func (l *slogLogger) Info(msg string, args ...any) {
l.logger.Info(msg, args...)
}
func (l *slogLogger) Warn(msg string, args ...any) {
l.logger.Warn(msg, args...)
}
func (l *slogLogger) Error(msg string, args ...any) {
l.logger.Error(msg, args...)
}
func (l *slogLogger) With(args ...any) Logger {
return &slogLogger{
logger: l.logger.With(args...),
}
}
func (l *slogLogger) WithContext(ctx context.Context) Logger {
// Could extract trace ID, request ID, etc. from context
return l
}

View File

@@ -0,0 +1,137 @@
// Package observability provides logging and metrics infrastructure
package observability
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
// Metrics defines the metrics collection interface
type Metrics interface {
// RecordSwapEvent records a swap event being processed
RecordSwapEvent(protocol string, success bool)
// RecordParseLatency records parsing latency in seconds
RecordParseLatency(protocol string, latencySeconds float64)
// RecordArbitrageOpportunity records an arbitrage opportunity
RecordArbitrageOpportunity(profit float64)
// RecordExecution records a trade execution
RecordExecution(success bool, netProfit float64)
// IncrementPoolCacheSize increments the pool cache size
IncrementPoolCacheSize()
// DecrementPoolCacheSize decrements the pool cache size
DecrementPoolCacheSize()
}
// prometheusMetrics implements Metrics using Prometheus
type prometheusMetrics struct {
swapEventsTotal *prometheus.CounterVec
parseLatency *prometheus.HistogramVec
arbitrageOpportunities *prometheus.CounterVec
arbitrageProfit *prometheus.HistogramVec
executionsTotal *prometheus.CounterVec
executionProfit *prometheus.HistogramVec
poolCacheSize prometheus.Gauge
}
// NewMetrics creates a new Prometheus metrics collector
func NewMetrics(namespace string) Metrics {
return &prometheusMetrics{
swapEventsTotal: promauto.NewCounterVec(
prometheus.CounterOpts{
Namespace: namespace,
Name: "swap_events_total",
Help: "Total number of swap events processed",
},
[]string{"protocol", "status"},
),
parseLatency: promauto.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: namespace,
Name: "parse_latency_seconds",
Help: "Latency of parsing swap events in seconds",
Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0},
},
[]string{"protocol"},
),
arbitrageOpportunities: promauto.NewCounterVec(
prometheus.CounterOpts{
Namespace: namespace,
Name: "arbitrage_opportunities_total",
Help: "Total number of arbitrage opportunities detected",
},
[]string{"status"},
),
arbitrageProfit: promauto.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: namespace,
Name: "arbitrage_profit_eth",
Help: "Arbitrage profit in ETH",
Buckets: []float64{0.01, 0.05, 0.1, 0.5, 1.0, 5.0, 10.0},
},
[]string{"status"},
),
executionsTotal: promauto.NewCounterVec(
prometheus.CounterOpts{
Namespace: namespace,
Name: "executions_total",
Help: "Total number of trade executions",
},
[]string{"status"},
),
executionProfit: promauto.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: namespace,
Name: "execution_profit_eth",
Help: "Execution profit in ETH (after gas)",
Buckets: []float64{-1.0, -0.1, -0.01, 0, 0.01, 0.1, 1.0, 10.0},
},
[]string{"status"},
),
poolCacheSize: promauto.NewGauge(
prometheus.GaugeOpts{
Namespace: namespace,
Name: "pool_cache_size",
Help: "Current number of pools in cache",
},
),
}
}
func (m *prometheusMetrics) RecordSwapEvent(protocol string, success bool) {
status := "success"
if !success {
status = "failure"
}
m.swapEventsTotal.WithLabelValues(protocol, status).Inc()
}
func (m *prometheusMetrics) RecordParseLatency(protocol string, latencySeconds float64) {
m.parseLatency.WithLabelValues(protocol).Observe(latencySeconds)
}
func (m *prometheusMetrics) RecordArbitrageOpportunity(profit float64) {
m.arbitrageOpportunities.WithLabelValues("detected").Inc()
m.arbitrageProfit.WithLabelValues("detected").Observe(profit)
}
func (m *prometheusMetrics) RecordExecution(success bool, netProfit float64) {
status := "success"
if !success {
status = "failure"
}
m.executionsTotal.WithLabelValues(status).Inc()
m.executionProfit.WithLabelValues(status).Observe(netProfit)
}
func (m *prometheusMetrics) IncrementPoolCacheSize() {
m.poolCacheSize.Inc()
}
func (m *prometheusMetrics) DecrementPoolCacheSize() {
m.poolCacheSize.Dec()
}

40
pkg/parsers/interface.go Normal file
View File

@@ -0,0 +1,40 @@
// Package parsers defines the parser interface and factory for protocol-specific parsers
package parsers
import (
"context"
"github.com/ethereum/go-ethereum/core/types"
mevtypes "github.com/your-org/mev-bot/pkg/types"
)
// Parser defines the interface for protocol-specific parsers
type Parser interface {
// ParseLog parses a single log entry into a SwapEvent
ParseLog(ctx context.Context, log types.Log, tx *types.Transaction) (*mevtypes.SwapEvent, error)
// ParseReceipt parses all relevant logs from a transaction receipt
ParseReceipt(ctx context.Context, receipt *types.Receipt, tx *types.Transaction) ([]*mevtypes.SwapEvent, error)
// SupportsLog checks if this parser can handle the given log
SupportsLog(log types.Log) bool
// Protocol returns the protocol type this parser handles
Protocol() mevtypes.ProtocolType
}
// Factory creates protocol-specific parsers
type Factory interface {
// GetParser returns a parser for the given protocol
GetParser(protocol mevtypes.ProtocolType) (Parser, error)
// ParseLog routes a log to the appropriate parser
ParseLog(ctx context.Context, log types.Log, tx *types.Transaction) (*mevtypes.SwapEvent, error)
// ParseTransaction parses all swap events from a transaction
ParseTransaction(ctx context.Context, tx *types.Transaction, receipt *types.Receipt) ([]*mevtypes.SwapEvent, error)
// RegisterParser registers a new parser for a protocol
RegisterParser(protocol mevtypes.ProtocolType, parser Parser) error
}

50
pkg/types/errors.go Normal file
View File

@@ -0,0 +1,50 @@
package types
import "errors"
// Validation errors
var (
ErrInvalidTxHash = errors.New("invalid transaction hash")
ErrInvalidPoolAddress = errors.New("invalid pool address")
ErrInvalidToken0Address = errors.New("invalid token0 address (zero address)")
ErrInvalidToken1Address = errors.New("invalid token1 address (zero address)")
ErrInvalidToken0Decimals = errors.New("invalid token0 decimals")
ErrInvalidToken1Decimals = errors.New("invalid token1 decimals")
ErrUnknownProtocol = errors.New("unknown protocol")
ErrZeroAmounts = errors.New("all swap amounts are zero")
ErrInvalidAmount = errors.New("invalid amount")
ErrInsufficientLiquidity = errors.New("insufficient liquidity")
)
// Parser errors
var (
ErrUnsupportedProtocol = errors.New("unsupported protocol")
ErrInvalidEventSignature = errors.New("invalid event signature")
ErrInvalidLogData = errors.New("invalid log data")
ErrFailedToDecode = errors.New("failed to decode event")
ErrMissingPoolInfo = errors.New("missing pool info from cache")
)
// Cache errors
var (
ErrPoolNotFound = errors.New("pool not found in cache")
ErrCacheNotInitialized = errors.New("cache not initialized")
ErrDuplicatePool = errors.New("pool already exists in cache")
)
// Arbitrage errors
var (
ErrNoOpportunity = errors.New("no arbitrage opportunity found")
ErrBelowMinProfit = errors.New("profit below minimum threshold")
ErrInvalidPath = errors.New("invalid arbitrage path")
ErrCircularDependency = errors.New("circular dependency in path")
)
// Execution errors
var (
ErrExecutionFailed = errors.New("execution failed")
ErrInsufficientBalance = errors.New("insufficient balance")
ErrSlippageExceeded = errors.New("slippage tolerance exceeded")
ErrGasPriceTooHigh = errors.New("gas price too high")
ErrTransactionReverted = errors.New("transaction reverted")
)

124
pkg/types/pool.go Normal file
View File

@@ -0,0 +1,124 @@
package types
import (
"math/big"
"github.com/ethereum/go-ethereum/common"
)
// PoolInfo contains comprehensive pool information
type PoolInfo struct {
// Pool identification
Address common.Address
Protocol ProtocolType
PoolType string // e.g., "constant-product", "stable", "weighted"
// Token information
Token0 common.Address
Token1 common.Address
Token0Decimals uint8
Token1Decimals uint8
Token0Symbol string
Token1Symbol string
// Reserves/Liquidity
Reserve0 *big.Int
Reserve1 *big.Int
Liquidity *big.Int
// Fee information
Fee uint32 // Fee in basis points (e.g., 30 = 0.3%)
FeeGrowth0 *big.Int // UniswapV3 fee growth
FeeGrowth1 *big.Int // UniswapV3 fee growth
// Protocol-specific data
SqrtPriceX96 *big.Int // UniswapV3/Camelot V3
Tick *int32 // UniswapV3/Camelot V3
TickSpacing int32 // UniswapV3/Camelot V3
AmpCoefficient *big.Int // Curve amplification coefficient
// Pool state
IsActive bool
BlockNumber uint64
LastUpdate uint64
}
// Validate checks if the pool info is valid
func (p *PoolInfo) Validate() error {
if p.Address == (common.Address{}) {
return ErrInvalidPoolAddress
}
if p.Token0 == (common.Address{}) {
return ErrInvalidToken0Address
}
if p.Token1 == (common.Address{}) {
return ErrInvalidToken1Address
}
if p.Token0Decimals == 0 || p.Token0Decimals > 18 {
return ErrInvalidToken0Decimals
}
if p.Token1Decimals == 0 || p.Token1Decimals > 18 {
return ErrInvalidToken1Decimals
}
if p.Protocol == ProtocolUnknown {
return ErrUnknownProtocol
}
return nil
}
// GetTokenPair returns the token pair as a sorted tuple
func (p *PoolInfo) GetTokenPair() (common.Address, common.Address) {
if p.Token0.Big().Cmp(p.Token1.Big()) < 0 {
return p.Token0, p.Token1
}
return p.Token1, p.Token0
}
// CalculatePrice calculates the price of token0 in terms of token1
func (p *PoolInfo) CalculatePrice() *big.Float {
if p.Reserve0 == nil || p.Reserve1 == nil || p.Reserve0.Sign() == 0 {
return big.NewFloat(0)
}
// Scale reserves to 18 decimals for consistent calculation
reserve0Scaled := scaleToDecimals(p.Reserve0, p.Token0Decimals, 18)
reserve1Scaled := scaleToDecimals(p.Reserve1, p.Token1Decimals, 18)
// Price = Reserve1 / Reserve0
reserve0Float := new(big.Float).SetInt(reserve0Scaled)
reserve1Float := new(big.Float).SetInt(reserve1Scaled)
price := new(big.Float).Quo(reserve1Float, reserve0Float)
return price
}
// scaleToDecimals scales an amount from one decimal precision to another
func scaleToDecimals(amount *big.Int, fromDecimals, toDecimals uint8) *big.Int {
if fromDecimals == toDecimals {
return new(big.Int).Set(amount)
}
if fromDecimals < toDecimals {
// Scale up
multiplier := new(big.Int).Exp(
big.NewInt(10),
big.NewInt(int64(toDecimals-fromDecimals)),
nil,
)
return new(big.Int).Mul(amount, multiplier)
}
// Scale down
divisor := new(big.Int).Exp(
big.NewInt(10),
big.NewInt(int64(fromDecimals-toDecimals)),
nil,
)
return new(big.Int).Div(amount, divisor)
}

115
pkg/types/swap.go Normal file
View File

@@ -0,0 +1,115 @@
// Package types defines core data types for MEV Bot V2
package types
import (
"math/big"
"time"
"github.com/ethereum/go-ethereum/common"
)
// ProtocolType identifies the DEX protocol
type ProtocolType string
const (
ProtocolUniswapV2 ProtocolType = "uniswap-v2"
ProtocolUniswapV3 ProtocolType = "uniswap-v3"
ProtocolUniswapV4 ProtocolType = "uniswap-v4"
ProtocolCurve ProtocolType = "curve"
ProtocolBalancerV2 ProtocolType = "balancer-v2"
ProtocolBalancerV3 ProtocolType = "balancer-v3"
ProtocolKyberClassic ProtocolType = "kyber-classic"
ProtocolKyberElastic ProtocolType = "kyber-elastic"
ProtocolCamelotV2 ProtocolType = "camelot-v2"
ProtocolCamelotV3AlgebraV1 ProtocolType = "camelot-v3-algebra-v1"
ProtocolCamelotV3AlgebraV19 ProtocolType = "camelot-v3-algebra-v1.9"
ProtocolCamelotV3AlgebraIntegral ProtocolType = "camelot-v3-algebra-integral"
ProtocolCamelotV3AlgebraDirectional ProtocolType = "camelot-v3-algebra-directional"
ProtocolUnknown ProtocolType = "unknown"
)
// SwapEvent represents a parsed swap event from any protocol
type SwapEvent struct {
// Event metadata
TxHash common.Hash
BlockNumber uint64
LogIndex uint
Timestamp time.Time
// Pool information
PoolAddress common.Address
Protocol ProtocolType
// Token information
Token0 common.Address
Token1 common.Address
Token0Decimals uint8
Token1Decimals uint8
// Swap amounts (scaled to 18 decimals internally)
Amount0In *big.Int
Amount1In *big.Int
Amount0Out *big.Int
Amount1Out *big.Int
// Additional protocol-specific data
Fee *big.Int // Fee amount (if applicable)
SqrtPriceX96 *big.Int // UniswapV3/Camelot V3 price
Liquidity *big.Int // Current liquidity
Tick *int32 // UniswapV3/Camelot V3 tick
// Sender and recipient
Sender common.Address
Recipient common.Address
}
// Validate checks if the swap event is valid
func (s *SwapEvent) Validate() error {
if s.TxHash == (common.Hash{}) {
return ErrInvalidTxHash
}
if s.PoolAddress == (common.Address{}) {
return ErrInvalidPoolAddress
}
if s.Token0 == (common.Address{}) {
return ErrInvalidToken0Address
}
if s.Token1 == (common.Address{}) {
return ErrInvalidToken1Address
}
if s.Protocol == ProtocolUnknown {
return ErrUnknownProtocol
}
// At least one amount should be non-zero
if isZero(s.Amount0In) && isZero(s.Amount1In) && isZero(s.Amount0Out) && isZero(s.Amount1Out) {
return ErrZeroAmounts
}
return nil
}
// GetInputToken returns the token being swapped in
func (s *SwapEvent) GetInputToken() (common.Address, *big.Int) {
if !isZero(s.Amount0In) {
return s.Token0, s.Amount0In
}
return s.Token1, s.Amount1In
}
// GetOutputToken returns the token being swapped out
func (s *SwapEvent) GetOutputToken() (common.Address, *big.Int) {
if !isZero(s.Amount0Out) {
return s.Token0, s.Amount0Out
}
return s.Token1, s.Amount1Out
}
// isZero checks if a big.Int is nil or zero
func isZero(n *big.Int) bool {
return n == nil || n.Sign() == 0
}

View File

@@ -0,0 +1,71 @@
// Package validation defines the validation interface for swap events and pools
package validation
import (
"context"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/your-org/mev-bot/pkg/types"
)
// Validator defines the interface for validating swap events and pools
type Validator interface {
// ValidateSwapEvent validates a swap event
ValidateSwapEvent(ctx context.Context, event *types.SwapEvent) error
// ValidatePoolInfo validates pool information
ValidatePoolInfo(ctx context.Context, pool *types.PoolInfo) error
// FilterValid filters a slice of swap events, returning only valid ones
FilterValid(ctx context.Context, events []*types.SwapEvent) []*types.SwapEvent
// GetValidationRules returns the current validation rules
GetValidationRules() *ValidationRules
}
// ValidationRules defines the rules for validation
type ValidationRules struct {
// Reject zero addresses
RejectZeroAddresses bool
// Reject zero amounts
RejectZeroAmounts bool
// Minimum amount threshold (in wei, scaled to 18 decimals)
MinAmount *big.Int
// Maximum amount threshold (in wei, scaled to 18 decimals)
MaxAmount *big.Int
// Allowed protocols (empty = all allowed)
AllowedProtocols map[types.ProtocolType]bool
// Reject events from blacklisted pools
BlacklistedPools map[common.Address]bool
// Reject events from blacklisted tokens
BlacklistedTokens map[common.Address]bool
// Validate decimal precision
ValidateDecimals bool
// Maximum acceptable slippage (in basis points, e.g. 50 = 0.5%)
MaxSlippageBps uint32
}
// DefaultValidationRules returns the default validation rules
func DefaultValidationRules() *ValidationRules {
return &ValidationRules{
RejectZeroAddresses: true,
RejectZeroAmounts: true,
MinAmount: big.NewInt(1000), // 0.000000000000001 ETH minimum
MaxAmount: new(big.Int).Exp(big.NewInt(10), big.NewInt(30), nil), // 1 trillion tokens max
AllowedProtocols: make(map[types.ProtocolType]bool),
BlacklistedPools: make(map[common.Address]bool),
BlacklistedTokens: make(map[common.Address]bool),
ValidateDecimals: true,
MaxSlippageBps: 50, // 0.5% max slippage
}
}