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>
351 lines
10 KiB
Go
351 lines
10 KiB
Go
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
|
|
}
|