package execution import ( "context" "log/slog" "math/big" "os" "testing" "time" "github.com/ethereum/go-ethereum/common" "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 TestDefaultTransactionBuilderConfig(t *testing.T) { config := DefaultTransactionBuilderConfig() assert.NotNil(t, config) assert.Equal(t, uint16(50), config.DefaultSlippageBPS) assert.Equal(t, uint16(300), config.MaxSlippageBPS) assert.Equal(t, float64(1.2), config.GasLimitMultiplier) assert.Equal(t, uint64(3000000), config.MaxGasLimit) assert.Equal(t, 5*time.Minute, config.DefaultDeadline) } func TestNewTransactionBuilder(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) chainID := big.NewInt(42161) // Arbitrum builder := NewTransactionBuilder(nil, chainID, logger) assert.NotNil(t, builder) assert.NotNil(t, builder.config) assert.Equal(t, chainID, builder.chainID) assert.NotNil(t, builder.uniswapV2Encoder) assert.NotNil(t, builder.uniswapV3Encoder) assert.NotNil(t, builder.curveEncoder) } func TestTransactionBuilder_BuildTransaction_SingleSwap_UniswapV2(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) chainID := big.NewInt(42161) builder := NewTransactionBuilder(nil, chainID, logger) opp := &arbitrage.Opportunity{ ID: "test-opp-1", Type: arbitrage.OpportunityTypeTwoPool, InputToken: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), InputAmount: big.NewInt(1e18), OutputToken: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"), OutputAmount: big.NewInt(1500e6), Path: []arbitrage.SwapStep{ { Protocol: mevtypes.ProtocolUniswapV2, TokenIn: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), TokenOut: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"), AmountIn: big.NewInt(1e18), AmountOut: big.NewInt(1500e6), PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000001"), }, }, EstimatedGas: 150000, } fromAddress := common.HexToAddress("0x0000000000000000000000000000000000000002") tx, err := builder.BuildTransaction(context.Background(), opp, fromAddress) require.NoError(t, err) assert.NotNil(t, tx) assert.NotEmpty(t, tx.To) assert.NotEmpty(t, tx.Data) assert.NotNil(t, tx.Value) assert.Greater(t, tx.GasLimit, uint64(0)) assert.NotNil(t, tx.MaxFeePerGas) assert.NotNil(t, tx.MaxPriorityFeePerGas) assert.NotNil(t, tx.MinOutput) assert.False(t, tx.RequiresFlashloan) assert.Equal(t, uint16(50), tx.Slippage) // Default slippage } func TestTransactionBuilder_BuildTransaction_SingleSwap_UniswapV3(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) chainID := big.NewInt(42161) builder := NewTransactionBuilder(nil, chainID, logger) opp := &arbitrage.Opportunity{ ID: "test-opp-2", Type: arbitrage.OpportunityTypeTwoPool, InputToken: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), InputAmount: big.NewInt(1e18), OutputToken: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"), OutputAmount: big.NewInt(1500e6), Path: []arbitrage.SwapStep{ { Protocol: mevtypes.ProtocolUniswapV3, TokenIn: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), TokenOut: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"), AmountIn: big.NewInt(1e18), AmountOut: big.NewInt(1500e6), PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000001"), Fee: 3000, // 0.3% }, }, EstimatedGas: 150000, } fromAddress := common.HexToAddress("0x0000000000000000000000000000000000000002") tx, err := builder.BuildTransaction(context.Background(), opp, fromAddress) require.NoError(t, err) assert.NotNil(t, tx) assert.NotEmpty(t, tx.To) assert.NotEmpty(t, tx.Data) assert.Equal(t, UniswapV3SwapRouterAddress, tx.To) } func TestTransactionBuilder_BuildTransaction_MultiHop_UniswapV2(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) chainID := big.NewInt(42161) builder := NewTransactionBuilder(nil, chainID, logger) opp := &arbitrage.Opportunity{ ID: "test-opp-3", Type: arbitrage.OpportunityTypeMultiHop, InputToken: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), InputAmount: big.NewInt(1e18), OutputToken: common.HexToAddress("0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f"), OutputAmount: big.NewInt(1e7), Path: []arbitrage.SwapStep{ { Protocol: mevtypes.ProtocolUniswapV2, TokenIn: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), TokenOut: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"), AmountIn: big.NewInt(1e18), AmountOut: big.NewInt(1500e6), PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000001"), }, { Protocol: mevtypes.ProtocolUniswapV2, TokenIn: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"), TokenOut: common.HexToAddress("0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f"), AmountIn: big.NewInt(1500e6), AmountOut: big.NewInt(1e7), PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000002"), }, }, EstimatedGas: 250000, } fromAddress := common.HexToAddress("0x0000000000000000000000000000000000000002") tx, err := builder.BuildTransaction(context.Background(), opp, fromAddress) require.NoError(t, err) assert.NotNil(t, tx) assert.NotEmpty(t, tx.To) assert.NotEmpty(t, tx.Data) assert.Equal(t, UniswapV2RouterAddress, tx.To) } func TestTransactionBuilder_BuildTransaction_MultiHop_UniswapV3(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) chainID := big.NewInt(42161) builder := NewTransactionBuilder(nil, chainID, logger) opp := &arbitrage.Opportunity{ ID: "test-opp-4", Type: arbitrage.OpportunityTypeMultiHop, InputToken: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), InputAmount: big.NewInt(1e18), OutputToken: common.HexToAddress("0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f"), OutputAmount: big.NewInt(1e7), Path: []arbitrage.SwapStep{ { Protocol: mevtypes.ProtocolUniswapV3, TokenIn: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), TokenOut: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"), AmountIn: big.NewInt(1e18), AmountOut: big.NewInt(1500e6), PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000001"), Fee: 3000, }, { Protocol: mevtypes.ProtocolUniswapV3, TokenIn: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"), TokenOut: common.HexToAddress("0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f"), AmountIn: big.NewInt(1500e6), AmountOut: big.NewInt(1e7), PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000002"), Fee: 500, }, }, EstimatedGas: 250000, } fromAddress := common.HexToAddress("0x0000000000000000000000000000000000000002") tx, err := builder.BuildTransaction(context.Background(), opp, fromAddress) require.NoError(t, err) assert.NotNil(t, tx) assert.Equal(t, UniswapV3SwapRouterAddress, tx.To) } func TestTransactionBuilder_BuildTransaction_Curve(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) chainID := big.NewInt(42161) builder := NewTransactionBuilder(nil, chainID, logger) opp := &arbitrage.Opportunity{ ID: "test-opp-5", Type: arbitrage.OpportunityTypeTwoPool, InputToken: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"), InputAmount: big.NewInt(1500e6), OutputToken: common.HexToAddress("0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9"), OutputAmount: big.NewInt(1500e6), Path: []arbitrage.SwapStep{ { Protocol: mevtypes.ProtocolCurve, TokenIn: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"), TokenOut: common.HexToAddress("0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9"), AmountIn: big.NewInt(1500e6), AmountOut: big.NewInt(1500e6), PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000001"), }, }, EstimatedGas: 200000, } fromAddress := common.HexToAddress("0x0000000000000000000000000000000000000002") tx, err := builder.BuildTransaction(context.Background(), opp, fromAddress) require.NoError(t, err) assert.NotNil(t, tx) // For Curve, tx.To should be the pool address assert.Equal(t, opp.Path[0].PoolAddress, tx.To) } func TestTransactionBuilder_BuildTransaction_EmptyPath(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) chainID := big.NewInt(42161) builder := NewTransactionBuilder(nil, chainID, logger) opp := &arbitrage.Opportunity{ ID: "test-opp-6", InputToken: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), InputAmount: big.NewInt(1e18), Path: []arbitrage.SwapStep{}, } fromAddress := common.HexToAddress("0x0000000000000000000000000000000000000002") _, err := builder.BuildTransaction(context.Background(), opp, fromAddress) assert.Error(t, err) assert.Contains(t, err.Error(), "empty swap path") } func TestTransactionBuilder_BuildTransaction_UnsupportedProtocol(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) chainID := big.NewInt(42161) builder := NewTransactionBuilder(nil, chainID, logger) opp := &arbitrage.Opportunity{ ID: "test-opp-7", InputToken: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), InputAmount: big.NewInt(1e18), Path: []arbitrage.SwapStep{ { Protocol: "unknown_protocol", TokenIn: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), TokenOut: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"), AmountIn: big.NewInt(1e18), AmountOut: big.NewInt(1500e6), PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000001"), }, }, } fromAddress := common.HexToAddress("0x0000000000000000000000000000000000000002") _, err := builder.BuildTransaction(context.Background(), opp, fromAddress) assert.Error(t, err) assert.Contains(t, err.Error(), "unsupported protocol") } func TestTransactionBuilder_calculateMinOutput(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) chainID := big.NewInt(42161) builder := NewTransactionBuilder(nil, chainID, logger) tests := []struct { name string outputAmount *big.Int slippageBPS uint16 expectedMin *big.Int }{ { name: "0.5% slippage", outputAmount: big.NewInt(1000e6), slippageBPS: 50, expectedMin: big.NewInt(995e6), // 0.5% less }, { name: "1% slippage", outputAmount: big.NewInt(1000e6), slippageBPS: 100, expectedMin: big.NewInt(990e6), // 1% less }, { name: "3% slippage", outputAmount: big.NewInt(1000e6), slippageBPS: 300, expectedMin: big.NewInt(970e6), // 3% less }, { name: "Zero slippage", outputAmount: big.NewInt(1000e6), slippageBPS: 0, expectedMin: big.NewInt(1000e6), // No change }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { minOutput := builder.calculateMinOutput(tt.outputAmount, tt.slippageBPS) assert.Equal(t, tt.expectedMin, minOutput) }) } } func TestTransactionBuilder_calculateGasLimit(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) chainID := big.NewInt(42161) builder := NewTransactionBuilder(nil, chainID, logger) tests := []struct { name string estimatedGas uint64 expectedMin uint64 expectedMax uint64 }{ { name: "Normal gas estimate", estimatedGas: 150000, expectedMin: 180000, // 150k * 1.2 expectedMax: 180001, }, { name: "High gas estimate", estimatedGas: 2500000, expectedMin: 3000000, // Capped at max expectedMax: 3000000, }, { name: "Zero gas estimate", estimatedGas: 0, expectedMin: 0, // 0 * 1.2 expectedMax: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gasLimit := builder.calculateGasLimit(tt.estimatedGas) assert.GreaterOrEqual(t, gasLimit, tt.expectedMin) assert.LessOrEqual(t, gasLimit, tt.expectedMax) }) } } func TestTransactionBuilder_SignTransaction(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) chainID := big.NewInt(42161) builder := NewTransactionBuilder(nil, chainID, logger) // Create a test private key privateKey, err := crypto.GenerateKey() require.NoError(t, err) tx := &SwapTransaction{ To: common.HexToAddress("0x0000000000000000000000000000000000000001"), Data: []byte{0x01, 0x02, 0x03, 0x04}, Value: big.NewInt(0), GasLimit: 180000, MaxFeePerGas: big.NewInt(100e9), // 100 gwei MaxPriorityFeePerGas: big.NewInt(2e9), // 2 gwei } nonce := uint64(5) signedTx, err := builder.SignTransaction(tx, nonce, crypto.FromECDSA(privateKey)) require.NoError(t, err) assert.NotNil(t, signedTx) assert.Equal(t, nonce, signedTx.Nonce()) assert.Equal(t, tx.To, *signedTx.To()) assert.Equal(t, tx.GasLimit, signedTx.Gas()) assert.Equal(t, tx.MaxFeePerGas, signedTx.GasFeeCap()) assert.Equal(t, tx.MaxPriorityFeePerGas, signedTx.GasTipCap()) } func TestTransactionBuilder_SignTransaction_InvalidKey(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) chainID := big.NewInt(42161) builder := NewTransactionBuilder(nil, chainID, logger) tx := &SwapTransaction{ To: common.HexToAddress("0x0000000000000000000000000000000000000001"), Data: []byte{0x01, 0x02, 0x03, 0x04}, Value: big.NewInt(0), GasLimit: 180000, MaxFeePerGas: big.NewInt(100e9), MaxPriorityFeePerGas: big.NewInt(2e9), } nonce := uint64(5) invalidKey := []byte{0x01, 0x02, 0x03} // Too short _, err := builder.SignTransaction(tx, nonce, invalidKey) assert.Error(t, err) } func TestTransactionBuilder_CustomSlippage(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) chainID := big.NewInt(42161) config := DefaultTransactionBuilderConfig() config.DefaultSlippageBPS = 100 // 1% slippage builder := NewTransactionBuilder(config, chainID, logger) opp := &arbitrage.Opportunity{ ID: "test-opp-8", Type: arbitrage.OpportunityTypeTwoPool, InputToken: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), InputAmount: big.NewInt(1e18), OutputToken: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"), OutputAmount: big.NewInt(1000e6), Path: []arbitrage.SwapStep{ { Protocol: mevtypes.ProtocolUniswapV2, TokenIn: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), TokenOut: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"), AmountIn: big.NewInt(1e18), AmountOut: big.NewInt(1000e6), PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000001"), }, }, EstimatedGas: 150000, } fromAddress := common.HexToAddress("0x0000000000000000000000000000000000000002") tx, err := builder.BuildTransaction(context.Background(), opp, fromAddress) require.NoError(t, err) assert.Equal(t, uint16(100), tx.Slippage) // MinOutput should be 990e6 (1% slippage on 1000e6) assert.Equal(t, big.NewInt(990e6), tx.MinOutput) } func TestTransactionBuilder_ZeroAmounts(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) chainID := big.NewInt(42161) builder := NewTransactionBuilder(nil, chainID, logger) opp := &arbitrage.Opportunity{ ID: "test-opp-9", InputToken: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), InputAmount: big.NewInt(0), OutputToken: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"), OutputAmount: big.NewInt(0), Path: []arbitrage.SwapStep{ { Protocol: mevtypes.ProtocolUniswapV2, TokenIn: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), TokenOut: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"), AmountIn: big.NewInt(0), AmountOut: big.NewInt(0), PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000001"), }, }, EstimatedGas: 150000, } fromAddress := common.HexToAddress("0x0000000000000000000000000000000000000002") tx, err := builder.BuildTransaction(context.Background(), opp, fromAddress) require.NoError(t, err) assert.Equal(t, big.NewInt(0), tx.MinOutput) } // Benchmark tests func BenchmarkTransactionBuilder_BuildTransaction(b *testing.B) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) chainID := big.NewInt(42161) builder := NewTransactionBuilder(nil, chainID, logger) opp := &arbitrage.Opportunity{ ID: "bench-opp", Type: arbitrage.OpportunityTypeTwoPool, InputToken: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), InputAmount: big.NewInt(1e18), OutputToken: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"), OutputAmount: big.NewInt(1500e6), Path: []arbitrage.SwapStep{ { Protocol: mevtypes.ProtocolUniswapV2, TokenIn: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), TokenOut: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"), AmountIn: big.NewInt(1e18), AmountOut: big.NewInt(1500e6), PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000001"), }, }, EstimatedGas: 150000, } fromAddress := common.HexToAddress("0x0000000000000000000000000000000000000002") b.ResetTimer() for i := 0; i < b.N; i++ { _, _ = builder.BuildTransaction(context.Background(), opp, fromAddress) } } func BenchmarkTransactionBuilder_SignTransaction(b *testing.B) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) chainID := big.NewInt(42161) builder := NewTransactionBuilder(nil, chainID, logger) privateKey, _ := crypto.GenerateKey() tx := &SwapTransaction{ To: common.HexToAddress("0x0000000000000000000000000000000000000001"), Data: []byte{0x01, 0x02, 0x03, 0x04}, Value: big.NewInt(0), GasLimit: 180000, MaxFeePerGas: big.NewInt(100e9), MaxPriorityFeePerGas: big.NewInt(2e9), } nonce := uint64(5) b.ResetTimer() for i := 0; i < b.N; i++ { _, _ = builder.SignTransaction(tx, nonce, crypto.FromECDSA(privateKey)) } }