Some checks failed
V2 CI/CD Pipeline / Unit Tests (100% Coverage Required) (push) Has been cancelled
V2 CI/CD Pipeline / Pre-Flight Checks (push) Has been cancelled
V2 CI/CD Pipeline / Build & Dependencies (push) Has been cancelled
V2 CI/CD Pipeline / Code Quality & Linting (push) Has been cancelled
V2 CI/CD Pipeline / Integration Tests (push) Has been cancelled
V2 CI/CD Pipeline / Performance Benchmarks (push) Has been cancelled
V2 CI/CD Pipeline / Decimal Precision Validation (push) Has been cancelled
V2 CI/CD Pipeline / Modularity Validation (push) Has been cancelled
V2 CI/CD Pipeline / Final Validation Summary (push) Has been cancelled
**Implementation:** - Created UniswapV2Parser with ParseLog() and ParseReceipt() methods - Proper event signature detection (Swap event) - Token extraction from pool cache with decimal scaling - Automatic scaling to 18 decimals for internal representation - Support for multiple swaps per transaction **Testing:** - Comprehensive unit tests with 100% coverage - Tests for valid/invalid events, batch parsing, edge cases - Mock logger and pool cache for isolated testing **Validation & Logging:** - SwapLogger: Saves detected swaps to JSON files for testing - Individual swap logging with raw log data - Batch logging for multi-swap transactions - Log cleanup for old entries (configurable retention) - ArbiscanValidator: Verifies parsed swaps against Arbiscan API - Compares pool address, tx hash, block number, log index - Validates sender and recipient addresses - Detects and logs discrepancies for investigation - Batch validation support for transactions with multiple swaps **Type System Updates:** - Exported ScaleToDecimals() function for use across parsers - Updated tests to use exported function name - Consistent decimal handling (USDC 6, WBTC 8, WETH 18) **Use Cases:** 1. Real-time parsing: parser.ParseLog() for individual events 2. Transaction analysis: parser.ParseReceipt() for all swaps 3. Accuracy verification: validator.ValidateSwap() against Arbiscan 4. Testing: Load saved logs and replay for regression testing **Task:** P2-002 (UniswapV2 parser base implementation) **Coverage:** 100% (enforced in CI/CD) **Protocol:** UniswapV2 on Arbitrum 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
484 lines
12 KiB
Go
484 lines
12 KiB
Go
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"
|
|
)
|
|
|
|
// mockLogger implements mevtypes.Logger for testing
|
|
type mockLogger struct{}
|
|
|
|
func (m *mockLogger) Debug(msg string, args ...any) {}
|
|
func (m *mockLogger) Info(msg string, args ...any) {}
|
|
func (m *mockLogger) Warn(msg string, args ...any) {}
|
|
func (m *mockLogger) Error(msg string, args ...any) {}
|
|
func (m *mockLogger) With(args ...any) mevtypes.Logger {
|
|
return m
|
|
}
|
|
func (m *mockLogger) WithContext(ctx context.Context) mevtypes.Logger {
|
|
return m
|
|
}
|
|
|
|
func TestNewUniswapV2Parser(t *testing.T) {
|
|
cache := cache.NewPoolCache()
|
|
logger := &mockLogger{}
|
|
|
|
parser := NewUniswapV2Parser(cache, logger)
|
|
|
|
if parser == nil {
|
|
t.Fatal("NewUniswapV2Parser returned nil")
|
|
}
|
|
|
|
if parser.cache != cache {
|
|
t.Error("NewUniswapV2Parser cache not set correctly")
|
|
}
|
|
|
|
if parser.logger != logger {
|
|
t.Error("NewUniswapV2Parser logger not set correctly")
|
|
}
|
|
}
|
|
|
|
func TestUniswapV2Parser_Protocol(t *testing.T) {
|
|
parser := NewUniswapV2Parser(cache.NewPoolCache(), &mockLogger{})
|
|
|
|
if parser.Protocol() != mevtypes.ProtocolUniswapV2 {
|
|
t.Errorf("Protocol() = %v, want %v", parser.Protocol(), mevtypes.ProtocolUniswapV2)
|
|
}
|
|
}
|
|
|
|
func TestUniswapV2Parser_SupportsLog(t *testing.T) {
|
|
parser := NewUniswapV2Parser(cache.NewPoolCache(), &mockLogger{})
|
|
|
|
tests := []struct {
|
|
name string
|
|
log types.Log
|
|
want bool
|
|
}{
|
|
{
|
|
name: "valid Swap event",
|
|
log: types.Log{
|
|
Topics: []common.Hash{SwapEventSignature},
|
|
},
|
|
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,
|
|
},
|
|
}
|
|
|
|
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 TestUniswapV2Parser_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.ProtocolUniswapV2,
|
|
Token0: token0,
|
|
Token1: token1,
|
|
Token0Decimals: 18,
|
|
Token1Decimals: 6,
|
|
Reserve0: big.NewInt(1000000),
|
|
Reserve1: big.NewInt(500000),
|
|
Fee: 30, // 0.3% in basis points
|
|
IsActive: true,
|
|
}
|
|
|
|
err := poolCache.Add(ctx, testPool)
|
|
if err != nil {
|
|
t.Fatalf("Failed to add test pool: %v", err)
|
|
}
|
|
|
|
parser := NewUniswapV2Parser(poolCache, &mockLogger{})
|
|
|
|
// Create test transaction
|
|
tx := types.NewTransaction(
|
|
0,
|
|
poolAddress,
|
|
big.NewInt(0),
|
|
0,
|
|
big.NewInt(0),
|
|
[]byte{},
|
|
)
|
|
|
|
// Encode event data: amount0In, amount1In, amount0Out, amount1Out
|
|
amount0In := big.NewInt(1000000000000000000) // 1 token0 (18 decimals)
|
|
amount1In := big.NewInt(0)
|
|
amount0Out := big.NewInt(0)
|
|
amount1Out := big.NewInt(500000) // 0.5 token1 (6 decimals)
|
|
|
|
// ABI encode the amounts
|
|
data := make([]byte, 128) // 4 * 32 bytes
|
|
amount0In.FillBytes(data[0:32])
|
|
amount1In.FillBytes(data[32:64])
|
|
amount0Out.FillBytes(data[64:96])
|
|
amount1Out.FillBytes(data[96:128])
|
|
|
|
sender := common.HexToAddress("0x4444444444444444444444444444444444444444")
|
|
recipient := common.HexToAddress("0x5555555555555555555555555555555555555555")
|
|
|
|
tests := []struct {
|
|
name string
|
|
log types.Log
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "valid swap event",
|
|
log: types.Log{
|
|
Address: poolAddress,
|
|
Topics: []common.Hash{
|
|
SwapEventSignature,
|
|
common.BytesToHash(sender.Bytes()),
|
|
common.BytesToHash(recipient.Bytes()),
|
|
},
|
|
Data: data,
|
|
BlockNumber: 1000,
|
|
Index: 0,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "unsupported log",
|
|
log: types.Log{
|
|
Address: poolAddress,
|
|
Topics: []common.Hash{
|
|
common.HexToHash("0x1234"),
|
|
},
|
|
Data: data,
|
|
BlockNumber: 1000,
|
|
Index: 0,
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "invalid number of topics",
|
|
log: types.Log{
|
|
Address: poolAddress,
|
|
Topics: []common.Hash{
|
|
SwapEventSignature,
|
|
common.BytesToHash(sender.Bytes()),
|
|
// Missing recipient topic
|
|
},
|
|
Data: data,
|
|
BlockNumber: 1000,
|
|
Index: 0,
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "pool not in cache",
|
|
log: types.Log{
|
|
Address: common.HexToAddress("0x9999999999999999999999999999999999999999"),
|
|
Topics: []common.Hash{
|
|
SwapEventSignature,
|
|
common.BytesToHash(sender.Bytes()),
|
|
common.BytesToHash(recipient.Bytes()),
|
|
},
|
|
Data: data,
|
|
BlockNumber: 1000,
|
|
Index: 0,
|
|
},
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
event, err := parser.ParseLog(ctx, tt.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.BlockNumber != tt.log.BlockNumber {
|
|
t.Errorf("BlockNumber = %v, want %v", event.BlockNumber, tt.log.BlockNumber)
|
|
}
|
|
|
|
if event.LogIndex != uint(tt.log.Index) {
|
|
t.Errorf("LogIndex = %v, want %v", event.LogIndex, tt.log.Index)
|
|
}
|
|
|
|
if event.PoolAddress != poolAddress {
|
|
t.Errorf("PoolAddress = %v, want %v", event.PoolAddress, poolAddress)
|
|
}
|
|
|
|
if event.Protocol != mevtypes.ProtocolUniswapV2 {
|
|
t.Errorf("Protocol = %v, want %v", event.Protocol, mevtypes.ProtocolUniswapV2)
|
|
}
|
|
|
|
if event.Token0 != token0 {
|
|
t.Errorf("Token0 = %v, want %v", event.Token0, token0)
|
|
}
|
|
|
|
if event.Token1 != token1 {
|
|
t.Errorf("Token1 = %v, want %v", event.Token1, token1)
|
|
}
|
|
|
|
if event.Token0Decimals != 18 {
|
|
t.Errorf("Token0Decimals = %v, want 18", event.Token0Decimals)
|
|
}
|
|
|
|
if event.Token1Decimals != 6 {
|
|
t.Errorf("Token1Decimals = %v, want 6", event.Token1Decimals)
|
|
}
|
|
|
|
if event.Sender != sender {
|
|
t.Errorf("Sender = %v, want %v", event.Sender, sender)
|
|
}
|
|
|
|
if event.Recipient != recipient {
|
|
t.Errorf("Recipient = %v, want %v", event.Recipient, recipient)
|
|
}
|
|
|
|
// Verify amounts are scaled to 18 decimals
|
|
expectedAmount0In := amount0In // Already 18 decimals
|
|
expectedAmount1Out := mevtypes.ScaleToDecimals(amount1Out, 6, 18)
|
|
|
|
if event.Amount0In.Cmp(expectedAmount0In) != 0 {
|
|
t.Errorf("Amount0In = %v, want %v", event.Amount0In, expectedAmount0In)
|
|
}
|
|
|
|
if event.Amount1Out.Cmp(expectedAmount1Out) != 0 {
|
|
t.Errorf("Amount1Out = %v, want %v", event.Amount1Out, expectedAmount1Out)
|
|
}
|
|
|
|
if event.Amount1In.Cmp(big.NewInt(0)) != 0 {
|
|
t.Errorf("Amount1In = %v, want 0", event.Amount1In)
|
|
}
|
|
|
|
if event.Amount0Out.Cmp(big.NewInt(0)) != 0 {
|
|
t.Errorf("Amount0Out = %v, want 0", event.Amount0Out)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUniswapV2Parser_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.ProtocolUniswapV2,
|
|
Token0: token0,
|
|
Token1: token1,
|
|
Token0Decimals: 18,
|
|
Token1Decimals: 6,
|
|
Reserve0: big.NewInt(1000000),
|
|
Reserve1: big.NewInt(500000),
|
|
Fee: 30, // 0.3% in basis points
|
|
IsActive: true,
|
|
}
|
|
|
|
err := poolCache.Add(ctx, testPool)
|
|
if err != nil {
|
|
t.Fatalf("Failed to add test pool: %v", err)
|
|
}
|
|
|
|
parser := NewUniswapV2Parser(poolCache, &mockLogger{})
|
|
|
|
// Create test transaction
|
|
tx := types.NewTransaction(
|
|
0,
|
|
poolAddress,
|
|
big.NewInt(0),
|
|
0,
|
|
big.NewInt(0),
|
|
[]byte{},
|
|
)
|
|
|
|
// Encode event data
|
|
amount0In := big.NewInt(1000000000000000000)
|
|
amount1In := big.NewInt(0)
|
|
amount0Out := big.NewInt(0)
|
|
amount1Out := big.NewInt(500000)
|
|
|
|
data := make([]byte, 128)
|
|
amount0In.FillBytes(data[0:32])
|
|
amount1In.FillBytes(data[32:64])
|
|
amount0Out.FillBytes(data[64:96])
|
|
amount1Out.FillBytes(data[96:128])
|
|
|
|
sender := common.HexToAddress("0x4444444444444444444444444444444444444444")
|
|
recipient := common.HexToAddress("0x5555555555555555555555555555555555555555")
|
|
|
|
tests := []struct {
|
|
name string
|
|
receipt *types.Receipt
|
|
wantCount int
|
|
}{
|
|
{
|
|
name: "receipt with single swap event",
|
|
receipt: &types.Receipt{
|
|
Logs: []*types.Log{
|
|
{
|
|
Address: poolAddress,
|
|
Topics: []common.Hash{
|
|
SwapEventSignature,
|
|
common.BytesToHash(sender.Bytes()),
|
|
common.BytesToHash(recipient.Bytes()),
|
|
},
|
|
Data: data,
|
|
BlockNumber: 1000,
|
|
Index: 0,
|
|
},
|
|
},
|
|
},
|
|
wantCount: 1,
|
|
},
|
|
{
|
|
name: "receipt with multiple swap events",
|
|
receipt: &types.Receipt{
|
|
Logs: []*types.Log{
|
|
{
|
|
Address: poolAddress,
|
|
Topics: []common.Hash{
|
|
SwapEventSignature,
|
|
common.BytesToHash(sender.Bytes()),
|
|
common.BytesToHash(recipient.Bytes()),
|
|
},
|
|
Data: data,
|
|
BlockNumber: 1000,
|
|
Index: 0,
|
|
},
|
|
{
|
|
Address: poolAddress,
|
|
Topics: []common.Hash{
|
|
SwapEventSignature,
|
|
common.BytesToHash(sender.Bytes()),
|
|
common.BytesToHash(recipient.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{
|
|
SwapEventSignature,
|
|
common.BytesToHash(sender.Bytes()),
|
|
common.BytesToHash(recipient.Bytes()),
|
|
},
|
|
Data: data,
|
|
BlockNumber: 1000,
|
|
Index: 0,
|
|
},
|
|
{
|
|
Address: poolAddress,
|
|
Topics: []common.Hash{
|
|
common.HexToHash("0x1234"), // Different event
|
|
},
|
|
Data: []byte{},
|
|
BlockNumber: 1000,
|
|
Index: 1,
|
|
},
|
|
},
|
|
},
|
|
wantCount: 1, // Only the Swap 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.ProtocolUniswapV2 {
|
|
t.Errorf("Event %d Protocol = %v, want %v", i, event.Protocol, mevtypes.ProtocolUniswapV2)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSwapEventSignature(t *testing.T) {
|
|
// Verify the event signature is correct
|
|
expected := crypto.Keccak256Hash([]byte("Swap(address,uint256,uint256,uint256,uint256,address)"))
|
|
|
|
if SwapEventSignature != expected {
|
|
t.Errorf("SwapEventSignature = %v, want %v", SwapEventSignature, expected)
|
|
}
|
|
}
|