package execution import ( "context" "log/slog" "math/big" "os" "testing" "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/your-org/mev-bot/pkg/arbitrage" mevtypes "github.com/your-org/mev-bot/pkg/types" ) func TestDefaultExecutorConfig(t *testing.T) { config := DefaultExecutorConfig() assert.NotNil(t, config) assert.Equal(t, uint64(1), config.ConfirmationBlocks) assert.Equal(t, 5*time.Minute, config.TimeoutPerTx) assert.Equal(t, 3, config.MaxRetries) assert.Equal(t, uint64(2), config.NonceMargin) assert.Equal(t, "fast", config.GasPriceStrategy) } func TestExecutor_getNextNonce(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) executor := &Executor{ logger: logger, currentNonce: 10, nonceCache: make(map[uint64]*PendingTransaction), } // Get first nonce nonce1 := executor.getNextNonce() assert.Equal(t, uint64(10), nonce1) assert.Equal(t, uint64(11), executor.currentNonce) // Get second nonce nonce2 := executor.getNextNonce() assert.Equal(t, uint64(11), nonce2) assert.Equal(t, uint64(12), executor.currentNonce) } func TestExecutor_releaseNonce(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) executor := &Executor{ logger: logger, currentNonce: 10, nonceCache: make(map[uint64]*PendingTransaction), } // Release current nonce - 1 (should work) executor.releaseNonce(9) assert.Equal(t, uint64(9), executor.currentNonce) // Release older nonce (should not work) executor.currentNonce = 10 executor.releaseNonce(5) assert.Equal(t, uint64(10), executor.currentNonce) // Should not change } func TestExecutor_trackPendingTransaction(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) executor := &Executor{ logger: logger, nonceCache: make(map[uint64]*PendingTransaction), } hash := common.HexToHash("0x123") nonce := uint64(5) opp := &arbitrage.Opportunity{ ID: "test-opp", InputAmount: big.NewInt(1e18), } executor.trackPendingTransaction(nonce, hash, opp) // Check transaction is tracked pending, exists := executor.nonceCache[nonce] assert.True(t, exists) assert.NotNil(t, pending) assert.Equal(t, hash, pending.Hash) assert.Equal(t, nonce, pending.Nonce) assert.Equal(t, opp, pending.Opportunity) assert.False(t, pending.Confirmed) assert.False(t, pending.Failed) } func TestExecutor_GetPendingTransactions(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) executor := &Executor{ logger: logger, nonceCache: make(map[uint64]*PendingTransaction), } // Add pending transactions executor.nonceCache[1] = &PendingTransaction{ Hash: common.HexToHash("0x01"), Confirmed: false, Failed: false, } executor.nonceCache[2] = &PendingTransaction{ Hash: common.HexToHash("0x02"), Confirmed: true, // Already confirmed Failed: false, } executor.nonceCache[3] = &PendingTransaction{ Hash: common.HexToHash("0x03"), Confirmed: false, Failed: false, } pending := executor.GetPendingTransactions() // Should only return unconfirmed, non-failed transactions assert.Len(t, pending, 2) } func TestExecutor_Stop(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) executor := &Executor{ logger: logger, stopCh: make(chan struct{}), stopped: false, } executor.Stop() assert.True(t, executor.stopped) // Calling Stop again should not panic executor.Stop() assert.True(t, executor.stopped) } func TestPendingTransaction_Fields(t *testing.T) { hash := common.HexToHash("0x123") nonce := uint64(5) opp := &arbitrage.Opportunity{ ID: "test-opp", } submittedAt := time.Now() pending := &PendingTransaction{ Hash: hash, Nonce: nonce, Opportunity: opp, SubmittedAt: submittedAt, LastChecked: submittedAt, Confirmed: false, Failed: false, FailReason: "", Receipt: nil, Retries: 0, } assert.Equal(t, hash, pending.Hash) assert.Equal(t, nonce, pending.Nonce) assert.Equal(t, opp, pending.Opportunity) assert.Equal(t, submittedAt, pending.SubmittedAt) assert.False(t, pending.Confirmed) assert.False(t, pending.Failed) assert.Equal(t, 0, pending.Retries) } func TestExecutionResult_Success(t *testing.T) { hash := common.HexToHash("0x123") actualProfit := big.NewInt(0.1e18) gasCost := big.NewInt(0.01e18) duration := 5 * time.Second result := &ExecutionResult{ Success: true, TxHash: hash, Receipt: nil, ActualProfit: actualProfit, GasCost: gasCost, Error: nil, Duration: duration, } assert.True(t, result.Success) assert.Equal(t, hash, result.TxHash) assert.Equal(t, actualProfit, result.ActualProfit) assert.Equal(t, gasCost, result.GasCost) assert.Nil(t, result.Error) assert.Equal(t, duration, result.Duration) } func TestExecutionResult_Failure(t *testing.T) { hash := common.HexToHash("0x123") err := assert.AnError duration := 2 * time.Second result := &ExecutionResult{ Success: false, TxHash: hash, Receipt: nil, ActualProfit: nil, GasCost: nil, Error: err, Duration: duration, } assert.False(t, result.Success) assert.Equal(t, hash, result.TxHash) assert.NotNil(t, result.Error) assert.Equal(t, duration, result.Duration) } func TestExecutorConfig_RPC(t *testing.T) { config := &ExecutorConfig{ PrivateKey: []byte{0x01, 0x02, 0x03}, WalletAddress: common.HexToAddress("0x123"), RPCEndpoint: "http://localhost:8545", PrivateRPCEndpoint: "http://flashbots:8545", UsePrivateRPC: true, } assert.NotEmpty(t, config.PrivateKey) assert.NotEmpty(t, config.WalletAddress) assert.Equal(t, "http://localhost:8545", config.RPCEndpoint) assert.Equal(t, "http://flashbots:8545", config.PrivateRPCEndpoint) assert.True(t, config.UsePrivateRPC) } func TestExecutorConfig_GasStrategy(t *testing.T) { tests := []struct { name string strategy string multiplier float64 maxIncrease float64 }{ { name: "Fast strategy", strategy: "fast", multiplier: 1.2, maxIncrease: 1.5, }, { name: "Market strategy", strategy: "market", multiplier: 1.0, maxIncrease: 1.3, }, { name: "Aggressive strategy", strategy: "aggressive", multiplier: 1.5, maxIncrease: 2.0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { config := &ExecutorConfig{ GasPriceStrategy: tt.strategy, GasPriceMultiplier: tt.multiplier, MaxGasPriceIncrement: tt.maxIncrease, } assert.Equal(t, tt.strategy, config.GasPriceStrategy) assert.Equal(t, tt.multiplier, config.GasPriceMultiplier) assert.Equal(t, tt.maxIncrease, config.MaxGasPriceIncrement) }) } } func TestExecutor_calculateActualProfit(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) executor := &Executor{ logger: logger, } // Create mock receipt receipt := &types.Receipt{ GasUsed: 150000, EffectiveGasPrice: big.NewInt(50e9), // 50 gwei } opp := &arbitrage.Opportunity{ GrossProfit: big.NewInt(0.2e18), // 0.2 ETH } actualProfit := executor.calculateActualProfit(receipt, opp) // Gas cost = 150000 * 50e9 = 0.0075 ETH // Actual profit = 0.2 - 0.0075 = 0.1925 ETH expectedProfit := big.NewInt(192500000000000000) // 0.1925 ETH assert.Equal(t, expectedProfit, actualProfit) } func TestExecutor_NonceManagement_Concurrent(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) executor := &Executor{ logger: logger, currentNonce: 10, nonceCache: make(map[uint64]*PendingTransaction), } // Simulate concurrent nonce requests nonces := make(chan uint64, 10) for i := 0; i < 10; i++ { go func() { nonce := executor.getNextNonce() nonces <- nonce }() } // Collect all nonces receivedNonces := make(map[uint64]bool) for i := 0; i < 10; i++ { nonce := <-nonces // Check for duplicates assert.False(t, receivedNonces[nonce], "Duplicate nonce detected") receivedNonces[nonce] = true } // All nonces should be unique and sequential assert.Len(t, receivedNonces, 10) assert.Equal(t, uint64(20), executor.currentNonce) } func TestExecutor_PendingTransaction_Timeout(t *testing.T) { submittedAt := time.Now().Add(-10 * time.Minute) pending := &PendingTransaction{ Hash: common.HexToHash("0x123"), SubmittedAt: submittedAt, LastChecked: time.Now(), Confirmed: false, Failed: false, Retries: 0, } timeout := 5 * time.Minute isTimedOut := time.Since(pending.SubmittedAt) > timeout assert.True(t, isTimedOut) } func TestExecutor_PendingTransaction_NotTimedOut(t *testing.T) { submittedAt := time.Now().Add(-2 * time.Minute) pending := &PendingTransaction{ Hash: common.HexToHash("0x123"), SubmittedAt: submittedAt, LastChecked: time.Now(), Confirmed: false, Failed: false, Retries: 0, } timeout := 5 * time.Minute isTimedOut := time.Since(pending.SubmittedAt) > timeout assert.False(t, isTimedOut) } func TestExecutor_PendingTransaction_MaxRetries(t *testing.T) { pending := &PendingTransaction{ Hash: common.HexToHash("0x123"), Retries: 3, } maxRetries := 3 canRetry := pending.Retries < maxRetries assert.False(t, canRetry) } func TestExecutor_PendingTransaction_CanRetry(t *testing.T) { pending := &PendingTransaction{ Hash: common.HexToHash("0x123"), Retries: 1, } maxRetries := 3 canRetry := pending.Retries < maxRetries assert.True(t, canRetry) } func TestExecutor_TransactionConfirmed(t *testing.T) { pending := &PendingTransaction{ Hash: common.HexToHash("0x123"), Confirmed: true, Failed: false, Receipt: &types.Receipt{Status: types.ReceiptStatusSuccessful}, } assert.True(t, pending.Confirmed) assert.False(t, pending.Failed) assert.NotNil(t, pending.Receipt) assert.Equal(t, types.ReceiptStatusSuccessful, pending.Receipt.Status) } func TestExecutor_TransactionFailed(t *testing.T) { pending := &PendingTransaction{ Hash: common.HexToHash("0x123"), Confirmed: true, Failed: true, FailReason: "transaction reverted", Receipt: &types.Receipt{Status: types.ReceiptStatusFailed}, } assert.True(t, pending.Confirmed) assert.True(t, pending.Failed) assert.Equal(t, "transaction reverted", pending.FailReason) assert.NotNil(t, pending.Receipt) assert.Equal(t, types.ReceiptStatusFailed, pending.Receipt.Status) } func TestExecutorConfig_Defaults(t *testing.T) { config := DefaultExecutorConfig() // Test all default values assert.Equal(t, uint64(1), config.ConfirmationBlocks) assert.Equal(t, 5*time.Minute, config.TimeoutPerTx) assert.Equal(t, 3, config.MaxRetries) assert.Equal(t, 5*time.Second, config.RetryDelay) assert.Equal(t, uint64(2), config.NonceMargin) assert.Equal(t, "fast", config.GasPriceStrategy) assert.Equal(t, float64(1.1), config.GasPriceMultiplier) assert.Equal(t, float64(1.5), config.MaxGasPriceIncrement) assert.Equal(t, 1*time.Second, config.MonitorInterval) assert.Equal(t, 1*time.Minute, config.CleanupInterval) } func TestExecutor_MultipleOpportunities(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) executor := &Executor{ logger: logger, currentNonce: 10, nonceCache: make(map[uint64]*PendingTransaction), } // Track multiple opportunities for i := 0; i < 5; i++ { hash := common.HexToHash(string(rune(i))) nonce := executor.getNextNonce() opp := &arbitrage.Opportunity{ ID: string(rune(i)), } executor.trackPendingTransaction(nonce, hash, opp) } // Check all are tracked assert.Len(t, executor.nonceCache, 5) assert.Equal(t, uint64(15), executor.currentNonce) // Get pending transactions pending := executor.GetPendingTransactions() assert.Len(t, pending, 5) } func TestExecutor_CleanupOldTransactions(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) executor := &Executor{ logger: logger, nonceCache: make(map[uint64]*PendingTransaction), } // Add old confirmed transaction oldTime := time.Now().Add(-2 * time.Hour) executor.nonceCache[1] = &PendingTransaction{ Hash: common.HexToHash("0x01"), Confirmed: true, LastChecked: oldTime, } // Add recent transaction executor.nonceCache[2] = &PendingTransaction{ Hash: common.HexToHash("0x02"), Confirmed: false, LastChecked: time.Now(), } // Simulate cleanup (cutoff = 1 hour) cutoff := time.Now().Add(-1 * time.Hour) for nonce, pending := range executor.nonceCache { if (pending.Confirmed || pending.Failed) && pending.LastChecked.Before(cutoff) { delete(executor.nonceCache, nonce) } } // Only recent transaction should remain assert.Len(t, executor.nonceCache, 1) _, exists := executor.nonceCache[2] assert.True(t, exists) } // Benchmark tests func BenchmarkExecutor_getNextNonce(b *testing.B) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) executor := &Executor{ logger: logger, currentNonce: 0, nonceCache: make(map[uint64]*PendingTransaction), } b.ResetTimer() for i := 0; i < b.N; i++ { _ = executor.getNextNonce() } } func BenchmarkExecutor_trackPendingTransaction(b *testing.B) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) executor := &Executor{ logger: logger, nonceCache: make(map[uint64]*PendingTransaction), } hash := common.HexToHash("0x123") opp := &arbitrage.Opportunity{ ID: "test-opp", } b.ResetTimer() for i := 0; i < b.N; i++ { executor.trackPendingTransaction(uint64(i), hash, opp) } } func BenchmarkExecutor_GetPendingTransactions(b *testing.B) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) executor := &Executor{ logger: logger, nonceCache: make(map[uint64]*PendingTransaction), } // Add 100 pending transactions for i := 0; i < 100; i++ { executor.nonceCache[uint64(i)] = &PendingTransaction{ Hash: common.HexToHash(string(rune(i))), Confirmed: false, Failed: false, } } b.ResetTimer() for i := 0; i < b.N; i++ { _ = executor.GetPendingTransactions() } }