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>
325 lines
9.4 KiB
Go
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
|