fix(critical): complete execution pipeline - all blockers fixed and operational
This commit is contained in:
401
pkg/pools/blacklist.go
Normal file
401
pkg/pools/blacklist.go
Normal file
@@ -0,0 +1,401 @@
|
||||
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
|
||||
}
|
||||
@@ -103,6 +103,7 @@ type PoolDiscovery struct {
|
||||
client *rpc.Client
|
||||
logger *logger.Logger
|
||||
create2Calculator *CREATE2Calculator
|
||||
blacklist *PoolBlacklist // Pool blacklist manager
|
||||
|
||||
// Storage
|
||||
pools map[string]*Pool // address -> pool
|
||||
@@ -133,6 +134,7 @@ func NewPoolDiscovery(rpcClient *rpc.Client, logger *logger.Logger) *PoolDiscove
|
||||
client: rpcClient,
|
||||
logger: logger,
|
||||
create2Calculator: NewCREATE2Calculator(logger, ethClient),
|
||||
blacklist: NewPoolBlacklist(logger), // Initialize blacklist
|
||||
pools: make(map[string]*Pool),
|
||||
exchanges: make(map[string]*Exchange),
|
||||
poolsFile: "data/pools.json",
|
||||
@@ -799,6 +801,13 @@ func (pd *PoolDiscovery) GetPoolCount() int {
|
||||
return len(pd.pools)
|
||||
}
|
||||
|
||||
// SavePoolCache saves discovered pools and exchanges to disk
|
||||
func (pd *PoolDiscovery) SavePoolCache() {
|
||||
pd.mutex.RLock()
|
||||
defer pd.mutex.RUnlock()
|
||||
pd.persistData()
|
||||
}
|
||||
|
||||
// GetExchangeCount returns the number of discovered exchanges
|
||||
func (pd *PoolDiscovery) GetExchangeCount() int {
|
||||
pd.mutex.RLock()
|
||||
@@ -806,14 +815,45 @@ func (pd *PoolDiscovery) GetExchangeCount() int {
|
||||
return len(pd.exchanges)
|
||||
}
|
||||
|
||||
// GetPool returns a pool by address
|
||||
// GetPool returns a pool by address (checks blacklist)
|
||||
func (pd *PoolDiscovery) GetPool(address string) (*Pool, bool) {
|
||||
pd.mutex.RLock()
|
||||
defer pd.mutex.RUnlock()
|
||||
|
||||
// Check blacklist first
|
||||
poolAddr := common.HexToAddress(address)
|
||||
if pd.blacklist != nil && pd.blacklist.IsBlacklisted(poolAddr) {
|
||||
pd.logger.Debug(fmt.Sprintf("Pool %s is blacklisted, skipping", address[:10]))
|
||||
return nil, false
|
||||
}
|
||||
|
||||
pool, exists := pd.pools[strings.ToLower(address)]
|
||||
return pool, exists
|
||||
}
|
||||
|
||||
// IsPoolBlacklisted checks if a pool is blacklisted
|
||||
func (pd *PoolDiscovery) IsPoolBlacklisted(address common.Address) bool {
|
||||
if pd.blacklist == nil {
|
||||
return false
|
||||
}
|
||||
return pd.blacklist.IsBlacklisted(address)
|
||||
}
|
||||
|
||||
// RecordPoolFailure records a pool failure for blacklisting
|
||||
func (pd *PoolDiscovery) RecordPoolFailure(poolAddress common.Address, reason string, protocol string, token0, token1 common.Address) {
|
||||
if pd.blacklist != nil {
|
||||
pd.blacklist.RecordFailure(poolAddress, reason, protocol, token0, token1)
|
||||
}
|
||||
}
|
||||
|
||||
// GetBlacklistStats returns blacklist statistics
|
||||
func (pd *PoolDiscovery) GetBlacklistStats() map[string]interface{} {
|
||||
if pd.blacklist == nil {
|
||||
return map[string]interface{}{"status": "blacklist not initialized"}
|
||||
}
|
||||
return pd.blacklist.GetBlacklistStats()
|
||||
}
|
||||
|
||||
// GetAllPools returns all discovered pools
|
||||
func (pd *PoolDiscovery) GetAllPools() map[string]*Pool {
|
||||
pd.mutex.RLock()
|
||||
|
||||
350
pkg/pools/pool_factory.go
Normal file
350
pkg/pools/pool_factory.go
Normal file
@@ -0,0 +1,350 @@
|
||||
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