Restructured project for V2 refactor: **Structure Changes:** - Moved all V1 code to orig/ folder (preserved with git mv) - Created docs/planning/ directory - Added orig/README_V1.md explaining V1 preservation **Planning Documents:** - 00_V2_MASTER_PLAN.md: Complete architecture overview - Executive summary of critical V1 issues - High-level component architecture diagrams - 5-phase implementation roadmap - Success metrics and risk mitigation - 07_TASK_BREAKDOWN.md: Atomic task breakdown - 99+ hours of detailed tasks - Every task < 2 hours (atomic) - Clear dependencies and success criteria - Organized by implementation phase **V2 Key Improvements:** - Per-exchange parsers (factory pattern) - Multi-layer strict validation - Multi-index pool cache - Background validation pipeline - Comprehensive observability **Critical Issues Addressed:** - Zero address tokens (strict validation + cache enrichment) - Parsing accuracy (protocol-specific parsers) - No audit trail (background validation channel) - Inefficient lookups (multi-index cache) - Stats disconnection (event-driven metrics) Next Steps: 1. Review planning documents 2. Begin Phase 1: Foundation (P1-001 through P1-010) 3. Implement parsers in Phase 2 4. Build cache system in Phase 3 5. Add validation pipeline in Phase 4 6. Migrate and test in Phase 5 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
380 lines
14 KiB
Go
380 lines
14 KiB
Go
package exchanges
|
|
|
|
import (
|
|
"fmt"
|
|
"math/big"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/ethclient"
|
|
|
|
"github.com/fraktal/mev-beta/internal/logger"
|
|
"github.com/fraktal/mev-beta/pkg/math"
|
|
)
|
|
|
|
// DexAggregatorPoolDetector implements PoolDetector for DEX aggregators
|
|
type DexAggregatorPoolDetector struct {
|
|
client *ethclient.Client
|
|
logger *logger.Logger
|
|
config *ExchangeConfig
|
|
registry *ExchangeRegistry
|
|
}
|
|
|
|
// NewDexAggregatorPoolDetector creates a new DEX aggregator pool detector
|
|
func NewDexAggregatorPoolDetector(client *ethclient.Client, logger *logger.Logger, config *ExchangeConfig, registry *ExchangeRegistry) *DexAggregatorPoolDetector {
|
|
return &DexAggregatorPoolDetector{
|
|
client: client,
|
|
logger: logger,
|
|
config: config,
|
|
registry: registry,
|
|
}
|
|
}
|
|
|
|
// GetAllPools returns all pools containing the specified tokens across all exchanges
|
|
func (d *DexAggregatorPoolDetector) GetAllPools(token0, token1 common.Address) ([]common.Address, error) {
|
|
// This would aggregate pools from all registered exchanges
|
|
var allPools []common.Address
|
|
|
|
// Get all supported exchanges for this token pair
|
|
exchanges := d.registry.GetExchangesForPair(token0, token1)
|
|
|
|
for _, exchangeConfig := range exchanges {
|
|
// Get the pool detector for this exchange
|
|
poolDetector := d.registry.GetPoolDetector(exchangeConfig.Type)
|
|
if poolDetector != nil {
|
|
pools, err := poolDetector.GetAllPools(token0, token1)
|
|
if err != nil {
|
|
d.logger.Warn(fmt.Sprintf("Error getting pools from exchange %s: %v", exchangeConfig.Name, err))
|
|
continue
|
|
}
|
|
allPools = append(allPools, pools...)
|
|
}
|
|
}
|
|
|
|
return allPools, nil
|
|
}
|
|
|
|
// GetPoolForPair returns the best pool address for a specific token pair across exchanges
|
|
func (d *DexAggregatorPoolDetector) GetPoolForPair(token0, token1 common.Address) (common.Address, error) {
|
|
// In a real implementation, this would find the pool with best pricing across exchanges
|
|
// For now, return a placeholder address
|
|
return common.HexToAddress("0x0"), nil
|
|
}
|
|
|
|
// GetSupportedFeeTiers returns supported fee tiers for the aggregated exchanges
|
|
func (d *DexAggregatorPoolDetector) GetSupportedFeeTiers() []int64 {
|
|
// Return a range that covers all possible fee tiers across exchanges
|
|
return []int64{100, 250, 300, 500, 1000, 2500, 3000, 5000, 10000} // Various fee tiers
|
|
}
|
|
|
|
// GetPoolType returns the pool type
|
|
func (d *DexAggregatorPoolDetector) GetPoolType() string {
|
|
return "dex_aggregator"
|
|
}
|
|
|
|
// DexAggregatorLiquidityFetcher implements LiquidityFetcher for DEX aggregators
|
|
type DexAggregatorLiquidityFetcher struct {
|
|
client *ethclient.Client
|
|
logger *logger.Logger
|
|
config *ExchangeConfig
|
|
registry *ExchangeRegistry
|
|
engine *math.ExchangePricingEngine
|
|
}
|
|
|
|
// NewDexAggregatorLiquidityFetcher creates a new DEX aggregator liquidity fetcher
|
|
func NewDexAggregatorLiquidityFetcher(client *ethclient.Client, logger *logger.Logger, config *ExchangeConfig, registry *ExchangeRegistry, engine *math.ExchangePricingEngine) *DexAggregatorLiquidityFetcher {
|
|
return &DexAggregatorLiquidityFetcher{
|
|
client: client,
|
|
logger: logger,
|
|
config: config,
|
|
registry: registry,
|
|
engine: engine,
|
|
}
|
|
}
|
|
|
|
// GetPoolData fetches pool information from the best available exchange for the token pair
|
|
func (f *DexAggregatorLiquidityFetcher) GetPoolData(poolAddress common.Address) (*math.PoolData, error) {
|
|
// For aggregator, this would depend on which exchange's pool is being represented
|
|
// For this implementation, we'll return placeholder data
|
|
return nil, fmt.Errorf("GetPoolData not directly supported for aggregator, use GetBestPoolData instead")
|
|
}
|
|
|
|
// GetTokenReserves fetches reserves from the best available exchange for a token pair
|
|
func (f *DexAggregatorLiquidityFetcher) GetTokenReserves(poolAddress, token0, token1 common.Address) (*big.Int, *big.Int, error) {
|
|
// This would aggregate reserves across exchanges
|
|
// For now, return placeholder values
|
|
return big.NewInt(0), big.NewInt(0), fmt.Errorf("not implemented for aggregator directly")
|
|
}
|
|
|
|
// GetBestPoolData fetches the best pool data across all exchanges for a token pair
|
|
func (f *DexAggregatorLiquidityFetcher) GetBestPoolData(token0, token1 common.Address) (*math.PoolData, math.ExchangeType, error) {
|
|
// Get all supported exchanges for this token pair
|
|
exchanges := f.registry.GetExchangesForPair(token0, token1)
|
|
bestAmountOut := big.NewInt(0)
|
|
var bestPoolData *math.PoolData
|
|
var bestExchangeType math.ExchangeType
|
|
|
|
for _, exchangeConfig := range exchanges {
|
|
// Get the liquidity fetcher for this exchange
|
|
liquidityFetcher := f.registry.GetLiquidityFetcher(exchangeConfig.Type)
|
|
if liquidityFetcher != nil {
|
|
// Get pool data for this exchange
|
|
poolData, err := liquidityFetcher.GetPoolData(common.HexToAddress(exchangeConfig.FactoryAddress.Hex())) // This is a simplification
|
|
if err != nil {
|
|
f.logger.Warn(fmt.Sprintf("Error getting pool data from exchange %s: %v", exchangeConfig.Name, err))
|
|
continue
|
|
}
|
|
|
|
// Calculate amount out for a standard amount to compare
|
|
testAmount := big.NewInt(1000000000000000000) // 1 ETH equivalent
|
|
decimalAmountIn, err := math.NewUniversalDecimal(testAmount, 18, "AMOUNT_IN")
|
|
if err != nil {
|
|
f.logger.Warn(fmt.Sprintf("Error creating decimal amount: %v", err))
|
|
continue
|
|
}
|
|
|
|
pricer, err := f.engine.GetExchangePricer(poolData.ExchangeType)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
amountOut, err := pricer.CalculateAmountOut(decimalAmountIn, poolData)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
// Check if this is the best rate
|
|
if amountOut.Value.Cmp(bestAmountOut) > 0 {
|
|
bestAmountOut = amountOut.Value
|
|
bestPoolData = poolData
|
|
bestExchangeType = poolData.ExchangeType
|
|
}
|
|
}
|
|
}
|
|
|
|
if bestPoolData == nil {
|
|
return nil, "", fmt.Errorf("no pools found for token pair")
|
|
}
|
|
|
|
return bestPoolData, bestExchangeType, nil
|
|
}
|
|
|
|
// GetPoolPrice calculates the best price of token1 in terms of token0 across exchanges
|
|
func (f *DexAggregatorLiquidityFetcher) GetPoolPrice(poolAddress common.Address) (*big.Float, error) {
|
|
// For an aggregator, we'd need to look up which exchange the pool belongs to
|
|
// For this simplified version, return an error
|
|
return nil, fmt.Errorf("GetPoolPrice not directly supported for aggregator, use GetBestPoolPrice instead")
|
|
}
|
|
|
|
// GetBestPoolPrice calculates the best price across all exchanges for a token pair
|
|
func (f *DexAggregatorLiquidityFetcher) GetBestPoolPrice(token0, token1 common.Address) (*big.Float, math.ExchangeType, error) {
|
|
poolData, exchangeType, err := f.GetBestPoolData(token0, token1)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
pricer, err := f.engine.GetExchangePricer(poolData.ExchangeType)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
spotPrice, err := pricer.GetSpotPrice(poolData)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
// Convert the UniversalDecimal Value to a *big.Float
|
|
result := new(big.Float).SetInt(spotPrice.Value)
|
|
return result, exchangeType, nil
|
|
}
|
|
|
|
// GetLiquidityDepth calculates the liquidity depth across all exchanges
|
|
func (f *DexAggregatorLiquidityFetcher) GetLiquidityDepth(poolAddress, tokenIn common.Address, amount *big.Int) (*big.Int, error) {
|
|
// This would aggregate liquidity across exchanges
|
|
// For now, return placeholder
|
|
return amount, nil
|
|
}
|
|
|
|
// DexAggregatorSwapRouter implements SwapRouter for DEX aggregators
|
|
type DexAggregatorSwapRouter struct {
|
|
client *ethclient.Client
|
|
logger *logger.Logger
|
|
config *ExchangeConfig
|
|
registry *ExchangeRegistry
|
|
engine *math.ExchangePricingEngine
|
|
}
|
|
|
|
// NewDexAggregatorSwapRouter creates a new DEX aggregator swap router
|
|
func NewDexAggregatorSwapRouter(client *ethclient.Client, logger *logger.Logger, config *ExchangeConfig, registry *ExchangeRegistry, engine *math.ExchangePricingEngine) *DexAggregatorSwapRouter {
|
|
return &DexAggregatorSwapRouter{
|
|
client: client,
|
|
logger: logger,
|
|
config: config,
|
|
registry: registry,
|
|
engine: engine,
|
|
}
|
|
}
|
|
|
|
// CalculateSwap calculates the expected best output amount across all exchanges
|
|
func (r *DexAggregatorSwapRouter) CalculateSwap(tokenIn, tokenOut common.Address, amountIn *big.Int) (*big.Int, error) {
|
|
// Get the best available rate across all exchanges
|
|
exchanges := r.registry.GetExchangesForPair(tokenIn, tokenOut)
|
|
bestAmountOut := big.NewInt(0)
|
|
|
|
for _, exchangeConfig := range exchanges {
|
|
// Get the swap router for this exchange
|
|
swapRouter := r.registry.GetSwapRouter(exchangeConfig.Type)
|
|
if swapRouter != nil {
|
|
amountOut, err := swapRouter.CalculateSwap(tokenIn, tokenOut, amountIn)
|
|
if err != nil {
|
|
r.logger.Warn(fmt.Sprintf("Error calculating swap on exchange %s: %v", exchangeConfig.Name, err))
|
|
continue
|
|
}
|
|
|
|
// Check if this is the best rate
|
|
if amountOut.Cmp(bestAmountOut) > 0 {
|
|
bestAmountOut = amountOut
|
|
}
|
|
}
|
|
}
|
|
|
|
if bestAmountOut.Sign() <= 0 {
|
|
return nil, fmt.Errorf("no favorable swap route found across exchanges")
|
|
}
|
|
|
|
return bestAmountOut, nil
|
|
}
|
|
|
|
// CalculateMultiSwap calculates the output amount considering multiple exchanges and routing
|
|
func (r *DexAggregatorSwapRouter) CalculateMultiSwap(tokenIn, tokenOut common.Address, amountIn *big.Int) (*big.Int, []math.ExchangeType, error) {
|
|
// Find best path across exchanges
|
|
exchanges := r.registry.GetExchangesForPair(tokenIn, tokenOut)
|
|
bestAmountOut := big.NewInt(0)
|
|
var bestRoutes []math.ExchangeType
|
|
|
|
for _, exchangeConfig := range exchanges {
|
|
// Get the swap router for this exchange
|
|
swapRouter := r.registry.GetSwapRouter(exchangeConfig.Type)
|
|
if swapRouter != nil {
|
|
amountOut, err := swapRouter.CalculateSwap(tokenIn, tokenOut, amountIn)
|
|
if err != nil {
|
|
r.logger.Warn(fmt.Sprintf("Error calculating swap on exchange %s: %v", exchangeConfig.Name, err))
|
|
continue
|
|
}
|
|
|
|
// Check if this is the best rate
|
|
if amountOut.Cmp(bestAmountOut) > 0 {
|
|
bestAmountOut = amountOut
|
|
bestRoutes = []math.ExchangeType{exchangeConfig.Type}
|
|
}
|
|
}
|
|
}
|
|
|
|
if bestAmountOut.Sign() <= 0 {
|
|
return nil, nil, fmt.Errorf("no favorable swap route found across exchanges")
|
|
}
|
|
|
|
return bestAmountOut, bestRoutes, nil
|
|
}
|
|
|
|
// GenerateSwapData generates the calldata for an aggregated swap transaction
|
|
func (r *DexAggregatorSwapRouter) GenerateSwapData(tokenIn, tokenOut common.Address, amountIn, minAmountOut *big.Int, deadline *big.Int) ([]byte, error) {
|
|
// In a real implementation, this would generate data for a complex multi-exchange swap
|
|
// For now, return placeholder
|
|
return []byte{}, nil
|
|
}
|
|
|
|
// GetSwapRoute returns the best route for a swap across all exchanges
|
|
func (r *DexAggregatorSwapRouter) GetSwapRoute(tokenIn, tokenOut common.Address) ([]common.Address, error) {
|
|
// Find the best exchange for this swap
|
|
exchanges := r.registry.GetExchangesForPair(tokenIn, tokenOut)
|
|
if len(exchanges) == 0 {
|
|
return nil, fmt.Errorf("no exchanges support this token pair")
|
|
}
|
|
|
|
// For this simplified implementation, return a direct route
|
|
return []common.Address{tokenIn, tokenOut}, nil
|
|
}
|
|
|
|
// GetBestSwapRoute returns the best route considering multiple exchanges
|
|
func (r *DexAggregatorSwapRouter) GetBestSwapRoute(tokenIn, tokenOut common.Address) ([]common.Address, math.ExchangeType, error) {
|
|
// Find the best exchange for this swap
|
|
exchanges := r.registry.GetExchangesForPair(tokenIn, tokenOut)
|
|
bestAmountOut := big.NewInt(0)
|
|
var bestExchange math.ExchangeType
|
|
|
|
for _, exchangeConfig := range exchanges {
|
|
// Get the swap router for this exchange
|
|
swapRouter := r.registry.GetSwapRouter(exchangeConfig.Type)
|
|
if swapRouter != nil {
|
|
amountOut, err := swapRouter.CalculateSwap(tokenIn, tokenOut, big.NewInt(1000000000000000000)) // Use 1 ETH equivalent to compare
|
|
if err != nil {
|
|
r.logger.Warn(fmt.Sprintf("Error calculating swap on exchange %s: %v", exchangeConfig.Name, err))
|
|
continue
|
|
}
|
|
|
|
// Check if this is the best rate
|
|
if amountOut.Cmp(bestAmountOut) > 0 {
|
|
bestAmountOut = amountOut
|
|
bestExchange = exchangeConfig.Type
|
|
}
|
|
}
|
|
}
|
|
|
|
if bestAmountOut.Sign() <= 0 {
|
|
return nil, "", fmt.Errorf("no favorable swap route found across exchanges")
|
|
}
|
|
|
|
// Return the best route for the exchange type
|
|
return []common.Address{tokenIn, tokenOut}, bestExchange, nil
|
|
}
|
|
|
|
// ValidateSwap validates a swap across exchanges before execution
|
|
func (r *DexAggregatorSwapRouter) ValidateSwap(tokenIn, tokenOut common.Address, amountIn *big.Int) error {
|
|
if amountIn.Sign() <= 0 {
|
|
return fmt.Errorf("amountIn must be positive")
|
|
}
|
|
|
|
if tokenIn == tokenOut {
|
|
return fmt.Errorf("tokenIn and tokenOut cannot be the same")
|
|
}
|
|
|
|
if tokenIn == common.HexToAddress("0x0") || tokenOut == common.HexToAddress("0x0") {
|
|
return fmt.Errorf("invalid token addresses")
|
|
}
|
|
|
|
// Check if any exchange supports this pair
|
|
if !r.registry.IsPairSupported(tokenIn, tokenOut) {
|
|
return fmt.Errorf("no exchange supports this token pair")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RegisterDexAggregatorWithRegistry registers DEX aggregator implementation with the exchange registry
|
|
func RegisterDexAggregatorWithRegistry(registry *ExchangeRegistry) error {
|
|
// Note: For a complete implementation, we would need to define a specific type for aggregators
|
|
// For now, we're not registering it as it needs a dedicated ExchangeType
|
|
|
|
// config := &ExchangeConfig{
|
|
// Type: math.ExchangeUniswapV2, // Using a placeholder type for the aggregator
|
|
// Name: "DEX Aggregator",
|
|
// FactoryAddress: common.HexToAddress("0x0"), // Aggregators don't have a factory address
|
|
// RouterAddress: common.HexToAddress("0x0"), // Would actually be aggregator contract address
|
|
// PoolInitCodeHash: "",
|
|
// SwapSelector: []byte{0x00, 0x00, 0x00, 0x00}, // Placeholder
|
|
// StableSwapSelector: []byte{0x00, 0x00, 0x00, 0x00}, // Placeholder
|
|
// ChainID: 1, // Ethereum mainnet
|
|
// SupportsFlashSwaps: true,
|
|
// RequiresApproval: true,
|
|
// MaxHops: 5, // Can route through multiple exchanges
|
|
// DefaultSlippagePercent: 0.5,
|
|
// Url: "https://dex.ag",
|
|
// ApiUrl: "https://api.dex.ag",
|
|
// }
|
|
|
|
// Note: For a complete implementation, we would need to define a specific type for aggregators
|
|
// registry.exchanges[math.ExchangeDexAggregator] = config
|
|
|
|
return nil
|
|
}
|