Files
mev-beta/pkg/profitcalc/profitcalc_test.go
2025-11-08 10:37:52 -06:00

355 lines
11 KiB
Go

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")
}
}
}