Files
mev-beta/pkg/parsers/arbiscan_validator.go
Administrator 9982573a8b fix(types): add missing types and fix compilation errors - WIP
Fixed compilation errors in integration code:

Type System Fixes:
- Add types.Logger type alias (*slog.Logger)
- Add PoolInfo.LiquidityUSD field
- Add ProtocolSushiSwap and ProtocolCamelot constants
- Fix time.Now() call in arbiscan_validator.go

Pool Discovery Fixes:
- Change cache from *cache.PoolCache to cache.PoolCache (interface)
- Add context.Context parameters to cache.Add() and cache.Count() calls
- Fix protocol type from string to ProtocolType

Docker Fixes:
- Add .dockerignore to exclude test files and docs
- Add go mod tidy step in Dockerfile
- Add //go:build examples tag to example_usage.go

Still Remaining:
- Arbitrage package needs similar interface fixes
- SwapEvent.TokenIn/TokenOut field name issues
- More cache interface method calls need context

Progress: Parser and pool discovery packages now compile correctly.
Integration code (main.go, sequencer, pools) partially working.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 19:30:00 +01:00

325 lines
9.4 KiB
Go

package parsers
import (
"context"
"encoding/json"
"fmt"
"io"
"math/big"
"net/http"
"net/url"
"strings"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/your-org/mev-bot/pkg/types"
)
// ArbiscanValidator validates parsed swap events against Arbiscan API
type ArbiscanValidator struct {
apiKey string
baseURL string
httpClient *http.Client
logger types.Logger
swapLogger *SwapLogger
}
// ArbiscanLog represents a log entry from Arbiscan API
type ArbiscanLog struct {
Address string `json:"address"`
Topics []string `json:"topics"`
Data string `json:"data"`
BlockNumber string `json:"blockNumber"`
TimeStamp string `json:"timeStamp"`
GasPrice string `json:"gasPrice"`
GasUsed string `json:"gasUsed"`
LogIndex string `json:"logIndex"`
TransactionHash string `json:"transactionHash"`
TransactionIndex string `json:"transactionIndex"`
}
// ArbiscanAPIResponse represents the API response from Arbiscan
type ArbiscanAPIResponse struct {
Status string `json:"status"`
Message string `json:"message"`
Result []ArbiscanLog `json:"result"`
}
// ValidationResult contains the result of validating a swap event
type ValidationResult struct {
IsValid bool `json:"is_valid"`
SwapEvent *types.SwapEvent `json:"swap_event"`
ArbiscanLog *ArbiscanLog `json:"arbiscan_log"`
Discrepancies []string `json:"discrepancies,omitempty"`
ValidatedAt time.Time `json:"validated_at"`
}
// NewArbiscanValidator creates a new Arbiscan validator
func NewArbiscanValidator(apiKey string, logger types.Logger, swapLogger *SwapLogger) *ArbiscanValidator {
return &ArbiscanValidator{
apiKey: apiKey,
baseURL: "https://api.arbiscan.io/api",
httpClient: &http.Client{
Timeout: 10 * time.Second,
},
logger: logger,
swapLogger: swapLogger,
}
}
// ValidateSwap validates a parsed swap event against Arbiscan
func (v *ArbiscanValidator) ValidateSwap(ctx context.Context, event *types.SwapEvent) (*ValidationResult, error) {
// Fetch transaction logs from Arbiscan
logs, err := v.getTransactionLogs(ctx, event.TxHash)
if err != nil {
return nil, fmt.Errorf("failed to fetch logs from Arbiscan: %w", err)
}
// Find the matching log by log index
var matchingLog *ArbiscanLog
for i := range logs {
logIndex := parseHexToUint64(logs[i].LogIndex)
if logIndex == uint64(event.LogIndex) {
matchingLog = &logs[i]
break
}
}
if matchingLog == nil {
return &ValidationResult{
IsValid: false,
SwapEvent: event,
Discrepancies: []string{fmt.Sprintf("log index %d not found in Arbiscan response", event.LogIndex)},
ValidatedAt: time.Now(),
}, nil
}
// Compare parsed event with Arbiscan data
discrepancies := v.compareSwapWithArbiscan(event, matchingLog)
result := &ValidationResult{
IsValid: len(discrepancies) == 0,
SwapEvent: event,
ArbiscanLog: matchingLog,
Discrepancies: discrepancies,
ValidatedAt: time.Now(),
}
// Log validation result
if len(discrepancies) > 0 {
v.logger.Warn("swap validation discrepancies found",
"txHash", event.TxHash.Hex(),
"logIndex", event.LogIndex,
"discrepancies", strings.Join(discrepancies, "; "),
)
// Save discrepancy to swap logger for investigation
if v.swapLogger != nil {
rawData := common.Hex2Bytes(strings.TrimPrefix(matchingLog.Data, "0x"))
if err := v.swapLogger.LogSwap(ctx, event, rawData, matchingLog.Topics, "arbiscan_validation"); err != nil {
v.logger.Error("failed to log discrepancy", "error", err)
}
}
} else {
v.logger.Debug("swap validation successful",
"txHash", event.TxHash.Hex(),
"logIndex", event.LogIndex,
)
}
return result, nil
}
// getTransactionLogs fetches transaction logs from Arbiscan API
func (v *ArbiscanValidator) getTransactionLogs(ctx context.Context, txHash common.Hash) ([]ArbiscanLog, error) {
// Build API URL
params := url.Values{}
params.Set("module", "logs")
params.Set("action", "getLogs")
params.Set("txhash", txHash.Hex())
params.Set("apikey", v.apiKey)
apiURL := fmt.Sprintf("%s?%s", v.baseURL, params.Encode())
// Create request
req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
// Execute request
resp, err := v.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to execute request: %w", err)
}
defer resp.Body.Close()
// Read response
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
// Parse response
var apiResp ArbiscanAPIResponse
if err := json.Unmarshal(body, &apiResp); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
if apiResp.Status != "1" {
return nil, fmt.Errorf("Arbiscan API error: %s", apiResp.Message)
}
return apiResp.Result, nil
}
// compareSwapWithArbiscan compares a parsed swap event with Arbiscan log data
func (v *ArbiscanValidator) compareSwapWithArbiscan(event *types.SwapEvent, log *ArbiscanLog) []string {
var discrepancies []string
// Compare pool address
if !strings.EqualFold(event.PoolAddress.Hex(), log.Address) {
discrepancies = append(discrepancies, fmt.Sprintf(
"pool address mismatch: parsed=%s arbiscan=%s",
event.PoolAddress.Hex(), log.Address,
))
}
// Compare transaction hash
if !strings.EqualFold(event.TxHash.Hex(), log.TransactionHash) {
discrepancies = append(discrepancies, fmt.Sprintf(
"tx hash mismatch: parsed=%s arbiscan=%s",
event.TxHash.Hex(), log.TransactionHash,
))
}
// Compare block number
arbiscanBlockNumber := parseHexToUint64(log.BlockNumber)
if event.BlockNumber != arbiscanBlockNumber {
discrepancies = append(discrepancies, fmt.Sprintf(
"block number mismatch: parsed=%d arbiscan=%d",
event.BlockNumber, arbiscanBlockNumber,
))
}
// Compare log index
arbiscanLogIndex := parseHexToUint64(log.LogIndex)
if uint64(event.LogIndex) != arbiscanLogIndex {
discrepancies = append(discrepancies, fmt.Sprintf(
"log index mismatch: parsed=%d arbiscan=%d",
event.LogIndex, arbiscanLogIndex,
))
}
// Compare topics (event signature and indexed parameters)
if len(log.Topics) > 0 {
expectedSignature := log.Topics[0]
// UniswapV2 Swap signature
if strings.EqualFold(expectedSignature, SwapEventSignature.Hex()) {
// Validate sender and recipient from topics
if len(log.Topics) >= 3 {
arbiscanSender := common.HexToAddress(log.Topics[1])
arbiscanRecipient := common.HexToAddress(log.Topics[2])
if event.Sender != arbiscanSender {
discrepancies = append(discrepancies, fmt.Sprintf(
"sender mismatch: parsed=%s arbiscan=%s",
event.Sender.Hex(), arbiscanSender.Hex(),
))
}
if event.Recipient != arbiscanRecipient {
discrepancies = append(discrepancies, fmt.Sprintf(
"recipient mismatch: parsed=%s arbiscan=%s",
event.Recipient.Hex(), arbiscanRecipient.Hex(),
))
}
}
}
}
// Note: We don't compare amounts directly here because they're scaled
// and Arbiscan returns raw values. The caller should handle amount validation
// by comparing raw log data if needed.
return discrepancies
}
// ValidateSwapBatch validates multiple swap events from the same transaction
func (v *ArbiscanValidator) ValidateSwapBatch(ctx context.Context, events []*types.SwapEvent) ([]*ValidationResult, error) {
if len(events) == 0 {
return nil, nil
}
// All events should have the same transaction hash
txHash := events[0].TxHash
// Fetch transaction logs once for all events
logs, err := v.getTransactionLogs(ctx, txHash)
if err != nil {
return nil, fmt.Errorf("failed to fetch logs from Arbiscan: %w", err)
}
// Validate each event
results := make([]*ValidationResult, 0, len(events))
for _, event := range events {
// Find matching log
var matchingLog *ArbiscanLog
for i := range logs {
logIndex := parseHexToUint64(logs[i].LogIndex)
if logIndex == uint64(event.LogIndex) {
matchingLog = &logs[i]
break
}
}
if matchingLog == nil {
results = append(results, &ValidationResult{
IsValid: false,
SwapEvent: event,
Discrepancies: []string{fmt.Sprintf("log index %d not found", event.LogIndex)},
ValidatedAt: time.Now(),
})
continue
}
// Compare
discrepancies := v.compareSwapWithArbiscan(event, matchingLog)
results = append(results, &ValidationResult{
IsValid: len(discrepancies) == 0,
SwapEvent: event,
ArbiscanLog: matchingLog,
Discrepancies: discrepancies,
ValidatedAt: time.Now(),
})
}
return results, nil
}
// parseHexToUint64 parses a hex string to uint64
func parseHexToUint64(hex string) uint64 {
if strings.HasPrefix(hex, "0x") {
hex = hex[2:]
}
n := new(big.Int)
n.SetString(hex, 16)
return n.Uint64()
}
// GetValidationStats returns validation statistics
type ValidationStats struct {
TotalValidated int `json:"total_validated"`
Valid int `json:"valid"`
Invalid int `json:"invalid"`
ErrorRate float64 `json:"error_rate"`
LastValidated time.Time `json:"last_validated"`
}
// Note: To use this validator in production:
// 1. Create validator: validator := NewArbiscanValidator(apiKey, logger, swapLogger)
// 2. After parsing a swap: result, err := validator.ValidateSwap(ctx, event)
// 3. Check result.IsValid and log discrepancies
// 4. Use for spot-checking in production or continuous validation in testing