//go:build integration && legacy && forked // +build integration,legacy,forked // Package integration provides integration tests for the MEV bot using a forked Arbitrum environment package integration import ( "context" "fmt" "math/big" "testing" "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/fraktal/mev-beta/internal/config" "github.com/fraktal/mev-beta/internal/logger" "github.com/fraktal/mev-beta/internal/ratelimit" "github.com/fraktal/mev-beta/pkg/contracts" "github.com/fraktal/mev-beta/pkg/database" "github.com/fraktal/mev-beta/pkg/market" "github.com/fraktal/mev-beta/pkg/monitor" "github.com/fraktal/mev-beta/pkg/orchestrator" "github.com/fraktal/mev-beta/pkg/pools" "github.com/fraktal/mev-beta/pkg/scanner" ) // TestFullArbitragePipeline tests the complete arbitrage detection and execution pipeline // using a forked Arbitrum environment func TestFullArbitragePipeline(t *testing.T) { // Skip this test in short mode if testing.Short() { t.Skip("skipping integration test in short mode") } // Create test logger log := logger.New("debug", "text", "") // Create test configuration cfg := createTestConfig() // Connect to the forked environment client, err := ethclient.Dial(cfg.Arbitrum.RPCEndpoint) require.NoError(t, err, "failed to connect to forked Arbitrum") defer client.Close() // Verify connection by getting chain ID chainID, err := client.ChainID(context.Background()) require.NoError(t, err, "failed to get chain ID") log.Info(fmt.Sprintf("Connected to forked Arbitrum chain ID: %s", chainID.String())) // Create rate limiter rateLimiter := ratelimit.NewLimiterManager(&cfg.Arbitrum) // Create market manager marketMgr := market.NewMarketManager(&cfg.Uniswap, log) // Create database (in-memory for testing) dbCfg := &config.DatabaseConfig{ File: ":memory:", MaxOpenConnections: 10, MaxIdleConnections: 5, } db, err := database.NewDatabase(dbCfg, log) require.NoError(t, err, "failed to create database") defer db.Close() // Create contract executor contractExecutor, err := contracts.NewContractExecutor(cfg, log) require.NoError(t, err, "failed to create contract executor") defer contractExecutor.Close() // Create market scanner scanner := scanner.NewMarketScanner(&cfg.Bot, log, contractExecutor, db) // Create MEV coordinator coordinator := orchestrator.NewMEVCoordinator(cfg, log, marketMgr, scanner, db) // Create Arbitrum monitor monitor, err := monitor.NewArbitrumMonitor( &cfg.Arbitrum, &cfg.Bot, log, rateLimiter, marketMgr, scanner, coordinator, ) require.NoError(t, err, "failed to create Arbitrum monitor") // Test the full pipeline t.Run("TestArbitrageDetection", func(t *testing.T) { testArbitrageDetection(t, client, monitor, scanner, marketMgr, log) }) t.Run("TestPoolDiscovery", func(t *testing.T) { testPoolDiscovery(t, client, marketMgr, log) }) t.Run("TestArbitrageExecution", func(t *testing.T) { testArbitrageExecution(t, client, contractExecutor, log) }) // Cleanup monitor.Stop() scanner.Stop() coordinator.Stop() } // createTestConfig creates a test configuration for the integration tests func createTestConfig() *config.Config { return &config.Config{ Arbitrum: config.ArbitrumConfig{ RPCEndpoint: "http://localhost:8545", // Anvil default port WSEndpoint: "", ChainID: 31337, // Anvil default chain ID RateLimit: config.RateLimitConfig{ RequestsPerSecond: 10, MaxConcurrent: 5, Burst: 20, }, FallbackEndpoints: []config.EndpointConfig{}, }, Bot: config.BotConfig{ Enabled: true, PollingInterval: 1, MinProfitThreshold: 0.01, // Lower threshold for testing GasPriceMultiplier: 1.2, MaxWorkers: 2, ChannelBufferSize: 10, RPCTimeout: 30, }, Uniswap: config.UniswapConfig{ FactoryAddress: "0x1F98431c8aD98523631AE4a59f267346ea31F984", PositionManagerAddress: "0xC36442b4a4522E871399CD717aBDD847Ab11FE88", FeeTiers: []int64{500, 3000, 10000}, Cache: config.CacheConfig{ Enabled: true, Expiration: 300, MaxSize: 10000, }, }, Log: config.LogConfig{ Level: "debug", Format: "text", File: "", }, Database: config.DatabaseConfig{ File: ":memory:", MaxOpenConnections: 10, MaxIdleConnections: 5, }, Ethereum: config.EthereumConfig{ PrivateKey: "", // Will be set by environment or test setup AccountAddress: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", // Default Anvil account GasPriceMultiplier: 1.2, }, Contracts: config.ContractsConfig{ ArbitrageExecutor: "0x...", // Will be deployed during test setup FlashSwapper: "0x...", // Will be deployed during test setup AuthorizedCallers: []string{ "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", // Default Anvil account }, AuthorizedDEXes: []string{ "0x1F98431c8aD98523631AE4a59f267346ea31F984", // Uniswap V3 Factory "0xf1D7CC64Fb4452F05c498126312eBE29f30Fbcf9", // Uniswap V2 Factory "0xc35DADB65012eC5796536bD9864eD8773aBc74C4", // SushiSwap Factory }, }, } } // testArbitrageDetection tests arbitrage opportunity detection func testArbitrageDetection(t *testing.T, client *ethclient.Client, monitor *monitor.ArbitrumMonitor, scanner *scanner.MarketScanner, marketMgr *market.MarketManager, log *logger.Logger) { log.Info("Testing arbitrage detection...") // Create context with timeout ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() // Start monitoring in a goroutine monitorDone := make(chan error, 1) go func() { monitorDone <- monitor.Start(ctx) }() // Let the monitor run for a bit to detect events time.Sleep(5 * time.Second) // Check if any arbitrage opportunities were detected // In a real test, we would simulate price movements to create opportunities log.Info("Arbitrage detection test completed") } // testPoolDiscovery tests pool discovery functionality func testPoolDiscovery(t *testing.T, client *ethclient.Client, marketMgr *market.MarketManager, log *logger.Logger) { log.Info("Testing pool discovery...") // Test discovering pools for common token pairs knownPairs := []struct { token0 common.Address token1 common.Address }{ { token0: common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), // USDC token1: common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"), // WETH }, { token0: common.HexToAddress("0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8"), // USDT token1: common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"), // WETH }, } // Create CREATE2 calculator for pool discovery calculator := pools.NewCREATE2Calculator(log) // Test pool discovery for each pair for _, pair := range knownPairs { log.Info(fmt.Sprintf("Discovering pools for %s-%s", pair.token0.Hex(), pair.token1.Hex())) // Use CREATE2 calculator to find potential pools pools, err := calculator.FindPoolsForTokenPair(pair.token0, pair.token1) if err != nil { log.Warn(fmt.Sprintf("Failed to discover pools for %s-%s: %v", pair.token0.Hex(), pair.token1.Hex(), err)) continue } log.Info(fmt.Sprintf("Found %d potential pools for %s-%s", len(pools), pair.token0.Hex(), pair.token1.Hex())) // Validate each pool for _, pool := range pools { log.Debug(fmt.Sprintf("Validating pool: %s (factory: %s)", pool.PoolAddr.Hex(), pool.Factory)) // In a real implementation, we would validate that the pool actually exists // and has liquidity. For now, we just log the discovery. } } log.Info("Pool discovery test completed") } // testArbitrageExecution tests arbitrage execution functionality func testArbitrageExecution(t *testing.T, client *ethclient.Client, contractExecutor *contracts.ContractExecutor, log *logger.Logger) { log.Info("Testing arbitrage execution...") // Create a mock arbitrage opportunity for testing mockOpportunity := scanner.ArbitrageOpportunity{ Path: []string{ "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", // WETH }, Pools: []string{ "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640", // Known USDC/WETH pool }, Profit: big.NewInt(1000000000000000000), // 1 ETH profit estimate GasEstimate: big.NewInt(300000), // Estimated gas cost ROI: 5.0, // 5% ROI Protocol: "UniswapV3", } // Test execution (this will fail in testing but we can verify the setup) log.Info("Setting up arbitrage execution test...") // In a real test, we would: // 1. Deploy the contracts to the forked environment // 2. Fund the test account with tokens // 3. Create actual arbitrage opportunities by manipulating pool states // 4. Execute the arbitrage and verify profits log.Info("Arbitrage execution test setup completed") } // TestContractBindings tests that all contract bindings are working correctly func TestContractBindings(t *testing.T) { // Skip this test in short mode if testing.Short() { t.Skip("skipping contract binding test in short mode") } log := logger.New("debug", "text", "") cfg := createTestConfig() // Connect to the forked environment client, err := ethclient.Dial(cfg.Arbitrum.RPCEndpoint) require.NoError(t, err, "failed to connect to forked Arbitrum") defer client.Close() // Test contract executor creation contractExecutor, err := contracts.NewContractExecutor(cfg, log) require.NoError(t, err, "failed to create contract executor") defer contractExecutor.Close() // Verify contract executor was created successfully assert.NotNil(t, contractExecutor) assert.NotNil(t, contractExecutor.Client()) log.Info("Contract bindings test completed successfully") } // TestDatabaseIntegration tests database integration with the scanner func TestDatabaseIntegration(t *testing.T) { // Skip this test in short mode if testing.Short() { t.Skip("skipping database integration test in short mode") } log := logger.New("debug", "text", "") // Create in-memory database for testing dbCfg := &config.DatabaseConfig{ File: ":memory:", MaxOpenConnections: 10, MaxIdleConnections: 5, } db, err := database.NewDatabase(dbCfg, log) require.NoError(t, err, "failed to create database") defer db.Close() // Test inserting swap event swapEvent := &database.SwapEvent{ Timestamp: time.Now(), BlockNumber: 12345678, TxHash: common.HexToHash("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"), PoolAddress: common.HexToAddress("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"), Token0: common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), // USDC Token1: common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"), // WETH Amount0In: big.NewInt(1000000000), // 1000 USDC Amount1In: big.NewInt(0), Amount0Out: big.NewInt(0), Amount1Out: big.NewInt(500000000000000000), // 0.5 WETH Sender: common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678"), Recipient: common.HexToAddress("0x8765432109fedcba8765432109fedcba87654321"), Protocol: "uniswap_v3", } err = db.InsertSwapEvent(swapEvent) assert.NoError(t, err, "failed to insert swap event") // Test inserting liquidity event liquidityEvent := &database.LiquidityEvent{ Timestamp: time.Now(), BlockNumber: 12345679, TxHash: common.HexToHash("0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"), PoolAddress: common.HexToAddress("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"), Token0: common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), // USDC Token1: common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"), // WETH Liquidity: big.NewInt(1000000000000000000), // 1 ETH equivalent Amount0: big.NewInt(2000000000), // 2000 USDC Amount1: big.NewInt(1000000000000000000), // 1 WETH Sender: common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678"), Recipient: common.HexToAddress("0x8765432109fedcba8765432109fedcba87654321"), EventType: "add", Protocol: "uniswap_v3", } err = db.InsertLiquidityEvent(liquidityEvent) assert.NoError(t, err, "failed to insert liquidity event") // Test inserting pool data poolData := &database.PoolData{ Address: common.HexToAddress("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"), Token0: common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), // USDC Token1: common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"), // WETH Fee: 3000, // 0.3% Liquidity: big.NewInt(1000000000000000000), // 1 ETH equivalent SqrtPriceX96: big.NewInt(2505414483750470000), // Realistic price Tick: 200000, // Corresponding tick LastUpdated: time.Now(), Protocol: "uniswap_v3", } err = db.InsertPoolData(poolData) assert.NoError(t, err, "failed to insert pool data") // Test retrieving recent swap events swaps, err := db.GetRecentSwapEvents(10) assert.NoError(t, err, "failed to get recent swap events") assert.Len(t, swaps, 1, "expected 1 swap event") if len(swaps) > 0 { assert.Equal(t, swapEvent.PoolAddress, swaps[0].PoolAddress, "pool address mismatch") assert.Equal(t, swapEvent.Token0, swaps[0].Token0, "token0 mismatch") assert.Equal(t, swapEvent.Token1, swaps[0].Token1, "token1 mismatch") assert.Equal(t, swapEvent.Protocol, swaps[0].Protocol, "protocol mismatch") } // Test retrieving recent liquidity events liquidityEvents, err := db.GetRecentLiquidityEvents(10) assert.NoError(t, err, "failed to get recent liquidity events") assert.Len(t, liquidityEvents, 1, "expected 1 liquidity event") if len(liquidityEvents) > 0 { assert.Equal(t, liquidityEvent.PoolAddress, liquidityEvents[0].PoolAddress, "pool address mismatch") assert.Equal(t, liquidityEvent.Token0, liquidityEvents[0].Token0, "token0 mismatch") assert.Equal(t, liquidityEvent.Token1, liquidityEvents[0].Token1, "token1 mismatch") assert.Equal(t, liquidityEvent.EventType, liquidityEvents[0].EventType, "event type mismatch") assert.Equal(t, liquidityEvent.Protocol, liquidityEvents[0].Protocol, "protocol mismatch") } // Test retrieving pool data retrievedPool, err := db.GetPoolData(poolData.Address) assert.NoError(t, err, "failed to get pool data") assert.Equal(t, poolData.Address, retrievedPool.Address, "pool address mismatch") assert.Equal(t, poolData.Token0, retrievedPool.Token0, "token0 mismatch") assert.Equal(t, poolData.Token1, retrievedPool.Token1, "token1 mismatch") assert.Equal(t, poolData.Fee, retrievedPool.Fee, "fee mismatch") assert.Equal(t, poolData.Protocol, retrievedPool.Protocol, "protocol mismatch") log.Info("Database integration test completed successfully") }