package profitcalc import ( "context" "math/big" "testing" "time" "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/assert" "github.com/fraktal/mev-beta/internal/logger" ) func TestNewProfitCalculator(t *testing.T) { log := logger.New("info", "text", "") calc := NewProfitCalculator(log) assert.NotNil(t, calc) assert.Equal(t, log, calc.logger) assert.NotNil(t, calc.minProfitThreshold) assert.NotNil(t, calc.gasPrice) assert.Equal(t, uint64(100000), calc.gasLimit) assert.Equal(t, 0.03, calc.maxSlippage) assert.NotNil(t, calc.slippageProtector) } func TestProfitCalculatorDefaults(t *testing.T) { log := logger.New("info", "text", "") calc := NewProfitCalculator(log) // Verify default configuration values assert.Equal(t, int64(1000000000000000), calc.minProfitThreshold.Int64()) // 0.001 ETH assert.Equal(t, int64(100000000), calc.gasPrice.Int64()) // 0.1 gwei assert.Equal(t, 30*time.Second, calc.gasPriceUpdateInterval) assert.Equal(t, 0.03, calc.maxSlippage) // 3% max slippage } func TestAnalyzeSwapOpportunityPositiveProfit(t *testing.T) { log := logger.New("info", "text", "") calc := NewProfitCalculator(log) ctx := context.Background() tokenA := common.HexToAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48") // USDC tokenB := common.HexToAddress("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") // WETH amountIn := big.NewFloat(1000.0) // 1000 USDC amountOut := big.NewFloat(1.05) // 1.05 ETH (profitable) opp := calc.AnalyzeSwapOpportunity(ctx, tokenA, tokenB, amountIn, amountOut, "UniswapV3") assert.NotNil(t, opp) assert.Equal(t, tokenA, opp.TokenA) assert.Equal(t, tokenB, opp.TokenB) assert.Equal(t, amountIn, opp.AmountIn) assert.Equal(t, amountOut, opp.AmountOut) assert.NotEmpty(t, opp.ID) assert.NotZero(t, opp.Timestamp) } func TestAnalyzeSwapOpportunityZeroAmount(t *testing.T) { log := logger.New("info", "text", "") calc := NewProfitCalculator(log) ctx := context.Background() tokenA := common.HexToAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48") tokenB := common.HexToAddress("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") amountIn := big.NewFloat(0.0) // Zero input amountOut := big.NewFloat(1.0) // Non-zero output opp := calc.AnalyzeSwapOpportunity(ctx, tokenA, tokenB, amountIn, amountOut, "UniswapV3") assert.NotNil(t, opp) assert.False(t, opp.IsExecutable) // Should not be executable with zero input } func TestAnalyzeSwapOpportunityNegativeProfit(t *testing.T) { log := logger.New("info", "text", "") calc := NewProfitCalculator(log) ctx := context.Background() tokenA := common.HexToAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48") tokenB := common.HexToAddress("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") amountIn := big.NewFloat(1000.0) // 1000 USDC amountOut := big.NewFloat(0.90) // 0.90 ETH (loss) opp := calc.AnalyzeSwapOpportunity(ctx, tokenA, tokenB, amountIn, amountOut, "UniswapV3") assert.NotNil(t, opp) assert.False(t, opp.IsExecutable) // Not executable due to loss } func TestAnalyzeSwapOpportunityBelowMinProfit(t *testing.T) { log := logger.New("info", "text", "") calc := NewProfitCalculator(log) ctx := context.Background() tokenA := common.HexToAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48") tokenB := common.HexToAddress("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") amountIn := big.NewFloat(10.0) // 10 USDC amountOut := big.NewFloat(0.01) // 0.01 ETH (tiny profit) opp := calc.AnalyzeSwapOpportunity(ctx, tokenA, tokenB, amountIn, amountOut, "UniswapV3") assert.NotNil(t, opp) // May not be executable if profit is below threshold assert.NotEmpty(t, opp.ID) } func TestCalculateProfitMargin(t *testing.T) { log := logger.New("info", "text", "") calc := NewProfitCalculator(log) // Test cases with different profit margins tests := []struct { name string amountIn *big.Float amountOut *big.Float expectMargin float64 }{ { name: "100% profit margin", amountIn: big.NewFloat(1.0), amountOut: big.NewFloat(2.0), expectMargin: 1.0, // 100% profit }, { name: "50% profit margin", amountIn: big.NewFloat(100.0), amountOut: big.NewFloat(150.0), expectMargin: 0.5, // 50% profit }, { name: "0% profit margin", amountIn: big.NewFloat(100.0), amountOut: big.NewFloat(100.0), expectMargin: 0.0, // Break even }, { name: "Negative margin (loss)", amountIn: big.NewFloat(100.0), amountOut: big.NewFloat(90.0), expectMargin: -0.1, // 10% loss }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Verify that calculator can handle these inputs ctx := context.Background() tokenA := common.HexToAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48") tokenB := common.HexToAddress("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") opp := calc.AnalyzeSwapOpportunity(ctx, tokenA, tokenB, tt.amountIn, tt.amountOut, "UniswapV3") assert.NotNil(t, opp) }) } } func TestGasCostCalculation(t *testing.T) { log := logger.New("info", "text", "") calc := NewProfitCalculator(log) // Verify gas limit is set correctly for Arbitrum L2 assert.Equal(t, uint64(100000), calc.gasLimit) // Verify gas price is reasonable (0.1 gwei for Arbitrum) assert.Equal(t, int64(100000000), calc.gasPrice.Int64()) // Test gas cost calculation (gas price * gas limit) gasPrice := new(big.Int).Set(calc.gasPrice) gasLimit := new(big.Int).SetUint64(calc.gasLimit) gasCost := new(big.Int).Mul(gasPrice, gasLimit) assert.NotNil(t, gasCost) assert.True(t, gasCost.Sign() > 0) } func TestSlippageProtection(t *testing.T) { log := logger.New("info", "text", "") calc := NewProfitCalculator(log) assert.NotNil(t, calc.slippageProtector) assert.Equal(t, 0.03, calc.maxSlippage) // 3% max slippage // Test with amount that would incur slippage amountOut := big.NewFloat(100.0) maxAcceptableSlippage := calc.maxSlippage // Minimum acceptable output with slippage protection minOutput := new(big.Float).Mul(amountOut, big.NewFloat(1.0-maxAcceptableSlippage)) assert.NotNil(t, minOutput) assert.True(t, minOutput.Cmp(big.NewFloat(0)) > 0) } func TestMinProfitThreshold(t *testing.T) { log := logger.New("info", "text", "") calc := NewProfitCalculator(log) minProfit := calc.minProfitThreshold.Int64() assert.Equal(t, int64(1000000000000000), minProfit) // 0.001 ETH // Verify this is a reasonable threshold for Arbitrum // 0.001 ETH at $2000/ETH = $2 minimum profit assert.True(t, minProfit > 0) } func TestOpportunityIDGeneration(t *testing.T) { log := logger.New("info", "text", "") calc := NewProfitCalculator(log) ctx := context.Background() tokenA := common.HexToAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48") tokenB := common.HexToAddress("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") amountIn := big.NewFloat(100.0) amountOut := big.NewFloat(1.0) opp1 := calc.AnalyzeSwapOpportunity(ctx, tokenA, tokenB, amountIn, amountOut, "UniswapV3") time.Sleep(1 * time.Millisecond) // Ensure timestamp difference opp2 := calc.AnalyzeSwapOpportunity(ctx, tokenA, tokenB, amountIn, amountOut, "UniswapV3") assert.NotEmpty(t, opp1.ID) assert.NotEmpty(t, opp2.ID) // IDs should be different (include timestamp) // Both IDs should be properly formatted assert.Contains(t, opp1.ID, "arb_") assert.Contains(t, opp2.ID, "arb_") } func TestOpportunityTimestamp(t *testing.T) { log := logger.New("info", "text", "") calc := NewProfitCalculator(log) ctx := context.Background() tokenA := common.HexToAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48") tokenB := common.HexToAddress("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") amountIn := big.NewFloat(100.0) amountOut := big.NewFloat(1.0) before := time.Now() opp := calc.AnalyzeSwapOpportunity(ctx, tokenA, tokenB, amountIn, amountOut, "UniswapV3") after := time.Now() assert.NotZero(t, opp.Timestamp) assert.True(t, opp.Timestamp.After(before) || opp.Timestamp.Equal(before)) assert.True(t, opp.Timestamp.Before(after) || opp.Timestamp.Equal(after)) } func TestMultipleProtocols(t *testing.T) { log := logger.New("info", "text", "") calc := NewProfitCalculator(log) ctx := context.Background() tokenA := common.HexToAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48") tokenB := common.HexToAddress("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") amountIn := big.NewFloat(100.0) amountOut := big.NewFloat(1.05) protocols := []string{"UniswapV2", "UniswapV3", "SushiSwap", "Camelot"} for _, protocol := range protocols { opp := calc.AnalyzeSwapOpportunity(ctx, tokenA, tokenB, amountIn, amountOut, protocol) assert.NotNil(t, opp) assert.Equal(t, tokenA, opp.TokenA) assert.Equal(t, tokenB, opp.TokenB) } } func TestLargeAmounts(t *testing.T) { log := logger.New("info", "text", "") calc := NewProfitCalculator(log) ctx := context.Background() tokenA := common.HexToAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48") tokenB := common.HexToAddress("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") // Test with very large amounts largeAmount := big.NewFloat(1000000.0) // 1M USDC amountOut := big.NewFloat(500.0) // 500 WETH opp := calc.AnalyzeSwapOpportunity(ctx, tokenA, tokenB, largeAmount, amountOut, "UniswapV3") assert.NotNil(t, opp) assert.Equal(t, largeAmount, opp.AmountIn) assert.Equal(t, amountOut, opp.AmountOut) } func TestSmallAmounts(t *testing.T) { log := logger.New("info", "text", "") calc := NewProfitCalculator(log) ctx := context.Background() tokenA := common.HexToAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48") tokenB := common.HexToAddress("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") // Test with very small amounts (dust) smallAmount := big.NewFloat(0.001) // 0.001 USDC amountOut := big.NewFloat(0.0000005) opp := calc.AnalyzeSwapOpportunity(ctx, tokenA, tokenB, smallAmount, amountOut, "UniswapV3") assert.NotNil(t, opp) assert.False(t, opp.IsExecutable) // Likely below minimum threshold } func TestContextCancellation(t *testing.T) { log := logger.New("info", "text", "") calc := NewProfitCalculator(log) ctx, cancel := context.WithCancel(context.Background()) cancel() tokenA := common.HexToAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48") tokenB := common.HexToAddress("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") amountIn := big.NewFloat(100.0) amountOut := big.NewFloat(1.05) // Should handle cancelled context gracefully opp := calc.AnalyzeSwapOpportunity(ctx, tokenA, tokenB, amountIn, amountOut, "UniswapV3") assert.NotNil(t, opp) } func TestProfitCalculatorConcurrency(t *testing.T) { log := logger.New("info", "text", "") calc := NewProfitCalculator(log) done := make(chan bool) errors := make(chan error) // Test concurrent opportunity analysis for i := 0; i < 10; i++ { go func(index int) { ctx := context.Background() tokenA := common.HexToAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48") tokenB := common.HexToAddress("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") amountIn := big.NewFloat(100.0) amountOut := big.NewFloat(1.05) opp := calc.AnalyzeSwapOpportunity(ctx, tokenA, tokenB, amountIn, amountOut, "UniswapV3") if opp == nil { errors <- assert.AnError } done <- true }(i) } // Wait for all goroutines for i := 0; i < 10; i++ { select { case <-done: // Success case <-errors: t.Fatal("Concurrent operation failed") } } }