removed the fucking vendor files

This commit is contained in:
Krypto Kajun
2025-09-16 11:05:47 -05:00
parent 42244ab42b
commit bccc122a85
1451 changed files with 48752 additions and 472999 deletions

371
pkg/pools/create2.go Normal file
View File

@@ -0,0 +1,371 @@
package pools
import (
"fmt"
"math/big"
"sort"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"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
}
// 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) *CREATE2Calculator {
calc := &CREATE2Calculator{
logger: logger,
factories: make(map[string]*FactoryConfig),
}
// 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 non-standard pool creation
func (c *CREATE2Calculator) calculateCurvePoolAddress(token0, token1 common.Address, fee uint32) (common.Address, error) {
// Curve uses a different mechanism - often registry-based
// For now, return a placeholder calculation
// In practice, you'd need to:
// 1. Query the Curve registry
// 2. Use Curve's specific pool creation logic
// 3. Handle different Curve pool types (stable, crypto, etc.)
c.logger.Warn("Curve pool address calculation not fully implemented - using placeholder")
// Placeholder calculation using simple hash
data := make([]byte, 0, 48)
data = append(data, token0.Bytes()...)
data = append(data, token1.Bytes()...)
data = append(data, big.NewInt(int64(fee)).Bytes()...)
hash := crypto.Keccak256(data)
var addr common.Address
copy(addr[:], hash[12:])
return addr, nil
}
// 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
}

366
pkg/pools/create2_test.go Normal file
View File

@@ -0,0 +1,366 @@
package pools
import (
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/fraktal/mev-beta/internal/logger"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestNewCREATE2Calculator tests the creation of a new CREATE2 calculator
func TestNewCREATE2Calculator(t *testing.T) {
logger := logger.New("info", "text", "")
calc := NewCREATE2Calculator(logger)
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", "")
calc := NewCREATE2Calculator(logger)
// 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)
// 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)
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)
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)
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)
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)
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)
// 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)
// 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)
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)
// 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)
}

View File

