package parsers import ( "context" "math/big" "testing" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "coppertone.tech/fraktal/mev-bot/pkg/cache" pkgtypes "coppertone.tech/fraktal/mev-bot/pkg/types" ) func TestNewUniswapV2Parser(t *testing.T) { tests := []struct { name string cache cache.PoolCache logger *mockLogger wantError bool }{ { name: "valid inputs", cache: cache.NewPoolCache(), logger: &mockLogger{}, wantError: false, }, { name: "nil cache", cache: nil, logger: &mockLogger{}, wantError: true, }, { name: "nil logger", cache: cache.NewPoolCache(), logger: nil, wantError: false, // We allow nil logger, will use default or just log to void }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { parser, err := NewUniswapV2Parser(tt.cache, tt.logger) if tt.wantError { assert.Error(t, err) assert.Nil(t, parser) } else { assert.NoError(t, err) assert.NotNil(t, parser) assert.Equal(t, pkgtypes.ProtocolUniswapV2, parser.Protocol()) } }) } } func TestUniswapV2Parser_Protocol(t *testing.T) { parser, err := NewUniswapV2Parser(cache.NewPoolCache(), &mockLogger{}) require.NoError(t, err) assert.Equal(t, pkgtypes.ProtocolUniswapV2, parser.Protocol()) } func TestUniswapV2Parser_SupportsLog(t *testing.T) { parser, err := NewUniswapV2Parser(cache.NewPoolCache(), &mockLogger{}) require.NoError(t, err) tests := []struct { name string log types.Log supports bool }{ { name: "swap event", log: types.Log{ Topics: []common.Hash{UniswapV2SwapSignature}, }, supports: true, }, { name: "mint event", log: types.Log{ Topics: []common.Hash{UniswapV2MintSignature}, }, supports: true, }, { name: "burn event", log: types.Log{ Topics: []common.Hash{UniswapV2BurnSignature}, }, supports: true, }, { name: "unsupported event", log: types.Log{ Topics: []common.Hash{common.HexToHash("0x1234567890")}, }, supports: false, }, { name: "no topics", log: types.Log{ Topics: []common.Hash{}, }, supports: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := parser.SupportsLog(tt.log) assert.Equal(t, tt.supports, result) }) } } func TestUniswapV2Parser_ParseSwap(t *testing.T) { // Setup poolCache := cache.NewPoolCache() parser, err := NewUniswapV2Parser(poolCache, &mockLogger{}) require.NoError(t, err) // Add a test pool to cache poolAddress := common.HexToAddress("0x1234567890123456789012345678901234567890") token0 := common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1") // WETH on Arbitrum token1 := common.HexToAddress("0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8") // USDC on Arbitrum pool := &pkgtypes.PoolInfo{ Address: poolAddress, Protocol: pkgtypes.ProtocolUniswapV2, Token0: token0, Token1: token1, Token0Decimals: 18, Token1Decimals: 6, } ctx := context.Background() err = poolCache.Add(ctx, pool) require.NoError(t, err) // Create a minimal transaction for testing testTx := types.NewTx(&types.LegacyTx{ Nonce: 0, GasPrice: big.NewInt(1), Gas: 21000, To: &poolAddress, Value: big.NewInt(0), Data: []byte{}, }) tests := []struct { name string log types.Log tx *types.Transaction wantError bool validate func(t *testing.T, event *pkgtypes.SwapEvent) }{ { name: "swap token0 for token1", log: types.Log{ Address: poolAddress, Topics: []common.Hash{ UniswapV2SwapSignature, common.HexToHash("0x000000000000000000000000abcdef0123456789abcdef0123456789abcdef01"), // sender common.HexToHash("0x000000000000000000000000fedcba9876543210fedcba9876543210fedcba98"), // recipient }, Data: encodeSwapData( big.NewInt(1000000000000000000), // 1 WETH in (amount0In) big.NewInt(0), // 0 USDC in (amount1In) big.NewInt(0), // 0 WETH out (amount0Out) big.NewInt(3000000000), // 3000 USDC out (amount1Out) ), BlockNumber: 100, Index: 5, }, tx: testTx, wantError: false, validate: func(t *testing.T, event *pkgtypes.SwapEvent) { assert.Equal(t, pkgtypes.ProtocolUniswapV2, event.Protocol) assert.Equal(t, poolAddress, event.PoolAddress) assert.Equal(t, token0, event.Token0) assert.Equal(t, token1, event.Token1) // Swapping token0 for token1 assert.Equal(t, "1000000000000000000", event.Amount0In.String()) assert.Equal(t, "0", event.Amount1In.String()) assert.Equal(t, "0", event.Amount0Out.String()) assert.Equal(t, "3000000000", event.Amount1Out.String()) assert.Equal(t, uint64(100), event.BlockNumber) assert.Equal(t, uint(5), event.LogIndex) }, }, { name: "swap token1 for token0", log: types.Log{ Address: poolAddress, Topics: []common.Hash{ UniswapV2SwapSignature, common.HexToHash("0x000000000000000000000000abcdef0123456789abcdef0123456789abcdef01"), common.HexToHash("0x000000000000000000000000fedcba9876543210fedcba9876543210fedcba98"), }, Data: encodeSwapData( big.NewInt(0), // 0 WETH in (amount0In) big.NewInt(3000000000), // 3000 USDC in (amount1In) big.NewInt(1000000000000000000), // 1 WETH out (amount0Out) big.NewInt(0), // 0 USDC out (amount1Out) ), BlockNumber: 101, Index: 3, }, tx: testTx, wantError: false, validate: func(t *testing.T, event *pkgtypes.SwapEvent) { // Swapping token1 for token0 assert.Equal(t, "0", event.Amount0In.String()) assert.Equal(t, "3000000000", event.Amount1In.String()) assert.Equal(t, "1000000000000000000", event.Amount0Out.String()) assert.Equal(t, "0", event.Amount1Out.String()) }, }, { name: "zero amounts should error", log: types.Log{ Address: poolAddress, Topics: []common.Hash{ UniswapV2SwapSignature, common.HexToHash("0x000000000000000000000000abcdef0123456789abcdef0123456789abcdef01"), common.HexToHash("0x000000000000000000000000fedcba9876543210fedcba9876543210fedcba98"), }, Data: encodeSwapData( big.NewInt(0), // 0 in big.NewInt(0), // 0 in big.NewInt(0), // 0 out big.NewInt(0), // 0 out ), BlockNumber: 102, Index: 1, }, tx: testTx, wantError: true, }, { name: "pool not in cache should error", log: types.Log{ Address: common.HexToAddress("0xUNKNOWN00000000000000000000000000000000"), Topics: []common.Hash{ UniswapV2SwapSignature, common.HexToHash("0x000000000000000000000000abcdef0123456789abcdef0123456789abcdef01"), common.HexToHash("0x000000000000000000000000fedcba9876543210fedcba9876543210fedcba98"), }, Data: encodeSwapData( big.NewInt(1000000000000000000), big.NewInt(0), big.NewInt(0), big.NewInt(3000000000), ), BlockNumber: 103, Index: 2, }, tx: testTx, wantError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { event, err := parser.ParseLog(ctx, tt.log, tt.tx) if tt.wantError { assert.Error(t, err) assert.Nil(t, event) } else { assert.NoError(t, err) assert.NotNil(t, event) if tt.validate != nil { tt.validate(t, event) } } }) } } func TestUniswapV2Parser_ParseMintBurn(t *testing.T) { poolCache := cache.NewPoolCache() parser, err := NewUniswapV2Parser(poolCache, &mockLogger{}) require.NoError(t, err) ctx := context.Background() tx := &types.Transaction{} t.Run("mint event returns nil", func(t *testing.T) { log := types.Log{ Topics: []common.Hash{UniswapV2MintSignature}, } event, err := parser.ParseLog(ctx, log, tx) assert.NoError(t, err) assert.Nil(t, event) // Mint events are ignored in MVP }) t.Run("burn event returns nil", func(t *testing.T) { log := types.Log{ Topics: []common.Hash{UniswapV2BurnSignature}, } event, err := parser.ParseLog(ctx, log, tx) assert.NoError(t, err) assert.Nil(t, event) // Burn events are ignored in MVP }) } func TestUniswapV2Parser_ParseReceipt(t *testing.T) { // Setup poolCache := cache.NewPoolCache() parser, err := NewUniswapV2Parser(poolCache, &mockLogger{}) require.NoError(t, err) // Add test pool poolAddress := common.HexToAddress("0x1234567890123456789012345678901234567890") pool := &pkgtypes.PoolInfo{ Address: poolAddress, Protocol: pkgtypes.ProtocolUniswapV2, Token0: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), // WETH on Arbitrum Token1: common.HexToAddress("0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8"), // USDC on Arbitrum Token0Decimals: 18, Token1Decimals: 6, } ctx := context.Background() err = poolCache.Add(ctx, pool) require.NoError(t, err) t.Run("parse receipt with multiple logs", func(t *testing.T) { testTx := types.NewTx(&types.LegacyTx{ Nonce: 0, GasPrice: big.NewInt(1), Gas: 21000, To: &poolAddress, Value: big.NewInt(0), Data: []byte{}, }) receipt := &types.Receipt{ Logs: []*types.Log{ // Valid swap { Address: poolAddress, Topics: []common.Hash{ UniswapV2SwapSignature, common.HexToHash("0x000000000000000000000000abcdef0123456789abcdef0123456789abcdef01"), common.HexToHash("0x000000000000000000000000fedcba9876543210fedcba9876543210fedcba98"), }, Data: encodeSwapData( big.NewInt(1000000000000000000), big.NewInt(0), big.NewInt(0), big.NewInt(3000000000), ), BlockNumber: 100, Index: 0, }, // Mint event (should be ignored) { Topics: []common.Hash{UniswapV2MintSignature}, Index: 1, }, // Unsupported event (should be skipped) { Topics: []common.Hash{common.HexToHash("0xUNSUPPORTED")}, Index: 2, }, }, } events, err := parser.ParseReceipt(ctx, receipt, testTx) assert.NoError(t, err) assert.Len(t, events, 1) // Only the valid swap assert.Equal(t, pkgtypes.ProtocolUniswapV2, events[0].Protocol) }) t.Run("parse receipt with no supported logs", func(t *testing.T) { testTx := types.NewTx(&types.LegacyTx{ Nonce: 0, GasPrice: big.NewInt(1), Gas: 21000, To: &poolAddress, Value: big.NewInt(0), Data: []byte{}, }) receipt := &types.Receipt{ Logs: []*types.Log{ { Topics: []common.Hash{common.HexToHash("0xUNSUPPORTED")}, }, }, } events, err := parser.ParseReceipt(ctx, receipt, testTx) assert.NoError(t, err) assert.Len(t, events, 0) }) }