feat(parsers): implement UniswapV2 parser with logging and validation
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
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>
This commit is contained in:
167
pkg/parsers/uniswap_v2.go
Normal file
167
pkg/parsers/uniswap_v2.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package parsers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"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"
|
||||
)
|
||||
|
||||
// UniswapV2 Swap event signature:
|
||||
// event Swap(address indexed sender, uint amount0In, uint amount1In, uint amount0Out, uint amount1Out, address indexed to)
|
||||
var (
|
||||
// SwapEventSignature is the event signature for UniswapV2 Swap events
|
||||
SwapEventSignature = crypto.Keccak256Hash([]byte("Swap(address,uint256,uint256,uint256,uint256,address)"))
|
||||
)
|
||||
|
||||
// UniswapV2Parser implements the Parser interface for UniswapV2 pools
|
||||
type UniswapV2Parser struct {
|
||||
cache cache.PoolCache
|
||||
logger mevtypes.Logger
|
||||
}
|
||||
|
||||
// NewUniswapV2Parser creates a new UniswapV2 parser
|
||||
func NewUniswapV2Parser(cache cache.PoolCache, logger mevtypes.Logger) *UniswapV2Parser {
|
||||
return &UniswapV2Parser{
|
||||
cache: cache,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Protocol returns the protocol type this parser handles
|
||||
func (p *UniswapV2Parser) Protocol() mevtypes.ProtocolType {
|
||||
return mevtypes.ProtocolUniswapV2
|
||||
}
|
||||
|
||||
// SupportsLog checks if this parser can handle the given log
|
||||
func (p *UniswapV2Parser) SupportsLog(log types.Log) bool {
|
||||
// Check if log has the Swap event signature
|
||||
if len(log.Topics) == 0 {
|
||||
return false
|
||||
}
|
||||
return log.Topics[0] == SwapEventSignature
|
||||
}
|
||||
|
||||
// ParseLog parses a UniswapV2 Swap event from a log
|
||||
func (p *UniswapV2Parser) ParseLog(ctx context.Context, log types.Log, tx *types.Transaction) (*mevtypes.SwapEvent, error) {
|
||||
// Verify this is a Swap event
|
||||
if !p.SupportsLog(log) {
|
||||
return nil, fmt.Errorf("unsupported log")
|
||||
}
|
||||
|
||||
// Get pool info from cache to extract token addresses and decimals
|
||||
poolInfo, err := p.cache.GetByAddress(ctx, log.Address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("pool not found in cache: %w", err)
|
||||
}
|
||||
|
||||
// Parse event data
|
||||
// Data contains: amount0In, amount1In, amount0Out, amount1Out (non-indexed)
|
||||
// Topics contain: [signature, sender, to] (indexed)
|
||||
if len(log.Topics) != 3 {
|
||||
return nil, fmt.Errorf("invalid number of topics: expected 3, got %d", len(log.Topics))
|
||||
}
|
||||
|
||||
// Define ABI for data decoding
|
||||
uint256Type, err := abi.NewType("uint256", "", nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create uint256 type: %w", err)
|
||||
}
|
||||
|
||||
arguments := abi.Arguments{
|
||||
{Type: uint256Type, Name: "amount0In"},
|
||||
{Type: uint256Type, Name: "amount1In"},
|
||||
{Type: uint256Type, Name: "amount0Out"},
|
||||
{Type: uint256Type, Name: "amount1Out"},
|
||||
}
|
||||
|
||||
// Decode data
|
||||
values, err := arguments.Unpack(log.Data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode event data: %w", err)
|
||||
}
|
||||
|
||||
if len(values) != 4 {
|
||||
return nil, fmt.Errorf("invalid number of values: expected 4, got %d", len(values))
|
||||
}
|
||||
|
||||
// Extract indexed parameters from topics
|
||||
sender := common.BytesToAddress(log.Topics[1].Bytes())
|
||||
recipient := common.BytesToAddress(log.Topics[2].Bytes())
|
||||
|
||||
// Extract amounts from decoded data
|
||||
amount0In := values[0].(*big.Int)
|
||||
amount1In := values[1].(*big.Int)
|
||||
amount0Out := values[2].(*big.Int)
|
||||
amount1Out := values[3].(*big.Int)
|
||||
|
||||
// Scale amounts to 18 decimals for internal representation
|
||||
amount0InScaled := mevtypes.ScaleToDecimals(amount0In, poolInfo.Token0Decimals, 18)
|
||||
amount1InScaled := mevtypes.ScaleToDecimals(amount1In, poolInfo.Token1Decimals, 18)
|
||||
amount0OutScaled := mevtypes.ScaleToDecimals(amount0Out, poolInfo.Token0Decimals, 18)
|
||||
amount1OutScaled := mevtypes.ScaleToDecimals(amount1Out, poolInfo.Token1Decimals, 18)
|
||||
|
||||
// Create swap event
|
||||
event := &mevtypes.SwapEvent{
|
||||
TxHash: tx.Hash(),
|
||||
BlockNumber: log.BlockNumber,
|
||||
LogIndex: uint(log.Index),
|
||||
PoolAddress: log.Address,
|
||||
Protocol: mevtypes.ProtocolUniswapV2,
|
||||
Token0: poolInfo.Token0,
|
||||
Token1: poolInfo.Token1,
|
||||
Token0Decimals: poolInfo.Token0Decimals,
|
||||
Token1Decimals: poolInfo.Token1Decimals,
|
||||
Amount0In: amount0InScaled,
|
||||
Amount1In: amount1InScaled,
|
||||
Amount0Out: amount0OutScaled,
|
||||
Amount1Out: amount1OutScaled,
|
||||
Sender: sender,
|
||||
Recipient: recipient,
|
||||
Fee: big.NewInt(int64(poolInfo.Fee)),
|
||||
}
|
||||
|
||||
// Validate the parsed event
|
||||
if err := event.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("validation failed: %w", err)
|
||||
}
|
||||
|
||||
p.logger.Debug("parsed UniswapV2 swap event",
|
||||
"txHash", event.TxHash.Hex(),
|
||||
"pool", event.PoolAddress.Hex(),
|
||||
"token0", event.Token0.Hex(),
|
||||
"token1", event.Token1.Hex(),
|
||||
)
|
||||
|
||||
return event, nil
|
||||
}
|
||||
|
||||
// ParseReceipt parses all UniswapV2 Swap events from a transaction receipt
|
||||
func (p *UniswapV2Parser) ParseReceipt(ctx context.Context, receipt *types.Receipt, tx *types.Transaction) ([]*mevtypes.SwapEvent, error) {
|
||||
var events []*mevtypes.SwapEvent
|
||||
|
||||
for _, log := range receipt.Logs {
|
||||
if p.SupportsLog(*log) {
|
||||
event, err := p.ParseLog(ctx, *log, tx)
|
||||
if err != nil {
|
||||
// Log error but continue processing other logs
|
||||
p.logger.Warn("failed to parse log",
|
||||
"txHash", tx.Hash().Hex(),
|
||||
"logIndex", log.Index,
|
||||
"error", err,
|
||||
)
|
||||
continue
|
||||
}
|
||||
events = append(events, event)
|
||||
}
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
||||
Reference in New Issue
Block a user