@@ -1,6 +1,7 @@
package pools
import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
@@ -10,8 +11,11 @@ import (
"sync"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/rpc"
"github.com/fraktal/mev-beta/internal/logger"
"github.com/fraktal/mev-beta/pkg/uniswap"
)
// Pool represents a discovered liquidity pool
@@ -28,6 +32,8 @@ type Pool struct {
LastUpdated time.Time `json:"lastUpdated"`
TotalVolume *big.Int `json:"totalVolume"`
SwapCount uint64 `json:"swapCount"`
CreatedAt time.Time `json:"createdAt"`
BlockNumber uint64 `json:"blockNumber"`
}
// Exchange represents a discovered exchange/DEX
@@ -70,8 +76,9 @@ type SwapEvent struct {
// PoolDiscovery manages dynamic pool and exchange discovery
type PoolDiscovery struct {
client *rpc.Client
logger *logger.Logger
client *rpc.Client
logger *logger.Logger
create2Calculator *CREATE2Calculator
// Storage
pools map[string]*Pool // address -> pool
@@ -98,6 +105,7 @@ func NewPoolDiscovery(rpcClient *rpc.Client, logger *logger.Logger) *PoolDiscove
pd := &PoolDiscovery{
client: rpcClient,
logger: logger,
create2Calculator: NewCREATE2Calculator(logger),
pools: make(map[string]*Pool),
exchanges: make(map[string]*Exchange),
poolsFile: "data/pools.json",
@@ -441,15 +449,226 @@ func (pd *PoolDiscovery) addressFromTopic(topic interface{}) string {
}
func (pd *PoolDiscovery) handleLiquidityEvent(poolAddress string, topics []interface{}, logData map[string]interface{}, txHash, eventType string) {
// Implementation for liquidity events
pool, exists := pd.pools[strings.ToLower(poolAddress)]
if !exists {
// Try to discover this unknown pool
pd.discoverPoolFromSwap(poolAddress, txHash)
return
}
// Parse liquidity event data
data, ok := logData["data"].(string)
if !ok {
return
}
eventData := pd.parseLiquidityData(data, eventType)
if eventData == nil {
return
}
// Update pool liquidity
if eventType == "Mint" && eventData.AmountIn != nil {
if pool.Liquidity == nil {
pool.Liquidity = big.NewInt(0)
}
pool.Liquidity.Add(pool.Liquidity, eventData.AmountIn)
} else if eventType == "Burn" && eventData.AmountIn != nil {
if pool.Liquidity != nil {
pool.Liquidity.Sub(pool.Liquidity, eventData.AmountIn)
}
}
pool.LastUpdated = time.Now()
// Create liquidity event for potential future use
_ = &LiquidityEvent{
TxHash: txHash,
Pool: poolAddress,
Type: strings.ToLower(eventType),
Amount0: eventData.AmountIn,
Amount1: eventData.AmountOut,
Liquidity: pool.Liquidity,
Timestamp: time.Now(),
BlockNumber: 0, // Would be set by caller
}
pd.logger.Opportunity(txHash, "", poolAddress, "LIQUIDITY_EVENT", pool.Protocol, 0, 0, 0, 0, map[string]interface{}{
"eventType": eventType,
"amount0": eventData.AmountIn.String(),
"amount1": eventData.AmountOut.String(),
"newLiquidity": pool.Liquidity.String(),
})
pd.persistData()
}
func (pd *PoolDiscovery) handleSyncEvent(poolAddress string, topics []interface{}, logData map[string]interface{}, txHash string) {
// Implementation for sync events to update reserves
pool, exists := pd.pools[strings.ToLower(poolAddress)]
if !exists {
pd.discoverPoolFromSwap(poolAddress, txHash)
return
}
// Parse sync event data (Uniswap V2 reserves update)
data, ok := logData["data"].(string)
if !ok {
return
}
syncData := pd.parseSyncData(data)
if syncData == nil {
return
}
// Update pool reserves
pool.Reserves0 = syncData.Reserve0
pool.Reserves1 = syncData.Reserve1
pool.LastUpdated = time.Now()
pd.logger.Debug(fmt.Sprintf("Updated reserves for pool %s: Reserve0=%s, Reserve1=%s",
poolAddress, syncData.Reserve0.String(), syncData.Reserve1.String()))
pd.persistData()
}
func (pd *PoolDiscovery) discoverPoolFromSwap(poolAddress, txHash string) {
// Implementation to discover unknown pools from swap events
// Check if we already know this pool
if _, exists := pd.pools[strings.ToLower(poolAddress)]; exists {
return
}
pd.logger.Info(fmt.Sprintf("Discovering unknown pool from swap: %s", poolAddress))
// Create Ethereum client to query pool contract
// Get RPC endpoint from config or environment
rpcEndpoint := os.Getenv("ARBITRUM_RPC_ENDPOINT")
if rpcEndpoint == "" {
rpcEndpoint = "wss://arbitrum-mainnet.core.chainstack.com/f69d14406bc00700da9b936504e1a870" // fallback
}
client, err := ethclient.Dial(rpcEndpoint)
if err != nil {
pd.logger.Error(fmt.Sprintf("Failed to connect to Ethereum node for pool discovery: %v", err))
return
}
defer client.Close()
address := common.HexToAddress(poolAddress)
// Validate that this is a real pool contract
if !uniswap.IsValidPool(context.Background(), client, address) {
pd.logger.Debug(fmt.Sprintf("Address %s is not a valid pool contract", poolAddress))
return
}
// Create Uniswap V3 pool interface to fetch real data
uniswapPool := uniswap.NewUniswapV3Pool(address, client)
// Fetch pool state with timeout
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
poolState, err := uniswapPool.GetPoolState(ctx)
if err != nil {
pd.logger.Error(fmt.Sprintf("Failed to fetch pool state for %s: %v", poolAddress, err))
return
}
// Determine protocol (could be enhanced to detect different protocols)
protocol := "UniswapV3"
// Try to determine factory address by checking against known factories
factory := ""
for factoryAddr, proto := range pd.knownFactories {
// This is a simplified check - in practice you'd call the pool's factory() function
if proto == protocol {
factory = factoryAddr
break
}
}
// Create pool entry with real data
pool := &Pool{
Address: poolAddress,
Token0: poolState.Token0.Hex(),
Token1: poolState.Token1.Hex(),
Fee: uint32(poolState.Fee),
Protocol: protocol,
Factory: factory,
Liquidity: poolState.Liquidity.ToBig(),
LastUpdated: time.Now(),
TotalVolume: big.NewInt(0),
SwapCount: 0,
}
pd.pools[strings.ToLower(poolAddress)] = pool
pd.logger.Opportunity(txHash, "", poolAddress, "POOL_DISCOVERED", protocol, 0, 0, 0, 0, map[string]interface{}{
"source": "swap_event",
"poolAddress": poolAddress,
"protocol": protocol,
"token0": poolState.Token0.Hex(),
"token1": poolState.Token1.Hex(),
"fee": poolState.Fee,
"liquidity": poolState.Liquidity.String(),
"discoveredAt": time.Now(),
})
pd.persistData()
}
// parseLiquidityData parses liquidity event data
func (pd *PoolDiscovery) parseLiquidityData(data, eventType string) *SwapData {
if len(data) < 2 {
return nil
}
dataBytes, err := hex.DecodeString(data[2:])
if err != nil {
return nil
}
if len(dataBytes) < 64 { // 2 * 32 bytes minimum
return nil
}
amount0 := new(big.Int).SetBytes(dataBytes[0:32])
amount1 := new(big.Int).SetBytes(dataBytes[32:64])
return &SwapData{
AmountIn: amount0,
AmountOut: amount1,
}
}
// SyncData represents reserves from a sync event
type SyncData struct {
Reserve0 *big.Int
Reserve1 *big.Int
}
// parseSyncData parses sync event data (Uniswap V2)
func (pd *PoolDiscovery) parseSyncData(data string) *SyncData {
if len(data) < 2 {
return nil
}
dataBytes, err := hex.DecodeString(data[2:])
if err != nil {
return nil
}
if len(dataBytes) < 64 { // 2 * 32 bytes
return nil
}
reserve0 := new(big.Int).SetBytes(dataBytes[0:32])
reserve1 := new(big.Int).SetBytes(dataBytes[32:64])
return &SyncData{
Reserve0: reserve0,
Reserve1: reserve1,
}
}
// persistData saves pools and exchanges to files
@@ -514,3 +733,187 @@ func (pd *PoolDiscovery) GetAllPools() map[string]*Pool {
}
return pools
}
// DiscoverPoolsForTokenPair uses CREATE2 to discover all possible pools for a token pair
func (pd *PoolDiscovery) DiscoverPoolsForTokenPair(token0, token1 common.Address) ([]*Pool, error) {
// Use CREATE2 calculator to find all possible pool addresses
poolIdentifiers, err := pd.create2Calculator.FindPoolsForTokenPair(token0, token1)
if err != nil {
return nil, fmt.Errorf("failed to calculate pool addresses: %w", err)
}
pools := make([]*Pool, 0)
for _, poolId := range poolIdentifiers {
// Check if pool exists on-chain
exists, err := pd.verifyPoolExists(poolId.PoolAddr)
if err != nil {
pd.logger.Debug(fmt.Sprintf("Failed to verify pool %s: %v", poolId.PoolAddr.Hex(), err))
continue
}
if !exists {
pd.logger.Debug(fmt.Sprintf("Pool %s does not exist on-chain", poolId.PoolAddr.Hex()))
continue
}
// Create pool object
pool := &Pool{
Address: poolId.PoolAddr.Hex(),
Token0: poolId.Token0.Hex(),
Token1: poolId.Token1.Hex(),
Fee: poolId.Fee,
Protocol: poolId.Factory,
Factory: poolId.Factory,
CreatedAt: time.Now(),
}
// Get additional pool data
if err := pd.enrichPoolData(pool); err != nil {
pd.logger.Debug(fmt.Sprintf("Failed to enrich pool data for %s: %v", pool.Address, err))
}
pools = append(pools, pool)
// Add to our cache
pd.addPool(pool)
}
pd.logger.Info(fmt.Sprintf("Discovered %d pools for token pair %s/%s",
len(pools), token0.Hex(), token1.Hex()))
return pools, nil
}
// verifyPoolExists checks if a pool actually exists at the calculated address
func (pd *PoolDiscovery) verifyPoolExists(poolAddr common.Address) (bool, error) {
// Check if there's code at the address
var result string
err := pd.client.Call(&result, "eth_getCode", poolAddr.Hex(), "latest")
if err != nil {
return false, fmt.Errorf("failed to get code: %w", err)
}
// If there's no code, the pool doesn't exist
if result == "0x" || result == "" {
return false, nil
}
return true, nil
}
// enrichPoolData gets additional data about a pool
func (pd *PoolDiscovery) enrichPoolData(pool *Pool) error {
poolAddr := common.HexToAddress(pool.Address)
// For Uniswap V3 pools, get slot0 data
if pool.Protocol == "uniswap_v3" || pool.Protocol == "camelot_v3" {
return pd.enrichUniswapV3PoolData(pool, poolAddr)
}
// For Uniswap V2 style pools, get reserves
if pool.Protocol == "uniswap_v2" || pool.Protocol == "sushiswap" {
return pd.enrichUniswapV2PoolData(pool, poolAddr)
}
return nil
}
// enrichUniswapV3PoolData gets Uniswap V3 specific pool data
func (pd *PoolDiscovery) enrichUniswapV3PoolData(pool *Pool, poolAddr common.Address) error {
// Get slot0 data (price, tick, etc.)
slot0ABI := `[{"inputs":[],"name":"slot0","outputs":[{"internalType":"uint160","name":"sqrtPriceX96","type":"uint160"},{"internalType":"int24","name":"tick","type":"int24"},{"internalType":"uint16","name":"observationIndex","type":"uint16"},{"internalType":"uint16","name":"observationCardinality","type":"uint16"},{"internalType":"uint16","name":"observationCardinalityNext","type":"uint16"},{"internalType":"uint8","name":"feeProtocol","type":"uint8"},{"internalType":"bool","name":"unlocked","type":"bool"}],"stateMutability":"view","type":"function"}]`
contractABI, err := uniswap.ParseABI(slot0ABI)
if err != nil {
return fmt.Errorf("failed to parse slot0 ABI: %w", err)
}
callData, err := contractABI.Pack("slot0")
if err != nil {
return fmt.Errorf("failed to pack slot0 call: %w", err)
}
var result string
err = pd.client.Call(&result, "eth_call", map[string]interface{}{
"to": poolAddr.Hex(),
"data": "0x" + hex.EncodeToString(callData),
}, "latest")
if err != nil {
return fmt.Errorf("slot0 call failed: %w", err)
}
// Decode result
resultBytes, err := hex.DecodeString(strings.TrimPrefix(result, "0x"))
if err != nil {
return fmt.Errorf("failed to decode result: %w", err)
}
if len(resultBytes) == 0 {
return fmt.Errorf("empty result from slot0 call")
}
// Store the fact that this is a valid V3 pool
pool.BlockNumber = 0 // Will be set when we detect the creation event
return nil
}
// enrichUniswapV2PoolData gets Uniswap V2 specific pool data
func (pd *PoolDiscovery) enrichUniswapV2PoolData(pool *Pool, poolAddr common.Address) error {
// Get reserves from getReserves()
reservesABI := `[{"inputs":[],"name":"getReserves","outputs":[{"internalType":"uint112","name":"_reserve0","type":"uint112"},{"internalType":"uint112","name":"_reserve1","type":"uint112"},{"internalType":"uint32","name":"_blockTimestampLast","type":"uint32"}],"stateMutability":"view","type":"function"}]`
contractABI, err := uniswap.ParseABI(reservesABI)
if err != nil {
return fmt.Errorf("failed to parse reserves ABI: %w", err)
}
callData, err := contractABI.Pack("getReserves")
if err != nil {
return fmt.Errorf("failed to pack getReserves call: %w", err)
}
var result string
err = pd.client.Call(&result, "eth_call", map[string]interface{}{
"to": poolAddr.Hex(),
"data": "0x" + hex.EncodeToString(callData),
}, "latest")
if err != nil {
return fmt.Errorf("getReserves call failed: %w", err)
}
// Decode result
resultBytes, err := hex.DecodeString(strings.TrimPrefix(result, "0x"))
if err != nil {
return fmt.Errorf("failed to decode result: %w", err)
}
if len(resultBytes) >= 64 {
// Extract reserves (first 32 bytes for reserve0, second 32 bytes for reserve1)
reserve0 := new(big.Int).SetBytes(resultBytes[:32])
reserve1 := new(big.Int).SetBytes(resultBytes[32:64])
pool.Reserves0 = reserve0
pool.Reserves1 = reserve1
}
return nil
}
// ValidatePoolAddress validates a pool address using CREATE2 calculation
func (pd *PoolDiscovery) ValidatePoolAddress(factoryName string, token0, token1 common.Address, fee uint32, poolAddr common.Address) bool {
return pd.create2Calculator.ValidatePoolAddress(factoryName, token0, token1, fee, poolAddr)
}
// addPool adds a pool to the cache
func (pd *PoolDiscovery) addPool(pool *Pool) {
pd.mutex.Lock()
defer pd.mutex.Unlock()
if pd.pools == nil {
pd.pools = make(map[string]*Pool)
}
pd.pools[pool.Address] = pool
}

322
pkg/pools/discovery_test.go Normal file
View File

@@ -0,0 +1,322 @@
package pools
import (
"math/big"
"testing"
"time"
"github.com/fraktal/mev-beta/internal/logger"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// 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)
}