package exchanges import ( "math/big" "testing" "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/assert" "github.com/fraktal/mev-beta/internal/logger" "github.com/fraktal/mev-beta/pkg/math" ) func TestTokenInfo(t *testing.T) { token := TokenInfo{ Address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", Symbol: "USDC", Name: "USD Coin", Decimals: 6, } assert.Equal(t, "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", token.Address) assert.Equal(t, "USDC", token.Symbol) assert.Equal(t, "USD Coin", token.Name) assert.Equal(t, uint8(6), token.Decimals) } func TestTokenPair(t *testing.T) { pair := TokenPair{ Token0: TokenInfo{ Address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", Symbol: "USDC", Name: "USD Coin", Decimals: 6, }, Token1: TokenInfo{ Address: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", Symbol: "WETH", Name: "Wrapped Ether", Decimals: 18, }, } assert.Equal(t, "USDC", pair.Token0.Symbol) assert.Equal(t, "WETH", pair.Token1.Symbol) assert.Equal(t, uint8(6), pair.Token0.Decimals) assert.Equal(t, uint8(18), pair.Token1.Decimals) } func TestExchangeConfig(t *testing.T) { config := &ExchangeConfig{ Type: math.ExchangeType("UniswapV3"), Name: "Uniswap V3", FactoryAddress: common.HexToAddress("0x1F98431c8aD98523631AE4a59f267346ea31F984"), RouterAddress: common.HexToAddress("0xE592427A0AEce92De3Edee1F18E0157C05861564"), PoolInitCodeHash: "0xe34f199b19b2b4d5f547212444eee88396eaf8a3b7d1f1da4c3f27f65c13bfe9", SwapSelector: []byte{0x41, 0x4b, 0xf3, 0x89}, StableSwapSelector: []byte{0x41, 0x4b, 0xf3, 0x8a}, ChainID: 42161, // Arbitrum SupportsFlashSwaps: true, RequiresApproval: true, MaxHops: 3, DefaultSlippagePercent: 0.01, Url: "https://app.uniswap.org", ApiUrl: "https://api.uniswap.org", } assert.Equal(t, math.ExchangeType("UniswapV3"), config.Type) assert.Equal(t, "Uniswap V3", config.Name) assert.Equal(t, int64(42161), config.ChainID) assert.True(t, config.SupportsFlashSwaps) assert.True(t, config.RequiresApproval) assert.Equal(t, 3, config.MaxHops) assert.Equal(t, 0.01, config.DefaultSlippagePercent) } func TestNewExchangeRegistry(t *testing.T) { log := logger.New("info", "text", "") registry := NewExchangeRegistry(nil, log) assert.NotNil(t, registry) assert.NotNil(t, registry.exchanges) assert.NotNil(t, registry.poolDetectors) assert.NotNil(t, registry.liquidityFetchers) assert.NotNil(t, registry.swapRouters) assert.Equal(t, log, registry.logger) assert.Equal(t, 0, len(registry.exchanges)) } func TestExchangeRegistryRegisterExchange(t *testing.T) { log := logger.New("info", "text", "") registry := NewExchangeRegistry(nil, log) config := &ExchangeConfig{ Type: math.ExchangeType("UniswapV3"), Name: "Uniswap V3", FactoryAddress: common.HexToAddress("0x1F98431c8aD98523631AE4a59f267346ea31F984"), MaxHops: 3, } // Test manual registration (if method exists) config.Type = math.ExchangeType(config.Type) registry.exchanges[config.Type] = config assert.NotNil(t, registry.exchanges[config.Type]) assert.Equal(t, "Uniswap V3", registry.exchanges[config.Type].Name) } func TestExchangeConfigUniswapV2(t *testing.T) { config := &ExchangeConfig{ Type: math.ExchangeType("UniswapV2"), Name: "Uniswap V2", FactoryAddress: common.HexToAddress("0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f"), RouterAddress: common.HexToAddress("0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"), SupportsFlashSwaps: false, RequiresApproval: true, MaxHops: 3, DefaultSlippagePercent: 0.03, } assert.Equal(t, math.ExchangeType("UniswapV2"), config.Type) assert.False(t, config.SupportsFlashSwaps) assert.Equal(t, 0.03, config.DefaultSlippagePercent) } func TestExchangeConfigSushiSwap(t *testing.T) { config := &ExchangeConfig{ Type: math.ExchangeType("SushiSwap"), Name: "SushiSwap", FactoryAddress: common.HexToAddress("0xc35DADB65012eC5796536bD9864eD8773aBc74C4"), RouterAddress: common.HexToAddress("0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506"), SupportsFlashSwaps: false, RequiresApproval: true, MaxHops: 3, DefaultSlippagePercent: 0.03, } assert.Equal(t, math.ExchangeType("SushiSwap"), config.Type) assert.Equal(t, "SushiSwap", config.Name) } func TestExchangeConfigBalancer(t *testing.T) { config := &ExchangeConfig{ Type: math.ExchangeType("Balancer"), Name: "Balancer", FactoryAddress: common.HexToAddress("0xBA12222222228d8Ba445958a75a0704d566BF2C8"), SupportsFlashSwaps: true, RequiresApproval: true, MaxHops: 4, DefaultSlippagePercent: 0.02, } assert.Equal(t, math.ExchangeType("Balancer"), config.Type) assert.True(t, config.SupportsFlashSwaps) assert.Equal(t, 4, config.MaxHops) } func TestExchangeConfigCamelot(t *testing.T) { config := &ExchangeConfig{ Type: math.ExchangeType("Camelot"), Name: "Camelot", FactoryAddress: common.HexToAddress("0x1a3c9B1d2F0529D97f2afC5136Cc23e58f1FD35B"), SupportsFlashSwaps: false, RequiresApproval: true, MaxHops: 3, DefaultSlippagePercent: 0.03, } assert.Equal(t, math.ExchangeType("Camelot"), config.Type) assert.False(t, config.SupportsFlashSwaps) } func TestPoolDetectorInterface(t *testing.T) { // Verify interface is defined correctly var _ PoolDetector = (*mockPoolDetector)(nil) } func TestLiquidityFetcherInterface(t *testing.T) { // Verify interface is defined correctly var _ LiquidityFetcher = (*mockLiquidityFetcher)(nil) } func TestSwapRouterInterface(t *testing.T) { // Verify interface is defined correctly var _ SwapRouter = (*mockSwapRouter)(nil) } func TestExchangeRegistryMultipleExchanges(t *testing.T) { log := logger.New("info", "text", "") registry := NewExchangeRegistry(nil, log) configs := []struct { exchangeType string name string maxHops int }{ {"UniswapV3", "Uniswap V3", 3}, {"UniswapV2", "Uniswap V2", 3}, {"SushiSwap", "SushiSwap", 3}, {"Balancer", "Balancer", 4}, {"Camelot", "Camelot", 3}, } for _, tc := range configs { config := &ExchangeConfig{ Type: math.ExchangeType(tc.exchangeType), Name: tc.name, MaxHops: tc.maxHops, } registry.exchanges[config.Type] = config } assert.Equal(t, 5, len(registry.exchanges)) assert.Equal(t, "Uniswap V3", registry.exchanges[math.ExchangeType("UniswapV3")].Name) assert.Equal(t, "Balancer", registry.exchanges[math.ExchangeType("Balancer")].Name) } func TestTokenInfoDecimalHandling(t *testing.T) { tests := []struct { name string decimals uint8 symbol string }{ {"USDC (6 decimals)", 6, "USDC"}, {"USDT (6 decimals)", 6, "USDT"}, {"DAI (18 decimals)", 18, "DAI"}, {"WETH (18 decimals)", 18, "WETH"}, {"WBTC (8 decimals)", 8, "WBTC"}, {"Custom (12 decimals)", 12, "CUSTOM"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { token := TokenInfo{ Symbol: tt.symbol, Decimals: tt.decimals, } assert.Equal(t, tt.decimals, token.Decimals) assert.Equal(t, tt.symbol, token.Symbol) }) } } func TestExchangeConfigSlippagePercentages(t *testing.T) { tests := []struct { name string slippage float64 }{ {"Conservative (0.5%)", 0.005}, {"Standard (1%)", 0.01}, {"Moderate (3%)", 0.03}, {"Aggressive (5%)", 0.05}, {"Max (10%)", 0.10}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { config := &ExchangeConfig{ DefaultSlippagePercent: tt.slippage, } assert.Equal(t, tt.slippage, config.DefaultSlippagePercent) assert.True(t, config.DefaultSlippagePercent > 0) assert.True(t, config.DefaultSlippagePercent < 0.5) // Reasonable upper bound }) } } func TestExchangeConfigArbitrumChainID(t *testing.T) { config := &ExchangeConfig{ Name: "Arbitrum Exchange", ChainID: 42161, // Arbitrum mainnet } assert.Equal(t, int64(42161), config.ChainID) } func TestTokenPairSwapped(t *testing.T) { token0 := TokenInfo{ Address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", Symbol: "USDC", Decimals: 6, } token1 := TokenInfo{ Address: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", Symbol: "WETH", Decimals: 18, } pair := TokenPair{Token0: token0, Token1: token1} swapped := TokenPair{Token0: token1, Token1: token0} assert.NotEqual(t, pair.Token0.Symbol, swapped.Token0.Symbol) assert.Equal(t, pair.Token0.Symbol, swapped.Token1.Symbol) assert.Equal(t, pair.Token1.Symbol, swapped.Token0.Symbol) } // Mock implementations for interface testing type mockPoolDetector struct{} func (m *mockPoolDetector) GetAllPools(token0, token1 common.Address) ([]common.Address, error) { return nil, nil } func (m *mockPoolDetector) GetPoolForPair(token0, token1 common.Address) (common.Address, error) { return common.Address{}, nil } func (m *mockPoolDetector) GetSupportedFeeTiers() []int64 { return nil } func (m *mockPoolDetector) GetPoolType() string { return "mock" } type mockLiquidityFetcher struct{} func (m *mockLiquidityFetcher) GetPoolData(poolAddress common.Address) (*math.PoolData, error) { return nil, nil } func (m *mockLiquidityFetcher) GetTokenReserves(poolAddress, token0, token1 common.Address) (*big.Int, *big.Int, error) { return nil, nil, nil } func (m *mockLiquidityFetcher) GetPoolPrice(poolAddress common.Address) (*big.Float, error) { return nil, nil } func (m *mockLiquidityFetcher) GetLiquidityDepth(poolAddress, tokenIn common.Address, amount *big.Int) (*big.Int, error) { return nil, nil } type mockSwapRouter struct{} func (m *mockSwapRouter) CalculateSwap(tokenIn, tokenOut common.Address, amountIn *big.Int) (*big.Int, error) { return nil, nil } func (m *mockSwapRouter) GenerateSwapData(tokenIn, tokenOut common.Address, amountIn, minAmountOut *big.Int, deadline *big.Int) ([]byte, error) { return nil, nil } func (m *mockSwapRouter) GetSwapRoute(tokenIn, tokenOut common.Address) ([]common.Address, error) { return nil, nil } func (m *mockSwapRouter) ValidateSwap(tokenIn, tokenOut common.Address, amountIn *big.Int) error { return nil }