feat: create v2-prep branch with comprehensive planning
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>
This commit is contained in:
@@ -1,402 +0,0 @@
|
||||
package pools
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
)
|
||||
|
||||
// PoolBlacklist manages a list of pools that consistently fail
|
||||
type PoolBlacklist struct {
|
||||
mu sync.RWMutex
|
||||
logger *logger.Logger
|
||||
blacklist map[common.Address]*BlacklistEntry
|
||||
failureThreshold int
|
||||
failureWindow time.Duration
|
||||
persistFile string
|
||||
}
|
||||
|
||||
// BlacklistEntry represents a blacklisted pool
|
||||
type BlacklistEntry struct {
|
||||
Address common.Address `json:"address"`
|
||||
FailureCount int `json:"failure_count"`
|
||||
LastFailure time.Time `json:"last_failure"`
|
||||
FirstFailure time.Time `json:"first_failure"`
|
||||
FailureReason string `json:"failure_reason"`
|
||||
Protocol string `json:"protocol"`
|
||||
TokenPair [2]common.Address `json:"token_pair"`
|
||||
Permanent bool `json:"permanent"`
|
||||
AddedAt time.Time `json:"added_at"`
|
||||
}
|
||||
|
||||
// NewPoolBlacklist creates a new pool blacklist manager
|
||||
func NewPoolBlacklist(logger *logger.Logger) *PoolBlacklist {
|
||||
pb := &PoolBlacklist{
|
||||
logger: logger,
|
||||
blacklist: make(map[common.Address]*BlacklistEntry),
|
||||
failureThreshold: 5, // Blacklist after 5 failures
|
||||
failureWindow: time.Hour, // Within 1 hour
|
||||
persistFile: "logs/pool_blacklist.json",
|
||||
}
|
||||
|
||||
// Load existing blacklist from file
|
||||
pb.loadFromFile()
|
||||
|
||||
// Start periodic cleanup of old entries
|
||||
go pb.periodicCleanup()
|
||||
|
||||
return pb
|
||||
}
|
||||
|
||||
// RecordFailure records a pool failure and checks if it should be blacklisted
|
||||
func (pb *PoolBlacklist) RecordFailure(poolAddress common.Address, reason string, protocol string, token0, token1 common.Address) {
|
||||
pb.mu.Lock()
|
||||
defer pb.mu.Unlock()
|
||||
|
||||
entry, exists := pb.blacklist[poolAddress]
|
||||
now := time.Now()
|
||||
|
||||
if !exists {
|
||||
// First failure - create new entry but don't blacklist yet
|
||||
entry = &BlacklistEntry{
|
||||
Address: poolAddress,
|
||||
FailureCount: 1,
|
||||
FirstFailure: now,
|
||||
LastFailure: now,
|
||||
FailureReason: reason,
|
||||
Protocol: protocol,
|
||||
TokenPair: [2]common.Address{token0, token1},
|
||||
Permanent: false,
|
||||
AddedAt: now,
|
||||
}
|
||||
pb.blacklist[poolAddress] = entry
|
||||
|
||||
pb.logger.Warn(fmt.Sprintf("🚨 POOL FAILURE [1/%d]: Pool %s (%s) - %s | Tokens: %s/%s",
|
||||
pb.failureThreshold,
|
||||
poolAddress.Hex()[:10],
|
||||
protocol,
|
||||
reason,
|
||||
token0.Hex()[:10],
|
||||
token1.Hex()[:10]))
|
||||
|
||||
pb.logger.Info(fmt.Sprintf("📊 Pool Blacklist Status: %d pools blacklisted, %d monitoring",
|
||||
pb.countPermanentlyBlacklisted(), len(pb.blacklist)))
|
||||
return
|
||||
}
|
||||
|
||||
// Update existing entry
|
||||
entry.FailureCount++
|
||||
entry.LastFailure = now
|
||||
entry.FailureReason = reason
|
||||
|
||||
// Check if we should permanently blacklist
|
||||
if entry.FailureCount >= pb.failureThreshold {
|
||||
if !entry.Permanent {
|
||||
entry.Permanent = true
|
||||
pb.logger.Error(fmt.Sprintf("⛔ POOL BLACKLISTED: %s (%s) after %d failures",
|
||||
poolAddress.Hex(),
|
||||
protocol,
|
||||
entry.FailureCount))
|
||||
|
||||
pb.logger.Error(fmt.Sprintf("📝 Blacklist Details:\n"+
|
||||
" - Pool: %s\n"+
|
||||
" - Protocol: %s\n"+
|
||||
" - Tokens: %s / %s\n"+
|
||||
" - Failures: %d\n"+
|
||||
" - First Failure: %s\n"+
|
||||
" - Last Failure: %s\n"+
|
||||
" - Reason: %s\n"+
|
||||
" - Duration: %s",
|
||||
poolAddress.Hex(),
|
||||
protocol,
|
||||
token0.Hex(),
|
||||
token1.Hex(),
|
||||
entry.FailureCount,
|
||||
entry.FirstFailure.Format("2006-01-02 15:04:05"),
|
||||
entry.LastFailure.Format("2006-01-02 15:04:05"),
|
||||
reason,
|
||||
now.Sub(entry.FirstFailure).String()))
|
||||
|
||||
// Persist to file
|
||||
pb.saveToFile()
|
||||
}
|
||||
} else {
|
||||
pb.logger.Warn(fmt.Sprintf("🚨 POOL FAILURE [%d/%d]: Pool %s (%s) - %s | Tokens: %s/%s",
|
||||
entry.FailureCount,
|
||||
pb.failureThreshold,
|
||||
poolAddress.Hex()[:10],
|
||||
protocol,
|
||||
reason,
|
||||
token0.Hex()[:10],
|
||||
token1.Hex()[:10]))
|
||||
}
|
||||
|
||||
// Log current blacklist statistics
|
||||
if entry.FailureCount%2 == 0 { // Log stats every 2 failures
|
||||
pb.logStatistics()
|
||||
}
|
||||
}
|
||||
|
||||
// IsBlacklisted checks if a pool is blacklisted
|
||||
func (pb *PoolBlacklist) IsBlacklisted(poolAddress common.Address) bool {
|
||||
pb.mu.RLock()
|
||||
defer pb.mu.RUnlock()
|
||||
|
||||
entry, exists := pb.blacklist[poolAddress]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if permanently blacklisted
|
||||
if entry.Permanent {
|
||||
// Log access attempt to blacklisted pool (throttled)
|
||||
if time.Since(entry.LastFailure) > time.Minute {
|
||||
pb.logger.Debug(fmt.Sprintf("⚠️ Skipping blacklisted pool %s (failed %d times, reason: %s)",
|
||||
poolAddress.Hex()[:10],
|
||||
entry.FailureCount,
|
||||
entry.FailureReason))
|
||||
entry.LastFailure = time.Now() // Update to throttle logging
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if within failure window
|
||||
if time.Since(entry.FirstFailure) > pb.failureWindow {
|
||||
// Outside window - reset the entry
|
||||
delete(pb.blacklist, poolAddress)
|
||||
pb.logger.Debug(fmt.Sprintf("🔄 Pool %s removed from monitoring (failure window expired)",
|
||||
poolAddress.Hex()[:10]))
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// GetBlacklistStats returns statistics about the blacklist
|
||||
func (pb *PoolBlacklist) GetBlacklistStats() map[string]interface{} {
|
||||
pb.mu.RLock()
|
||||
defer pb.mu.RUnlock()
|
||||
|
||||
permanentCount := 0
|
||||
temporaryCount := 0
|
||||
totalFailures := 0
|
||||
|
||||
reasonCounts := make(map[string]int)
|
||||
protocolCounts := make(map[string]int)
|
||||
|
||||
for _, entry := range pb.blacklist {
|
||||
if entry.Permanent {
|
||||
permanentCount++
|
||||
} else {
|
||||
temporaryCount++
|
||||
}
|
||||
totalFailures += entry.FailureCount
|
||||
reasonCounts[entry.FailureReason]++
|
||||
protocolCounts[entry.Protocol]++
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"total_entries": len(pb.blacklist),
|
||||
"permanent_blacklist": permanentCount,
|
||||
"temporary_monitor": temporaryCount,
|
||||
"total_failures": totalFailures,
|
||||
"failure_reasons": reasonCounts,
|
||||
"protocols_affected": protocolCounts,
|
||||
}
|
||||
}
|
||||
|
||||
// ClearBlacklist clears the entire blacklist (for testing/recovery)
|
||||
func (pb *PoolBlacklist) ClearBlacklist() {
|
||||
pb.mu.Lock()
|
||||
defer pb.mu.Unlock()
|
||||
|
||||
oldCount := len(pb.blacklist)
|
||||
pb.blacklist = make(map[common.Address]*BlacklistEntry)
|
||||
|
||||
pb.logger.Info(fmt.Sprintf("🔄 Pool blacklist cleared: %d entries removed", oldCount))
|
||||
pb.saveToFile()
|
||||
}
|
||||
|
||||
// RemoveFromBlacklist removes a specific pool from the blacklist
|
||||
func (pb *PoolBlacklist) RemoveFromBlacklist(poolAddress common.Address) {
|
||||
pb.mu.Lock()
|
||||
defer pb.mu.Unlock()
|
||||
|
||||
if entry, exists := pb.blacklist[poolAddress]; exists {
|
||||
delete(pb.blacklist, poolAddress)
|
||||
pb.logger.Info(fmt.Sprintf("✅ Pool %s removed from blacklist (was %s, %d failures)",
|
||||
poolAddress.Hex()[:10],
|
||||
entry.FailureReason,
|
||||
entry.FailureCount))
|
||||
pb.saveToFile()
|
||||
}
|
||||
}
|
||||
|
||||
// periodicCleanup removes old non-permanent entries
|
||||
func (pb *PoolBlacklist) periodicCleanup() {
|
||||
ticker := time.NewTicker(5 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
pb.mu.Lock()
|
||||
now := time.Now()
|
||||
removed := 0
|
||||
|
||||
for addr, entry := range pb.blacklist {
|
||||
// Remove non-permanent entries older than failure window
|
||||
if !entry.Permanent && now.Sub(entry.FirstFailure) > pb.failureWindow {
|
||||
delete(pb.blacklist, addr)
|
||||
removed++
|
||||
}
|
||||
}
|
||||
|
||||
if removed > 0 {
|
||||
pb.logger.Info(fmt.Sprintf("🧹 Pool blacklist cleanup: %d temporary entries removed", removed))
|
||||
pb.saveToFile()
|
||||
}
|
||||
pb.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// saveToFile persists the blacklist to disk
|
||||
func (pb *PoolBlacklist) saveToFile() {
|
||||
data, err := json.MarshalIndent(pb.blacklist, "", " ")
|
||||
if err != nil {
|
||||
pb.logger.Error(fmt.Sprintf("Failed to marshal blacklist: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
err = os.WriteFile(pb.persistFile, data, 0644)
|
||||
if err != nil {
|
||||
pb.logger.Error(fmt.Sprintf("Failed to save blacklist to file: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
pb.logger.Debug(fmt.Sprintf("💾 Pool blacklist saved: %d entries", len(pb.blacklist)))
|
||||
}
|
||||
|
||||
// loadFromFile loads the blacklist from disk
|
||||
func (pb *PoolBlacklist) loadFromFile() {
|
||||
data, err := os.ReadFile(pb.persistFile)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
pb.logger.Error(fmt.Sprintf("Failed to read blacklist file: %v", err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Try to unmarshal as map first (new format)
|
||||
var blacklistMap map[common.Address]*BlacklistEntry
|
||||
err = json.Unmarshal(data, &blacklistMap)
|
||||
if err == nil {
|
||||
pb.blacklist = blacklistMap
|
||||
pb.logger.Info(fmt.Sprintf("📂 Pool blacklist loaded (map format): %d entries (%d permanent)",
|
||||
len(pb.blacklist), pb.countPermanentlyBlacklisted()))
|
||||
return
|
||||
}
|
||||
|
||||
// If that fails, try array format (legacy DataFetcher format)
|
||||
type LegacyBlacklistEntry struct {
|
||||
Address string `json:"address"`
|
||||
FailureCount int `json:"failure_count"`
|
||||
LastFailure time.Time `json:"last_failure"`
|
||||
LastReason string `json:"last_reason"`
|
||||
FirstSeen time.Time `json:"first_seen"`
|
||||
IsBlacklisted bool `json:"is_blacklisted"`
|
||||
BlacklistedAt time.Time `json:"blacklisted_at"`
|
||||
ConsecutiveFails int `json:"consecutive_fails"`
|
||||
}
|
||||
|
||||
var legacyEntries []LegacyBlacklistEntry
|
||||
err = json.Unmarshal(data, &legacyEntries)
|
||||
if err != nil {
|
||||
pb.logger.Error(fmt.Sprintf("Failed to unmarshal blacklist (tried both formats): %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Convert legacy format to new format
|
||||
pb.blacklist = make(map[common.Address]*BlacklistEntry)
|
||||
permanentCount := 0
|
||||
for _, legacy := range legacyEntries {
|
||||
if legacy.IsBlacklisted {
|
||||
addr := common.HexToAddress(legacy.Address)
|
||||
pb.blacklist[addr] = &BlacklistEntry{
|
||||
Address: addr,
|
||||
FailureCount: legacy.FailureCount,
|
||||
LastFailure: legacy.LastFailure,
|
||||
FirstFailure: legacy.FirstSeen,
|
||||
FailureReason: legacy.LastReason,
|
||||
Protocol: "Unknown",
|
||||
TokenPair: [2]common.Address{},
|
||||
Permanent: legacy.IsBlacklisted,
|
||||
AddedAt: legacy.BlacklistedAt,
|
||||
}
|
||||
permanentCount++
|
||||
}
|
||||
}
|
||||
|
||||
pb.logger.Info(fmt.Sprintf("📂 Pool blacklist loaded (legacy format): %d blacklisted from %d total entries",
|
||||
permanentCount, len(legacyEntries)))
|
||||
}
|
||||
|
||||
// countPermanentlyBlacklisted returns the number of permanently blacklisted pools
|
||||
func (pb *PoolBlacklist) countPermanentlyBlacklisted() int {
|
||||
count := 0
|
||||
for _, entry := range pb.blacklist {
|
||||
if entry.Permanent {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// logStatistics logs detailed statistics about the blacklist
|
||||
func (pb *PoolBlacklist) logStatistics() {
|
||||
stats := pb.GetBlacklistStats()
|
||||
|
||||
pb.logger.Info(fmt.Sprintf("📊 Pool Blacklist Statistics:\n"+
|
||||
" - Total Entries: %d\n"+
|
||||
" - Permanent Blacklist: %d\n"+
|
||||
" - Temporary Monitor: %d\n"+
|
||||
" - Total Failures Recorded: %d",
|
||||
stats["total_entries"],
|
||||
stats["permanent_blacklist"],
|
||||
stats["temporary_monitor"],
|
||||
stats["total_failures"]))
|
||||
|
||||
// Log failure reasons
|
||||
if reasons, ok := stats["failure_reasons"].(map[string]int); ok && len(reasons) > 0 {
|
||||
pb.logger.Info("📈 Failure Reasons:")
|
||||
for reason, count := range reasons {
|
||||
pb.logger.Info(fmt.Sprintf(" - %s: %d pools", reason, count))
|
||||
}
|
||||
}
|
||||
|
||||
// Log affected protocols
|
||||
if protocols, ok := stats["protocols_affected"].(map[string]int); ok && len(protocols) > 0 {
|
||||
pb.logger.Info("🔗 Affected Protocols:")
|
||||
for protocol, count := range protocols {
|
||||
pb.logger.Info(fmt.Sprintf(" - %s: %d pools", protocol, count))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetBlacklistedPools returns a list of all blacklisted pool addresses
|
||||
func (pb *PoolBlacklist) GetBlacklistedPools() []common.Address {
|
||||
pb.mu.RLock()
|
||||
defer pb.mu.RUnlock()
|
||||
|
||||
pools := make([]common.Address, 0, len(pb.blacklist))
|
||||
for addr, entry := range pb.blacklist {
|
||||
if entry.Permanent {
|
||||
pools = append(pools, addr)
|
||||
}
|
||||
}
|
||||
return pools
|
||||
}
|
||||
@@ -1,586 +0,0 @@
|
||||
package pools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum"
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
)
|
||||
|
||||
// CREATE2Calculator handles CREATE2 address calculations for various DEX factories
|
||||
type CREATE2Calculator struct {
|
||||
logger *logger.Logger
|
||||
factories map[string]*FactoryConfig
|
||||
ethClient *ethclient.Client
|
||||
curveCache map[string]common.Address // Cache for Curve pool addresses
|
||||
}
|
||||
|
||||
// FactoryConfig contains the configuration for a DEX factory
|
||||
type FactoryConfig struct {
|
||||
Name string // Factory name (e.g., "uniswap_v3", "sushiswap")
|
||||
Address common.Address // Factory contract address
|
||||
InitCodeHash common.Hash // Init code hash for CREATE2 calculation
|
||||
FeeStructure FeeStructure // How fees are encoded
|
||||
SortTokens bool // Whether tokens should be sorted
|
||||
}
|
||||
|
||||
// FeeStructure defines how fees are handled in address calculation
|
||||
type FeeStructure struct {
|
||||
HasFee bool // Whether fee is part of salt
|
||||
FeePositions []int // Byte positions where fee is encoded
|
||||
DefaultFees []uint32 // Default fee tiers
|
||||
}
|
||||
|
||||
// PoolIdentifier uniquely identifies a pool
|
||||
type PoolIdentifier struct {
|
||||
Factory string // Factory name
|
||||
Token0 common.Address // First token (lower address if sorted)
|
||||
Token1 common.Address // Second token (higher address if sorted)
|
||||
Fee uint32 // Fee tier
|
||||
PoolAddr common.Address // Calculated pool address
|
||||
}
|
||||
|
||||
// NewCREATE2Calculator creates a new CREATE2 calculator
|
||||
func NewCREATE2Calculator(logger *logger.Logger, ethClient *ethclient.Client) *CREATE2Calculator {
|
||||
calc := &CREATE2Calculator{
|
||||
logger: logger,
|
||||
factories: make(map[string]*FactoryConfig),
|
||||
ethClient: ethClient,
|
||||
curveCache: make(map[string]common.Address),
|
||||
}
|
||||
|
||||
// Initialize with known factory configurations
|
||||
calc.initializeFactories()
|
||||
|
||||
return calc
|
||||
}
|
||||
|
||||
// initializeFactories sets up configurations for known DEX factories
|
||||
func (c *CREATE2Calculator) initializeFactories() {
|
||||
// Uniswap V3 Factory
|
||||
c.factories["uniswap_v3"] = &FactoryConfig{
|
||||
Name: "uniswap_v3",
|
||||
Address: common.HexToAddress("0x1F98431c8aD98523631AE4a59f267346ea31F984"),
|
||||
InitCodeHash: common.HexToHash("0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54"),
|
||||
FeeStructure: FeeStructure{
|
||||
HasFee: true,
|
||||
DefaultFees: []uint32{500, 3000, 10000}, // 0.05%, 0.3%, 1%
|
||||
},
|
||||
SortTokens: true,
|
||||
}
|
||||
|
||||
// Uniswap V2 Factory
|
||||
c.factories["uniswap_v2"] = &FactoryConfig{
|
||||
Name: "uniswap_v2",
|
||||
Address: common.HexToAddress("0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f"),
|
||||
InitCodeHash: common.HexToHash("0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f"),
|
||||
FeeStructure: FeeStructure{
|
||||
HasFee: false,
|
||||
DefaultFees: []uint32{3000}, // Fixed 0.3%
|
||||
},
|
||||
SortTokens: true,
|
||||
}
|
||||
|
||||
// SushiSwap Factory (same as Uniswap V2 but different address)
|
||||
c.factories["sushiswap"] = &FactoryConfig{
|
||||
Name: "sushiswap",
|
||||
Address: common.HexToAddress("0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac"),
|
||||
InitCodeHash: common.HexToHash("0xe18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303"),
|
||||
FeeStructure: FeeStructure{
|
||||
HasFee: false,
|
||||
DefaultFees: []uint32{3000}, // Fixed 0.3%
|
||||
},
|
||||
SortTokens: true,
|
||||
}
|
||||
|
||||
// Camelot V3 (Arbitrum-specific)
|
||||
c.factories["camelot_v3"] = &FactoryConfig{
|
||||
Name: "camelot_v3",
|
||||
Address: common.HexToAddress("0x1a3c9B1d2F0529D97f2afC5136Cc23e58f1FD35B"),
|
||||
InitCodeHash: common.HexToHash("0xa856464ae65f7619087bc369daaf7e387dae1e5af69cfa7935850ebf754b04c1"),
|
||||
FeeStructure: FeeStructure{
|
||||
HasFee: true,
|
||||
DefaultFees: []uint32{500, 3000, 10000}, // Similar to Uniswap V3
|
||||
},
|
||||
SortTokens: true,
|
||||
}
|
||||
|
||||
// Curve Factory (simplified - Curve uses different math)
|
||||
c.factories["curve"] = &FactoryConfig{
|
||||
Name: "curve",
|
||||
Address: common.HexToAddress("0xF18056Bbd320E96A48e3Fbf8bC061322531aac99"),
|
||||
InitCodeHash: common.HexToHash("0x00"), // Curve doesn't use standard CREATE2
|
||||
FeeStructure: FeeStructure{
|
||||
HasFee: true,
|
||||
DefaultFees: []uint32{400}, // 0.04% typical
|
||||
},
|
||||
SortTokens: false, // Curve maintains token order
|
||||
}
|
||||
}
|
||||
|
||||
// CalculatePoolAddress calculates the pool address using CREATE2
|
||||
func (c *CREATE2Calculator) CalculatePoolAddress(factoryName string, token0, token1 common.Address, fee uint32) (common.Address, error) {
|
||||
factory, exists := c.factories[factoryName]
|
||||
if !exists {
|
||||
return common.Address{}, fmt.Errorf("unknown factory: %s", factoryName)
|
||||
}
|
||||
|
||||
// Sort tokens if required by the factory
|
||||
if factory.SortTokens {
|
||||
if token0.Big().Cmp(token1.Big()) > 0 {
|
||||
token0, token1 = token1, token0
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate salt based on factory type
|
||||
salt, err := c.calculateSalt(factory, token0, token1, fee)
|
||||
if err != nil {
|
||||
return common.Address{}, fmt.Errorf("failed to calculate salt: %w", err)
|
||||
}
|
||||
|
||||
// Special handling for factories that don't use standard CREATE2
|
||||
if factoryName == "curve" {
|
||||
return c.calculateCurvePoolAddress(token0, token1, fee)
|
||||
}
|
||||
|
||||
// Standard CREATE2 calculation:
|
||||
// address = keccak256(0xff + factory_address + salt + init_code_hash)[12:]
|
||||
|
||||
// Prepare the data for hashing
|
||||
data := make([]byte, 0, 85) // 1 + 20 + 32 + 32 = 85 bytes
|
||||
data = append(data, 0xff) // 1 byte
|
||||
data = append(data, factory.Address.Bytes()...) // 20 bytes
|
||||
data = append(data, salt...) // 32 bytes
|
||||
data = append(data, factory.InitCodeHash.Bytes()...) // 32 bytes
|
||||
|
||||
// Calculate keccak256 hash
|
||||
hash := crypto.Keccak256(data)
|
||||
|
||||
// Take the last 20 bytes as the address
|
||||
var poolAddr common.Address
|
||||
copy(poolAddr[:], hash[12:])
|
||||
|
||||
c.logger.Debug(fmt.Sprintf("Calculated %s pool address: %s for tokens %s/%s fee %d",
|
||||
factoryName, poolAddr.Hex(), token0.Hex(), token1.Hex(), fee))
|
||||
|
||||
return poolAddr, nil
|
||||
}
|
||||
|
||||
// calculateSalt generates the salt for CREATE2 calculation
|
||||
func (c *CREATE2Calculator) calculateSalt(factory *FactoryConfig, token0, token1 common.Address, fee uint32) ([]byte, error) {
|
||||
|
||||
switch factory.Name {
|
||||
case "uniswap_v3", "camelot_v3":
|
||||
// Uniswap V3 salt: keccak256(abi.encode(token0, token1, fee))
|
||||
return c.calculateUniswapV3Salt(token0, token1, fee)
|
||||
|
||||
case "uniswap_v2", "sushiswap":
|
||||
// Uniswap V2 salt: keccak256(abi.encodePacked(token0, token1))
|
||||
return c.calculateUniswapV2Salt(token0, token1)
|
||||
|
||||
default:
|
||||
// Generic salt: keccak256(abi.encode(token0, token1, fee))
|
||||
return c.calculateGenericSalt(token0, token1, fee)
|
||||
}
|
||||
}
|
||||
|
||||
// calculateUniswapV3Salt calculates salt for Uniswap V3 style factories
|
||||
func (c *CREATE2Calculator) calculateUniswapV3Salt(token0, token1 common.Address, fee uint32) ([]byte, error) {
|
||||
// ABI encode: token0 (32 bytes) + token1 (32 bytes) + fee (32 bytes)
|
||||
data := make([]byte, 0, 96)
|
||||
|
||||
// Pad addresses to 32 bytes
|
||||
token0Padded := make([]byte, 32)
|
||||
token1Padded := make([]byte, 32)
|
||||
feePadded := make([]byte, 32)
|
||||
|
||||
copy(token0Padded[12:], token0.Bytes())
|
||||
copy(token1Padded[12:], token1.Bytes())
|
||||
|
||||
// Convert fee to big endian 32 bytes
|
||||
feeBig := big.NewInt(int64(fee))
|
||||
feeBytes := feeBig.Bytes()
|
||||
copy(feePadded[32-len(feeBytes):], feeBytes)
|
||||
|
||||
data = append(data, token0Padded...)
|
||||
data = append(data, token1Padded...)
|
||||
data = append(data, feePadded...)
|
||||
|
||||
hash := crypto.Keccak256(data)
|
||||
return hash, nil
|
||||
}
|
||||
|
||||
// calculateUniswapV2Salt calculates salt for Uniswap V2 style factories
|
||||
func (c *CREATE2Calculator) calculateUniswapV2Salt(token0, token1 common.Address) ([]byte, error) {
|
||||
// ABI encodePacked: token0 (20 bytes) + token1 (20 bytes)
|
||||
data := make([]byte, 0, 40)
|
||||
data = append(data, token0.Bytes()...)
|
||||
data = append(data, token1.Bytes()...)
|
||||
|
||||
hash := crypto.Keccak256(data)
|
||||
return hash, nil
|
||||
}
|
||||
|
||||
// calculateGenericSalt calculates salt for generic factories
|
||||
func (c *CREATE2Calculator) calculateGenericSalt(token0, token1 common.Address, fee uint32) ([]byte, error) {
|
||||
// Similar to Uniswap V3 but may have different encoding
|
||||
return c.calculateUniswapV3Salt(token0, token1, fee)
|
||||
}
|
||||
|
||||
// calculateCurvePoolAddress handles Curve's registry-based pool discovery
|
||||
func (c *CREATE2Calculator) calculateCurvePoolAddress(token0, token1 common.Address, fee uint32) (common.Address, error) {
|
||||
// Curve uses a registry-based system rather than deterministic CREATE2
|
||||
// We need to query multiple Curve registries to find pools
|
||||
if c.ethClient == nil {
|
||||
return common.Address{}, fmt.Errorf("ethereum client not configured for curve registry lookups")
|
||||
}
|
||||
|
||||
// Create cache key
|
||||
cacheKey := fmt.Sprintf("%s-%s-%d", token0.Hex(), token1.Hex(), fee)
|
||||
if cached, exists := c.curveCache[cacheKey]; exists {
|
||||
c.logger.Debug(fmt.Sprintf("Using cached Curve pool address: %s", cached.Hex()))
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Curve registry addresses on Arbitrum
|
||||
registries := []common.Address{
|
||||
common.HexToAddress("0x0000000022D53366457F9d5E68Ec105046FC4383"), // Main Registry
|
||||
common.HexToAddress("0x90E00ACe148ca3b23Ac1bC8C240C2a7Dd9c2d7f5"), // Factory Registry
|
||||
common.HexToAddress("0xF18056Bbd320E96A48e3Fbf8bC061322531aac99"), // Crypto Registry
|
||||
common.HexToAddress("0x7D86446dDb609eD0F5f8684AcF30380a356b2B4c"), // Metapool Factory
|
||||
}
|
||||
|
||||
// Try each registry to find the pool
|
||||
for i, registryAddr := range registries {
|
||||
poolAddr, err := c.queryCurveRegistry(ctx, registryAddr, token0, token1, i)
|
||||
if err != nil {
|
||||
c.logger.Debug(fmt.Sprintf("Registry %s failed: %v", registryAddr.Hex(), err))
|
||||
continue
|
||||
}
|
||||
|
||||
if poolAddr != (common.Address{}) {
|
||||
c.logger.Debug(fmt.Sprintf("Found Curve pool %s in registry %s for tokens %s/%s",
|
||||
poolAddr.Hex(), registryAddr.Hex(), token0.Hex(), token1.Hex()))
|
||||
|
||||
// Cache the result
|
||||
c.curveCache[cacheKey] = poolAddr
|
||||
return poolAddr, nil
|
||||
}
|
||||
}
|
||||
|
||||
// If no pool found in registries, try deterministic calculation for newer Curve factories
|
||||
return c.calculateCurveDeterministicAddress(token0, token1, fee)
|
||||
}
|
||||
|
||||
// FindPoolsForTokenPair finds all possible pools for a token pair across all factories
|
||||
func (c *CREATE2Calculator) FindPoolsForTokenPair(token0, token1 common.Address) ([]*PoolIdentifier, error) {
|
||||
pools := make([]*PoolIdentifier, 0)
|
||||
|
||||
for factoryName, factory := range c.factories {
|
||||
// Sort tokens if required
|
||||
sortedToken0, sortedToken1 := token0, token1
|
||||
if factory.SortTokens && token0.Big().Cmp(token1.Big()) > 0 {
|
||||
sortedToken0, sortedToken1 = token1, token0
|
||||
}
|
||||
|
||||
// Try each default fee tier for this factory
|
||||
for _, fee := range factory.FeeStructure.DefaultFees {
|
||||
poolAddr, err := c.CalculatePoolAddress(factoryName, sortedToken0, sortedToken1, fee)
|
||||
if err != nil {
|
||||
c.logger.Debug(fmt.Sprintf("Failed to calculate pool address for %s: %v", factoryName, err))
|
||||
continue
|
||||
}
|
||||
|
||||
pool := &PoolIdentifier{
|
||||
Factory: factoryName,
|
||||
Token0: sortedToken0,
|
||||
Token1: sortedToken1,
|
||||
Fee: fee,
|
||||
PoolAddr: poolAddr,
|
||||
}
|
||||
|
||||
pools = append(pools, pool)
|
||||
}
|
||||
}
|
||||
|
||||
c.logger.Debug(fmt.Sprintf("Found %d potential pools for tokens %s/%s",
|
||||
len(pools), token0.Hex(), token1.Hex()))
|
||||
|
||||
return pools, nil
|
||||
}
|
||||
|
||||
// ValidatePoolAddress verifies if a calculated address matches an expected address
|
||||
func (c *CREATE2Calculator) ValidatePoolAddress(factoryName string, token0, token1 common.Address, fee uint32, expectedAddr common.Address) bool {
|
||||
calculatedAddr, err := c.CalculatePoolAddress(factoryName, token0, token1, fee)
|
||||
if err != nil {
|
||||
c.logger.Debug(fmt.Sprintf("Validation failed - calculation error: %v", err))
|
||||
return false
|
||||
}
|
||||
|
||||
match := calculatedAddr == expectedAddr
|
||||
c.logger.Debug(fmt.Sprintf("Pool address validation: calculated=%s, expected=%s, match=%v",
|
||||
calculatedAddr.Hex(), expectedAddr.Hex(), match))
|
||||
|
||||
return match
|
||||
}
|
||||
|
||||
// GetFactoryConfig returns the configuration for a specific factory
|
||||
func (c *CREATE2Calculator) GetFactoryConfig(factoryName string) (*FactoryConfig, error) {
|
||||
factory, exists := c.factories[factoryName]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("unknown factory: %s", factoryName)
|
||||
}
|
||||
|
||||
// Return a copy to prevent modification
|
||||
configCopy := *factory
|
||||
return &configCopy, nil
|
||||
}
|
||||
|
||||
// AddCustomFactory adds a custom factory configuration
|
||||
func (c *CREATE2Calculator) AddCustomFactory(config *FactoryConfig) error {
|
||||
if config.Name == "" {
|
||||
return fmt.Errorf("factory name cannot be empty")
|
||||
}
|
||||
|
||||
if config.Address == (common.Address{}) {
|
||||
return fmt.Errorf("factory address cannot be zero")
|
||||
}
|
||||
|
||||
c.factories[config.Name] = config
|
||||
c.logger.Info(fmt.Sprintf("Added custom factory: %s at %s", config.Name, config.Address.Hex()))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListFactories returns the names of all configured factories
|
||||
func (c *CREATE2Calculator) ListFactories() []string {
|
||||
names := make([]string, 0, len(c.factories))
|
||||
for name := range c.factories {
|
||||
names = append(names, name)
|
||||
}
|
||||
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
|
||||
// CalculateInitCodeHash calculates the init code hash for a given bytecode
|
||||
// This is useful when adding new factories
|
||||
func CalculateInitCodeHash(initCode []byte) common.Hash {
|
||||
return crypto.Keccak256Hash(initCode)
|
||||
}
|
||||
|
||||
// VerifyFactorySupport checks if a factory supports CREATE2 pool creation
|
||||
func (c *CREATE2Calculator) VerifyFactorySupport(factoryName string) error {
|
||||
factory, exists := c.factories[factoryName]
|
||||
if !exists {
|
||||
return fmt.Errorf("factory %s not configured", factoryName)
|
||||
}
|
||||
|
||||
// Basic validation
|
||||
if factory.Address == (common.Address{}) {
|
||||
return fmt.Errorf("factory %s has zero address", factoryName)
|
||||
}
|
||||
|
||||
if factory.InitCodeHash == (common.Hash{}) && factoryName != "curve" {
|
||||
return fmt.Errorf("factory %s has zero init code hash", factoryName)
|
||||
}
|
||||
|
||||
if len(factory.FeeStructure.DefaultFees) == 0 {
|
||||
return fmt.Errorf("factory %s has no default fees configured", factoryName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// queryCurveRegistry queries a specific Curve registry for pool information
|
||||
func (c *CREATE2Calculator) queryCurveRegistry(ctx context.Context, registryAddr, token0, token1 common.Address, registryType int) (common.Address, error) {
|
||||
// Different registry types have different interfaces
|
||||
switch registryType {
|
||||
case 0: // Main Registry
|
||||
return c.queryMainCurveRegistry(ctx, registryAddr, token0, token1)
|
||||
case 1: // Factory Registry
|
||||
return c.queryFactoryCurveRegistry(ctx, registryAddr, token0, token1)
|
||||
case 2: // Crypto Registry
|
||||
return c.queryCryptoCurveRegistry(ctx, registryAddr, token0, token1)
|
||||
case 3: // Metapool Factory
|
||||
return c.queryMetapoolCurveRegistry(ctx, registryAddr, token0, token1)
|
||||
default:
|
||||
return common.Address{}, fmt.Errorf("unknown registry type: %d", registryType)
|
||||
}
|
||||
}
|
||||
|
||||
// queryMainCurveRegistry queries the main Curve registry
|
||||
func (c *CREATE2Calculator) queryMainCurveRegistry(ctx context.Context, registryAddr, token0, token1 common.Address) (common.Address, error) {
|
||||
// Query Curve registry using find_pool_for_coins function
|
||||
c.logger.Debug(fmt.Sprintf("Querying main Curve registry %s for tokens %s/%s",
|
||||
registryAddr.Hex(), token0.Hex(), token1.Hex()))
|
||||
|
||||
if c.ethClient == nil {
|
||||
return common.Address{}, fmt.Errorf("ethereum client not configured for curve registry lookups")
|
||||
}
|
||||
|
||||
// Curve registry ABI for find_pool_for_coins function
|
||||
registryABI := `[{"name":"find_pool_for_coins","outputs":[{"type":"address","name":""}],"inputs":[{"type":"address","name":"_from"},{"type":"address","name":"_to"}],"stateMutability":"view","type":"function"}]`
|
||||
|
||||
parsedABI, err := abi.JSON(strings.NewReader(registryABI))
|
||||
if err != nil {
|
||||
return common.Address{}, fmt.Errorf("failed to parse registry ABI: %w", err)
|
||||
}
|
||||
|
||||
// Pack the function call
|
||||
callData, err := parsedABI.Pack("find_pool_for_coins", token0, token1)
|
||||
if err != nil {
|
||||
return common.Address{}, fmt.Errorf("failed to pack registry call: %w", err)
|
||||
}
|
||||
|
||||
// Make the contract call
|
||||
callMsg := ethereum.CallMsg{
|
||||
To: ®istryAddr,
|
||||
Data: callData,
|
||||
}
|
||||
|
||||
result, err := c.ethClient.CallContract(ctx, callMsg, nil)
|
||||
if err != nil {
|
||||
return common.Address{}, fmt.Errorf("registry call failed: %w", err)
|
||||
}
|
||||
|
||||
// Unpack the result
|
||||
var poolAddr common.Address
|
||||
if err := parsedABI.UnpackIntoInterface(&poolAddr, "find_pool_for_coins", result); err != nil {
|
||||
return common.Address{}, fmt.Errorf("failed to unpack result: %w", err)
|
||||
}
|
||||
|
||||
// Check if a valid pool was found
|
||||
if poolAddr == (common.Address{}) {
|
||||
// Try with reversed token order
|
||||
callData, err = parsedABI.Pack("find_pool_for_coins", token1, token0)
|
||||
if err != nil {
|
||||
return common.Address{}, fmt.Errorf("failed to pack reversed registry call: %w", err)
|
||||
}
|
||||
|
||||
callMsg.Data = callData
|
||||
result, err = c.ethClient.CallContract(ctx, callMsg, nil)
|
||||
if err != nil {
|
||||
return common.Address{}, fmt.Errorf("reversed registry call failed: %w", err)
|
||||
}
|
||||
|
||||
if err := parsedABI.UnpackIntoInterface(&poolAddr, "find_pool_for_coins", result); err != nil {
|
||||
return common.Address{}, fmt.Errorf("failed to unpack reversed result: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return poolAddr, nil
|
||||
}
|
||||
|
||||
// queryFactoryCurveRegistry queries the Curve factory registry
|
||||
func (c *CREATE2Calculator) queryFactoryCurveRegistry(ctx context.Context, registryAddr, token0, token1 common.Address) (common.Address, error) {
|
||||
// Factory registry handles newer permissionless pools
|
||||
c.logger.Debug(fmt.Sprintf("Querying factory Curve registry %s for tokens %s/%s",
|
||||
registryAddr.Hex(), token0.Hex(), token1.Hex()))
|
||||
|
||||
// Would implement actual registry query here
|
||||
return common.Address{}, nil
|
||||
}
|
||||
|
||||
// queryCryptoCurveRegistry queries the Curve crypto registry
|
||||
func (c *CREATE2Calculator) queryCryptoCurveRegistry(ctx context.Context, registryAddr, token0, token1 common.Address) (common.Address, error) {
|
||||
// Crypto registry handles volatile asset pools
|
||||
c.logger.Debug(fmt.Sprintf("Querying crypto Curve registry %s for tokens %s/%s",
|
||||
registryAddr.Hex(), token0.Hex(), token1.Hex()))
|
||||
|
||||
// Would implement actual registry query here
|
||||
return common.Address{}, nil
|
||||
}
|
||||
|
||||
// queryMetapoolCurveRegistry queries the Curve metapool factory
|
||||
func (c *CREATE2Calculator) queryMetapoolCurveRegistry(ctx context.Context, registryAddr, token0, token1 common.Address) (common.Address, error) {
|
||||
// Metapool factory handles pools paired with base pools
|
||||
c.logger.Debug(fmt.Sprintf("Querying metapool Curve registry %s for tokens %s/%s",
|
||||
registryAddr.Hex(), token0.Hex(), token1.Hex()))
|
||||
|
||||
// Would implement actual registry query here
|
||||
return common.Address{}, nil
|
||||
}
|
||||
|
||||
// calculateCurveDeterministicAddress calculates Curve pool address deterministically for newer factories
|
||||
func (c *CREATE2Calculator) calculateCurveDeterministicAddress(token0, token1 common.Address, fee uint32) (common.Address, error) {
|
||||
// Some newer Curve factories do use deterministic CREATE2
|
||||
// This handles those cases
|
||||
|
||||
c.logger.Debug(fmt.Sprintf("Calculating deterministic Curve address for tokens %s/%s fee %d",
|
||||
token0.Hex(), token1.Hex(), fee))
|
||||
|
||||
// Curve's CREATE2 implementation varies by factory
|
||||
// For stable pools: salt = keccak256(coins, A, fee)
|
||||
// For crypto pools: salt = keccak256(coins, A, gamma, mid_fee, out_fee, allowed_extra_profit, fee_gamma, adjustment_step, admin_fee, ma_half_time, initial_price)
|
||||
|
||||
// Simplified implementation for stable pools
|
||||
coins := []common.Address{token0, token1}
|
||||
if token0.Big().Cmp(token1.Big()) > 0 {
|
||||
coins = []common.Address{token1, token0}
|
||||
}
|
||||
|
||||
// Typical Curve stable pool parameters
|
||||
A := big.NewInt(200) // Amplification parameter
|
||||
feeInt := big.NewInt(int64(fee))
|
||||
|
||||
// Create salt: keccak256(abi.encode(coins, A, fee))
|
||||
saltData := make([]byte, 0, 96) // 2*32 + 32 + 32
|
||||
|
||||
// Encode coins (32 bytes each)
|
||||
coin0Padded := make([]byte, 32)
|
||||
coin1Padded := make([]byte, 32)
|
||||
copy(coin0Padded[12:], coins[0].Bytes())
|
||||
copy(coin1Padded[12:], coins[1].Bytes())
|
||||
|
||||
// Encode A parameter (32 bytes)
|
||||
APadded := make([]byte, 32)
|
||||
ABytes := A.Bytes()
|
||||
copy(APadded[32-len(ABytes):], ABytes)
|
||||
|
||||
// Encode fee (32 bytes)
|
||||
feePadded := make([]byte, 32)
|
||||
feeBytes := feeInt.Bytes()
|
||||
copy(feePadded[32-len(feeBytes):], feeBytes)
|
||||
|
||||
saltData = append(saltData, coin0Padded...)
|
||||
saltData = append(saltData, coin1Padded...)
|
||||
saltData = append(saltData, APadded...)
|
||||
saltData = append(saltData, feePadded...)
|
||||
|
||||
salt := crypto.Keccak256Hash(saltData)
|
||||
|
||||
// Use Curve factory config for CREATE2
|
||||
factory := c.factories["curve"]
|
||||
if factory.InitCodeHash == (common.Hash{}) {
|
||||
// For factories without init code hash, use registry-based approach
|
||||
return common.Address{}, fmt.Errorf("deterministic calculation not supported for this Curve factory")
|
||||
}
|
||||
|
||||
// Standard CREATE2 calculation
|
||||
data := make([]byte, 0, 85)
|
||||
data = append(data, 0xff)
|
||||
data = append(data, factory.Address.Bytes()...)
|
||||
data = append(data, salt.Bytes()...)
|
||||
data = append(data, factory.InitCodeHash.Bytes()...)
|
||||
|
||||
hash := crypto.Keccak256(data)
|
||||
var poolAddr common.Address
|
||||
copy(poolAddr[:], hash[12:])
|
||||
|
||||
c.logger.Debug(fmt.Sprintf("Calculated deterministic Curve pool address: %s", poolAddr.Hex()))
|
||||
return poolAddr, nil
|
||||
}
|
||||
@@ -1,373 +0,0 @@
|
||||
//go:build legacy_pools
|
||||
// +build legacy_pools
|
||||
|
||||
package pools
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
)
|
||||
|
||||
// TestNewCREATE2Calculator tests the creation of a new CREATE2 calculator
|
||||
func TestNewCREATE2Calculator(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
var ethClient *ethclient.Client // nil for testing
|
||||
calc := NewCREATE2Calculator(logger, ethClient)
|
||||
|
||||
require.NotNil(t, calc)
|
||||
assert.NotNil(t, calc.logger)
|
||||
assert.NotNil(t, calc.factories)
|
||||
assert.NotEmpty(t, calc.factories)
|
||||
|
||||
// Check that key factories are initialized
|
||||
assert.Contains(t, calc.factories, "uniswap_v3")
|
||||
assert.Contains(t, calc.factories, "uniswap_v2")
|
||||
assert.Contains(t, calc.factories, "sushiswap")
|
||||
assert.Contains(t, calc.factories, "camelot_v3")
|
||||
assert.Contains(t, calc.factories, "curve")
|
||||
}
|
||||
|
||||
// TestInitializeFactories tests the initialization of factory configurations
|
||||
func TestInitializeFactories(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
var ethClient *ethclient.Client // nil for testing
|
||||
calc := NewCREATE2Calculator(logger, ethClient)
|
||||
|
||||
// Test Uniswap V3 factory configuration
|
||||
uniswapV3, exists := calc.factories["uniswap_v3"]
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, "uniswap_v3", uniswapV3.Name)
|
||||
assert.Equal(t, "0x1F98431c8aD98523631AE4a59f267346ea31F984", uniswapV3.Address.Hex())
|
||||
assert.Equal(t, "0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54", uniswapV3.InitCodeHash.Hex())
|
||||
assert.True(t, uniswapV3.FeeStructure.HasFee)
|
||||
assert.Equal(t, []uint32{500, 3000, 10000}, uniswapV3.FeeStructure.DefaultFees)
|
||||
assert.True(t, uniswapV3.SortTokens)
|
||||
|
||||
// Test Uniswap V2 factory configuration
|
||||
uniswapV2, exists := calc.factories["uniswap_v2"]
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, "uniswap_v2", uniswapV2.Name)
|
||||
assert.Equal(t, "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f", uniswapV2.Address.Hex())
|
||||
assert.Equal(t, "0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f", uniswapV2.InitCodeHash.Hex())
|
||||
assert.False(t, uniswapV2.FeeStructure.HasFee)
|
||||
assert.Equal(t, []uint32{3000}, uniswapV2.FeeStructure.DefaultFees)
|
||||
assert.True(t, uniswapV2.SortTokens)
|
||||
|
||||
// Test SushiSwap factory configuration
|
||||
sushiswap, exists := calc.factories["sushiswap"]
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, "sushiswap", sushiswap.Name)
|
||||
assert.Equal(t, "0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac", sushiswap.Address.Hex())
|
||||
assert.Equal(t, "0xe18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303", sushiswap.InitCodeHash.Hex())
|
||||
assert.False(t, sushiswap.FeeStructure.HasFee)
|
||||
assert.Equal(t, []uint32{3000}, sushiswap.FeeStructure.DefaultFees)
|
||||
assert.True(t, sushiswap.SortTokens)
|
||||
}
|
||||
|
||||
// TestCalculatePoolAddress tests pool address calculation
|
||||
func TestCalculatePoolAddress(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
calc := NewCREATE2Calculator(logger, nil)
|
||||
|
||||
// Test with unknown factory
|
||||
addr, err := calc.CalculatePoolAddress("unknown_factory", common.Address{}, common.Address{}, 3000)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, common.Address{}, addr)
|
||||
assert.Contains(t, err.Error(), "unknown factory")
|
||||
|
||||
// Test with valid Uniswap V3 configuration
|
||||
token0 := common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48") // USDC
|
||||
token1 := common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2") // WETH
|
||||
fee := uint32(3000)
|
||||
|
||||
addr, err = calc.CalculatePoolAddress("uniswap_v3", token0, token1, fee)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEqual(t, common.Address{}, addr)
|
||||
|
||||
// Test with valid Uniswap V2 configuration
|
||||
addr, err = calc.CalculatePoolAddress("uniswap_v2", token0, token1, fee)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEqual(t, common.Address{}, addr)
|
||||
|
||||
// Test token sorting for Uniswap V3 (tokens should be sorted)
|
||||
// When token0 > token1, they should be swapped internally
|
||||
addrSorted, err := calc.CalculatePoolAddress("uniswap_v3", token1, token0, fee) // Swapped order
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Addresses should be the same because tokens are sorted internally
|
||||
assert.Equal(t, addr.Hex(), addrSorted.Hex())
|
||||
|
||||
// Test with SushiSwap
|
||||
addr, err = calc.CalculatePoolAddress("sushiswap", token0, token1, fee)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEqual(t, common.Address{}, addr)
|
||||
}
|
||||
|
||||
// TestCalculateSalt tests salt calculation for different protocols
|
||||
func TestCalculateSalt(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
calc := NewCREATE2Calculator(logger, nil)
|
||||
|
||||
token0 := common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48") // USDC
|
||||
token1 := common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2") // WETH
|
||||
fee := uint32(3000)
|
||||
|
||||
// Test Uniswap V3 salt calculation
|
||||
factory := calc.factories["uniswap_v3"]
|
||||
salt, err := calc.calculateSalt(factory, token0, token1, fee)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, salt)
|
||||
assert.Len(t, salt, 32)
|
||||
|
||||
// Test Uniswap V2 salt calculation
|
||||
factory = calc.factories["uniswap_v2"]
|
||||
salt, err = calc.calculateSalt(factory, token0, token1, fee)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, salt)
|
||||
assert.Len(t, salt, 32)
|
||||
|
||||
// Test generic salt calculation
|
||||
factory = calc.factories["sushiswap"]
|
||||
salt, err = calc.calculateSalt(factory, token0, token1, fee)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, salt)
|
||||
assert.Len(t, salt, 32)
|
||||
}
|
||||
|
||||
// TestCalculateUniswapV3Salt tests Uniswap V3 specific salt calculation
|
||||
func TestCalculateUniswapV3Salt(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
calc := NewCREATE2Calculator(logger, nil)
|
||||
|
||||
token0 := common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48") // USDC
|
||||
token1 := common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2") // WETH
|
||||
fee := uint32(3000)
|
||||
|
||||
salt, err := calc.calculateUniswapV3Salt(token0, token1, fee)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, salt)
|
||||
assert.Len(t, salt, 32)
|
||||
|
||||
// Test with different order (should produce different salt)
|
||||
salt2, err := calc.calculateUniswapV3Salt(token1, token0, fee)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEqual(t, salt, salt2)
|
||||
|
||||
// Test with different fee (should produce different salt)
|
||||
salt3, err := calc.calculateUniswapV3Salt(token0, token1, 500)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEqual(t, salt, salt3)
|
||||
}
|
||||
|
||||
// TestCalculateUniswapV2Salt tests Uniswap V2 specific salt calculation
|
||||
func TestCalculateUniswapV2Salt(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
calc := NewCREATE2Calculator(logger, nil)
|
||||
|
||||
token0 := common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48") // USDC
|
||||
token1 := common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2") // WETH
|
||||
|
||||
salt, err := calc.calculateUniswapV2Salt(token0, token1)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, salt)
|
||||
assert.Len(t, salt, 32)
|
||||
|
||||
// Test with different order (should produce different salt)
|
||||
salt2, err := calc.calculateUniswapV2Salt(token1, token0)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEqual(t, salt, salt2)
|
||||
}
|
||||
|
||||
// TestFindPoolsForTokenPair tests finding pools for a token pair
|
||||
func TestFindPoolsForTokenPair(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
calc := NewCREATE2Calculator(logger, nil)
|
||||
|
||||
token0 := common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48") // USDC
|
||||
token1 := common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2") // WETH
|
||||
|
||||
pools, err := calc.FindPoolsForTokenPair(token0, token1)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, pools)
|
||||
assert.NotEmpty(t, pools)
|
||||
|
||||
// Should find pools for multiple factories
|
||||
assert.True(t, len(pools) >= 3) // At least Uniswap V2, V3, and SushiSwap
|
||||
|
||||
// Check that each pool has required fields
|
||||
for _, pool := range pools {
|
||||
assert.NotEmpty(t, pool.Factory)
|
||||
assert.NotEqual(t, common.Address{}, pool.Token0)
|
||||
assert.NotEqual(t, common.Address{}, pool.Token1)
|
||||
assert.NotEqual(t, uint32(0), pool.Fee)
|
||||
assert.NotEqual(t, common.Address{}, pool.PoolAddr)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidatePoolAddress tests pool address validation
|
||||
func TestValidatePoolAddress(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
calc := NewCREATE2Calculator(logger, nil)
|
||||
|
||||
token0 := common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48") // USDC
|
||||
token1 := common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2") // WETH
|
||||
fee := uint32(3000)
|
||||
|
||||
// Calculate an expected address
|
||||
expectedAddr, err := calc.CalculatePoolAddress("uniswap_v3", token0, token1, fee)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEqual(t, common.Address{}, expectedAddr)
|
||||
|
||||
// Validate the address
|
||||
isValid := calc.ValidatePoolAddress("uniswap_v3", token0, token1, fee, expectedAddr)
|
||||
assert.True(t, isValid)
|
||||
|
||||
// Test with incorrect address
|
||||
wrongAddr := common.HexToAddress("0x1234567890123456789012345678901234567890")
|
||||
isValid = calc.ValidatePoolAddress("uniswap_v3", token0, token1, fee, wrongAddr)
|
||||
assert.False(t, isValid)
|
||||
|
||||
// Test with unknown factory
|
||||
isValid = calc.ValidatePoolAddress("unknown_factory", token0, token1, fee, expectedAddr)
|
||||
assert.False(t, isValid)
|
||||
}
|
||||
|
||||
// TestGetFactoryConfig tests getting factory configuration
|
||||
func TestGetFactoryConfig(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
calc := NewCREATE2Calculator(logger, nil)
|
||||
|
||||
// Test getting existing factory
|
||||
config, err := calc.GetFactoryConfig("uniswap_v3")
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, config)
|
||||
assert.Equal(t, "uniswap_v3", config.Name)
|
||||
assert.Equal(t, "0x1F98431c8aD98523631AE4a59f267346ea31F984", config.Address.Hex())
|
||||
|
||||
// Test getting non-existent factory
|
||||
config, err = calc.GetFactoryConfig("unknown_factory")
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, config)
|
||||
assert.Contains(t, err.Error(), "unknown factory")
|
||||
}
|
||||
|
||||
// TestAddCustomFactory tests adding a custom factory
|
||||
func TestAddCustomFactory(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
calc := NewCREATE2Calculator(logger, nil)
|
||||
|
||||
// Test with invalid config (empty name)
|
||||
invalidConfig := &FactoryConfig{
|
||||
Name: "",
|
||||
Address: common.HexToAddress("0x1234567890123456789012345678901234567890"),
|
||||
InitCodeHash: common.HexToHash("0x1234567890123456789012345678901234567890123456789012345678901234"),
|
||||
FeeStructure: FeeStructure{
|
||||
HasFee: true,
|
||||
DefaultFees: []uint32{1000},
|
||||
},
|
||||
SortTokens: true,
|
||||
}
|
||||
|
||||
err := calc.AddCustomFactory(invalidConfig)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "factory name cannot be empty")
|
||||
|
||||
// Test with invalid config (zero address)
|
||||
invalidConfig2 := &FactoryConfig{
|
||||
Name: "test_factory",
|
||||
Address: common.Address{},
|
||||
InitCodeHash: common.HexToHash("0x1234567890123456789012345678901234567890123456789012345678901234"),
|
||||
FeeStructure: FeeStructure{
|
||||
HasFee: true,
|
||||
DefaultFees: []uint32{1000},
|
||||
},
|
||||
SortTokens: true,
|
||||
}
|
||||
|
||||
err = calc.AddCustomFactory(invalidConfig2)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "factory address cannot be zero")
|
||||
|
||||
// Test with valid config
|
||||
validConfig := &FactoryConfig{
|
||||
Name: "test_factory",
|
||||
Address: common.HexToAddress("0x1234567890123456789012345678901234567890"),
|
||||
InitCodeHash: common.HexToHash("0x1234567890123456789012345678901234567890123456789012345678901234"),
|
||||
FeeStructure: FeeStructure{
|
||||
HasFee: true,
|
||||
DefaultFees: []uint32{1000},
|
||||
},
|
||||
SortTokens: true,
|
||||
}
|
||||
|
||||
err = calc.AddCustomFactory(validConfig)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify the factory was added
|
||||
config, err := calc.GetFactoryConfig("test_factory")
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, config)
|
||||
assert.Equal(t, "test_factory", config.Name)
|
||||
assert.Equal(t, "0x1234567890123456789012345678901234567890", config.Address.Hex())
|
||||
}
|
||||
|
||||
// TestListFactories tests listing all factories
|
||||
func TestListFactories(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
calc := NewCREATE2Calculator(logger, nil)
|
||||
|
||||
factories := calc.ListFactories()
|
||||
assert.NotEmpty(t, factories)
|
||||
assert.Contains(t, factories, "uniswap_v3")
|
||||
assert.Contains(t, factories, "uniswap_v2")
|
||||
assert.Contains(t, factories, "sushiswap")
|
||||
assert.Contains(t, factories, "camelot_v3")
|
||||
assert.Contains(t, factories, "curve")
|
||||
|
||||
// Factories should be sorted
|
||||
sorted := true
|
||||
for i := 1; i < len(factories); i++ {
|
||||
if factories[i-1] > factories[i] {
|
||||
sorted = false
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, sorted)
|
||||
}
|
||||
|
||||
// TestCalculateInitCodeHash tests init code hash calculation
|
||||
func TestCalculateInitCodeHash(t *testing.T) {
|
||||
// Test with empty init code
|
||||
hash := CalculateInitCodeHash([]byte{})
|
||||
assert.Equal(t, "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", hash.Hex())
|
||||
|
||||
// Test with sample init code
|
||||
sampleCode := []byte("hello world")
|
||||
hash = CalculateInitCodeHash(sampleCode)
|
||||
assert.Equal(t, "0x47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad", hash.Hex())
|
||||
}
|
||||
|
||||
// TestVerifyFactorySupport tests factory support verification
|
||||
func TestVerifyFactorySupport(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
calc := NewCREATE2Calculator(logger, nil)
|
||||
|
||||
// Test with non-existent factory
|
||||
err := calc.VerifyFactorySupport("unknown_factory")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "factory unknown_factory not configured")
|
||||
|
||||
// Test with valid factory
|
||||
err = calc.VerifyFactorySupport("uniswap_v3")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test with Curve (special case)
|
||||
err = calc.VerifyFactorySupport("curve")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,326 +0,0 @@
|
||||
//go:build legacy_pools
|
||||
// +build legacy_pools
|
||||
|
||||
package pools
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
)
|
||||
|
||||
// TestNewPoolDiscovery tests the creation of a new PoolDiscovery
|
||||
func TestNewPoolDiscovery(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
|
||||
// Test with nil client (for testing purposes)
|
||||
pd := NewPoolDiscovery(nil, logger)
|
||||
|
||||
require.NotNil(t, pd)
|
||||
assert.NotNil(t, pd.pools)
|
||||
assert.NotNil(t, pd.exchanges)
|
||||
assert.NotNil(t, pd.eventSignatures)
|
||||
assert.NotNil(t, pd.knownFactories)
|
||||
assert.NotNil(t, pd.minLiquidityThreshold)
|
||||
assert.Equal(t, 0.01, pd.priceImpactThreshold)
|
||||
}
|
||||
|
||||
// TestInitializeEventSignatures tests the initialization of event signatures
|
||||
func TestInitializeEventSignatures(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
pd := NewPoolDiscovery(nil, logger)
|
||||
|
||||
// Check that key event signatures are present
|
||||
assert.Contains(t, pd.eventSignatures, "0x0d3648bd0f6ba80134a33ba9275ac585d9d315f0ad8355cddefde31afa28d0e9") // PairCreated
|
||||
assert.Contains(t, pd.eventSignatures, "0x783cca1c0412dd0d695e784568c96da2e9c22ff989357a2e8b1d9b2b4e6b7118") // PoolCreated
|
||||
assert.Contains(t, pd.eventSignatures, "0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822") // Swap
|
||||
}
|
||||
|
||||
// TestInitializeKnownFactories tests the initialization of known factories
|
||||
func TestInitializeKnownFactories(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
pd := NewPoolDiscovery(nil, logger)
|
||||
|
||||
// Check that key factories are present
|
||||
assert.Contains(t, pd.knownFactories, "0xf1d7cc64fb4452f05c498126312ebe29f30fbcf9") // Uniswap V2
|
||||
assert.Contains(t, pd.knownFactories, "0x1f98431c8ad98523631ae4a59f267346ea31f984") // Uniswap V3
|
||||
assert.Contains(t, pd.knownFactories, "0xc35dadb65012ec5796536bd9864ed8773abc74c4") // SushiSwap
|
||||
}
|
||||
|
||||
// TestPoolDiscovery_GetPoolCount tests getting pool count
|
||||
func TestPoolDiscovery_GetPoolCount(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
pd := NewPoolDiscovery(nil, logger)
|
||||
|
||||
// Initially should be zero
|
||||
count := pd.GetPoolCount()
|
||||
assert.Equal(t, 0, count)
|
||||
|
||||
// Add a test pool
|
||||
pool := &Pool{
|
||||
Address: "0x1234567890123456789012345678901234567890",
|
||||
Token0: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
Token1: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
Fee: 3000,
|
||||
Protocol: "UniswapV3",
|
||||
Factory: "0x1f98431c8ad98523631ae4a59f267346ea31f984",
|
||||
LastUpdated: time.Now(),
|
||||
TotalVolume: big.NewInt(0),
|
||||
SwapCount: 0,
|
||||
}
|
||||
|
||||
pd.pools["0x1234567890123456789012345678901234567890"] = pool
|
||||
|
||||
// Should now be one
|
||||
count = pd.GetPoolCount()
|
||||
assert.Equal(t, 1, count)
|
||||
}
|
||||
|
||||
// TestPoolDiscovery_GetExchangeCount tests getting exchange count
|
||||
func TestPoolDiscovery_GetExchangeCount(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
pd := NewPoolDiscovery(nil, logger)
|
||||
|
||||
// Initially should be zero
|
||||
count := pd.GetExchangeCount()
|
||||
assert.Equal(t, 0, count)
|
||||
|
||||
// Add a test exchange
|
||||
exchange := &Exchange{
|
||||
Name: "TestExchange",
|
||||
Router: "0x1234567890123456789012345678901234567890",
|
||||
Factory: "0x1f98431c8ad98523631ae4a59f267346ea31f984",
|
||||
Protocol: "UniswapV3",
|
||||
Version: "1.0",
|
||||
Discovered: time.Now(),
|
||||
PoolCount: 0,
|
||||
TotalVolume: big.NewInt(0),
|
||||
}
|
||||
|
||||
pd.exchanges["0x1234567890123456789012345678901234567890"] = exchange
|
||||
|
||||
// Should now be one
|
||||
count = pd.GetExchangeCount()
|
||||
assert.Equal(t, 1, count)
|
||||
}
|
||||
|
||||
// TestPoolDiscovery_GetPool tests getting a pool by address
|
||||
func TestPoolDiscovery_GetPool(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
pd := NewPoolDiscovery(nil, logger)
|
||||
|
||||
// Test getting non-existent pool
|
||||
pool, exists := pd.GetPool("0x1234567890123456789012345678901234567890")
|
||||
assert.False(t, exists)
|
||||
assert.Nil(t, pool)
|
||||
|
||||
// Add a test pool
|
||||
testPool := &Pool{
|
||||
Address: "0x1234567890123456789012345678901234567890",
|
||||
Token0: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
Token1: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
Fee: 3000,
|
||||
Protocol: "UniswapV3",
|
||||
Factory: "0x1f98431c8ad98523631ae4a59f267346ea31f984",
|
||||
LastUpdated: time.Now(),
|
||||
TotalVolume: big.NewInt(1000000000000000000),
|
||||
SwapCount: 5,
|
||||
}
|
||||
|
||||
pd.pools["0x1234567890123456789012345678901234567890"] = testPool
|
||||
|
||||
// Test getting existing pool
|
||||
pool, exists = pd.GetPool("0x1234567890123456789012345678901234567890")
|
||||
assert.True(t, exists)
|
||||
assert.NotNil(t, pool)
|
||||
assert.Equal(t, "0x1234567890123456789012345678901234567890", pool.Address)
|
||||
assert.Equal(t, "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", pool.Token0)
|
||||
assert.Equal(t, "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", pool.Token1)
|
||||
assert.Equal(t, uint32(3000), pool.Fee)
|
||||
assert.Equal(t, "UniswapV3", pool.Protocol)
|
||||
assert.Equal(t, "0x1f98431c8ad98523631ae4a59f267346ea31f984", pool.Factory)
|
||||
assert.Equal(t, int64(1000000000000000000), pool.TotalVolume.Int64())
|
||||
assert.Equal(t, uint64(5), pool.SwapCount)
|
||||
}
|
||||
|
||||
// TestPoolDiscovery_GetAllPools tests getting all pools
|
||||
func TestPoolDiscovery_GetAllPools(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
pd := NewPoolDiscovery(nil, logger)
|
||||
|
||||
// Test getting all pools when empty
|
||||
pools := pd.GetAllPools()
|
||||
assert.Empty(t, pools)
|
||||
|
||||
// Add test pools
|
||||
pool1 := &Pool{
|
||||
Address: "0x1234567890123456789012345678901234567890",
|
||||
Token0: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
Token1: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
Fee: 3000,
|
||||
Protocol: "UniswapV3",
|
||||
Factory: "0x1f98431c8ad98523631ae4a59f267346ea31f984",
|
||||
LastUpdated: time.Now(),
|
||||
TotalVolume: big.NewInt(1000000000000000000),
|
||||
SwapCount: 5,
|
||||
}
|
||||
|
||||
pool2 := &Pool{
|
||||
Address: "0x0987654321098765432109876543210987654321",
|
||||
Token0: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
Token1: "0x1f98431c8ad98523631ae4a59f267346ea31f984",
|
||||
Fee: 500,
|
||||
Protocol: "UniswapV2",
|
||||
Factory: "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f",
|
||||
LastUpdated: time.Now(),
|
||||
TotalVolume: big.NewInt(2000000000000000000),
|
||||
SwapCount: 10,
|
||||
}
|
||||
|
||||
pd.pools["0x1234567890123456789012345678901234567890"] = pool1
|
||||
pd.pools["0x0987654321098765432109876543210987654321"] = pool2
|
||||
|
||||
// Test getting all pools
|
||||
pools = pd.GetAllPools()
|
||||
assert.Len(t, pools, 2)
|
||||
assert.Contains(t, pools, "0x1234567890123456789012345678901234567890")
|
||||
assert.Contains(t, pools, "0x0987654321098765432109876543210987654321")
|
||||
|
||||
// Verify pool data
|
||||
retrievedPool1 := pools["0x1234567890123456789012345678901234567890"]
|
||||
assert.Equal(t, pool1.Address, retrievedPool1.Address)
|
||||
assert.Equal(t, pool1.Token0, retrievedPool1.Token0)
|
||||
assert.Equal(t, pool1.Token1, retrievedPool1.Token1)
|
||||
assert.Equal(t, pool1.Fee, retrievedPool1.Fee)
|
||||
assert.Equal(t, pool1.Protocol, retrievedPool1.Protocol)
|
||||
assert.Equal(t, pool1.Factory, retrievedPool1.Factory)
|
||||
|
||||
retrievedPool2 := pools["0x0987654321098765432109876543210987654321"]
|
||||
assert.Equal(t, pool2.Address, retrievedPool2.Address)
|
||||
assert.Equal(t, pool2.Token0, retrievedPool2.Token0)
|
||||
assert.Equal(t, pool2.Token1, retrievedPool2.Token1)
|
||||
assert.Equal(t, pool2.Fee, retrievedPool2.Fee)
|
||||
assert.Equal(t, pool2.Protocol, retrievedPool2.Protocol)
|
||||
assert.Equal(t, pool2.Factory, retrievedPool2.Factory)
|
||||
}
|
||||
|
||||
// TestPoolDiscovery_PersistData tests data persistence functionality
|
||||
func TestPoolDiscovery_PersistData(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
pd := NewPoolDiscovery(nil, logger)
|
||||
|
||||
// Add test data
|
||||
pool := &Pool{
|
||||
Address: "0x1234567890123456789012345678901234567890",
|
||||
Token0: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
Token1: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
Fee: 3000,
|
||||
Protocol: "UniswapV3",
|
||||
Factory: "0x1f98431c8ad98523631ae4a59f267346ea31f984",
|
||||
LastUpdated: time.Now(),
|
||||
TotalVolume: big.NewInt(1000000000000000000),
|
||||
SwapCount: 5,
|
||||
}
|
||||
|
||||
pd.pools["0x1234567890123456789012345678901234567890"] = pool
|
||||
|
||||
// Test persistence (this will create files in the data directory)
|
||||
// Note: This test doesn't verify file contents, but ensures the method doesn't panic
|
||||
pd.persistData()
|
||||
|
||||
// Test loading persisted data
|
||||
pd.loadPersistedData()
|
||||
|
||||
// Verify data is still there
|
||||
assert.Len(t, pd.pools, 1)
|
||||
_, exists := pd.pools["0x1234567890123456789012345678901234567890"]
|
||||
assert.True(t, exists)
|
||||
}
|
||||
|
||||
// TestCalculatePriceImpact tests price impact calculation
|
||||
func TestCalculatePriceImpact(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
pd := NewPoolDiscovery(nil, logger)
|
||||
|
||||
// Create a test pool with reserves
|
||||
pool := &Pool{
|
||||
Reserves0: func() *big.Int { val, _ := big.NewInt(0).SetString("10000000000000000000", 10); return val }(), // 10 tokens
|
||||
Reserves1: func() *big.Int { val, _ := big.NewInt(0).SetString("20000000000000000000", 10); return val }(), // 20 tokens
|
||||
}
|
||||
|
||||
// Test with zero reserves (should return 0)
|
||||
emptyPool := &Pool{}
|
||||
priceImpact := pd.calculatePriceImpact(emptyPool, big.NewInt(1000000000000000000), big.NewInt(1000000000000000000))
|
||||
assert.Equal(t, 0.0, priceImpact)
|
||||
|
||||
// Test with valid reserves
|
||||
amountIn := big.NewInt(1000000000000000000) // 1 token
|
||||
amountOut := big.NewInt(1990000000000000000) // ~1.99 tokens (due to 0.5% fee)
|
||||
priceImpact = pd.calculatePriceImpact(pool, amountIn, amountOut)
|
||||
|
||||
// Price impact should be positive (small value due to small trade size)
|
||||
assert.True(t, priceImpact >= 0.0)
|
||||
|
||||
// Test with larger trade
|
||||
largeAmountIn := func() *big.Int { val, _ := big.NewInt(0).SetString("1000000000000000000", 10); return val }() // 1 token
|
||||
largeAmountOut := func() *big.Int { val, _ := big.NewInt(0).SetString("1800000000000000000", 10); return val }() // ~1.8 tokens (larger price impact)
|
||||
priceImpact = pd.calculatePriceImpact(pool, largeAmountIn, largeAmountOut)
|
||||
|
||||
// Larger trade should have larger price impact
|
||||
assert.True(t, priceImpact >= 0.0)
|
||||
}
|
||||
|
||||
// TestParseSwapData tests parsing of swap data
|
||||
func TestParseSwapData(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
pd := NewPoolDiscovery(nil, logger)
|
||||
|
||||
// Test with empty data
|
||||
result := pd.parseSwapData("", "UniswapV2")
|
||||
assert.Nil(t, result)
|
||||
|
||||
// Test with short data
|
||||
result = pd.parseSwapData("0x", "UniswapV2")
|
||||
assert.Nil(t, result)
|
||||
|
||||
// Test parseLiquidityData
|
||||
data := "0x0000000000000000000000000000000000000000000000000000000000000001" +
|
||||
"0000000000000000000000000000000000000000000000000000000000000002"
|
||||
|
||||
result2 := pd.parseLiquidityData(data, "Mint")
|
||||
assert.NotNil(t, result2)
|
||||
assert.Equal(t, int64(1), result2.AmountIn.Int64())
|
||||
assert.Equal(t, int64(2), result2.AmountOut.Int64())
|
||||
|
||||
// Test parseSyncData
|
||||
syncData := "0000000000000000000000000000000000000000000000000000000000000001" +
|
||||
"0000000000000000000000000000000000000000000000000000000000000002"
|
||||
|
||||
result3 := pd.parseSyncData(syncData)
|
||||
assert.NotNil(t, result3)
|
||||
assert.Equal(t, int64(1), result3.Reserve0.Int64())
|
||||
assert.Equal(t, int64(2), result3.Reserve1.Int64())
|
||||
}
|
||||
|
||||
// TestPoolDiscovery_AddressFromTopic tests address extraction from topics
|
||||
func TestPoolDiscovery_AddressFromTopic(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
pd := NewPoolDiscovery(nil, logger)
|
||||
|
||||
// Test with invalid topic
|
||||
result := pd.addressFromTopic(nil)
|
||||
assert.Equal(t, "", result)
|
||||
|
||||
// Test with short topic
|
||||
result = pd.addressFromTopic("0x1234")
|
||||
assert.Equal(t, "", result)
|
||||
|
||||
// Test with valid topic (this function expects the full 32-byte topic)
|
||||
result = pd.addressFromTopic("0x0000000000000000000000001234567890123456789012345678901234567890")
|
||||
assert.Equal(t, "0x1234567890123456789012345678901234567890", result)
|
||||
}
|
||||
@@ -1,350 +0,0 @@
|
||||
package pools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
|
||||
"github.com/fraktal/mev-beta/pkg/bindings/algebra"
|
||||
"github.com/fraktal/mev-beta/pkg/bindings/algebraintegral"
|
||||
"github.com/fraktal/mev-beta/pkg/bindings/uniswapv2"
|
||||
"github.com/fraktal/mev-beta/pkg/bindings/uniswapv3"
|
||||
"github.com/fraktal/mev-beta/pkg/dex"
|
||||
)
|
||||
|
||||
// PoolBinding is a unified interface for all pool bindings
|
||||
type PoolBinding interface {
|
||||
Token0() (common.Address, error)
|
||||
Token1() (common.Address, error)
|
||||
GetReserves() (*big.Int, *big.Int, error)
|
||||
GetFee() (*big.Int, error)
|
||||
GetLiquidity() (*big.Int, error)
|
||||
Address() common.Address
|
||||
Type() string
|
||||
}
|
||||
|
||||
// UniswapV2Binding wraps the UniswapV2 pair binding
|
||||
type UniswapV2Binding struct {
|
||||
pool *uniswapv2.UniswapV2Pair
|
||||
address common.Address
|
||||
}
|
||||
|
||||
func (b *UniswapV2Binding) Token0() (common.Address, error) {
|
||||
return b.pool.Token0(&bind.CallOpts{})
|
||||
}
|
||||
|
||||
func (b *UniswapV2Binding) Token1() (common.Address, error) {
|
||||
return b.pool.Token1(&bind.CallOpts{})
|
||||
}
|
||||
|
||||
func (b *UniswapV2Binding) GetReserves() (*big.Int, *big.Int, error) {
|
||||
reserves, err := b.pool.GetReserves(&bind.CallOpts{})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return reserves.Reserve0, reserves.Reserve1, nil
|
||||
}
|
||||
|
||||
func (b *UniswapV2Binding) GetFee() (*big.Int, error) {
|
||||
// UniswapV2 has fixed 0.3% fee (3/1000)
|
||||
return big.NewInt(3), nil
|
||||
}
|
||||
|
||||
func (b *UniswapV2Binding) GetLiquidity() (*big.Int, error) {
|
||||
reserve0, reserve1, err := b.GetReserves()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Return sum of reserves as liquidity indicator
|
||||
total := new(big.Int).Add(reserve0, reserve1)
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (b *UniswapV2Binding) Address() common.Address {
|
||||
return b.address
|
||||
}
|
||||
|
||||
func (b *UniswapV2Binding) Type() string {
|
||||
return "UniswapV2"
|
||||
}
|
||||
|
||||
// UniswapV3Binding wraps the UniswapV3 pool binding
|
||||
type UniswapV3Binding struct {
|
||||
pool *uniswapv3.UniswapV3Pool
|
||||
address common.Address
|
||||
}
|
||||
|
||||
func (b *UniswapV3Binding) Token0() (common.Address, error) {
|
||||
return b.pool.Token0(&bind.CallOpts{})
|
||||
}
|
||||
|
||||
func (b *UniswapV3Binding) Token1() (common.Address, error) {
|
||||
return b.pool.Token1(&bind.CallOpts{})
|
||||
}
|
||||
|
||||
func (b *UniswapV3Binding) GetReserves() (*big.Int, *big.Int, error) {
|
||||
// V3 doesn't have simple reserves, use liquidity
|
||||
liquidity, err := b.pool.Liquidity(&bind.CallOpts{})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
// Return liquidity as both reserves for compatibility
|
||||
return liquidity, liquidity, nil
|
||||
}
|
||||
|
||||
func (b *UniswapV3Binding) GetFee() (*big.Int, error) {
|
||||
fee, err := b.pool.Fee(&bind.CallOpts{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Fee is already *big.Int from the binding
|
||||
return fee, nil
|
||||
}
|
||||
|
||||
func (b *UniswapV3Binding) GetLiquidity() (*big.Int, error) {
|
||||
liquidity, err := b.pool.Liquidity(&bind.CallOpts{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// liquidity is already *big.Int from the binding
|
||||
return liquidity, nil
|
||||
}
|
||||
|
||||
func (b *UniswapV3Binding) Address() common.Address {
|
||||
return b.address
|
||||
}
|
||||
|
||||
func (b *UniswapV3Binding) Type() string {
|
||||
return "UniswapV3"
|
||||
}
|
||||
|
||||
// AlgebraBinding wraps the Algebra pool binding
|
||||
type AlgebraBinding struct {
|
||||
pool *algebra.AlgebraPool
|
||||
address common.Address
|
||||
}
|
||||
|
||||
func (b *AlgebraBinding) Token0() (common.Address, error) {
|
||||
return b.pool.Token0(&bind.CallOpts{})
|
||||
}
|
||||
|
||||
func (b *AlgebraBinding) Token1() (common.Address, error) {
|
||||
return b.pool.Token1(&bind.CallOpts{})
|
||||
}
|
||||
|
||||
func (b *AlgebraBinding) GetReserves() (*big.Int, *big.Int, error) {
|
||||
// Algebra doesn't have simple reserves, use liquidity
|
||||
liquidity, err := b.pool.Liquidity(&bind.CallOpts{})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return liquidity, liquidity, nil
|
||||
}
|
||||
|
||||
func (b *AlgebraBinding) GetFee() (*big.Int, error) {
|
||||
// Algebra has dynamic fees, get from globalState
|
||||
state, err := b.pool.GlobalState(&bind.CallOpts{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return big.NewInt(int64(state.Fee)), nil
|
||||
}
|
||||
|
||||
func (b *AlgebraBinding) GetLiquidity() (*big.Int, error) {
|
||||
return b.pool.Liquidity(&bind.CallOpts{})
|
||||
}
|
||||
|
||||
func (b *AlgebraBinding) Address() common.Address {
|
||||
return b.address
|
||||
}
|
||||
|
||||
func (b *AlgebraBinding) Type() string {
|
||||
return "Algebra"
|
||||
}
|
||||
|
||||
// AlgebraIntegralBinding wraps the Algebra Integral pool binding
|
||||
type AlgebraIntegralBinding struct {
|
||||
pool *algebraintegral.AlgebraIntegralPool
|
||||
address common.Address
|
||||
}
|
||||
|
||||
func (b *AlgebraIntegralBinding) Token0() (common.Address, error) {
|
||||
return b.pool.Token0(&bind.CallOpts{})
|
||||
}
|
||||
|
||||
func (b *AlgebraIntegralBinding) Token1() (common.Address, error) {
|
||||
return b.pool.Token1(&bind.CallOpts{})
|
||||
}
|
||||
|
||||
func (b *AlgebraIntegralBinding) GetReserves() (*big.Int, *big.Int, error) {
|
||||
// Algebra Integral doesn't have simple reserves, use liquidity
|
||||
liquidity, err := b.pool.Liquidity(&bind.CallOpts{})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return liquidity, liquidity, nil
|
||||
}
|
||||
|
||||
func (b *AlgebraIntegralBinding) GetFee() (*big.Int, error) {
|
||||
// Algebra Integral has dynamic fees, get from globalState
|
||||
state, err := b.pool.GlobalState(&bind.CallOpts{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Extract fee from the state struct
|
||||
return big.NewInt(int64(state.LastFee)), nil
|
||||
}
|
||||
|
||||
func (b *AlgebraIntegralBinding) GetLiquidity() (*big.Int, error) {
|
||||
return b.pool.Liquidity(&bind.CallOpts{})
|
||||
}
|
||||
|
||||
func (b *AlgebraIntegralBinding) Address() common.Address {
|
||||
return b.address
|
||||
}
|
||||
|
||||
func (b *AlgebraIntegralBinding) Type() string {
|
||||
return "AlgebraIntegral"
|
||||
}
|
||||
|
||||
// PoolFactory creates pool bindings based on detected type
|
||||
type PoolFactory struct {
|
||||
client *ethclient.Client
|
||||
detector *dex.PoolDetector
|
||||
}
|
||||
|
||||
// NewPoolFactory creates a new pool factory
|
||||
func NewPoolFactory(client *ethclient.Client) *PoolFactory {
|
||||
return &PoolFactory{
|
||||
client: client,
|
||||
detector: dex.NewPoolDetector(client),
|
||||
}
|
||||
}
|
||||
|
||||
// CreateBinding creates the appropriate binding for a pool address
|
||||
func (f *PoolFactory) CreateBinding(ctx context.Context, poolAddress common.Address) (PoolBinding, error) {
|
||||
// First detect the pool type
|
||||
poolInfo, err := f.detector.DetectPoolType(ctx, poolAddress)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to detect pool type: %w", err)
|
||||
}
|
||||
|
||||
// Create the appropriate binding based on type
|
||||
poolType := string(poolInfo.Type)
|
||||
switch poolType {
|
||||
case "uniswap_v2", "sushiswap_v2", "camelot_v2", "traderjoe_v2", "pancake_v2":
|
||||
pool, err := uniswapv2.NewUniswapV2Pair(poolAddress, f.client)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create UniswapV2 binding: %w", err)
|
||||
}
|
||||
return &UniswapV2Binding{
|
||||
pool: pool,
|
||||
address: poolAddress,
|
||||
}, nil
|
||||
|
||||
case "uniswap_v3":
|
||||
pool, err := uniswapv3.NewUniswapV3Pool(poolAddress, f.client)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create UniswapV3 binding: %w", err)
|
||||
}
|
||||
return &UniswapV3Binding{
|
||||
pool: pool,
|
||||
address: poolAddress,
|
||||
}, nil
|
||||
|
||||
case "algebra_v1", "quickswap_v3", "camelot_v3":
|
||||
pool, err := algebra.NewAlgebraPool(poolAddress, f.client)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create Algebra binding: %w", err)
|
||||
}
|
||||
return &AlgebraBinding{
|
||||
pool: pool,
|
||||
address: poolAddress,
|
||||
}, nil
|
||||
|
||||
case "algebra_integral":
|
||||
pool, err := algebraintegral.NewAlgebraIntegralPool(poolAddress, f.client)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create AlgebraIntegral binding: %w", err)
|
||||
}
|
||||
return &AlgebraIntegralBinding{
|
||||
pool: pool,
|
||||
address: poolAddress,
|
||||
}, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported pool type: %s", poolType)
|
||||
}
|
||||
}
|
||||
|
||||
// GetPoolTokens gets the token addresses for a pool using bindings
|
||||
func (f *PoolFactory) GetPoolTokens(ctx context.Context, poolAddress common.Address) (token0, token1 common.Address, err error) {
|
||||
binding, err := f.CreateBinding(ctx, poolAddress)
|
||||
if err != nil {
|
||||
return common.Address{}, common.Address{}, err
|
||||
}
|
||||
|
||||
token0, err = binding.Token0()
|
||||
if err != nil {
|
||||
return common.Address{}, common.Address{}, fmt.Errorf("failed to get token0: %w", err)
|
||||
}
|
||||
|
||||
token1, err = binding.Token1()
|
||||
if err != nil {
|
||||
return common.Address{}, common.Address{}, fmt.Errorf("failed to get token1: %w", err)
|
||||
}
|
||||
|
||||
return token0, token1, nil
|
||||
}
|
||||
|
||||
// GetPoolInfo gets comprehensive pool information using bindings
|
||||
func (f *PoolFactory) GetPoolInfo(ctx context.Context, poolAddress common.Address) (map[string]interface{}, error) {
|
||||
binding, err := f.CreateBinding(ctx, poolAddress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token0, err := binding.Token0()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token1, err := binding.Token1()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fee, err := binding.GetFee()
|
||||
if err != nil {
|
||||
// Non-critical, some pools don't have fees
|
||||
fee = big.NewInt(0)
|
||||
}
|
||||
|
||||
liquidity, err := binding.GetLiquidity()
|
||||
if err != nil {
|
||||
// Non-critical
|
||||
liquidity = big.NewInt(0)
|
||||
}
|
||||
|
||||
reserve0, reserve1, err := binding.GetReserves()
|
||||
if err != nil {
|
||||
// Non-critical for V3 pools
|
||||
reserve0 = big.NewInt(0)
|
||||
reserve1 = big.NewInt(0)
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"address": poolAddress.Hex(),
|
||||
"type": binding.Type(),
|
||||
"token0": token0.Hex(),
|
||||
"token1": token1.Hex(),
|
||||
"fee": fee.String(),
|
||||
"liquidity": liquidity.String(),
|
||||
"reserve0": reserve0.String(),
|
||||
"reserve1": reserve1.String(),
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user