package parsers import ( "context" "math/big" "testing" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/your-org/mev-bot/pkg/cache" mevtypes "github.com/your-org/mev-bot/pkg/types" ) func TestNewUniswapV3Parser(t *testing.T) { cache := cache.NewPoolCache() logger := &mockLogger{} parser := NewUniswapV3Parser(cache, logger) if parser == nil { t.Fatal("NewUniswapV3Parser returned nil") } if parser.cache != cache { t.Error("NewUniswapV3Parser cache not set correctly") } if parser.logger != logger { t.Error("NewUniswapV3Parser logger not set correctly") } } func TestUniswapV3Parser_Protocol(t *testing.T) { parser := NewUniswapV3Parser(cache.NewPoolCache(), &mockLogger{}) if parser.Protocol() != mevtypes.ProtocolUniswapV3 { t.Errorf("Protocol() = %v, want %v", parser.Protocol(), mevtypes.ProtocolUniswapV3) } } func TestUniswapV3Parser_SupportsLog(t *testing.T) { parser := NewUniswapV3Parser(cache.NewPoolCache(), &mockLogger{}) tests := []struct { name string log types.Log want bool }{ { name: "valid Swap event", log: types.Log{ Topics: []common.Hash{SwapV3EventSignature}, }, want: true, }, { name: "empty topics", log: types.Log{ Topics: []common.Hash{}, }, want: false, }, { name: "wrong event signature", log: types.Log{ Topics: []common.Hash{common.HexToHash("0x1234")}, }, want: false, }, { name: "V2 swap event signature", log: types.Log{ Topics: []common.Hash{SwapEventSignature}, }, want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := parser.SupportsLog(tt.log); got != tt.want { t.Errorf("SupportsLog() = %v, want %v", got, tt.want) } }) } } func TestUniswapV3Parser_ParseLog(t *testing.T) { ctx := context.Background() // Create pool cache and add test pool poolCache := cache.NewPoolCache() poolAddress := common.HexToAddress("0x1111111111111111111111111111111111111111") token0 := common.HexToAddress("0x2222222222222222222222222222222222222222") token1 := common.HexToAddress("0x3333333333333333333333333333333333333333") testPool := &mevtypes.PoolInfo{ Address: poolAddress, Protocol: mevtypes.ProtocolUniswapV3, Token0: token0, Token1: token1, Token0Decimals: 18, Token1Decimals: 6, Reserve0: big.NewInt(1000000), Reserve1: big.NewInt(500000), Fee: 500, // 0.05% in basis points IsActive: true, } err := poolCache.Add(ctx, testPool) if err != nil { t.Fatalf("Failed to add test pool: %v", err) } parser := NewUniswapV3Parser(poolCache, &mockLogger{}) // Create test transaction tx := types.NewTransaction( 0, poolAddress, big.NewInt(0), 0, big.NewInt(0), []byte{}, ) sender := common.HexToAddress("0x4444444444444444444444444444444444444444") recipient := common.HexToAddress("0x5555555555555555555555555555555555555555") tests := []struct { name string amount0 *big.Int // Signed amount1 *big.Int // Signed sqrtPriceX96 *big.Int liquidity *big.Int tick int32 wantAmount0In *big.Int wantAmount1In *big.Int wantAmount0Out *big.Int wantAmount1Out *big.Int wantErr bool }{ { name: "swap token0 for token1 (exact input)", amount0: big.NewInt(-1000000000000000000), // -1 token0 (user sends) amount1: big.NewInt(500000), // +0.5 token1 (user receives) sqrtPriceX96: new(big.Int).Lsh(big.NewInt(1), 96), liquidity: big.NewInt(1000000), tick: 100, wantAmount0In: big.NewInt(1000000000000000000), // 1 token0 scaled to 18 wantAmount1In: big.NewInt(0), wantAmount0Out: big.NewInt(0), wantAmount1Out: mevtypes.ScaleToDecimals(big.NewInt(500000), 6, 18), // 0.5 token1 scaled to 18 wantErr: false, }, { name: "swap token1 for token0 (exact input)", amount0: big.NewInt(1000000000000000000), // +1 token0 (user receives) amount1: big.NewInt(-500000), // -0.5 token1 (user sends) sqrtPriceX96: new(big.Int).Lsh(big.NewInt(1), 96), liquidity: big.NewInt(1000000), tick: -100, wantAmount0In: big.NewInt(0), wantAmount1In: mevtypes.ScaleToDecimals(big.NewInt(500000), 6, 18), // 0.5 token1 scaled to 18 wantAmount0Out: big.NewInt(1000000000000000000), // 1 token0 scaled to 18 wantAmount1Out: big.NewInt(0), wantErr: false, }, { name: "both tokens negative (should not happen but test parsing)", amount0: big.NewInt(-1000000000000000000), amount1: big.NewInt(-500000), sqrtPriceX96: new(big.Int).Lsh(big.NewInt(1), 96), liquidity: big.NewInt(1000000), tick: 0, wantAmount0In: big.NewInt(1000000000000000000), wantAmount1In: mevtypes.ScaleToDecimals(big.NewInt(500000), 6, 18), wantAmount0Out: big.NewInt(0), wantAmount1Out: big.NewInt(0), wantErr: false, }, { name: "both tokens positive (should not happen but test parsing)", amount0: big.NewInt(1000000000000000000), amount1: big.NewInt(500000), sqrtPriceX96: new(big.Int).Lsh(big.NewInt(1), 96), liquidity: big.NewInt(1000000), tick: 0, wantAmount0In: big.NewInt(0), wantAmount1In: big.NewInt(0), wantAmount0Out: big.NewInt(1000000000000000000), wantAmount1Out: mevtypes.ScaleToDecimals(big.NewInt(500000), 6, 18), wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Encode event data: amount0, amount1, sqrtPriceX96, liquidity, tick data := make([]byte, 32*5) // 5 * 32 bytes // int256 amount0 if tt.amount0.Sign() < 0 { // Two's complement for negative numbers negAmount0 := new(big.Int).Neg(tt.amount0) negAmount0.Sub(new(big.Int).Lsh(big.NewInt(1), 256), negAmount0) negAmount0.FillBytes(data[0:32]) } else { tt.amount0.FillBytes(data[0:32]) } // int256 amount1 if tt.amount1.Sign() < 0 { // Two's complement for negative numbers negAmount1 := new(big.Int).Neg(tt.amount1) negAmount1.Sub(new(big.Int).Lsh(big.NewInt(1), 256), negAmount1) negAmount1.FillBytes(data[32:64]) } else { tt.amount1.FillBytes(data[32:64]) } // uint160 sqrtPriceX96 tt.sqrtPriceX96.FillBytes(data[64:96]) // uint128 liquidity tt.liquidity.FillBytes(data[96:128]) // int24 tick tickBig := big.NewInt(int64(tt.tick)) if tt.tick < 0 { // Two's complement for 24-bit negative number negTick := new(big.Int).Neg(tickBig) negTick.Sub(new(big.Int).Lsh(big.NewInt(1), 24), negTick) tickBytes := negTick.Bytes() // Pad to 32 bytes copy(data[128+(32-len(tickBytes)):], tickBytes) } else { tickBig.FillBytes(data[128:160]) } log := types.Log{ Address: poolAddress, Topics: []common.Hash{ SwapV3EventSignature, common.BytesToHash(sender.Bytes()), common.BytesToHash(recipient.Bytes()), }, Data: data, BlockNumber: 1000, Index: 0, } event, err := parser.ParseLog(ctx, log, tx) if tt.wantErr { if err == nil { t.Error("ParseLog() expected error, got nil") } return } if err != nil { t.Fatalf("ParseLog() unexpected error: %v", err) } if event == nil { t.Fatal("ParseLog() returned nil event") } // Verify event fields if event.TxHash != tx.Hash() { t.Errorf("TxHash = %v, want %v", event.TxHash, tx.Hash()) } if event.Protocol != mevtypes.ProtocolUniswapV3 { t.Errorf("Protocol = %v, want %v", event.Protocol, mevtypes.ProtocolUniswapV3) } if event.Amount0In.Cmp(tt.wantAmount0In) != 0 { t.Errorf("Amount0In = %v, want %v", event.Amount0In, tt.wantAmount0In) } if event.Amount1In.Cmp(tt.wantAmount1In) != 0 { t.Errorf("Amount1In = %v, want %v", event.Amount1In, tt.wantAmount1In) } if event.Amount0Out.Cmp(tt.wantAmount0Out) != 0 { t.Errorf("Amount0Out = %v, want %v", event.Amount0Out, tt.wantAmount0Out) } if event.Amount1Out.Cmp(tt.wantAmount1Out) != 0 { t.Errorf("Amount1Out = %v, want %v", event.Amount1Out, tt.wantAmount1Out) } if event.SqrtPriceX96.Cmp(tt.sqrtPriceX96) != 0 { t.Errorf("SqrtPriceX96 = %v, want %v", event.SqrtPriceX96, tt.sqrtPriceX96) } if event.Liquidity.Cmp(tt.liquidity) != 0 { t.Errorf("Liquidity = %v, want %v", event.Liquidity, tt.liquidity) } if event.Tick == nil { t.Error("Tick is nil") } else if *event.Tick != tt.tick { t.Errorf("Tick = %v, want %v", *event.Tick, tt.tick) } }) } } func TestUniswapV3Parser_ParseReceipt(t *testing.T) { ctx := context.Background() // Create pool cache and add test pool poolCache := cache.NewPoolCache() poolAddress := common.HexToAddress("0x1111111111111111111111111111111111111111") token0 := common.HexToAddress("0x2222222222222222222222222222222222222222") token1 := common.HexToAddress("0x3333333333333333333333333333333333333333") testPool := &mevtypes.PoolInfo{ Address: poolAddress, Protocol: mevtypes.ProtocolUniswapV3, Token0: token0, Token1: token1, Token0Decimals: 18, Token1Decimals: 6, Reserve0: big.NewInt(1000000), Reserve1: big.NewInt(500000), Fee: 500, IsActive: true, } err := poolCache.Add(ctx, testPool) if err != nil { t.Fatalf("Failed to add test pool: %v", err) } parser := NewUniswapV3Parser(poolCache, &mockLogger{}) // Create test transaction tx := types.NewTransaction( 0, poolAddress, big.NewInt(0), 0, big.NewInt(0), []byte{}, ) // Encode minimal valid event data amount0 := big.NewInt(-1000000000000000000) // -1 token0 amount1 := big.NewInt(500000) // +0.5 token1 sqrtPriceX96 := new(big.Int).Lsh(big.NewInt(1), 96) liquidity := big.NewInt(1000000) tick := big.NewInt(100) data := make([]byte, 32*5) // Negative amount0 (two's complement) negAmount0 := new(big.Int).Neg(amount0) negAmount0.Sub(new(big.Int).Lsh(big.NewInt(1), 256), negAmount0) negAmount0.FillBytes(data[0:32]) amount1.FillBytes(data[32:64]) sqrtPriceX96.FillBytes(data[64:96]) liquidity.FillBytes(data[96:128]) tick.FillBytes(data[128:160]) sender := common.HexToAddress("0x4444444444444444444444444444444444444444") recipient := common.HexToAddress("0x5555555555555555555555555555555555555555") tests := []struct { name string receipt *types.Receipt wantCount int }{ { name: "receipt with single V3 swap event", receipt: &types.Receipt{ Logs: []*types.Log{ { Address: poolAddress, Topics: []common.Hash{ SwapV3EventSignature, common.BytesToHash(sender.Bytes()), common.BytesToHash(recipient.Bytes()), }, Data: data, BlockNumber: 1000, Index: 0, }, }, }, wantCount: 1, }, { name: "receipt with multiple V3 swap events", receipt: &types.Receipt{ Logs: []*types.Log{ { Address: poolAddress, Topics: []common.Hash{ SwapV3EventSignature, common.BytesToHash(sender.Bytes()), common.BytesToHash(recipient.Bytes()), }, Data: data, BlockNumber: 1000, Index: 0, }, { Address: poolAddress, Topics: []common.Hash{ SwapV3EventSignature, common.BytesToHash(sender.Bytes()), common.BytesToHash(recipient.Bytes()), }, Data: data, BlockNumber: 1000, Index: 1, }, }, }, wantCount: 2, }, { name: "receipt with mixed V2 and V3 events", receipt: &types.Receipt{ Logs: []*types.Log{ { Address: poolAddress, Topics: []common.Hash{ SwapV3EventSignature, common.BytesToHash(sender.Bytes()), common.BytesToHash(recipient.Bytes()), }, Data: data, BlockNumber: 1000, Index: 0, }, { Address: poolAddress, Topics: []common.Hash{ SwapEventSignature, // V2 signature common.BytesToHash(sender.Bytes()), common.BytesToHash(recipient.Bytes()), }, Data: []byte{}, BlockNumber: 1000, Index: 1, }, }, }, wantCount: 1, // Only the V3 event }, { name: "empty receipt", receipt: &types.Receipt{ Logs: []*types.Log{}, }, wantCount: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { events, err := parser.ParseReceipt(ctx, tt.receipt, tx) if err != nil { t.Fatalf("ParseReceipt() unexpected error: %v", err) } if len(events) != tt.wantCount { t.Errorf("ParseReceipt() returned %d events, want %d", len(events), tt.wantCount) } // Verify all returned events are valid for i, event := range events { if event == nil { t.Errorf("Event %d is nil", i) continue } if event.Protocol != mevtypes.ProtocolUniswapV3 { t.Errorf("Event %d Protocol = %v, want %v", i, event.Protocol, mevtypes.ProtocolUniswapV3) } } }) } } func TestSwapV3EventSignature(t *testing.T) { // Verify the event signature is correct expected := crypto.Keccak256Hash([]byte("Swap(address,address,int256,int256,uint160,uint128,int24)")) if SwapV3EventSignature != expected { t.Errorf("SwapV3EventSignature = %v, want %v", SwapV3EventSignature, expected) } } func TestCalculatePriceFromSqrtPriceX96(t *testing.T) { tests := []struct { name string sqrtPriceX96 *big.Int token0Decimals uint8 token1Decimals uint8 wantNonZero bool }{ { name: "valid sqrtPriceX96", sqrtPriceX96: new(big.Int).Lsh(big.NewInt(1), 96), // Price = 1 token0Decimals: 18, token1Decimals: 18, wantNonZero: true, }, { name: "nil sqrtPriceX96", sqrtPriceX96: nil, token0Decimals: 18, token1Decimals: 18, wantNonZero: false, }, { name: "zero sqrtPriceX96", sqrtPriceX96: big.NewInt(0), token0Decimals: 18, token1Decimals: 18, wantNonZero: false, }, { name: "different decimals", sqrtPriceX96: new(big.Int).Lsh(big.NewInt(1), 96), token0Decimals: 18, token1Decimals: 6, wantNonZero: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { price := CalculatePriceFromSqrtPriceX96(tt.sqrtPriceX96, tt.token0Decimals, tt.token1Decimals) if tt.wantNonZero { if price.Sign() == 0 { t.Error("CalculatePriceFromSqrtPriceX96() returned zero, want non-zero") } } else { if price.Sign() != 0 { t.Error("CalculatePriceFromSqrtPriceX96() returned non-zero, want zero") } } }) } }