355 lines
11 KiB
Go
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")
|
|
}
|
|
}
|
|
}
|