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 TestNewCurveParser(t *testing.T) { cache := cache.NewPoolCache() logger := &mockLogger{} parser := NewCurveParser(cache, logger) if parser == nil { t.Fatal("NewCurveParser returned nil") } if parser.cache != cache { t.Error("NewCurveParser cache not set correctly") } if parser.logger != logger { t.Error("NewCurveParser logger not set correctly") } } func TestCurveParser_Protocol(t *testing.T) { parser := NewCurveParser(cache.NewPoolCache(), &mockLogger{}) if parser.Protocol() != mevtypes.ProtocolCurve { t.Errorf("Protocol() = %v, want %v", parser.Protocol(), mevtypes.ProtocolCurve) } } func TestCurveParser_SupportsLog(t *testing.T) { parser := NewCurveParser(cache.NewPoolCache(), &mockLogger{}) tests := []struct { name string log types.Log want bool }{ { name: "valid TokenExchange event", log: types.Log{ Topics: []common.Hash{CurveTokenExchangeSignature}, }, want: true, }, { name: "valid TokenExchangeUnderlying event", log: types.Log{ Topics: []common.Hash{CurveTokenExchangeUnderlyingSignature}, }, 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: "UniswapV2 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 TestCurveParser_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") // USDC token1 := common.HexToAddress("0x3333333333333333333333333333333333333333") // USDT testPool := &mevtypes.PoolInfo{ Address: poolAddress, Protocol: mevtypes.ProtocolCurve, Token0: token0, Token1: token1, Token0Decimals: 6, // USDC Token1Decimals: 6, // USDT Reserve0: big.NewInt(1000000000000), // 1M USDC Reserve1: big.NewInt(1000000000000), // 1M USDT Fee: 4, // 0.04% typical Curve fee IsActive: true, AmpCoefficient: big.NewInt(2000), // Typical A parameter for stablecoin pools } err := poolCache.Add(ctx, testPool) if err != nil { t.Fatalf("Failed to add test pool: %v", err) } parser := NewCurveParser(poolCache, &mockLogger{}) // Create test transaction tx := types.NewTransaction( 0, poolAddress, big.NewInt(0), 0, big.NewInt(0), []byte{}, ) buyer := common.HexToAddress("0x4444444444444444444444444444444444444444") tests := []struct { name string soldID int128 tokensSold *big.Int boughtID int128 tokensBought *big.Int wantAmount0In *big.Int wantAmount1In *big.Int wantAmount0Out *big.Int wantAmount1Out *big.Int wantErr bool }{ { name: "swap token0 for token1 (USDC → USDT)", soldID: 0, tokensSold: big.NewInt(1000000), // 1 USDC (6 decimals) boughtID: 1, tokensBought: big.NewInt(999500), // 0.9995 USDT (6 decimals) wantAmount0In: mevtypes.ScaleToDecimals(big.NewInt(1000000), 6, 18), wantAmount1In: big.NewInt(0), wantAmount0Out: big.NewInt(0), wantAmount1Out: mevtypes.ScaleToDecimals(big.NewInt(999500), 6, 18), wantErr: false, }, { name: "swap token1 for token0 (USDT → USDC)", soldID: 1, tokensSold: big.NewInt(1000000), // 1 USDT (6 decimals) boughtID: 0, tokensBought: big.NewInt(999500), // 0.9995 USDC (6 decimals) wantAmount0In: big.NewInt(0), wantAmount1In: mevtypes.ScaleToDecimals(big.NewInt(1000000), 6, 18), wantAmount0Out: mevtypes.ScaleToDecimals(big.NewInt(999500), 6, 18), wantAmount1Out: big.NewInt(0), wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Encode event data: sold_id, tokens_sold, bought_id, tokens_bought data := make([]byte, 32*4) // 4 * 32 bytes // int128 sold_id soldIDBig := big.NewInt(int64(tt.soldID)) soldIDBig.FillBytes(data[0:32]) // uint256 tokens_sold tt.tokensSold.FillBytes(data[32:64]) // int128 bought_id boughtIDBig := big.NewInt(int64(tt.boughtID)) boughtIDBig.FillBytes(data[64:96]) // uint256 tokens_bought tt.tokensBought.FillBytes(data[96:128]) log := types.Log{ Address: poolAddress, Topics: []common.Hash{ CurveTokenExchangeSignature, common.BytesToHash(buyer.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.ProtocolCurve { t.Errorf("Protocol = %v, want %v", event.Protocol, mevtypes.ProtocolCurve) } 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.Sender != buyer { t.Errorf("Sender = %v, want %v", event.Sender, buyer) } if event.Recipient != buyer { t.Errorf("Recipient = %v, want %v (Curve uses buyer for both)", event.Recipient, buyer) } }) } } func TestCurveParser_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.ProtocolCurve, Token0: token0, Token1: token1, Token0Decimals: 6, Token1Decimals: 6, Reserve0: big.NewInt(1000000000000), Reserve1: big.NewInt(1000000000000), Fee: 4, IsActive: true, AmpCoefficient: big.NewInt(2000), } err := poolCache.Add(ctx, testPool) if err != nil { t.Fatalf("Failed to add test pool: %v", err) } parser := NewCurveParser(poolCache, &mockLogger{}) // Create test transaction tx := types.NewTransaction( 0, poolAddress, big.NewInt(0), 0, big.NewInt(0), []byte{}, ) // Encode minimal valid event data soldID := big.NewInt(0) tokensSold := big.NewInt(1000000) boughtID := big.NewInt(1) tokensBought := big.NewInt(999500) data := make([]byte, 32*4) soldID.FillBytes(data[0:32]) tokensSold.FillBytes(data[32:64]) boughtID.FillBytes(data[64:96]) tokensBought.FillBytes(data[96:128]) buyer := common.HexToAddress("0x4444444444444444444444444444444444444444") tests := []struct { name string receipt *types.Receipt wantCount int }{ { name: "receipt with single Curve swap event", receipt: &types.Receipt{ Logs: []*types.Log{ { Address: poolAddress, Topics: []common.Hash{ CurveTokenExchangeSignature, common.BytesToHash(buyer.Bytes()), }, Data: data, BlockNumber: 1000, Index: 0, }, }, }, wantCount: 1, }, { name: "receipt with multiple Curve swap events", receipt: &types.Receipt{ Logs: []*types.Log{ { Address: poolAddress, Topics: []common.Hash{ CurveTokenExchangeSignature, common.BytesToHash(buyer.Bytes()), }, Data: data, BlockNumber: 1000, Index: 0, }, { Address: poolAddress, Topics: []common.Hash{ CurveTokenExchangeUnderlyingSignature, common.BytesToHash(buyer.Bytes()), }, Data: data, BlockNumber: 1000, Index: 1, }, }, }, wantCount: 2, }, { name: "receipt with mixed events", receipt: &types.Receipt{ Logs: []*types.Log{ { Address: poolAddress, Topics: []common.Hash{ CurveTokenExchangeSignature, common.BytesToHash(buyer.Bytes()), }, Data: data, BlockNumber: 1000, Index: 0, }, { Address: poolAddress, Topics: []common.Hash{ SwapEventSignature, // UniswapV2 signature common.BytesToHash(buyer.Bytes()), common.BytesToHash(buyer.Bytes()), }, Data: []byte{}, BlockNumber: 1000, Index: 1, }, }, }, wantCount: 1, // Only the Curve 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.ProtocolCurve { t.Errorf("Event %d Protocol = %v, want %v", i, event.Protocol, mevtypes.ProtocolCurve) } } }) } } func TestCurveTokenExchangeSignature(t *testing.T) { // Verify the event signature is correct expected := crypto.Keccak256Hash([]byte("TokenExchange(address,int128,uint256,int128,uint256)")) if CurveTokenExchangeSignature != expected { t.Errorf("CurveTokenExchangeSignature = %v, want %v", CurveTokenExchangeSignature, expected) } } func TestCurveTokenExchangeUnderlyingSignature(t *testing.T) { // Verify the underlying event signature is correct expected := crypto.Keccak256Hash([]byte("TokenExchangeUnderlying(address,int128,uint256,int128,uint256)")) if CurveTokenExchangeUnderlyingSignature != expected { t.Errorf("CurveTokenExchangeUnderlyingSignature = %v, want %v", CurveTokenExchangeUnderlyingSignature, expected) } }